diff --git a/Compilation b/Compilation new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/README.adoc b/README.adoc index 0ad72da1a5ba..e6bbdfbdc1d4 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,9 @@ -= Address Book (Level 4) += NUSSU Connect ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/nusCS2113-AY1819S1/addressbook-level4[image:https://travis-ci.org/nusCS2113-AY1819S1/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/CS2113-AY1819S1-F09-1/main[image:https://travis-ci.org/CS2113-AY1819S1-F09-1/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/Chocological/main-64rse[image:https://ci.appveyor.com/api/projects/status/rge5q76v8xnskc7p/branch/master?svg=true[Build status]] +https://coveralls.io/github/CS2113-AY1819S1-F09-1/main?branch=master[image:https://coveralls.io/repos/github/CS2113-AY1819S1-F09-1/main/badge.svg?branch=master[Coverage Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,27 +13,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. +NUSSU-Connect is an application for executive committee (exco) members of the NUS Student Union (NUSSU) to help them perform their daily tasks including managing recruitment, contacts, budgets and projects efficiently. + == 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 _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] +* The original source of the code is https://github.com/se-edu/[AddressBook-Level4] project created by SE-EDU initiative. == Licence : link:LICENSE[MIT] diff --git a/build.gradle b/build.gradle index f8e614f8b49b..230d27bd7809 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'NUSSU-Connect.jar' destinationDir = file("${buildDir}/jar/") } @@ -126,7 +126,7 @@ nonGuiTests.dependsOn test task(allTests) // `allTests` implies both `guiTests` and `nonGuiTests` -allTests.dependsOn guiTests +//allTests.dependsOn guiTests allTests.dependsOn nonGuiTests test { @@ -207,8 +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-name': 'NUSSU Connect', + 'site-githuburl': 'https://github.com/CS2113-AY1819S1-F09-1/NUSSU-Connect', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..50f51b8a9f29 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,48 @@ :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} + +image::NUSSU-ConnectPNGBanner.png[width="680", align=center"] +NUSSU-Connect is an application developed to help NUSSU manage people and +related functions relating to HR and Finance needs. It is currently a work in progress and is developed by +https://se-edu.github.io/docs/Team.html[F09-1] team. + + 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]] [<>] - -Role: Project Advisor - -''' +=== Boon Jun +image::ladderinc.png[width="150", align="left"] +{empty}[http://github.com/ladderinc[github]][<>] -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Scheduling and Tracking, Deliverables and deadlines + +Responsibilities: Search Pruning + Advanced Search and filter feature. ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Melvin Tan +image::chocological.png[width="150", align="left"] +{empty}[http://github.com/Chocological[github]] -Role: Developer + -Responsibilities: Data +Role: Integration and Testing + +Responsibilities: Login System ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Jonathan Ng +image::derpyplops.png[width="150", align="left"] +{empty}[http://github.com/derpyplops[github]] -Role: Developer + -Responsibilities: Dev Ops + Threading +Role: Documentation and Team Lead + +Responsibilities: HR & Roles ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Sanjukta Saha +image::sanjukta99.png[width="150", align="left"] +{empty}[http://github.com/sanjukta99[github]] -Role: Developer + -Responsibilities: UI +Role: Code Quality + +Responsibilities: Budgeting functionality for NUSSU and club treasurers ''' diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index ea58481e4740..ca1efa84bbad 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += NUSSU Connect - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -12,9 +12,9 @@ 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/CS2113-AY1819S1-F09-1/main/ -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `Team F09-1`      Since: `Sep 2018`      Licence: `MIT` == Setting up @@ -239,6 +239,343 @@ 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::login[] +=== Login system feature +==== Current Implementation + +The login mechanism utilizes an existing Java library, `FilteredList`, in order to filter out the relevant +account that is associated with an instance of a successful login. An object belonging to the `FilteredList` class, called +`filteredLoginDetails`, is instantiated at the start of the application. The `filteredLoginDetails` object initially +contains a complete list of all existing accounts stored in `LoginBook`. There is one crucial operation in `FilteredList`, +which is often used: + +* `FilteredList#setPredicate(predicate)` -- Filters the list of accounts in `filteredLoginDetails` according to the +predicates determined after the user inputs their login credentials. + +The operation above is called when the method, `updateFilteredLoginDetailsList(Predicate predicate)`, is +called in `ModelManager`. `ModelManager` implements `updateFilteredLoginDetailsList(Predicate predicate)` and +`getFilteredLoginDetailsList()` found in the `Model` interface. `getFilteredLoginDetailsList()` is called when the +list of accounts in `LoginBook` is filtered to the extent where only one or no account remains in the list. + +Given below is an example usage scenario and how the login mechanism behaves at each step. The sequence diagram below +demonstrates the flow of operation and interaction between the `Logic` and `Model` component in the login mechanism. +Specifically, the diagram shows what happens when the user inputs the correct login credentials. + +image::LoginSequenceDiagram.png[width="800"] + +Step 1. The user launches the application for the first time. The `filteredLoginDetails` object will be initialized with +a list of all the accounts in `LoginBook`. + +image::InitialLoginBookList.PNG[width="300"] + +Step 2. The user executes `login A1234568M zaq1xsw2cde3 member` command in the input box that matches an account in +`LoginBook`. `LogicManager` then calls `ParseCommand(login A1234568M zaq1xsw2cde3 member)` in `AddressBookParser`. + +image::CorrectIdPasswordRole.PNG[width="300"] + +Step 3. `AddressBookParser` instantiates the `LoginUserIdPasswordRoleCommandParser` object and simultaneously calls the `parse(args)` method, returning +an `LoginUserIdPasswordRoleCommand` object with the user input, parsed, to `AddressBookParser` and `LogicManager`. + +Step 4. `LogicManager` calls the `execute()` method in `LoginUserIdPasswordRoleCommand`. Next, `LoginUserIdPasswordRoleCommand` +calls `updateFilteredLoginDetailsList(updatedIdPredicate)` in `Model` with the computed predicate from user ID field input. + +Step 5. `Model` calls `setPredicate(updatedIdPredicate)` in `FilteredList`, which then filters out accounts whose user Id is a +mismatch with updatedIdPredicate. `filteredLoginDetails` is updated as shown below. + +image::ParseCorrectLoginDetailList.PNG[width="300"] + +Step 6. `LoginUserIdPasswordRoleCommand` calls `updateFilteredLoginDetailsList(updatedPasswordPredicate)` in `Model` with the +computed predicate from user password field input. + +Step 7. `Model` calls `setPredicate(updatedPasswordPredicate)` in `FilteredList`, which then further filters out accounts +whose password is a mismatch with updatedPasswordPredicate. `filteredLoginDetails` is further updated as shown below. + +image::ParseCorrectLoginDetailList.PNG[width="300"] + +Step 8. `LoginUserIdPasswordRoleCommand` calls `updateFilteredLoginDetailsList(updatedRolePredicate)` in `Model` with the +computed predicate from user role field input. + +Step 9. `Model` calls `setPredicate(updatedRolePredicate)` in `FilteredList`, which then further filters out accounts +whose role is a mismatch with updatedRolePredicate. `filteredLoginDetails` is further updated as shown below. + +image::ParseCorrectLoginDetailList.PNG[width="300"] + +[NOTE] +After step 9 is done, there should only be one account left in the list, assuming that the user input the correct login +details. As the loginbook does not allow duplicate accounts with the same user ID field as another account, there should +not be two or more accounts left in the list. + +[NOTE] +In step 2, if the user executes `login A1234566M zaq1xsw2cde janitor` command instead, the application will continue with steps +2 to 7, but instead of one account remaining at the end of the filtering process, there will be no account in the updated +list as shown in the image below. + +image::WrongIdPasswordRole.PNG[width="300"] +image::WrongIdOrPasswordOrRoleList.PNG[width="300"] + +[NOTE] +In step 2, if the user does not type in anything and gives a blank input for the login command instead, the application will throw a new ParseException +and consider the login attempt as unsuccessful and initiate a new pop-up window asking the user for input of login +credentials again, as shown in the image below. + +image::BlankLoginInput.PNG[width="300"] + +[NOTE] +In step 2, if the user types in a string of login input that has either the id, password or role parameters missing instead, the application will +throw a new ParseException and consider the login attempt as unsuccessful and initiate a new pop-up window asking the user +for input of login credentials again, as shown in the image below. + +image::MissingLoginInput.PNG[width="300"] + +[NOTE] +In all cases where the user inputs either the wrong ID, password, role, any combination of two or all three, there will +be no account left in the account list when `getFilteredLoginDetailsList` method in `Model` is called. The +`isLoginSuccessful` boolean in `LoginManager` will be set to false via the setter method, `setIsLoginSuccessful` +in `LoginManager`. This is done by the `checkUpdatedAccountListSetLoginCondition` method in `LoginUserIdPasswordRoleCommand`. +The `initializeLoginProcess` method in `MainWindow` will be called repeatedly until `isLoginSuccessful` is set to true. +The sequence diagram below shows the high level workflow of the login mechanism in the event of log-in failure. + +image::RepeatLoginSequenceDiagram.png[width="300"] + +The activity diagram below shows the overall picture of how the login mechanism works. + +image::LoginActivityDiagram.png[width="300"] +The activity diagram below is an extension of the activity diagram above. + +image::LoginExtendedActivityDiagram.png[width="300"] + +==== Design Considerations + +===== Preface +This section touches on the different aspects of design considerations encountered during the project in the implementation of the login feature. +It will also talk about the different alternatives in different design aspects, and its advantages and disadvantages. + +===== Aspect: How login data is stored + +* **Alternative 1 (current choice):** Saves login credentials in loginbook.xml in XML format. +** Pros: Easier to read, and versioning is possible. +** Cons: XML data file takes up more storage space. +* **Alternative 2:** Saves login credentials using JSON. +** Pros: Does not take up a lot of space. +** Cons: Harder to read. + +===== Aspect: Choice of hashing algorithms to hash login details securely + +* **Alternative 1 (current choice):** Using Base64 as the encoding and decoding scheme for login details +** Pros: Easier to implement, in the context of project time constraints +** Cons: Not as secure compared to other regular hashing algorithms like MD5 with added salt +* **Alternative 2:** Using MD5 with added salt as a hashing algorithm +** Pros: More secure than using Base64 as a hashing algorithm +** Cons: Harder and takes a longer time to implement +// end::login[] + +//tag::budget[] +=== Budgeting Feature +==== Current Implementation + +This feature has been implemented through 3 separate commands, each dealing with a separate stage in the calculation and +subsequent allocation of budgets by the NUSSU Executive Committee to all the clubs that submit the data required to +calculate the budget. The three commands are: `budget` - which handles the submission of data by the club treasurer required to calculate the +calculate the budget for that club, `calculatebudget` - which is to be used only by the NUSSU Executive Committee members +in order to calculate the budgets for each club and `viewbudget` - which lists the final budgets of all the clubs. + +==== Submission of Data + +Given below is an example usage scenario and the behaviour at each step of the `budget` command. + +Step 1. The user launches the application for the first time. 'filteredClubBudgets' will be initialised with an empty list +of all the club budgets in the address book. + +Step 2. The user (a club's treasurer) executes `budget c/Computing Club t/200 e/5` command in order to +submit the data for the calculation of her club's budget. The 'LogicManager' then calls the 'parseCommand' in the +'AddressBookParser'. + +image::BudgetCommand.png[width="800"] + +Step 3. The 'AddressBookParser' then returns a new 'BudgetCommandParser', which then parses the command to be executed and +creates a 'ClubBudgetElements' object called 'club' with the club's name, the expected turnout and the number of events. +Finally the 'BudgetCommand' is called with 'club' as the parameter. + +Step 4. The 'BudgetCommand' checks whether the 'club' is a duplicate and it is not, the 'BudgetCommand' calls the 'addClub' +command in 'Model' with 'club' as the parameter. + +Step 5. 'Model' calls 'addClub' in 'ReadOnlyAddressBook' and indicates that the address book's status has changed. + +Step 6. 'ReadOnlyAddressBook' calls the 'addClubBudget' command on an object 'clubs' of the 'UniqueClubsList' class, thus +adding the required club's data to the address book. + +Step 7. Finally a success message is displayed with the details that have been entered by the user. + +image::ExecuteBudgetCommand.png[width="800"] + +[NOTE] +In Step 4, had the user entered a club name that already existed in the list of clubs in the address book, then a duplicate +message would be shown, prompting the user to edit their entered command and try again. Execution of subsequent steps would be stopped until the +user entered a unique club name + +image::DuplicateBudget.png[width="800"] + +The image below is the sequence diagram for the functioning of the `budget` command: + +image::BudgetCommandSequenceDiagram.png[width="800"] + +//end::budget[] + +// tag::searchpruning[] +=== Search Pruning Feature + +The Search Pruning mechanism is facilitated by the `SearchHistoryManager`. +Within `SearchHistoryManager` is `searchHistoryStack`, storing `Predicate` objects. +The `Predicate` objects are used to filter `filteredListPerson` in ModelManager. + +A main `SearchHistoryManager` object named `searchHistoryManager` is stored within `ModelManager` and you could access `searchHistoryManager` by calling the +getter method `getSearchHistoryManager()`. + +IMPORTANT: `searchHistoryManger` is only meant for the filtering of +`Person` objects and Predicates irrelevant to the filtering of `Person` objects could not be stored in it. +If you want to utilize `SearchHistoryManager` for your own use case, you can initialize a new SearchHistoryManager object +by calling its default constructor. + +==== Current Implementation + +The main implementation behind SearchHistoryManager is a Stack Data Structure and the following 3 methods of `SearchHistoryManager` are exposed for your usage + + +* `executeNewSearch(Predicate predicate)` + +updates system search logic to the next state and returns a `Predicate` object storing the system search logic after the update. +* `revertLastSearch()` + +reverts system search logic to the previous state and returns a `Predicate` object storing + the system search logic after revert. +* `clearSearchHistory()` + +clears all system search logic from in-app memory. + + +Given below are illustrations to help you understand how each method works internally + + In the diagrams, UP is the short-form for User Predicate and SP is the short-form for System Predicate. + . User Predicate stores the logic specified by the user. This will not be used directly to filter the + list. + . System Predicate stores the search logic for the system which will be used to filter the list. + +NOTE: User Predicate and System Predicate are not actual Classes, they are simply there to help simplify the explanation. +In the actual implementation, there is no way to differentiate one Predicate from the other + +''' + +`executeNewSearch(Predicate predicate)` + + +Upon calling this method, there will be two different situations + + +* Situation 1: `searchHistoryStack` is empty + +Upon receiving a new User Predicate, SearchHistoryManager will simply push the new Predicate into `searchHistoryStack` +as a System Predicate which will be used to filter the list. + +image::executeNewSearchEmptyStack.png[width="800"] + +* Situation 2: `searchHistoryStack` is not empty + +Before pushing the new `Predicate` into the stack, `SearchHistoryManager` will first retrieve the System Predicate object at the top of the stack. +After retrieving it, it will call the `and()` method with the User Predicate, creating a new System Predicate which will then be pushed into the top +of the stack. + +image::executeNewSearchNonEmptyStack.png[width="800"] +''' +`revertLastSearch()` + + +This method will pop the System Predicate at the top of the stack. + +In the event that the stack is already empty, this method will throw `EmptyStackException`. + +image::undoSearchHistoryStack.png[width="800"] +''' +`clearSearchHistory()` + + +This method will simply empty the stack. + +image::clearSearchHistoryStack.png[width="800"] + +''' +The following sequence diagrams shows you how `find` and `undosearch` commands utilize `SearchHistoryManager` to perform Search Pruning. + + - `find` command + + +image::SearchPruningSequenceDiagram.png[width="800"] + + - `undosearch` command + + +image::undoSearchSequenceDiagram.png[width="800"] + + +==== Design Considerations + +Aspect: What data is stored in search history stack + + +* **Alternative 1(current choice):** Save a Stack of Predicates +** Pros: + . Does not need to store the data in search history explicitly which saves memory + + . Any form of Search Pruning done with Predicates can reuse `SearchHistoryManager` class + + +** Cons: +. Need to understand how `Predicate` works before utilizing this Class. +. `Predicate` objects by itself does not perform the Search Pruning. In NUSSU Connect, we have to pass the `Predicate` + into a `FilteredList` object to do the Search Pruning. + +* ** Alternative 2: ** Save a Stack of Lists containing Person objects in search history +** Pros: + . It is easy to understand that we are filtering according to Person objects from `SearchHistoryManager` class + +** Cons: + . More memory is required as Person objects has to be duplicated multiple times into a new List. + . Class is inextensible to store other objects other than those from a Person classes. +// end::searchpruning[] + +=== Add Skill Level Command + +==== Current Implementation + +The add skill mechanism builds on the `addressBookParser`. This as well +as it's subclass `addSkillCommandParser` ensures that the correct number of arguments +is given to the command. + +The following shows how the application handles the request to change a skill in one particular scenario. + +Step 1. The user launches the application. The application boots up and lists all members. + +Step 2. The user locates the person he wants to add on at Index 4. They execute the `asl 4 s/Photography l/60` command. + +Step 3.'LogicManager' calls the 'parseCommand' in the 'AddressBookParser', which calls `AddSkillCommandParser` to +parse it. + +Step 4. After parsing, the command is sent to the `Model` which alters the `Person` object by modifying their +`Skill` and `SkillLevel` properties. + +Step 5. The result is encapsulated as a `CommandResult` object which is passed back to the `UI`. + + +Before executing the command: + +image::aslbefore.png[width="800"] +After executing the command: + +image::aslafter.png[width="800"] + +==== Alternate implementations + +We considered two different ways to implement the Skill Class. + +* **Alternative 1**: Combining both Skill and SkillLevel properties together into a single class. + +** Pros: Resembles the real world, as there is a one-to-one mapping of Skill to SkillLevel. +** Cons: Harder to test, and violates Single Responsiblity Principle. + +* **Alternative 2 (Current Choice)**: Separating the Skill and SkillLevel classes into different classes. + +** Pros: Easier to test. +** Cons: Adds to the number of classes unnecessarily. + +==== Possible extensions + +* One possiblilty is to enhance the add command such that skills can be added together with the rest of the +information during addition of personal information. +* Another is to enhance the edit command, possibly depreciating the use of the add skill level command. +* Another is to enhance the storage such that multiple skills can be added per person. + // tag::undoredo[] === Undo/Redo feature ==== Current Implementation @@ -319,13 +656,6 @@ image::UndoRedoActivityDiagram.png[width="650"] ** 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[] -// tag::dataencryption[] -=== [Proposed] Data Encryption - -_{Explain here how the data encryption feature will be implemented}_ - -// end::dataencryption[] - === Logging We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. @@ -831,19 +1161,100 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un [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 +|`* * *` |executive officer of NUSSU |view which other committees my applicant has applied for |deconflict with the other members of the Executive Committee -|`* * *` |user |add a new person | +|`* * *` |executive officer of NUSSU |view the number of applicants with the relevant skills |assign them to the relevant subcommittees -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |member of NUSSU |find out how to contact another member within NUSSU |work more efficiently with them -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |someone that takes charge of sponsors for events hosted by NUSSU |filter my search such that I would be only looking at the list of sponsors |not need to look through the whole list of contact details -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |event organizer that is trying to find the contact details of some very specific group of people |have a search and filter function that is flexible enough |find all the search requirements can be done on the application without needing me to look through the whole list + +|`* * *` |any user trying to filter the list of contact details|have an intuitive way to filter a large list of people|so that I can get the information that I want easily and quickly + +|`* * *` |forgetful user utilizing the newly implemented search pruning feature|keep track of my past search commands|so that I would not need to commit what I typed to memory + +|`* * *` |member of the NUSSU treasury |have a budgeting function |fairly allocate budgets to the different clubs/projects + +|`* * *` |treasurer of a club |view the budget allocated to our club |discuss with my teammates and seek more funds if necessary + +|`* * *` |treasurer of a club |be able to store the data about how many members there are in my club, how many events we are planning to hold, and the expected turn out |be allocated a fair budget by the NUSSU treasury + +|`* * *` |treasurer of a club |use a budgeting function |plan the internal events of my club efficiently -|`*` |user with many persons in the address book |sort persons by name |locate a person easily +|`* * *` |member of the NUSSU treasury |view requests for grants from the clubs |allocate them the grant if the request is accepted by the NUSSU + +|`* * *` |executive member of NUSSU |log into the application |gain secure access to the application + +|`* * *` |executive member of NUSSU |create a new account for the application with my relevant role |gain access to certain features of the application relevant to my role when I log in using the created account details + +|`* * *` |executive member of NUSSU |log into the application specific to my role |gain access to certain features of the application relevant to my role when I log in + +|`* *` |general secretary of NUSSU |have the option to backup all, or even specific segments of application data into a data file |recover the required segments of data when there is an accidental deletion of data + +|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident |======================================================================= +**Others** + +**Must haves** + +As an executive officer of NUSSU, I would like to view which other committees my applicant has applied for so that I deconflict with the other members of the Executive Committee. + +As an executive officer of NUSSU, I can log into the app with the required credentials, so that I can access certain functionalities of the app exclusive to my credentials only. + +As an executive member of NUSSU, I can create a new account for myself to log into the app, so that I can access the functionalities of the app. + +As a member of NUSSU, I would like to find out how to contact another member within NUSSU so that I can work more efficiently with them. + +As someone that is looking for the contact information of someone in else in a committee, I would like to filter my search such that I would only be looking through the lists of that specific committee so that I will not need to look through the whole list of NUSSU members. + +As someone that takes charge of sponsors for events hosted by NUSSU, I would like to filter my search such that I would be only looking at the list of sponsors so that I will not need to look through the whole list of addressbook. + +As the organizer of events, I would like to keep track of the contact information for all the helpers for that event so that I can keep them updated on the progress of the events. + +As an organizer of an event, I would like to remove all users that helped with the event from my contact list so that the address book will be less cluttered. + +As a member of the NUSSU treasury I would like to have a budgeting function so that I can fairly allocate budgets to the different clubs/projects + +As the treasurer of a club I would like to view the budget allocated to our club so I can discuss with my teammates and seek more funds if necessary. + +As the treasurer of a club I would like to be able to store the data about how many members there are in my club, how many events we are planning to hold, and the expected turn out so that we can be allocated a fair budget by the NUSSU treasury. + +As the treasurer of a club I would like to use a budgeting function so that I can plan the internal events of my club efficiently. + +As a member of the NUSSU treasury I would like to view requests for grants from the clubs so that I can allocate them the grant if the request is accepted by the NUSSU. + +As an event organizer that is trying to find the contact details of some very specific group of people, I would like to have a search and filter function that is flexible enough so that all the search requirements can be done on the application without needing me to look through the whole list. + +**Nice to have** + +As the general secretary of NUSSU, I would like to have the option to backup all, or even specific segments of application data into a data file, so that in the event where accidental deletion or corruption of data occurs, I can recover the required segments of data. + +As the general secretary of NUSSU, I would like to view a list of dates reserved for committee meetings planned beforehand, so that i can prepare for the meetings adequately. + +As an executive member of NUSSU, I can pitch in proposal ideas into the proposal suggestions section through the community proposal voting system, so that I can find out just how popular my proposals are through the number of upvotes it receives. + +As an executive member of NUSSU, I can edit current proposal ideas in the proposals section, so that i can have the option to refine current proposals. + +As an executive member of NUSSU, I can delete selected proposal ideas in the proposals section, so that i can have the option to remove irrelevant proposals. + +As a student in the student union of NUSSU, I want to view the list of proposals currently suggested in the proposals section and upvote those that I like, so that I can find out more about the current proposals in place and express my favor in a particular proposal. + +As a student in the student union of NUSSU, I would like to be able to filter and search for proposal ideas based on keywords, so that I do not have to waste time searching through all the proposals just to find the one I want. + +As the student welfare secretary of NUSSU, I would like to view statistics showing the number of students who signed up for student welfare packs, so that logistically wise, I can plan student welfare goodie events better. + +As the general secretary of NUSSU, I would like to keep track of a register containing details of all members serving in the union. + +As the student life secretary of NUSSU, I would like to keep track of updated statistics showing the number of students in each faculty, so that i can plan and balance the events geared towards a specific faculty, logistically and relevancy wise. + +As someone that keeps track of the finances for hosting events, I would like an application that helps me simplify the process(Excel) so that I can do my work efficiently. + +As someone that records what was discussed in a meeting, I would like to be able to keep a record of what everyone said, so that we can use it as a future reference for further discussion. + +As someone that constantly sends email to other members of NUSSU/ Sponsors/ Public, I would like to have an access to multiple different templates of emails so that I can focus more on writing the content of the email instead of spending too much time on crafting the overall structure. + _{More to be added}_ [appendix] @@ -877,14 +1288,153 @@ Use case ends. + Use case resumes at step 2. +=== Use case: Sort by suitability by skill + +*MSS* + +1. User indicates he wants to sort by skills +2. NUSSU-Connect lists available skills, asks the user what he wants to sort by. +3. User indicates what he wants by selecting +4. NUSSU-Connect all skills. ++ +Use case ends. + +*Extensions* + +[none] +* 2a. User can sort by ascending or descending order ++ +Use case ends. +* 2b. User can see all above a certain threshold ++ +Use case ends. + +=== Use case: Add Skill + +*MSS* + +1. User indicates he wants to add skill +2. NUSSU-Connect lists available persons +3. User indicates person, skill, and skill level to add +4. NUSSU-Connect confirms addition ++ +Use case ends. + +=== Use case: Intuitive Filtering of Large Number of Contacts + +System: NUSSU Connect Application + +Actor: Typical NUSSU member + +*MSS* + +1. User requests application to display list of contacts +2. System returns list of contacts +3. User requests to find a specific group of people from list of contacts +4. System returns new List of Contacts filtered according to previous List + +Steps 3 - 4 are repeated until user found the desired group of people +5. User found the group of people that he/she is looking for ++ +Use case ends. + +*Extensions* + +[none] +* 4a. User makes an error and request to revert to previous List ++ +[none] +** 4a1. System reverts and displays the previous List ++ +Use case resumes at Step 3. + +* 3b. User request to revert List to initial state before filtering ++ +[none] +** 3b1. System reverts List to initial state. ++ +Use case ends. + +=== Use case: Excluding specific people from Search List + +System: NUSSU Connect Application + +Actor: Typical NUSSU member + +*MSS* + +1. User requests application to display list of contacts +2. System returns list of contacts +3. User requests to exclude a specific group of people from list of contacts +4. System returns new List of contacts according to the criteria set by the user + + +Use case ends + +=== Use case: Enforced criteria in Search List + +System: NUSSU Connect Application + +Actor: Typical NUSSU member + +*MSS* + +1. User requests application to display list of contacts +2. System returns list of contacts +3. User requests System to display a list of users that MUST follow a certain criteria +4. System returns new List of contacts according to the criteria set by the user + + +Use case ends + +=== Use case: Log into system + +*MSS* + +1. NUSSU-Connect prompts user to login first by entering login credentials +2. User types in login credentials along with the login command +3. NUSSU-Connect queries against login book and authorizes the user full access to NUSSU-Connect ++ +Use case ends. + +*Extensions* + +[none] +* 2a. User types in wrong password or user ID ++ +[none] +** 2a1. NUSSU-Connect continues to prompt user for login credentials before giving access to user ++ +Use case ends. + +=== Use case: Create new user accounts + +*MSS* + +1. User types in command to create a new account with chosen user ID and password +2. NUSSU prompts user to type in master password for creation of new account +3. User types in the correct master password +4. NUSSU-Connect creates new account with chosen login details, and shows successful execution message ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User creates a new account with a user ID which already exists ++ +[none] +** 1a1. NUSSU-Connect shows error message to user and does not create a new account ++ +Use case ends. +* 3a. User types in the wrong master password ++ +[none] +** 3a1. NUSSU-Connect shows error message to user and and does not create a new account ++ +Use case ends. + _{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. +. Must be able to accommodate the contact details of everyone in NUSSU + 1000 extra contact details. +. All exco members of NUSSU should be able to create an account. +. Passwords must be salted and hashed with md5. +. All commands must be completed within 1 second. +. The single and multi-input commands phrases should be easy to remember and intuitive to understand what they mean. _{More to be added}_ diff --git a/docs/LearningOutcomes.adoc b/docs/LearningOutcomes.adoc index 83cda0927226..59c232ab1273 100644 --- a/docs/LearningOutcomes.adoc +++ b/docs/LearningOutcomes.adoc @@ -49,6 +49,7 @@ image:LogicClassDiagram.png[width="800"] == Use Assertions `[LO-Assertions]` + Note how the AddressBook app uses Java ``assert``s to verify assumptions. *Resources* diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..8a588c7bb597 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += NUSSU Connect - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,13 +12,14 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2113-AY1819S1-F09-1/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` + +By: `Team F09-1` Since: `Sept 2018` == 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! +NUSSU-Connect is an application for executive committee (exco) members of the NUS Student Union (NUSSU) to help them perform their daily tasks including managing recruitment, contacts, budgets and projects efficiently. == Quick Start @@ -52,6 +53,91 @@ e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. * 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. ==== +// tag::logincreateaccount[] +=== Logging into system: `login` + +Logs into application using relevant credentials. + +Format: `login USERID PASSWORD ROLE` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to the password associated with the existing account +* ROLE refers to the role in NUSSU associated with the existing account +* The USERID must be in the `X1234567X` format, where X can only be upper case letter alphabets, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +* ROLE can only contain lower-case letter alphabets, and be only either `member`, `president` or `treasurer` +* There must be at least 1 space between USERID and PASSWORD, and between PASSWORD and ROLE +* There must be at least 1 space between `login` keyword and USERID +* The 3 parameters, USERID, PASSWORD and ROLE must be present in user input during the login process +* There must not be any additional unnecessary parameters in user input during the login process +* There must not be any spaces in USERID, PASSWORD and ROLE +**** + +Examples: + +* `login A1234567M zaq1xsw2cde3 president` + +Logs into Address book with user ID as A1234567M, password as zaq1xsw2cde3 and role as president. + +image::DefaultAccountDetails.PNG[width="300"] + +[NOTE] +==== +The default account login details for logging in when the application is launched for the very first time, can be illustrated in the picture +below. Enter the login details shown in the picture exactly, as all the login parameters are case-sensitive. Thus, any difference +in casing between the actual and expected input characters will lead to failure in logging into the application. +==== +image::DefaultAccountDetails.PNG[width="300"] + +[NOTE] +==== +After the user logs in successfully, they should expect to see the main window of the application as shown below. +==== +image::LoginSuccess.png[width="300"] + +[NOTE] +==== +If the user is unable to log in successfully, they should expect to see the login input field in a pop-up box again, asking the user +to input their login credentials again. +==== +image::BlankLoginInput.png[width="300"] + +[NOTE] +==== +User Id, Password and Role inputs are all case-sensitive! +==== + +=== Creating a new user account: `createaccount` + +Creates a new user account in the address book. + +Format: `createaccount USERID PASSWORD ROLE` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to any desired passphrases the new user wishes to have +* ROLE refers to the role in NUSSU associated with the existing account +* The USERID must be in the `X1234567X` format, where X can only be upper-case letter alphabets, and there must be exactly 7 digits between the two `X` +* USERID must not belong to an existing account +* ROLE can only contain lower-case letter alphabets, and be only either `member`, `president` or `treasurer` +* There must be at least 1 space between USERID and PASSWORD, and between PASSWORD and ROLE +* There must be at least 1 space between `createaccount` keyword and USERID +* The 3 parameters, USERID, PASSWORD and ROLE must be present in user input during the account creation process +* There must not be any additional unnecessary parameters in user input during the account creation process +* There must not be any spaces in USERID, PASSWORD and ROLE +**** + +Examples: + +* `createaccount A1234569M zaq1xsw2cde3 member` + +Creates a new account with user ID as A1234569M, password as zaq1xsw2cde3 and role as member in the address book. +The image below shows the outcome of a successful creation of a new account. + +image::CreateAccountSuccess.PNG[width="300"] + +The image below shows the outcome of an unsuccessful creation of a new account due to an account already existing. + +image::CreateAccountFailure.PNG[width="300"] +// end::logincreateaccount[] + === Viewing help : `help` Format: `help` @@ -69,10 +155,19 @@ Examples: * `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` -=== Listing all persons : `list` +=== Adding person's skill: `asl` -Shows a list of all persons in the address book. + -Format: `list` +Edits a person's skill in the address book. + +Format: `asl INDEX s/SKILL l/SKILL_LEVEL` + +[TIP] +A skill level must be an integer from 0 to 100 (inclusive). + +Examples: + +* `asl 2 s/Photography l/30` +* `asl 4 s/Java l/40` === Editing a person : `edit` @@ -94,35 +189,137 @@ Edits the phone number and email address of the 1st person to be `91234567` and * `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` +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +// tag::find[] +=== Locating persons: `find` + +Finds persons in the displayed list whose names/tags contain any of the given keywords. + +If the `\exclude` option is enabled, the matched person will be excluded from the list instead. + -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +Format: `find [\tag] [\exclude] KEYWORD [MORE_KEYWORDS]` **** -* The search is case insensitive. e.g `hans` will match `Hans` +* 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` +* If `\tag` option is not specified, find command will search according to names. +* The `\exclude` option will exclude any names/tags with the specified keywords +* The order of `\tag` and `\exclude` options cannot be swapped if you want to exclude tags from the displayed list. +* Back-to-back find commands utilizes the Search Pruning feature and you can get more details about this under the +Search Pruning Feature section. **** Examples: +* `find John` + * `find John` + Returns `john` and `John Doe` * `find Betsy Tim John` + Returns any person having names `Betsy`, `Tim`, or `John` +* `find \exclude Tom` + +Returns any person without the name `Tom`. -=== Deleting a person : `delete` +* `find \tag President` + +Returns any person with the tag `President` +* `find \tag President VicePresident` + +Returns any person with the tag `President` OR `VicePresident`. +* `find \tag \exclude President` + +Returns any person without the tag `President`. -Deletes the specified person from the address book. + -Format: `delete INDEX` +=== Undoing Find Commands: `undofind` + +Reverts the displayed list to the state before you perform your most recent find command + +Format: `undofind` +**** +* To be used in Search Pruning feature +**** + +=== Search Pruning Feature with list, find and undofind commands + +Since v1.1, the Search Pruning feature was introduced to NUSSU Connect that helps you +trim the list of contacts with every successive find command. This lets you search through the list +of contacts in a much more intuitive manner without the hassle of typing a long single line command that is +usually error-prone. + +The concept of the Search Pruning feature will be illustrated below. + + +**1. Search Pruning with Find Commands** + +**** +Assume that the original list of contacts contains the following six persons and you wanted to search for all persons +with the science tag. You could do this by executing the command `find \tag science`. + + +image::SearchPruning1st.png[align="left"] + +After executing the command the displayed list will now contain 2 persons, +both with the science tag. + +image::SearchPruning2nd.png[align="left"] + +The following message will be displayed in the Command Result Box to tell you the keywords that you have previously executed. +The "+" prefix before a keyword is used to denote that you chose to include all persons with the relevant keyword in +the displayed list. + + +image::SearchPruning3rd.png[align="left"] + +Next, you wanted to exclude everyone that has the tag `VPresident` and you could do that by executing the command + +`find \tag \exclude VPresident`. + + +image::SearchPruning4th.png[align="left"] + +The command will filter according to the previous displayed list instead of the original contacts list and the +displayed list now contains only 1 person with the President Tag as everyone with the VPresident tag have been excluded. + + +image::SearchPruning5th.png[align="left"] + +The Command Result Box will now display an extra vpresident keyword with the "-" prefix, denoting that all persons +with the vpresident tag has been excluded from the list + + +image::SearchPruning6th.png[align="left"] +**** + +**2 . Making a mistake and undoing it with undofind command** + +**** +Now assume that you have made a mistake and you want to revert to the list before you execute your most +recent find command. You can do so with the undofind command + + +image::SearchPruning7th.png[align="left"] + +After executing the undofind command, the displayed list is reverted to the state before the + +`find \tag \exclude VPresident` command was executed + + +image::SearchPruning8th.png[align="left"] +**** + +**3 . Reverting to initial state with list command** + +**** +You can revert to the initial state before any find commands are executed with the list command + +image::SearchPruning9th.png[align="left"] + +After executing the list command, all search history is cleared and the displayed list now contains all six persons. + +image::SearchPruning10th.png[align="left"] +**** + +// end::find[] + +=== Deleting : `delete` + +Deletes a specific person from the address book. + +Format: `delete [-a all] [INDEX]` **** * 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, ... +* If -a option is selected, all contacts on the list will be deleted and INDEX will be ignored. **** Examples: @@ -133,6 +330,10 @@ Deletes the 2nd person in the address book. * `find Betsy` + `delete 1` + Deletes the 1st person in the results of the `find` command. +* `list` + +`delete -a` + +Deletes everyone in the results of the `find` command. + === Selecting a person : `select` @@ -219,6 +420,59 @@ The `redo` command fails as there are no `undo` commands executed previously. Clears all entries from the address book. + Format: `clear` +=== Submitting data for budget allocation: `budget` + +Submits the data about number of events, expected turnout etc. + +Format: `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +Example: + +`budget c/Computing Club t/200 e/5` + +=== Calculating the budgets : `calculatebudget` + +Calculates the budgets for all the clubs in the address book using the total available budget +Format: `calculatebudget b/TOTAL AVAILABLE BUDGET IN SGD` + +Example: + +`calculatebudget b/50000` + +=== Viewing the allocated budget : `viewbudget` + +Shows all the allocated budgets to the user + +Format: `viewbudget` + +=== Submitting grant request : `request` + +Submits data about amount of grant needed, the reason and tags the importance level +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +Example: + +`request m/1000 r/for booking auditorium t/medium` + + +=== Viewing grant request : `viewrequest` +Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested. + +Format: `viewrequest` + +=== Accepting grant request: `accept` + +Approves a specific grant request from the list of grants. +Format: `accept [-a all] [INDEX]` + +**** +* Approves the request at the specified `INDEX`. +* The index refers to the index number shown in the displayed requests list. +* The index *must be a positive integer* 1, 2, 3, ... +* If -a option is selected, all requests on the list will be approved and INDEX will be ignored. +**** +Example: + +* `viewrequest` + +`accept 2` + +Approves the 2nd request in the list of grant requests. +* `viewrequest` + +`accept -a` + +Approves all the requests in the list. + === Exiting the program : `exit` Exits the program. + @@ -242,15 +496,20 @@ _{explain how the user can enable/disable data encryption}_ == Command Summary +* *Login* : `login USERID PASSWORD ROLE` +e.g. `login A1234567M zaq1xsw2cde3 member` +* *Create Account* : `createaccount USERID PASSWORD ROLE` + +e.g. `createaccount A1234567M zaq1xsw2cde3 member` * *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` + +* *Delete* : `delete [-a all][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` +* *Find* : `find [\tag] [\exclude] KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` + +e.g `find \tag President` +* *Undo Find* : `undofind` * *List* : `list` * *Help* : `help` * *Select* : `select INDEX` + @@ -258,3 +517,17 @@ e.g.`select 2` * *History* : `history` * *Undo* : `undo` * *Redo* : `redo` +* *Submitting data for budget* : `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +e.g. `budget c/Computing Club t/200 e/5` +* *Calculating budgets* : `calculatebudget b/TOTAL AVAILABLE BUDGET IN SGD` + +e.g. `calculatebudget b/50000` +* *Viewing all the budgets* : `viewbudget` +* *Submitting grant request* : request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +e.g. `request m/1000 r/for booking auditorium t/medium` +* *Viewing grant request* : `viewrequest` +* *Accepting a grant request* : `accept [-a all] [INDEX]` + +e.g. `viewrequest` + + `accept 2` + +e.g. `viewrequest` + + `accept -a` + diff --git a/docs/UserGuide_BACKUP_13948.adoc b/docs/UserGuide_BACKUP_13948.adoc new file mode 100644 index 000000000000..8d0a2215788e --- /dev/null +++ b/docs/UserGuide_BACKUP_13948.adoc @@ -0,0 +1,448 @@ += NUSSU Connect - User Guide +:site-section: UserGuide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +:xrefstyle: full +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:repoURL: https://github.com/CS2113-AY1819S1-F09-1/main + + +By: `Team F09-1` Since: `Sept 2018` + +== Introduction + +NUSSU-Connect is an application for executive committee (exco) members of the NUS Student Union (NUSSU) to help them perform their daily tasks including managing recruitment, contacts, budgets and projects efficiently. + +== 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. ++ +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: + +* *`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 + +. Refer to <> for details of each command. + +[[Features]] +== Features + +==== +*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. +==== + +=== Logging into system: `login` + +Logs into application using relevant credentials. + +Format: `login USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to the password associated with the existing account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `login X1234567X zaq1xsw2cde3` + +Logs into Address book with user ID as X1234567X and password as zaq1xsw2cde3. + +[NOTE] +==== +Password is case-sensitive! +==== + +=== Creating a new user account: `createaccount` + +Creates a new user account in the address book. + +Format: `createaccount user/USERID pass/PASSWORD` + +**** +* user/ and pass/ are required prefixes before USERID and PASSWORD +* USERID refers to student matriculation number +* PASSWORD refers to any desired passphrases the new user wishes to have +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must not belong to an existing account +**** + +Examples: + +* `createaccount user/X1234567X pass/zaq1xsw2cde3` + +Creates a new account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Deleting an existing user account: `deleteaccount` + +Deletes an existing account from the address book. + +Format: `deleteaccount USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `deleteaccount X1234567X zaq1xsw2cde3` + +Deletes the current account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Changing the password of an existing user account: `changepassword` + +Changes the password of an existing account from the address book. + +Format: `changepassword USERID CURRENTPASSWORD NEWPASSWORD` + +**** +* USERID refers to student matriculation number +* CURRENTPASSWORD refers to current passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +* NEWPASSWORD must be different from CURRENTPASSWORD +**** + +Examples: + +* `changepassword X1234567X zaq1xsw2cde3 1qaz2wsx3edc` + +Changes the password of the current account with user ID as X1234567X, from "zaq1xsw2cde3" to "1qaz2wsx3edc" in the address book. + +=== 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: + +* `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` + +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +=== Editing a person : `edit` + +Edits an existing person in the address book. + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +**** +* 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. +**** + +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: `find` + +Finds persons in the displayed list whose names/tags contain any of the given keywords. + +Format: `find [\tag tags] 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 full words will be matched e.g. `Han` will not match `Hans` +* For back-to-back find commands, the most recent find command will search according to the previous find command +**** + +Examples: + +* `find John` + +* `find John` + +Returns `john` and `John Doe` +* `find Betsy Tim John` + +Returns any person having names `Betsy`, `Tim`, or `John` +* `find \tag President` + +Returns any person with the tag `President` +* `find \tag President Vice_President` + +Returns any person with the tag `President` OR `Vice_President`. +**** +**Executing back-to-back find commands:** + +Assume that the displayed list contains 3 person initially. + +image::InitialList.png[align="left"] + +After executing the command "find \tag VIP" + +the displayed list will contain 2 person, both with the tag 'VIP' + +image::SecondList.png[align="left"] + +After executing the command "find \tag President" + +it will filter according to the previous displayed list + +the displayed list will contain 1 person, with the tag 'President' + + +image::ThirdList.png[align="left"] +**** + +=== Resetting Search History: `clearsearch` + +Resets search history back to the very initial stage before any find commands are executed + +Format: `clearsearch` +**** +* To be used together with the find command +**** + +=== Deleting : `delete` + +Deletes a specific person from the address book. + +Format: `delete [-a all] [INDEX]` + +**** +* 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, ... +* If -a option is selected, all contacts on the list will be deleted and INDEX will be ignored. +**** + +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. +* `list` + +`delete -a` + +Deletes everyone 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: + +* `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. + +=== Listing entered commands : `history` + +Lists all the commands that you have entered in reverse chronological order. + +Format: `history` + +[NOTE] +==== +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +==== + +// tag::undoredo[] +=== Undoing previous command : `undo` + +Restores the address book to the state before the previous _undoable_ command was executed. + +Format: `undo` + +[NOTE] +==== +Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +==== + +Examples: + +* `delete 1` + +`list` + +`undo` (reverses the `delete 1` command) + + +* `select 1` + +`list` + +`undo` + +The `undo` command fails as there are no undoable commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + + +=== Redoing the previously undone command : `redo` + +Reverses the most recent `undo` command. + +Format: `redo` + +Examples: + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + + +* `delete 1` + +`redo` + +The `redo` command fails as there are no `undo` commands executed previously. + +* `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[] + +=== Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +<<<<<<< HEAD +=== Submitting data for budget allocation: `budget` + +Submits the data about number of events, expected turnout etc. + +Format: `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +Example: + +`budget c/Computing Club t/200 e/5` +======= +=== Submitting data for budget allocation: `data` +Submits the data about number of events, expected turnout etc. + +Format: `data e/NUMBER OF EVENTS t/TURNOUT` +Example: + +`data e/5 t/200` + +>>>>>>> cf7a3827e267177ab295da9ebeb181586aa9c55f + +=== Viewing the allocated budget : `viewbudget` +Shows the allocated budget to the user + +Format: `viewbudget` + + +=== Submitting grant request : `request` +Submits data about amount of grant needed, the reason and tags the importance level +<<<<<<< HEAD +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +======= +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL +>>>>>>> cf7a3827e267177ab295da9ebeb181586aa9c55f +Example: + +`request m/1000 r/for booking auditorium t/medium` + + +=== Viewing grant request : `viewrequest` +Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested. + +Format: `viewrequest` + + +=== Accepting grant request: `accept` +Approves a specific grant request from the list of grants. +<<<<<<< HEAD +Format: `accept [-a all] [INDEX]` + +======= +Format: `accept [-a all] [INDEX] + +>>>>>>> cf7a3827e267177ab295da9ebeb181586aa9c55f +**** +* Approves the request at the specified `INDEX`. +* The index refers to the index number shown in the displayed requests list. +* The index *must be a positive integer* 1, 2, 3, ... +* If -a option is selected, all requests on the list will be approved and INDEX will be ignored. +**** +Example: + +* `viewrequest` + +`accept 2` + +Approves the 2nd request in the list of grant requests. +* `viewrequest` + +`accept -a` + +Approves all the requests in the list. + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== 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. + +// tag::dataencryption[] +=== Encrypting data files `[coming in v2.0]` + +_{explain how the user can enable/disable data encryption}_ +// end::dataencryption[] + +== FAQ + +*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. + +== Command Summary + +* *Login* : `login USERID PASSWORD` +e.g. `login A1234567M zaq1xsw2cde3` +* *Create Account* : `createaccount USERID PASSWORD` + +e.g. `createaccount user/A1234567M pass/zaq1xsw2cde3` +* *Delete Account* : `deleteaccount USERID PASSWORD` + +e.g. `deleteaccount A01234567M zaq1xsw2cde3` +* *Change Password* : `changepassword USERID CURRENTPASSWORD NEWPASSWORD` + +e.g. `changepassword A01234567M zaq1xsw2cde3 1qaz2wsx3edc` +* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + +* *Clear* : `clear` +* *Delete* : `delete [-a all][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 [\tag tags] KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` + +e.g `find \tag President` +* *List* : `list` +* *Help* : `help` +* *Select* : `select INDEX` + +e.g.`select 2` +* *History* : `history` +* *Undo* : `undo` +* *Redo* : `redo` +* *Submitting data for budget* : `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +e.g. `budget c/Computing Club t/200 e/5` +* *Submitting grant request* : request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +e.g. `request m/1000 r/for booking auditorium t/medium` +* *Viewing grant request* : `viewrequest` +* *Accepting a grant request* : `accept [-a all] [INDEX]` + +e.g. `viewrequest` + + `accept 2` + +e.g. `viewrequest` + + `accept -a` + diff --git a/docs/UserGuide_BASE_13948.adoc b/docs/UserGuide_BASE_13948.adoc new file mode 100644 index 000000000000..3fc4e846ce42 --- /dev/null +++ b/docs/UserGuide_BASE_13948.adoc @@ -0,0 +1,435 @@ += NUSSU Connect - User Guide +:site-section: UserGuide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +:xrefstyle: full +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:repoURL: https://github.com/CS2113-AY1819S1-F09-1/main + +By: `Team F09-1` Since: `Sept 2018` + +== Introduction + +NUSSU Connect is for the executive committee members of NUSSU who are comfortable with working with Command Line Interface (CLI). It is meant for fast typers within NUSSU and it could help them perform their daily tasks such as managing recruitment, contacts, budgets and projects much more efficiently. + +== 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. ++ +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: + +* *`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 + +. Refer to <> for details of each command. + +[[Features]] +== Features + +==== +*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. +==== + +=== Logging into system: `login` + +Logs into application using relevant credentials. + +Format: `X1234567X` for user ID fields, where X can be any lower or upper case letter alphabet + +Examples: + +* `login` + +`X1234567X zaq1xsw2cde3` + +Logs into Address book with user ID as X1234567X and password as zaq1xsw2cde3. + +[NOTE] +==== +Password is case-sensitive! +==== + +=== Creating a new user account: `create account` + +Creates a new user account in the address book. + +Format: `user/USERID pass/PASSWORD` + +**** +* user/ and pass/ are required prefixes before USERID and PASSWORD +* USERID refers to student matriculation number +* PASSWORD refers to any desired passphrases the new user wishes to have +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must not belong to an existing account +**** + +Examples: + +* `create account` + +`user/X1234567X pass/zaq1xsw2cde3` + +Creates a new account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Deleting an existing user account: `delete account` + +Deletes an existing account from the address book. + +Format: `USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `delete account` + +`X1234567X zaq1xsw2cde3` + +Deletes the current account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Changing the password of an existing user account: `change password` + +Changes the password of an existing account from the address book. + +Format: `USERID CURRENTPASSWORD NEWPASSWORD` + +**** +* USERID refers to student matriculation number +* CURRENTPASSWORD refers to current passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +* NEWPASSWORD must be different from CURRENTPASSWORD +**** + +Examples: + +* `change password` + +`X1234567X zaq1xsw2cde3 1qaz2wsx3edc` + +Changes the password of the current account with user ID as X1234567X, from "zaq1xsw2cde3" to "1qaz2wsx3edc" in the address book. + +=== 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: + +* `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` + +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +=== Editing a person : `edit` + +Edits an existing person in the address book. + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +**** +* 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. +**** + +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: `find` + +Finds persons in the displayed list whose names/tags contain any of the given keywords. + +Format: `find [\tag tags] 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 full words will be matched e.g. `Han` will not match `Hans` +* For back-to-back find commands, the most recent find command will search according to the previous find command +**** + +Examples: + +* `find John` + +* `find John` + +Returns `john` and `John Doe` +* `find Betsy Tim John` + +Returns any person having names `Betsy`, `Tim`, or `John` +* `find \tag President` + +Returns any person with the tag `President` +* `find \tag President Vice_President` + +Returns any person with the tag `President` OR `Vice_President`. +**** +**Executing back-to-back find commands:** + +Assume that the displayed list contains 3 person initially. + +image::InitialList.png[align="left"] + +After executing the command "find \tag VIP" + +the displayed list will contain 2 person, both with the tag 'VIP' + +image::SecondList.png[align="left"] + +After executing the command "find \tag President" + +it will filter according to the previous displayed list + +the displayed list will contain 1 person, with the tag 'President' + + + +image::ThirdList.png[align="left"] +**** + +=== Resetting Search History: `clearsearch` + +Resets search history back to the very initial stage before any find commands are executed +Format: `clearsearch` +**** +* To be used together with the find command +**** +=== Deleting : `delete` + +Deletes a specific person from the address book. + +Format: `delete [-a all] [INDEX]` + +**** +* 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, ... +* If -a option is selected, all contacts on the list will be deleted and INDEX will be ignored. +**** + +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. +* `list` + +`delete -a` + +Deletes everyone 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: + +* `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. + +=== Listing entered commands : `history` + +Lists all the commands that you have entered in reverse chronological order. + +Format: `history` + +[NOTE] +==== +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +==== + +// tag::undoredo[] +=== Undoing previous command : `undo` + +Restores the address book to the state before the previous _undoable_ command was executed. + +Format: `undo` + +[NOTE] +==== +Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +==== + +Examples: + +* `delete 1` + +`list` + +`undo` (reverses the `delete 1` command) + + +* `select 1` + +`list` + +`undo` + +The `undo` command fails as there are no undoable commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + + +=== Redoing the previously undone command : `redo` + +Reverses the most recent `undo` command. + +Format: `redo` + +Examples: + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + + +* `delete 1` + +`redo` + +The `redo` command fails as there are no `undo` commands executed previously. + +* `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[] + +=== Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +=== Submitting data for budget allocation: `data` + +Submits the data about number of events, expected turnout etc. + +Format: `data e/NUMBER OF EVENTS t/TURNOUT` + +Example: + +`data e/5 t/200` + +=== Viewing the allocated budget : `viewbudget` + +Shows the allocated budget to the user + +Format: `viewbudget` + +=== Submitting grant request : `request` + +Submits data about amount of grant needed, the reason and tags the importance level +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL + +Example: + +`request m/1000 r/for booking auditorium t/medium` + +=== Viewing grant request : `viewrequest` + +Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested. + +Format: `viewrequest` + +=== Accepting grant request: `accept` + +Approves a specific grant request from the list of grants. +Format: `accept [-a all] [INDEX] + +**** +* Approves the request at the specified `INDEX`. +* The index refers to the index number shown in the displayed requests list. +* The index *must be a positive integer* 1, 2, 3, ... +* If -a option is selected, all requests on the list will be approved and INDEX will be ignored. +**** +Example: + +* `viewrequest` + +`accept 2` + +Approves the 2nd request in the list of grant requests. +* `viewrequest` + +`accept -a` + +Approves all the requests in the list. + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== 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. + +// tag::dataencryption[] +=== Encrypting data files `[coming in v2.0]` + +_{explain how the user can enable/disable data encryption}_ +// end::dataencryption[] + +== FAQ + +*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. + +== Command Summary + +* *Login* : `USERID PASSWORD` +e.g. `login` + + `A01234567M zaq1xsw2cde3` +* *Create Account* : `USERID PASSWORD` + +e.g. `create account` + + `A01234567M zaq1xsw2cde3` +* *Delete Account* : `USERID PASSWORD` + +e.g. `delete account` + + `A01234567M zaq1xsw2cde3` +* *Change Password* : `USERID CURRENTPASSWORD NEWPASSWORD` + +e.g. `change password` + + `A01234567M zaq1xsw2cde3 1qaz2wsx3edc` +* *Clear* : `clear` +* *Delete* : `delete [-a all][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 [\tag tags] KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` + +e.g `find \tag President` +* *List* : `list` +* *Help* : `help` +* *Select* : `select INDEX` + +e.g.`select 2` +* *History* : `history` +* *Undo* : `undo` +* *Redo* : `redo` +* *Submitting data for budget* : `data e/NUMBER OF EVENTS t/TURNOUT` + +e.g. `data e/5 t/200` +* *Submitting grant request* : request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +e.g. `request m/1000 r/for booking auditorium t/medium` +* *Viewing grant request* : `viewrequest` +* *Accepting a grant request* : `accept [-a all] [INDEX]` + +e.g. `viewrequest` + + `accept 2` + +e.g. `viewrequest` + + `accept -a` + diff --git a/docs/UserGuide_LOCAL_13948.adoc b/docs/UserGuide_LOCAL_13948.adoc new file mode 100644 index 000000000000..ab70c82c4684 --- /dev/null +++ b/docs/UserGuide_LOCAL_13948.adoc @@ -0,0 +1,435 @@ += NUSSU Connect - User Guide +:site-section: UserGuide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +:xrefstyle: full +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:repoURL: https://github.com/CS2113-AY1819S1-F09-1/main + +By: `Team F09-1` Since: `Sept 2018` + +== Introduction + +NUSSU Connect is for the executive committee members of NUSSU who are comfortable with working with Command Line Interface (CLI). It is meant for fast typers within NUSSU and it could help them perform their daily tasks such as managing recruitment, contacts, budgets and projects much more efficiently. + +== 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. ++ +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: + +* *`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 + +. Refer to <> for details of each command. + +[[Features]] +== Features + +==== +*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. +==== + +=== Logging into system: `login` + +Logs into application using relevant credentials. + +Format: `X1234567X` for user ID fields, where X can be any lower or upper case letter alphabet + +Examples: + +* `login` + +`X1234567X zaq1xsw2cde3` + +Logs into Address book with user ID as X1234567X and password as zaq1xsw2cde3. + +[NOTE] +==== +Password is case-sensitive! +==== + +=== Creating a new user account: `create account` + +Creates a new user account in the address book. + +Format: `user/USERID pass/PASSWORD` + +**** +* user/ and pass/ are required prefixes before USERID and PASSWORD +* USERID refers to student matriculation number +* PASSWORD refers to any desired passphrases the new user wishes to have +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must not belong to an existing account +**** + +Examples: + +* `create account` + +`user/X1234567X pass/zaq1xsw2cde3` + +Creates a new account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Deleting an existing user account: `delete account` + +Deletes an existing account from the address book. + +Format: `USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `delete account` + +`X1234567X zaq1xsw2cde3` + +Deletes the current account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Changing the password of an existing user account: `change password` + +Changes the password of an existing account from the address book. + +Format: `USERID CURRENTPASSWORD NEWPASSWORD` + +**** +* USERID refers to student matriculation number +* CURRENTPASSWORD refers to current passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +* NEWPASSWORD must be different from CURRENTPASSWORD +**** + +Examples: + +* `change password` + +`X1234567X zaq1xsw2cde3 1qaz2wsx3edc` + +Changes the password of the current account with user ID as X1234567X, from "zaq1xsw2cde3" to "1qaz2wsx3edc" in the address book. + +=== 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: + +* `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` + +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +=== Editing a person : `edit` + +Edits an existing person in the address book. + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +**** +* 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. +**** + +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: `find` + +Finds persons in the displayed list whose names/tags contain any of the given keywords. + +Format: `find [\tag tags] 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 full words will be matched e.g. `Han` will not match `Hans` +* For back-to-back find commands, the most recent find command will search according to the previous find command +**** + +Examples: + +* `find John` + +* `find John` + +Returns `john` and `John Doe` +* `find Betsy Tim John` + +Returns any person having names `Betsy`, `Tim`, or `John` +* `find \tag President` + +Returns any person with the tag `President` +* `find \tag President Vice_President` + +Returns any person with the tag `President` OR `Vice_President`. +**** +**Executing back-to-back find commands:** + +Assume that the displayed list contains 3 person initially. + +image::InitialList.png[align="left"] + +After executing the command "find \tag VIP" + +the displayed list will contain 2 person, both with the tag 'VIP' + +image::SecondList.png[align="left"] + +After executing the command "find \tag President" + +it will filter according to the previous displayed list + +the displayed list will contain 1 person, with the tag 'President' + + + +image::ThirdList.png[align="left"] +**** + +=== Resetting Search History: `clearsearch` + +Resets search history back to the very initial stage before any find commands are executed +Format: `clearsearch` +**** +* To be used together with the find command +**** +=== Deleting : `delete` + +Deletes a specific person from the address book. + +Format: `delete [-a all] [INDEX]` + +**** +* 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, ... +* If -a option is selected, all contacts on the list will be deleted and INDEX will be ignored. +**** + +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. +* `list` + +`delete -a` + +Deletes everyone 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: + +* `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. + +=== Listing entered commands : `history` + +Lists all the commands that you have entered in reverse chronological order. + +Format: `history` + +[NOTE] +==== +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +==== + +// tag::undoredo[] +=== Undoing previous command : `undo` + +Restores the address book to the state before the previous _undoable_ command was executed. + +Format: `undo` + +[NOTE] +==== +Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +==== + +Examples: + +* `delete 1` + +`list` + +`undo` (reverses the `delete 1` command) + + +* `select 1` + +`list` + +`undo` + +The `undo` command fails as there are no undoable commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + + +=== Redoing the previously undone command : `redo` + +Reverses the most recent `undo` command. + +Format: `redo` + +Examples: + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + + +* `delete 1` + +`redo` + +The `redo` command fails as there are no `undo` commands executed previously. + +* `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[] + +=== Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +=== Submitting data for budget allocation: `budget` + +Submits the data about number of events, expected turnout etc. + +Format: `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +Example: + +`budget c/Computing Club t/200 e/5` + +=== Viewing the allocated budget : `viewbudget` + +Shows the allocated budget to the user + +Format: `viewbudget` + +=== Submitting grant request : `request` + +Submits data about amount of grant needed, the reason and tags the importance level +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +Example: + +`request m/1000 r/for booking auditorium t/medium` + +=== Viewing grant request : `viewrequest` + +Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested. + +Format: `viewrequest` + +=== Accepting grant request: `accept` + +Approves a specific grant request from the list of grants. +Format: `accept [-a all] [INDEX]` + +**** +* Approves the request at the specified `INDEX`. +* The index refers to the index number shown in the displayed requests list. +* The index *must be a positive integer* 1, 2, 3, ... +* If -a option is selected, all requests on the list will be approved and INDEX will be ignored. +**** +Example: + +* `viewrequest` + +`accept 2` + +Approves the 2nd request in the list of grant requests. +* `viewrequest` + +`accept -a` + +Approves all the requests in the list. + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== 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. + +// tag::dataencryption[] +=== Encrypting data files `[coming in v2.0]` + +_{explain how the user can enable/disable data encryption}_ +// end::dataencryption[] + +== FAQ + +*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. + +== Command Summary + +* *Login* : `USERID PASSWORD` +e.g. `login` + + `A01234567M zaq1xsw2cde3` +* *Create Account* : `USERID PASSWORD` + +e.g. `create account` + + `A01234567M zaq1xsw2cde3` +* *Delete Account* : `USERID PASSWORD` + +e.g. `delete account` + + `A01234567M zaq1xsw2cde3` +* *Change Password* : `USERID CURRENTPASSWORD NEWPASSWORD` + +e.g. `change password` + + `A01234567M zaq1xsw2cde3 1qaz2wsx3edc` +* *Clear* : `clear` +* *Delete* : `delete [-a all][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 [\tag tags] KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` + +e.g `find \tag President` +* *List* : `list` +* *Help* : `help` +* *Select* : `select INDEX` + +e.g.`select 2` +* *History* : `history` +* *Undo* : `undo` +* *Redo* : `redo` +* *Submitting data for budget* : `budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` + +e.g. `budget c/Computing Club t/200 e/5` +* *Submitting grant request* : request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +e.g. `request m/1000 r/for booking auditorium t/medium` +* *Viewing grant request* : `viewrequest` +* *Accepting a grant request* : `accept [-a all] [INDEX]` + +e.g. `viewrequest` + + `accept 2` + +e.g. `viewrequest` + + `accept -a` + diff --git a/docs/UserGuide_REMOTE_13948.adoc b/docs/UserGuide_REMOTE_13948.adoc new file mode 100644 index 000000000000..4b0cbb3c4ced --- /dev/null +++ b/docs/UserGuide_REMOTE_13948.adoc @@ -0,0 +1,428 @@ += NUSSU Connect - User Guide +:site-section: UserGuide +:toc: +:toc-title: +:toc-placement: preamble +:sectnums: +:imagesDir: images +:stylesDir: stylesheets +:xrefstyle: full +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:repoURL: https://github.com/CS2113-AY1819S1-F09-1/main + + +By: `Team F09-1` Since: `Sept 2018` + +== Introduction + +NUSSU-Connect is an application for executive committee (exco) members of the NUS Student Union (NUSSU) to help them perform their daily tasks including managing recruitment, contacts, budgets and projects efficiently. + +== 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. ++ +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: + +* *`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 + +. Refer to <> for details of each command. + +[[Features]] +== Features + +==== +*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. +==== + +=== Logging into system: `login` + +Logs into application using relevant credentials. + +Format: `login USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to the password associated with the existing account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `login X1234567X zaq1xsw2cde3` + +Logs into Address book with user ID as X1234567X and password as zaq1xsw2cde3. + +[NOTE] +==== +Password is case-sensitive! +==== + +=== Creating a new user account: `createaccount` + +Creates a new user account in the address book. + +Format: `createaccount user/USERID pass/PASSWORD` + +**** +* user/ and pass/ are required prefixes before USERID and PASSWORD +* USERID refers to student matriculation number +* PASSWORD refers to any desired passphrases the new user wishes to have +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must not belong to an existing account +**** + +Examples: + +* `createaccount user/X1234567X pass/zaq1xsw2cde3` + +Creates a new account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Deleting an existing user account: `deleteaccount` + +Deletes an existing account from the address book. + +Format: `deleteaccount USERID PASSWORD` + +**** +* USERID refers to student matriculation number +* PASSWORD refers to passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +**** + +Examples: + +* `deleteaccount X1234567X zaq1xsw2cde3` + +Deletes the current account with user ID as X1234567X and password as zaq1xsw2cde3 in the address book. + +=== Changing the password of an existing user account: `changepassword` + +Changes the password of an existing account from the address book. + +Format: `changepassword USERID CURRENTPASSWORD NEWPASSWORD` + +**** +* USERID refers to student matriculation number +* CURRENTPASSWORD refers to current passphrase associated with USERID of the account +* The USERID must be in the `X1234567X` format, where X can be any lower or upper case letter alphabet, and there must be exactly 7 digits between the two `X` +* USERID must belong to an existing account +* NEWPASSWORD must be different from CURRENTPASSWORD +**** + +Examples: + +* `changepassword X1234567X zaq1xsw2cde3 1qaz2wsx3edc` + +Changes the password of the current account with user ID as X1234567X, from "zaq1xsw2cde3" to "1qaz2wsx3edc" in the address book. + +=== 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: + +* `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` + +=== Listing all persons : `list` + +Shows a list of all persons in the address book. + +Format: `list` + +=== Editing a person : `edit` + +Edits an existing person in the address book. + +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +**** +* 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. +**** + +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: `find` + +Finds persons in the displayed list whose names/tags contain any of the given keywords. + +Format: `find [\tag tags] 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 full words will be matched e.g. `Han` will not match `Hans` +* For back-to-back find commands, the most recent find command will search according to the previous find command +**** + +Examples: + +* `find John` + +* `find John` + +Returns `john` and `John Doe` +* `find Betsy Tim John` + +Returns any person having names `Betsy`, `Tim`, or `John` +* `find \tag President` + +Returns any person with the tag `President` +* `find \tag President Vice_President` + +Returns any person with the tag `President` OR `Vice_President`. +**** +**Executing back-to-back find commands:** + +Assume that the displayed list contains 3 person initially. + +image::InitialList.png[align="left"] + +After executing the command "find \tag VIP" + +the displayed list will contain 2 person, both with the tag 'VIP' + +image::SecondList.png[align="left"] + +After executing the command "find \tag President" + +it will filter according to the previous displayed list + +the displayed list will contain 1 person, with the tag 'President' + + +image::ThirdList.png[align="left"] +**** + +=== Resetting Search History: `clearsearch` + +Resets search history back to the very initial stage before any find commands are executed + +Format: `clearsearch` +**** +* To be used together with the find command +**** + +=== Deleting : `delete` + +Deletes a specific person from the address book. + +Format: `delete [-a all] [INDEX]` + +**** +* 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, ... +* If -a option is selected, all contacts on the list will be deleted and INDEX will be ignored. +**** + +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. +* `list` + +`delete -a` + +Deletes everyone 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: + +* `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. + +=== Listing entered commands : `history` + +Lists all the commands that you have entered in reverse chronological order. + +Format: `history` + +[NOTE] +==== +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +==== + +// tag::undoredo[] +=== Undoing previous command : `undo` + +Restores the address book to the state before the previous _undoable_ command was executed. + +Format: `undo` + +[NOTE] +==== +Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +==== + +Examples: + +* `delete 1` + +`list` + +`undo` (reverses the `delete 1` command) + + +* `select 1` + +`list` + +`undo` + +The `undo` command fails as there are no undoable commands executed previously. + +* `delete 1` + +`clear` + +`undo` (reverses the `clear` command) + +`undo` (reverses the `delete 1` command) + + +=== Redoing the previously undone command : `redo` + +Reverses the most recent `undo` command. + +Format: `redo` + +Examples: + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`redo` (reapplies the `delete 1` command) + + +* `delete 1` + +`redo` + +The `redo` command fails as there are no `undo` commands executed previously. + +* `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[] + +=== Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +=== Submitting data for budget allocation: `data` +Submits the data about number of events, expected turnout etc. + +Format: `data e/NUMBER OF EVENTS t/TURNOUT` +Example: + +`data e/5 t/200` + + +=== Viewing the allocated budget : `viewbudget` +Shows the allocated budget to the user + +Format: `viewbudget` + + +=== Submitting grant request : `request` +Submits data about amount of grant needed, the reason and tags the importance level +Format: `request m/AMOUNT r/REASON t/IMPORTANCE LEVEL +Example: + +`request m/1000 r/for booking auditorium t/medium` + + +=== Viewing grant request : `viewrequest` +Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested. + +Format: `viewrequest` + + +=== Accepting grant request: `accept` +Approves a specific grant request from the list of grants. +Format: `accept [-a all] [INDEX] + +**** +* Approves the request at the specified `INDEX`. +* The index refers to the index number shown in the displayed requests list. +* The index *must be a positive integer* 1, 2, 3, ... +* If -a option is selected, all requests on the list will be approved and INDEX will be ignored. +**** +Example: + +* `viewrequest` + +`accept 2` + +Approves the 2nd request in the list of grant requests. +* `viewrequest` + +`accept -a` + +Approves all the requests in the list. + +=== Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +=== 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. + +// tag::dataencryption[] +=== Encrypting data files `[coming in v2.0]` + +_{explain how the user can enable/disable data encryption}_ +// end::dataencryption[] + +== FAQ + +*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. + +== Command Summary + +* *Login* : `login USERID PASSWORD` +e.g. `login A1234567M zaq1xsw2cde3` +* *Create Account* : `createaccount USERID PASSWORD` + +e.g. `createaccount user/A1234567M pass/zaq1xsw2cde3` +* *Delete Account* : `deleteaccount USERID PASSWORD` + +e.g. `deleteaccount A01234567M zaq1xsw2cde3` +* *Change Password* : `changepassword USERID CURRENTPASSWORD NEWPASSWORD` + +e.g. `changepassword A01234567M zaq1xsw2cde3 1qaz2wsx3edc` +* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + +* *Clear* : `clear` +* *Delete* : `delete [-a all][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 [\tag tags] KEYWORD [MORE_KEYWORDS]` + +e.g. `find James Jake` + +e.g `find \tag President` +* *List* : `list` +* *Help* : `help` +* *Select* : `select INDEX` + +e.g.`select 2` +* *History* : `history` +* *Undo* : `undo` +* *Redo* : `redo` +* *Submitting data for budget* : `data e/NUMBER OF EVENTS t/TURNOUT` + +e.g. `data e/5 t/200` +* *Submitting grant request* : request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` + +e.g. `request m/1000 r/for booking auditorium t/medium` +* *Viewing grant request* : `viewrequest` +* *Accepting a grant request* : `accept [-a all] [INDEX]` + +e.g. `viewrequest` + + `accept 2` + +e.g. `viewrequest` + + `accept -a` + diff --git a/docs/diagrams/BudgetCommandSequenceDiagram.pptx b/docs/diagrams/BudgetCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..65040dee6b70 Binary files /dev/null and b/docs/diagrams/BudgetCommandSequenceDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx index 3c976908eaa7..b62fa84b28e5 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/SearchPruningSequenceDiagram.pptx b/docs/diagrams/SearchPruningSequenceDiagram.pptx new file mode 100644 index 000000000000..7c6509a40e0c Binary files /dev/null and b/docs/diagrams/SearchPruningSequenceDiagram.pptx differ diff --git a/docs/diagrams/SearchPruningStateDiagram.pptx b/docs/diagrams/SearchPruningStateDiagram.pptx new file mode 100644 index 000000000000..7b990583dbb0 Binary files /dev/null and b/docs/diagrams/SearchPruningStateDiagram.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx index be29a9de7ca6..893c369b6e0e 100644 Binary files a/docs/diagrams/StorageComponentClassDiagram.pptx and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index 384d0a00e6ea..2639aa1b2b9a 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UndoSearchSequenceDiagram.pptx b/docs/diagrams/UndoSearchSequenceDiagram.pptx new file mode 100644 index 000000000000..0dd65ea9a0df Binary files /dev/null and b/docs/diagrams/UndoSearchSequenceDiagram.pptx differ diff --git a/docs/images/BlankLoginInput.PNG b/docs/images/BlankLoginInput.PNG new file mode 100644 index 000000000000..e46d72217c42 Binary files /dev/null and b/docs/images/BlankLoginInput.PNG differ diff --git a/docs/images/BudgetCommand.png b/docs/images/BudgetCommand.png new file mode 100644 index 000000000000..b27f46e6390f Binary files /dev/null and b/docs/images/BudgetCommand.png differ diff --git a/docs/images/BudgetCommandSequenceDiagram.png b/docs/images/BudgetCommandSequenceDiagram.png new file mode 100644 index 000000000000..05a1b31635bc Binary files /dev/null and b/docs/images/BudgetCommandSequenceDiagram.png differ diff --git a/docs/images/CorrectIdPasswordRole.PNG b/docs/images/CorrectIdPasswordRole.PNG new file mode 100644 index 000000000000..56bef8a3c4f9 Binary files /dev/null and b/docs/images/CorrectIdPasswordRole.PNG differ diff --git a/docs/images/CreateAccountFailure.PNG b/docs/images/CreateAccountFailure.PNG new file mode 100644 index 000000000000..9bcfd2578bec Binary files /dev/null and b/docs/images/CreateAccountFailure.PNG differ diff --git a/docs/images/CreateAccountSuccess.PNG b/docs/images/CreateAccountSuccess.PNG new file mode 100644 index 000000000000..ead5505c84b6 Binary files /dev/null and b/docs/images/CreateAccountSuccess.PNG differ diff --git a/docs/images/DefaultAccountDetails.PNG b/docs/images/DefaultAccountDetails.PNG new file mode 100644 index 000000000000..9c9f2cdbfea2 Binary files /dev/null and b/docs/images/DefaultAccountDetails.PNG differ diff --git a/docs/images/DuplicateBudget.png b/docs/images/DuplicateBudget.png new file mode 100644 index 000000000000..7c8cc2473fbf Binary files /dev/null and b/docs/images/DuplicateBudget.png differ diff --git a/docs/images/ExecuteBudgetCommand.png b/docs/images/ExecuteBudgetCommand.png new file mode 100644 index 000000000000..9b61714189bc Binary files /dev/null and b/docs/images/ExecuteBudgetCommand.png differ diff --git a/docs/images/ExtraLoginInput.PNG b/docs/images/ExtraLoginInput.PNG new file mode 100644 index 000000000000..e86f503b30c3 Binary files /dev/null and b/docs/images/ExtraLoginInput.PNG differ diff --git a/docs/images/InitialList.png b/docs/images/InitialList.png new file mode 100644 index 000000000000..875fc1f7e99e Binary files /dev/null and b/docs/images/InitialList.png differ diff --git a/docs/images/InitialLoginBookList.PNG b/docs/images/InitialLoginBookList.PNG new file mode 100644 index 000000000000..30cecf5a6be8 Binary files /dev/null and b/docs/images/InitialLoginBookList.PNG differ diff --git a/docs/images/InvalidLoginInput.PNG b/docs/images/InvalidLoginInput.PNG new file mode 100644 index 000000000000..5ddd49674814 Binary files /dev/null and b/docs/images/InvalidLoginInput.PNG differ diff --git a/docs/images/LoginActivityDiagram.png b/docs/images/LoginActivityDiagram.png new file mode 100644 index 000000000000..01ae3d00baa7 Binary files /dev/null and b/docs/images/LoginActivityDiagram.png differ diff --git a/docs/images/LoginExtendedActivityDiagram.png b/docs/images/LoginExtendedActivityDiagram.png new file mode 100644 index 000000000000..28fbc7753b54 Binary files /dev/null and b/docs/images/LoginExtendedActivityDiagram.png differ diff --git a/docs/images/LoginSequenceDiagram.png b/docs/images/LoginSequenceDiagram.png new file mode 100644 index 000000000000..c44fd46118af Binary files /dev/null and b/docs/images/LoginSequenceDiagram.png differ diff --git a/docs/images/LoginSuccess.PNG b/docs/images/LoginSuccess.PNG new file mode 100644 index 000000000000..3db52952ef9e Binary files /dev/null and b/docs/images/LoginSuccess.PNG differ diff --git a/docs/images/MissingLoginInput.PNG b/docs/images/MissingLoginInput.PNG new file mode 100644 index 000000000000..9bbb0830e7f0 Binary files /dev/null and b/docs/images/MissingLoginInput.PNG differ diff --git a/docs/images/NUSSU-Connect-Banner.jpg b/docs/images/NUSSU-Connect-Banner.jpg new file mode 100644 index 000000000000..9e3d755beebd Binary files /dev/null and b/docs/images/NUSSU-Connect-Banner.jpg differ diff --git a/docs/images/NUSSU-Connect1.jpg b/docs/images/NUSSU-Connect1.jpg new file mode 100644 index 000000000000..0b7510d05b55 Binary files /dev/null and b/docs/images/NUSSU-Connect1.jpg differ diff --git a/docs/images/NUSSU-ConnectPNGBanner.png b/docs/images/NUSSU-ConnectPNGBanner.png new file mode 100644 index 000000000000..43fb90b4aeee Binary files /dev/null and b/docs/images/NUSSU-ConnectPNGBanner.png differ diff --git a/docs/images/ParseCorrectLoginDetailList.PNG b/docs/images/ParseCorrectLoginDetailList.PNG new file mode 100644 index 000000000000..982685766b1c Binary files /dev/null and b/docs/images/ParseCorrectLoginDetailList.PNG differ diff --git a/docs/images/RepeatLoginSequenceDiagram.png b/docs/images/RepeatLoginSequenceDiagram.png new file mode 100644 index 000000000000..aea0eaf1188e Binary files /dev/null and b/docs/images/RepeatLoginSequenceDiagram.png differ diff --git a/docs/images/SearchPruning10th.png b/docs/images/SearchPruning10th.png new file mode 100644 index 000000000000..46a5ddfce262 Binary files /dev/null and b/docs/images/SearchPruning10th.png differ diff --git a/docs/images/SearchPruning1st.png b/docs/images/SearchPruning1st.png new file mode 100644 index 000000000000..523603581b52 Binary files /dev/null and b/docs/images/SearchPruning1st.png differ diff --git a/docs/images/SearchPruning2nd.png b/docs/images/SearchPruning2nd.png new file mode 100644 index 000000000000..41f115c6f04c Binary files /dev/null and b/docs/images/SearchPruning2nd.png differ diff --git a/docs/images/SearchPruning3rd.png b/docs/images/SearchPruning3rd.png new file mode 100644 index 000000000000..0fbbf4a7fdf1 Binary files /dev/null and b/docs/images/SearchPruning3rd.png differ diff --git a/docs/images/SearchPruning4th.png b/docs/images/SearchPruning4th.png new file mode 100644 index 000000000000..a9a3bf0f1da8 Binary files /dev/null and b/docs/images/SearchPruning4th.png differ diff --git a/docs/images/SearchPruning5th.png b/docs/images/SearchPruning5th.png new file mode 100644 index 000000000000..7e2db0616637 Binary files /dev/null and b/docs/images/SearchPruning5th.png differ diff --git a/docs/images/SearchPruning6th.png b/docs/images/SearchPruning6th.png new file mode 100644 index 000000000000..5c4283a52829 Binary files /dev/null and b/docs/images/SearchPruning6th.png differ diff --git a/docs/images/SearchPruning7th.png b/docs/images/SearchPruning7th.png new file mode 100644 index 000000000000..a7084c48e978 Binary files /dev/null and b/docs/images/SearchPruning7th.png differ diff --git a/docs/images/SearchPruning8th.png b/docs/images/SearchPruning8th.png new file mode 100644 index 000000000000..444e40722dbc Binary files /dev/null and b/docs/images/SearchPruning8th.png differ diff --git a/docs/images/SearchPruning9th.png b/docs/images/SearchPruning9th.png new file mode 100644 index 000000000000..d850baa27a69 Binary files /dev/null and b/docs/images/SearchPruning9th.png differ diff --git a/docs/images/SearchPruningSequenceDiagram.png b/docs/images/SearchPruningSequenceDiagram.png new file mode 100644 index 000000000000..c80397ac01bd Binary files /dev/null and b/docs/images/SearchPruningSequenceDiagram.png differ diff --git a/docs/images/SecondList.png b/docs/images/SecondList.png new file mode 100644 index 000000000000..8f843f645442 Binary files /dev/null and b/docs/images/SecondList.png differ diff --git a/docs/images/ThirdList.png b/docs/images/ThirdList.png new file mode 100644 index 000000000000..f9b664230d4e Binary files /dev/null and b/docs/images/ThirdList.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..3c4d6e722fd6 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/WrongIdOrPasswordOrRoleList.PNG b/docs/images/WrongIdOrPasswordOrRoleList.PNG new file mode 100644 index 000000000000..55c82f4160a2 Binary files /dev/null and b/docs/images/WrongIdOrPasswordOrRoleList.PNG differ diff --git a/docs/images/WrongIdPasswordRole.PNG b/docs/images/WrongIdPasswordRole.PNG new file mode 100644 index 000000000000..b27a539a29e1 Binary files /dev/null and b/docs/images/WrongIdPasswordRole.PNG differ diff --git a/docs/images/aslafter.png b/docs/images/aslafter.png new file mode 100644 index 000000000000..1818a878d5d6 Binary files /dev/null and b/docs/images/aslafter.png differ diff --git a/docs/images/aslbefore.png b/docs/images/aslbefore.png new file mode 100644 index 000000000000..0eeb0efa8af7 Binary files /dev/null and b/docs/images/aslbefore.png differ diff --git a/docs/images/chocological.png b/docs/images/chocological.png new file mode 100644 index 000000000000..e160bfc1475f Binary files /dev/null and b/docs/images/chocological.png differ diff --git a/docs/images/clearSearchHistoryStack.png b/docs/images/clearSearchHistoryStack.png new file mode 100644 index 000000000000..79205a3d0ba9 Binary files /dev/null and b/docs/images/clearSearchHistoryStack.png differ diff --git a/docs/images/derpyplops.png b/docs/images/derpyplops.png new file mode 100644 index 000000000000..9ef2f01b65e6 Binary files /dev/null and b/docs/images/derpyplops.png differ diff --git a/docs/images/executeNewSearchEmptyStack.png b/docs/images/executeNewSearchEmptyStack.png new file mode 100644 index 000000000000..ed2330d6b411 Binary files /dev/null and b/docs/images/executeNewSearchEmptyStack.png differ diff --git a/docs/images/executeNewSearchNonEmptyStack.png b/docs/images/executeNewSearchNonEmptyStack.png new file mode 100644 index 000000000000..5ffe4ed0caf0 Binary files /dev/null and b/docs/images/executeNewSearchNonEmptyStack.png differ diff --git a/docs/images/ladderinc.png b/docs/images/ladderinc.png new file mode 100644 index 000000000000..1079822ff3ea Binary files /dev/null and b/docs/images/ladderinc.png differ diff --git a/docs/images/sanjukta99.png b/docs/images/sanjukta99.png new file mode 100644 index 000000000000..89d3c08ac52b Binary files /dev/null and b/docs/images/sanjukta99.png differ diff --git a/docs/images/undoSearchHistoryStack.png b/docs/images/undoSearchHistoryStack.png new file mode 100644 index 000000000000..71776e29a15d Binary files /dev/null and b/docs/images/undoSearchHistoryStack.png differ diff --git a/docs/images/undoSearchSequenceDiagram.png b/docs/images/undoSearchSequenceDiagram.png new file mode 100644 index 000000000000..deac0065b7d5 Binary files /dev/null and b/docs/images/undoSearchSequenceDiagram.png differ diff --git a/docs/team/BoonJun.adoc b/docs/team/BoonJun.adoc new file mode 100644 index 000000000000..bf6a17c04716 --- /dev/null +++ b/docs/team/BoonJun.adoc @@ -0,0 +1,86 @@ += Boon Jun - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: NUSSU Connect + + +== Introduction + +This Portfolio is a documentation of the contributions that I have made for this project, titled NUSSU Connect. + +NUSSU Connect is a CLI application mainly targeted towards executive committee (exco) members of the NUS Student Union (NUSSU). +Being representatives of the NUS student body, the exco members has a part in many projects and initiatives. +This application therefore aims to help them perform some of these miscellaneous tasks that often comes from these projects much more easily. + +To do so, the application comes with several features. This includes budgets/expenses management, human resource allocation and managing of important contacts. +The original source code of this project is the https://github.com/se-edu/[AddressBook-Level4] project created by SE-EDU initiative, and hence, +it also comes with some features from the AB4 project. + +== Summary of contributions + +In this project, my main contribution is the Search Pruning feature. The main purpose of this feature is to +ease the process of managing a large list of contacts. +Further details about my contributions will be explained in this section. + +* *Major enhancement*: added *the ability to prune the list of contacts with find and undosearch commands* + +** What it does: allows users to search and reduce the size of the list of contacts in discrete steps. + +** Justification: When managing several large scale projects/events, users will find themselves in a situation +where they have to manage numerous of contacts. This includes student volunteers, sponsors, key organizers etc. Hence, a search pruning feature +allows the users to filter the list of contacts intuitively and help them retrieve the relevant results more easily. In the event that users +made a mistake in their search, users can utilize the `undosearch` command to rectify their mistake. + +** Highlights: This enhancement is mainly created from scratch with some help from documentations on how the `Predicate` class works. +This enhancement was also created with re-usability in mind and any form of filtering done with `Predicate` will be able to +utilize this feature. + +* *Minor enhancement*: added the feature that allows users to see a history of keywords they used in their search when utilizing the Search Pruning feature. + +** What it does: Every time when the user performs a search, the command result box will display the list of keywords describing how the current displayed list is being filtered. + +** Justification: When utilizing the search pruning feature, the users might forget the keywords that they have specified in the past. Therefore, this feature simply shows +the keywords that were previously executed by the user. + +* *Minor enhancement*: Improved find command by introducing searching by tags and exclude functionality + +** What it does: Searching by tags allows users to filter the contacts according to the tags (extra description). Exclude functionality allows users to remove +unwanted results from their search. + +* *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.1`, `v1.2` and `v1.2.1` (3 releases) on GitHub +*** Managed Milestones and Deadlines of the project. + +** Documentation: +*** Changing site-wide documentation settings in several files. +*** Minor tweaks to ReadMe, UserGuide and DeveloperGuide to meet the module requirements. (Pull Requests https://github.com[#12], https://github.com[#14], https://github.com[#23]) +*** Provided teammates with non-trivial PR reviews (Pull Requests https://github.com[#25], https://github.com[#76]) + +** Tools: +*** Integrated Coveralls to the team repository + +** Others: +*** Search Pruning Feature reused in Login/Logout feature of the application. + +== 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=find] + +== 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=searchpruning] diff --git a/docs/team/MelvinTanJitChong.adoc b/docs/team/MelvinTanJitChong.adoc new file mode 100644 index 000000000000..c1366ec7f262 --- /dev/null +++ b/docs/team/MelvinTanJitChong.adoc @@ -0,0 +1,126 @@ += Melvin Tan Jit Chong - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: NUSSU-Connect + +--- + +== Overview + +The purpose of this portfolio is to document a summary of my contributions to the project. This project is a software +engineering project that aims to solve some of the common issues that NUSSU is facing. +NUSSU-Connect is an application for executive committee (exco) members of the NUS Student Union (NUSSU) to help them perform their daily tasks including managing recruitment, contacts, budgets and projects efficiently. +Other team members working on this project are Jonathan Ng Hian Leong, Sanjukta, and Soh Boon Jun. + +=== List of Features + +[width="59%",cols="22%,<23%,<25%,<30%",options="header",] +|======================================================================= +|Feature |Feature description |Format | Example +|Logging into system |Logs into application using relevant credentials |`login USERID PASSWORD ROLE` |`login X1234567X zaq1xsw2cde3 member` + +|Creating a new user account |Creates a new user account in the address book |`createaccount USERID PASSWORD ROLE` |`createaccount X1234567X aq1xsw2cde3 member` + +|Search Pruning |Trim the list of contacts with every successive find command without the hassle of typing a long single line command |`find \tag ROLE` |`find \tag President` + +|Undoing Search Commands |Restores the displayed list to the state before you perform your most recent find command |`undosearch` | + +|Submitting data for budget allocation |Submits the data about number of events, expected turnout etc |`budget c/CLUB NAME t/TURNOUT e/NUMBER OF EVENTS` |`budget c/Computing Club t/200 e/5` + +|Calculating the budgets |Calculates the budgets for all the clubs in the address book using the total available budget |`calculatebudget b/TOTAL AVAILABLE BUDGET IN SGD` |`calculatebudget b/50000` + +|Viewing the allocated budget |Shows all the allocated budgets to the user |`viewbudget` | + +|Submitting grant request |Submits data about amount of grant needed, the reason and tags the importance level |`request m/AMOUNT r/REASON t/IMPORTANCE LEVEL` |`request m/1000 r/for booking auditorium t/medium` + +|Viewing grant request |Shows a list of all the grant request sorted by order of importance, and if there is a tie, by descending order of amount requested |`viewrequest` | + +|Accepting grant request |Approves a specific grant request from the list of grants |`accept [-a all] [INDEX]` |`viewrequest` + `accept 2` + +|Adding person's skill |Edits a person's skill in the address book |`asl INDEX s/SKILL l/SKILL_LEVEL` |`asl 2 s/Photography l/30 + +|Locating persons |Finds persons in the displayed list whose names/tags contain any of the given keywords |`find [\tag] [\exclude] KEYWORD [MORE_KEYWORDS]` |`find \tag President` + +|======================================================================= + + +== Summary of contributions + +== Preface +This section shows the various contributions made to the project, covering aspects such as enhancement features, codes contributed and other forms of contributions. + +* *Major enhancement*: added *the ability to securely log into the application with the relevant login credentials with a specific level of access to the application features associated with the login credentials* +** What it does: allows you to login into the application with a specific role so you are only able to access role-specific features within the application. +** Justification: This feature improves the product in terms of security significantly because it prevents any random user who does not possess the correct login credentials to be able to access the application. +Due to the fact that you can only log into the application with your relevant role, it prevents any unauthorized access to other features that are only accessible for other roles. +** Highlights: This enhancement is first designed, taking into consideration the security and practicality aspects that are highly specific to NUSSU committee members, which is our target user group. +There were a few challenges, namely the addition of new commands within the enhancement itself such as deletion of accounts and changing of account passwords, and +the way account credentials are encrypted. Owing to the project time constraints, a simpler encryption algorithm, compared to traditional encryption algorithms which are stronger, is used. +The current choice of the encryption methodology of login credentials ensures the security enhancement is still feasibly robust, yet does not demand an excess of time spent working on it. +** Credits: + +* *Minor enhancement*: added a create account command within the application. +** What it does: allows you to create a new account with your desired login credentials so that you can log into the application securely. +** Justification: This feature allows you to be able to create their own accounts so that you do not need to rely on the accounts of other users in order +to log into the application. +** Highlights: Similar to the major enhancement, this minor enhancement is designed in light of practical considerations of the committee members of NUSSU. +It is impractical for NUSSU, which consists of a numerous number of members, to all use the same account to log into the application. Without the account creation +enhancement, the multi-user access level feature within the login system would not be utilized at all. There should be multiple accounts with different roles that fit +different committee members with varying levels of authority within NUSSU, so that a member can create an account with an application access level relevant to his role +in NUSSU only. + +* *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 +*** Set up issue tracker for the team repository +*** Set up organization directory and team repository on GitHub +*** Set up continuous integration (CI) tools (Travis) and (Appveyor) on the team repository +** 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] +*** 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 login's multi-user access level feature I added was adopted by several project group mates (https://github.com[1], https://github.com[2]) +*** Renamed the product to "NUSSU-Connect" +*** Gave suggestions to reduce the time needed to create UML diagrams (ObjectAidUM as an Eclipse framework) +** Tools: + +== Contributions to the User Guide + +== Preface +This section will touch on two commands that you can use, namely the `login` and `createaccount` command. The `login` command allows you to log into the application securely +using your own login credentials. The `createaccount` command allows you to create a new account with your own desired login credentials. + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=logincreateaccount] + +include::../UserGuide.adoc[tag=login] + +== Contributions to the Developer Guide + +== Preface +This section will touch on two commands that you can use, namely the `login` and `createaccount` command. The `login` command allow you to log into the application securely +using your own login credentials. The `createaccount` command allows you to create a new account with your own desired login credentials. + +|=== +|_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=login] + +== PROJECT: PowerPointLabs + +--- + +_{Optionally, you may include other projects in your portfolio.}_ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2d80b69a7665..f538e847d2ea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Oct 05 00:10:51 SGT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip diff --git a/login/loginbook.xml b/login/loginbook.xml new file mode 100644 index 000000000000..d640b0326e57 --- /dev/null +++ b/login/loginbook.xml @@ -0,0 +1,11 @@ + + + + A1234567M + zaq1xsw2cde3 + + + A1234567M + zaq1xsw2cde3 + + diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..fb1ecb72c624 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -18,20 +18,25 @@ import seedu.address.commons.exceptions.DataConversionException; import seedu.address.commons.util.ConfigUtil; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.CommandHistory; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; import seedu.address.model.AddressBook; +import seedu.address.model.LoginBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLoginBook; import seedu.address.model.UserPrefs; import seedu.address.model.util.SampleDataUtil; import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonUserPrefsStorage; +import seedu.address.storage.LoginBookStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; import seedu.address.storage.XmlAddressBookStorage; +import seedu.address.storage.XmlLoginBookStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -40,7 +45,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 2, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -51,7 +56,6 @@ public class MainApp extends Application { protected Config config; protected UserPrefs userPrefs; - @Override public void init() throws Exception { logger.info("=============================[ Initializing AddressBook ]==========================="); @@ -62,8 +66,9 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); userPrefs = initPrefs(userPrefsStorage); + LoginBookStorage loginBookStorage = new XmlLoginBookStorage(userPrefs.getLoginBookFilePath()); AddressBookStorage addressBookStorage = new XmlAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + storage = new StorageManager(loginBookStorage, addressBookStorage, userPrefsStorage); initLogging(config); @@ -82,8 +87,23 @@ 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 loginBookOptional; Optional addressBookOptional; + ReadOnlyLoginBook initialLoginData; ReadOnlyAddressBook initialData; + try { + loginBookOptional = storage.readLoginBook(); + if (!loginBookOptional.isPresent()) { + logger.info("Login data file not found. Will be starting with a new LoginBook"); + } + initialLoginData = loginBookOptional.orElseGet(SampleDataUtil::getSampleLoginBook); + } catch (DataConversionException e) { + logger.warning("Login data file not in the correct format. Will be starting with an empty LoginBook"); + initialLoginData = new LoginBook(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty LoginBook"); + initialLoginData = new LoginBook(); + } try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { @@ -98,7 +118,7 @@ private Model initModelManager(Storage storage, UserPrefs userPrefs) { initialData = new AddressBook(); } - return new ModelManager(initialData, userPrefs); + return new ModelManager(initialLoginData, initialData, userPrefs); } private void initLogging(Config config) { @@ -180,7 +200,8 @@ private void initEventsCenter() { @Override public void start(Stage primaryStage) { logger.info("Starting AddressBook " + MainApp.VERSION); - ui.start(primaryStage); + CommandHistory history = new CommandHistory(); + ui.start(primaryStage, model, history); } @Override diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..c7e274826511 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -9,5 +9,6 @@ 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!"; + public static final String MESSAGE_LOGIN_LISTED_OVERVIEW = "Login successful!"; } diff --git a/src/main/java/seedu/address/commons/events/model/LoginBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/LoginBookChangedEvent.java new file mode 100644 index 000000000000..751597d2be4a --- /dev/null +++ b/src/main/java/seedu/address/commons/events/model/LoginBookChangedEvent.java @@ -0,0 +1,19 @@ +package seedu.address.commons.events.model; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.ReadOnlyLoginBook; + +/** Indicates the LoginBook in the model has changed*/ +public class LoginBookChangedEvent extends BaseEvent { + + public final ReadOnlyLoginBook data; + + public LoginBookChangedEvent(ReadOnlyLoginBook data) { + this.data = data; + } + + @Override + public String toString() { + return "number of accounts " + data.getLoginDetailsList().size(); + } +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb8..492796b744a2 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,32 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Checks for case, and a full word match is required. + *
examples:
+     *       containsWordCheckCase("ABc def", "AB") == false //not a full word match
+     *       containsWordCheckCase("ABc def", "Abc") == false // casing does not match
+     *       containsWordCheckCase("ABc def", "ABc") == true
+     *       
+ * @param sentence cannot be null + * @param word cannot be null, cannot be empty, must be a single word + */ + public static boolean containsWordCheckCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word"); + + String preppedSentence = sentence; + String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); + + return Arrays.stream(wordsInPreppedSentence) + .anyMatch(preppedWord::contentEquals); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/seedu/address/logic/CommandHistory.java index 39bca9b8df57..bdffdb1a1594 100644 --- a/src/main/java/seedu/address/logic/CommandHistory.java +++ b/src/main/java/seedu/address/logic/CommandHistory.java @@ -4,6 +4,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; /** * Stores the history of commands executed. @@ -34,6 +35,14 @@ public List getHistory() { return new LinkedList<>(userInputHistory); } + public String getLastExecutedCommand() { + try { + return userInputHistory.getLast(); + } catch (NoSuchElementException e) { + return null; + } + } + @Override public boolean equals(Object obj) { // short circuit if same object diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..86f7b5b8827d 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,6 +4,7 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.budgetelements.ClubBudgetElements; import seedu.address.model.person.Person; /** @@ -22,6 +23,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of clubs */ + ObservableList getFilteredClubsList(); + /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ ListElementPointer getHistorySnapshot(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9aff86fc33dc..42b0ad716b3e 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -11,6 +11,8 @@ import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.login.LoginDetails; import seedu.address.model.person.Person; /** @@ -40,11 +42,20 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } } + public ObservableList getFilteredLoginDetailsList() { + return model.getFilteredLoginDetailsList(); + } + @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredClubsList() { + return model.getFilteredClubsList(); + } + @Override public ListElementPointer getHistorySnapshot() { return new ListElementPointer(history.getHistory()); diff --git a/src/main/java/seedu/address/logic/LoginManager.java b/src/main/java/seedu/address/logic/LoginManager.java new file mode 100644 index 000000000000..36697199d37a --- /dev/null +++ b/src/main/java/seedu/address/logic/LoginManager.java @@ -0,0 +1,70 @@ +package seedu.address.logic; + +/** + * The manager of the login functionality of the app. + */ +public class LoginManager { + + private static boolean isCurrentlyCreatingAccount = false; + private static boolean isCurrentlyTesting = false; + private static boolean isLoginSuccessful = false; + private static boolean isMember = false; + private static boolean isPresident = false; + private static boolean isTreasurer = false; + + public LoginManager() {} + + public static boolean getIsCurrentlyCreatingAccount() { + return isCurrentlyCreatingAccount; + } + + public static boolean getIsCurrentlyTesting() { + return isCurrentlyTesting; + } + + public static boolean getIsLoginSuccessful() { + return isLoginSuccessful; + } + + public static boolean getIsMember() { + return isMember; + } + + public static boolean getIsPresident() { + return isPresident; + } + + public static boolean getIsTreasurer() { + return isTreasurer; + } + + public static void setIsCurrentlyCreatingAccount(boolean setCurrentlyCreatingAccount) { + isCurrentlyCreatingAccount = setCurrentlyCreatingAccount; + } + + public static void setIsCurrentlyTesting(boolean setCurrentlyTesting) { + isCurrentlyTesting = setCurrentlyTesting; + } + + public static void setIsLoginSuccessful(boolean setLoginSuccessful) { + isLoginSuccessful = setLoginSuccessful; + } + + public static void setIsMember(boolean setMember) { + isMember = setMember; + } + + public static void setIsPresident(boolean setPresident) { + isPresident = setPresident; + } + + public static void setIsTreasurer(boolean setTreasurer) { + isTreasurer = setTreasurer; + } + + public static void setAllRolesFalse() { + isMember = false; + isPresident = false; + isTreasurer = false; + } +} diff --git a/src/main/java/seedu/address/logic/arithmetic/CalculateTotalAttendees.java b/src/main/java/seedu/address/logic/arithmetic/CalculateTotalAttendees.java new file mode 100644 index 000000000000..ef1fa317a03a --- /dev/null +++ b/src/main/java/seedu/address/logic/arithmetic/CalculateTotalAttendees.java @@ -0,0 +1,44 @@ +package seedu.address.logic.arithmetic; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.model.budgetelements.ClubBudgetElements; + +/** + * Calculates the total number of attendees across all events, based on the club budget elements entered by club + * treasure. + */ +public class CalculateTotalAttendees { + private final List listOfClubs; + /** + * @param listOfClubs is the filtered list with which to calculate totalAttendees + */ + public CalculateTotalAttendees(List listOfClubs) { + requireNonNull(listOfClubs); + this.listOfClubs = listOfClubs; + } + + /** + * @return the totalAttendees + */ + public int arithmeticTotalAttendees() { + + int totalAttendees = 0; + + for (int i = 0; i < listOfClubs.size(); i++) { + + ClubBudgetElements currentClub = listOfClubs.get(i); + + int currentExpectedTurnout = Integer.parseInt(currentClub.getExpectedTurnout().toString()); + + int currentNumberOfEvents = Integer.parseInt(currentClub.getNumberOfEvents().toString()); + + totalAttendees += (currentExpectedTurnout * currentNumberOfEvents); + + } + + return totalAttendees; + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddSkillCommand.java b/src/main/java/seedu/address/logic/commands/AddSkillCommand.java new file mode 100644 index 000000000000..ba58cf359ce4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddSkillCommand.java @@ -0,0 +1,98 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILL; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; + +//@@author derpyplops-reused +//{The implementation is a simple renaming of the "remark" example in the Dev Guide. +// Code from it is also reused throughout the project. +// Which will be extended later to other uses.} + +/** + * Adds a skill for a person in the Addressbook. + */ +public class AddSkillCommand extends Command { + + public static final String COMMAND_WORD = "addskill"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the skill of the person identified " + + "by the index number used in the last person listing. " + + "Existing skill will be overwritten by the input.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_SKILL + "[SKILL]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_SKILL + "Can swim."; + + public static final String MESSAGE_SUCCESS = "New skill added: %1$s"; + + public static final String MESSAGE_ADD_SKILL_SUCCESS = "Added skill to person: %1$s"; + public static final String MESSAGE_DELETE_SKILL_SUCCESS = "Remove skill to person: %1$s"; + public static final String MESSAGE_INVALID_LEVEL = "Level %1$s is not valid, please enter an number (1..100)."; + public final SkillLevel dummySkillLevel = new SkillLevel(0); + private final Index index; + private final Skill skill; + /** + * @param index of the person in the filtered person list to edit the remark + * @param skill of the person to be updated to + */ + public AddSkillCommand(Index index, Skill skill) { + requireAllNonNull(index, skill); + this.index = index; + this.skill = skill; + } + + /** + * Creates an AddSkillCommand to add the specified {@code Skill} + */ + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person personToEdit = lastShownList.get(index.getZeroBased()); + Person editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), skill, dummySkillLevel, personToEdit.getTags()); + model.updatePerson(personToEdit, editedPerson); + model.resetSearchHistoryToInitialState(); + model.commitAddressBook(); + return new CommandResult(generateSuccessMessage(editedPerson)); + } + /** + * Generates a command execution success message based on whether the remark is added to or removed from + * {@code personToEdit}. + */ + private String generateSuccessMessage(Person personToEdit) { + String message = !skill.value.isEmpty() ? MESSAGE_ADD_SKILL_SUCCESS : MESSAGE_DELETE_SKILL_SUCCESS; + return String.format(message, personToEdit); + } + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof AddSkillCommand)) { + return false; + } + // state check + AddSkillCommand e = (AddSkillCommand) other; + return index.equals(e.index) + && skill.equals(e.skill); + } +} + +//@@author diff --git a/src/main/java/seedu/address/logic/commands/AddSkillLevelCommand.java b/src/main/java/seedu/address/logic/commands/AddSkillLevelCommand.java new file mode 100644 index 000000000000..cd46ba66893c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddSkillLevelCommand.java @@ -0,0 +1,94 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILLLEVEL; + +import java.util.List; + +import seedu.address.commons.core.Messages; + +import seedu.address.commons.core.index.Index; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; + +/** + * Changes the skill of an existing person in the address book, with a tagged skill skillLevel. + */ + +public class AddSkillLevelCommand extends Command { + public static final String COMMAND_WORD = "asl"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the skill of the person identified " + + "by the index number used in the last person listing. " + + "Existing skill will be overwritten by the input.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_SKILL + "[SKILL] " + PREFIX_SKILLLEVEL + "[LEVEL]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_SKILL + "Photography " + PREFIX_SKILLLEVEL + "4\n"; + + public static final String MESSAGE_ADD_SKILL_SUCCESS = "Added skill to Person: %1$s"; + public static final String MESSAGE_DELETE_SKILL_SUCCESS = "Removed skill from Person: %1$s"; + public static final String MESSAGE_SKILLLEVEL_CONSTRAINTS = "This skill level is not valid. " + + "Please enter a whole number between 0 to 100."; + private final Index index; + private final Skill skill; + private final SkillLevel skillLevel; + /** + * @param index of the person in the filtered person list to edit the skill + * @param skill of the person to be added + * @param skillLevel of the skill that the person has + */ + public AddSkillLevelCommand(Index index, Skill skill, SkillLevel skillLevel) { + requireAllNonNull(index, skill, skillLevel); + this.index = index; + this.skill = skill; + this.skillLevel = skillLevel; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + if (!skillLevel.isValidSkillLevel()) { + throw new CommandException(MESSAGE_SKILLLEVEL_CONSTRAINTS); + } + + Person editedPerson = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), skill, skillLevel, personToEdit.getTags()); + model.updatePerson(personToEdit, editedPerson); + model.resetSearchHistoryToInitialState(); + model.commitAddressBook(); + return new CommandResult(generateSuccessMessage(editedPerson)); + } + private String generateSuccessMessage(Person personToEdit) { + String message = !skill.value.isEmpty() ? MESSAGE_ADD_SKILL_SUCCESS : MESSAGE_DELETE_SKILL_SUCCESS; + return String.format(message, personToEdit); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + // instanceof handles nulls + if (!(other instanceof AddSkillLevelCommand)) { + return false; + } + // state check + AddSkillLevelCommand e = (AddSkillLevelCommand) other; + return index.equals(e.index) + && skill.equals(e.skill); + } +} diff --git a/src/main/java/seedu/address/logic/commands/BudgetCalculationCommand.java b/src/main/java/seedu/address/logic/commands/BudgetCalculationCommand.java new file mode 100644 index 000000000000..2a1643eb778e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/BudgetCalculationCommand.java @@ -0,0 +1,84 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TOTAL_BUDGET; + +import java.util.List; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.arithmetic.CalculateTotalAttendees; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.clubbudget.FinalClubBudget; +import seedu.address.model.clubbudget.TotalBudget; + +/** + * Calculates the budgets to be allocated to all the clubs in the list of clubs in the address book. + */ + +public class BudgetCalculationCommand extends Command { + + public static final String COMMAND_WORD = "calculatebudget"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Calculates the budgets to be allocated to all the " + + "clubs in the list of clubs in the address book. " + + "Parameters: " + + PREFIX_TOTAL_BUDGET + "TOTAL BUDGET (IN SGD) " + + "Example: " + COMMAND_WORD + " 50000 "; + + public static final String MESSAGE_CALCULATE_BUDGET_SUCCESS = "The budgets have been calculated."; + public static final String MESSAGE_INVALID_TOTAL_BUDGET = "Please enter a valid total budget!"; + public static final String MESSAGE_DUPLICATE_CLUB = "This is a duplicate club"; + + private final TotalBudget totalBudget; + + /** + * @param totalBudget of the person in the filtered clubs list to calculate the budget with + */ + public BudgetCalculationCommand(TotalBudget totalBudget) { + requireNonNull(totalBudget); + + this.totalBudget = totalBudget; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List listOfClubs = model.getFilteredClubsList(); + + if (Integer.parseInt(totalBudget.toString()) <= 0) { + throw new CommandException(MESSAGE_INVALID_TOTAL_BUDGET); + } + int i; + + int budgetPerPerson; + + CalculateTotalAttendees totalAttendees = new CalculateTotalAttendees(listOfClubs); + + budgetPerPerson = Integer.parseInt(totalBudget.toString()) / totalAttendees.arithmeticTotalAttendees(); + + for (i = 0; i < listOfClubs.size(); i++) { + + ClubBudgetElements currentClubForBudget = listOfClubs.get(i); + + int currenteo = Integer.parseInt(currentClubForBudget.getExpectedTurnout().toString()); + + int currentnoe = Integer.parseInt(currentClubForBudget.getNumberOfEvents().toString()); + + int totalClubMembers = currenteo * currentnoe; + + int currentClubsBudget = budgetPerPerson * totalClubMembers; + + FinalClubBudget toAdd = new FinalClubBudget(currentClubForBudget.getClubName(), currentClubsBudget); + + if (model.hasClubBudget(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CLUB); + } + model.addClubBudget(toAdd); + + } + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_CALCULATE_BUDGET_SUCCESS)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/BudgetCommand.java b/src/main/java/seedu/address/logic/commands/BudgetCommand.java new file mode 100644 index 000000000000..3632fc09ce0e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/BudgetCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CLUB_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EXPECTED_TURNOUT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUMBER_OF_EVENTS; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.budgetelements.ClubBudgetElements; + +/** + * Submits the data for budget allocation to the address book. + */ +public class BudgetCommand extends Command { + public static final String COMMAND_WORD = "budget"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Submits the data for calculating the budget of a" + + " club. " + + "Parameters: " + + PREFIX_CLUB_NAME + "CLUB NAME " + + PREFIX_EXPECTED_TURNOUT + "EXPECTED TURNOUT " + + PREFIX_NUMBER_OF_EVENTS + "NUMBER OF EVENTS " + + "Example: " + COMMAND_WORD + " " + + PREFIX_CLUB_NAME + "Computing Club " + + PREFIX_EXPECTED_TURNOUT + "200 " + + PREFIX_NUMBER_OF_EVENTS + "5 "; + + public static final String MESSAGE_SUCCESS = "Data submitted: %1$s"; + public static final String MESSAGE_DUPLICATE_CLUB = "This club's data already exists in the address book"; + + + private final ClubBudgetElements toAdd; + + /** + * Creates a BudgetCommand to add the specified {@code ClubBudgetElements} + */ + public BudgetCommand(ClubBudgetElements club) { + requireNonNull(club); + toAdd = club; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (model.hasClub(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_CLUB); + } + + model.addClub(toAdd); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof BudgetCommand // instanceof handles nulls + && toAdd.equals(((BudgetCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CreateAccountCommand.java b/src/main/java/seedu/address/logic/commands/CreateAccountCommand.java new file mode 100644 index 000000000000..ea9ce98bff8a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CreateAccountCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.LoginManager; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.login.LoginDetails; + +/** + * Adds an account to the login book + */ +public class CreateAccountCommand extends Command { + + public static final String COMMAND_WORD = "createaccount"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Creates an account for the login book. " + + "Parameters: USERID PASSWORD ROLE\n" + + "Example: " + COMMAND_WORD + " A1234567M zaq1xsw2cde3 treasurer"; + + + public static final String MESSAGE_SUCCESS = "New account created: %1$s"; + public static final String MESSAGE_DUPLICATE_ACCOUNT = "This account already exists in the login book"; + + private final LoginDetails toAdd; + + /** + * Creates a CreateAccountCommand to add the specified {@code LoginDetails} + */ + public CreateAccountCommand(LoginDetails loginDetails) { + requireNonNull(loginDetails); + this.toAdd = loginDetails; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (model.hasAccount(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_ACCOUNT); + } + + model.createAccount(toAdd); + LoginManager.setIsCurrentlyCreatingAccount(false); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CreateAccountCommand // instanceof handles nulls + && toAdd.equals(((CreateAccountCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index dc782d8e230f..f031ad5d6bc1 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -6,7 +6,6 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import java.util.Collections; import java.util.HashSet; @@ -19,12 +18,15 @@ import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; + import seedu.address.model.Model; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; import seedu.address.model.tag.Tag; /** @@ -83,7 +85,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.updatePerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.resetSearchHistoryToInitialState(); model.commitAddressBook(); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } @@ -99,9 +101,12 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Skill updatedSkill = personToEdit.getSkill(); + SkillLevel updatedSkillLevel = personToEdit.getSkillLevel(); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, + updatedAddress, updatedSkill, updatedSkillLevel, updatedTags); } @Override diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index beb178e3a3f5..90bc51a86ad9 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,43 +1,23 @@ package seedu.address.logic.commands; -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.logic.commands.formatter.KeywordsOutputFormatter; +import seedu.address.logic.parser.FindCommandParser; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. * Keyword matching is case insensitive. */ -public class FindCommand extends Command { +public abstract class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names/tags contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } + + "Parameters: [\\tag] [\\exclude] KEYWORD [MORE_KEYWORDS]...\n" + + "Example1: " + COMMAND_WORD + " alice bob charlie\n" + + "Example2: " + COMMAND_WORD + " " + FindCommandParser.EXCLUDE_OPTION_STRING + " alice\n" + + "Example3: " + COMMAND_WORD + " " + FindCommandParser.TAG_OPTION_STRING + " President"; - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } + protected boolean isExcludeMode = false; + protected KeywordsOutputFormatter formatter = new KeywordsOutputFormatter(); } diff --git a/src/main/java/seedu/address/logic/commands/FindNameSubCommand.java b/src/main/java/seedu/address/logic/commands/FindNameSubCommand.java new file mode 100644 index 000000000000..85e4f9a898ee --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindNameSubCommand.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.searchhistory.KeywordType; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindNameSubCommand extends FindCommand { + + private final NameContainsKeywordsPredicate predicate; + + public FindNameSubCommand(NameContainsKeywordsPredicate predicate) { + this(predicate, false); + } + + public FindNameSubCommand(NameContainsKeywordsPredicate predicate, boolean isExcludeMode) { + this.predicate = predicate; + this.isExcludeMode = isExcludeMode; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + if (isExcludeMode) { + model.recordKeywords(KeywordType.ExcludeNames, predicate.getLowerCaseKeywords()); + model.executeSearch(predicate.negate()); + } else { + model.recordKeywords(KeywordType.IncludeNames, predicate.getLowerCaseKeywords()); + model.executeSearch(predicate); + } + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size()) + + formatter.getOutputString(model.getReadOnlyKeywordsRecord())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindNameSubCommand // instanceof handles nulls + && predicate.equals(((FindNameSubCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindTagSubCommand.java b/src/main/java/seedu/address/logic/commands/FindTagSubCommand.java new file mode 100644 index 000000000000..8b5e8bbd0155 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindTagSubCommand.java @@ -0,0 +1,48 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.person.TagContainsKeywordsPredicate; +import seedu.address.model.searchhistory.KeywordType; + +/** + * Finds and lists all persons in address book whose tags contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindTagSubCommand extends FindCommand { + + private final TagContainsKeywordsPredicate predicate; + + public FindTagSubCommand(TagContainsKeywordsPredicate predicate) { + this(predicate, false); + } + + public FindTagSubCommand(TagContainsKeywordsPredicate predicate, boolean isExcludeMode) { + this.predicate = predicate; + this.isExcludeMode = isExcludeMode; + } + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + if (isExcludeMode) { + model.recordKeywords(KeywordType.ExcludeTags, predicate.getLowerCaseKeywords()); + model.executeSearch(predicate.negate()); + } else { + model.recordKeywords(KeywordType.IncludeTags, predicate.getLowerCaseKeywords()); + model.executeSearch(predicate); + } + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size()) + + formatter.getOutputString(model.getReadOnlyKeywordsRecord())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FindTagSubCommand // instanceof handles nulls + && predicate.equals(((FindTagSubCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 6d44824c7d1b..8fd552879270 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,7 +1,6 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import seedu.address.logic.CommandHistory; import seedu.address.model.Model; @@ -12,14 +11,12 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; - @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.resetSearchHistoryToInitialState(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/LoginCommand.java b/src/main/java/seedu/address/logic/commands/LoginCommand.java new file mode 100644 index 000000000000..9cbd3d9e5214 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoginCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import java.util.function.Predicate; + +import seedu.address.model.searchhistory.SearchHistoryManager; + + +/** + * Queries the login book to see if there is a user ID and password that matches input + * user ID and password. Used for the login process. + * Keyword matching is case insensitive for user ID and case sensitive for user Password. + */ +public abstract class LoginCommand extends Command { + + public static final String COMMAND_WORD = "login"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ":Login into NUSSU-Connect with input " + + "user ID, password and role." + + "Parameters: USERID PASSWORD ROLE\n" + + "Example: " + COMMAND_WORD + " A3583758X 1qaxcdwd2w member"; + + private SearchHistoryManager searchHistoryManager = new SearchHistoryManager(); + + /** + * Clears the current searchHistoryManager object of previous login input details in preparation for another + * login attempt so that the filtered login details list becomes unfiltered again. Returns a new predicate generated + * from user input login id to be used in the filtering of the login details list. + * @param idPredicate the predicate generated from user input login id + * @return a new predicate generated from user input login id + */ + Predicate getMostUpdatedIdPredicate(Predicate idPredicate) { + searchHistoryManager.clearSearchHistory(); + return searchHistoryManager.executeNewSearch(idPredicate); + } + + /** + * Returns a new predicate generated from user input login password to be used in the filtering of the + * login details list. + * @param passwordPredicate the predicate generated from user input login password + * @return a new predicate generated from user input login password + */ + Predicate getMostUpdatedPasswordPredicate(Predicate passwordPredicate) { + return searchHistoryManager.executeNewSearch(passwordPredicate); + } + + /** + * Returns a new predicate generated from user input login role to be used in the filtering of the + * login details list. + * @param rolePredicate the predicate generated from user input login role + * @return a new predicate generated from user input login role + */ + Predicate getMostUpdatedRolePredicate(Predicate rolePredicate) { + return searchHistoryManager.executeNewSearch(rolePredicate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/LoginUserIdPasswordRoleCommand.java b/src/main/java/seedu/address/logic/commands/LoginUserIdPasswordRoleCommand.java new file mode 100644 index 000000000000..82cbb3d76556 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoginUserIdPasswordRoleCommand.java @@ -0,0 +1,106 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_LOGIN_LISTED_OVERVIEW; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_ACCOUNTS; + +import java.util.function.Predicate; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.LoginManager; +import seedu.address.model.Model; +import seedu.address.model.login.UserIdContainsKeywordsPredicate; +import seedu.address.model.login.UserPasswordContainsKeywordsPredicate; +import seedu.address.model.login.UserRoleContainsKeywordsPredicate; + +/** + * Queries the login book to see if there is a user ID and password that matches input user ID and password. + * Used for the login process. + * Keyword matching is case insensitive for user ID but case sensitive for password. + */ +public class LoginUserIdPasswordRoleCommand extends LoginCommand { + + private final UserIdContainsKeywordsPredicate idPredicate; + private final UserPasswordContainsKeywordsPredicate passwordPredicate; + private final UserRoleContainsKeywordsPredicate rolePredicate; + + public LoginUserIdPasswordRoleCommand(UserIdContainsKeywordsPredicate idPredicate, + UserPasswordContainsKeywordsPredicate passwordPredicate, + UserRoleContainsKeywordsPredicate rolePredicate) { + super(); + requireNonNull(idPredicate); + requireNonNull(passwordPredicate); + requireNonNull(rolePredicate); + this.idPredicate = idPredicate; + this.passwordPredicate = passwordPredicate; + this.rolePredicate = rolePredicate; + + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + resetAccountList(model); + updateFilteredAccountList(model); + checkUpdatedAccountListSetLoginCondition(model); + return new CommandResult(MESSAGE_LOGIN_LISTED_OVERVIEW); + } + + /** + * Resets the previously updated accounts list in the model to a fresh state containing all existing accounts. + * @param model the current model being used to filter the accounts list + */ + private void resetAccountList(Model model) { + if (model.getFilteredLoginDetailsList().size() == 0) { // when user tries to login again after failed first try + model.updateFilteredLoginDetailsList(PREDICATE_SHOW_ALL_ACCOUNTS); // resets account list to show all + } + } + + /** + * Updates the account list in the model to reflect input of user login credentials. + * @param model the current model being used to filter the accounts list + */ + private void updateFilteredAccountList(Model model) { + Predicate updatedIdPredicate = getMostUpdatedIdPredicate(getIdPredicate()); + model.updateFilteredLoginDetailsList(updatedIdPredicate); + Predicate updatedPasswordPredicate = getMostUpdatedPasswordPredicate(getPasswordPredicate()); + model.updateFilteredLoginDetailsList(updatedPasswordPredicate); + Predicate updatedRolePredicate = getMostUpdatedRolePredicate(getRolePredicate()); + model.updateFilteredLoginDetailsList(updatedRolePredicate); + } + + /** + * Checks if there is an existing account in the account list in the model that matches input of user login + * credentials, and sets the login condition as successful only if the input user credentials is correct. + * @param model the current model being used to filter the accounts list + */ + private void checkUpdatedAccountListSetLoginCondition(Model model) { + if (model.getFilteredLoginDetailsList().size() != 0) { + LoginManager.setIsLoginSuccessful(true); + } else { + LoginManager.setAllRolesFalse(); + LoginManager.setIsLoginSuccessful(false); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof LoginUserIdPasswordRoleCommand // instanceof handles nulls + && getIdPredicate().equals(((LoginUserIdPasswordRoleCommand) other).getIdPredicate())) + && getPasswordPredicate().equals(((LoginUserIdPasswordRoleCommand) other).getPasswordPredicate()) + && getRolePredicate().equals(((LoginUserIdPasswordRoleCommand) other).getRolePredicate()); // state check + } + + public UserIdContainsKeywordsPredicate getIdPredicate() { + return idPredicate; + } + + public UserPasswordContainsKeywordsPredicate getPasswordPredicate() { + return passwordPredicate; + } + + public UserRoleContainsKeywordsPredicate getRolePredicate() { + return rolePredicate; + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..e3998c0870c5 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -1,7 +1,6 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; @@ -25,7 +24,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.resetSearchHistoryToInitialState(); 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..005e67336b89 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -1,7 +1,6 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; @@ -25,7 +24,7 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.resetSearchHistoryToInitialState(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/UndoFindCommand.java b/src/main/java/seedu/address/logic/commands/UndoFindCommand.java new file mode 100644 index 000000000000..963421662934 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoFindCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.formatter.KeywordsOutputFormatter; +import seedu.address.model.Model; +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; + +/** + * Reverts the {@code model}'s search history to its previous state. + */ +public class UndoFindCommand extends Command { + public static final String COMMAND_WORD = "undofind"; + public static final String MESSAGE_SUCCESS = "Undo success!"; + public static final String MESSAGE_FAILURE = "Search History is empty"; + private KeywordsOutputFormatter formatter = new KeywordsOutputFormatter(); + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + try { + model.revertLastSearch(); + return new CommandResult(MESSAGE_SUCCESS + formatter.getOutputString(model.getReadOnlyKeywordsRecord())); + } catch (EmptyHistoryException e) { + model.resetSearchHistoryToInitialState(); + return new CommandResult(MESSAGE_FAILURE); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewClubBudgetsCommand.java b/src/main/java/seedu/address/logic/commands/ViewClubBudgetsCommand.java new file mode 100644 index 000000000000..86e212e39ceb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewClubBudgetsCommand.java @@ -0,0 +1,69 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CLUB_NAME; + +import java.util.List; + +import seedu.address.logic.CommandHistory; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.budgetelements.ClubName; +import seedu.address.model.clubbudget.FinalClubBudget; + +/** + * Lists all persons in the address book to the user. + */ +public class ViewClubBudgetsCommand extends Command { + + public static final String COMMAND_WORD = "viewbudget"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows the club's budget. " + + "Parameters: " + + PREFIX_CLUB_NAME + "CLUB NAME " + + "Example: " + COMMAND_WORD + " " + + PREFIX_CLUB_NAME + "Computing Club "; + + public static final String MESSAGE_SUCCESS = "Club budget is: %1$s"; + public static final String MESSAGE_INVALID_CLUB = "This club's budget does not exist in the address book"; + + private final ClubName toShow; + + /** + * Creates a ViewClubBudgetCommand to view the specified {@code FinalClubBudget} + */ + public ViewClubBudgetsCommand(ClubName club) { + requireNonNull(club); + toShow = club; + } + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + /**if (!(model.hasClubBudget(toShow))) { + throw new CommandException(MESSAGE_INVALID_CLUB); + }*/ + + + List listOfBudgets = model.getFilteredClubBudgetsList(); + + int i; + + String budgetToShow = null; + + for (i = 0; i < listOfBudgets.size(); i++) { + + FinalClubBudget currentBudget = listOfBudgets.get(i); + + if (currentBudget.getClubName().equals(toShow)) { + budgetToShow = Integer.toString(currentBudget.getAllocatedBudget()); + System.out.println("the budget is " + budgetToShow); + return new CommandResult(String.format(MESSAGE_SUCCESS, budgetToShow)); + } + } + return new CommandResult(String.format(MESSAGE_SUCCESS, budgetToShow)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/formatter/KeywordsOutputFormatter.java b/src/main/java/seedu/address/logic/commands/formatter/KeywordsOutputFormatter.java new file mode 100644 index 000000000000..af4b52f35176 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/formatter/KeywordsOutputFormatter.java @@ -0,0 +1,105 @@ +package seedu.address.logic.commands.formatter; + +import seedu.address.model.searchhistory.KeywordType; +import seedu.address.model.searchhistory.ReadOnlyKeywordsRecord; + +/** + * A class that creates output strings of keywords according to KeywordsRecord + */ +public class KeywordsOutputFormatter { + + private static final int MAX_STRING_WIDTH = 100; + private static final int EXTRA_PADDING_LENGTH = 2; + private static final char INCLUDE_PREFIX = '+'; + private static final char EXCLUDE_PREFIX = '-'; + private static final String TAG_KEYWORDS_HEADING = "\n=============== Tag Keywords History ================\n"; + private static final String NAME_KEYWORDS_HEADING = "\n=============== Name Keywords History ===============\n"; + private static final String PREFIXED_KEYWORD_FORMAT = "%c%s "; + private StringBuffer outputString = new StringBuffer(); + private ReadOnlyKeywordsRecord record; + private int currentStringWidth = 0; + + public String getOutputString(ReadOnlyKeywordsRecord record) { + assert record != null; + this.record = record; + appendNameKeywordsToOutputString(); + appendTagKeywordsToOutputString(); + return outputString.toString(); + } + + /** + * Appends both included and excluded names to output string + */ + private void appendNameKeywordsToOutputString() { + if (hasNameKeywords()) { + currentStringWidth = 0; + outputString.append(NAME_KEYWORDS_HEADING); + for (String keyword: record.getKeywordSet(KeywordType.IncludeNames)) { + ensureStringWidthWithinLimit(); + appendIncludedKeyword(keyword); + recalculateCurrentStringWidth(keyword); + } + for (String keyword: record.getKeywordSet(KeywordType.ExcludeNames)) { + ensureStringWidthWithinLimit(); + appendExcludedKeyword(keyword); + recalculateCurrentStringWidth(keyword); + } + } + } + + /** + * Appends both included and excluded tags to output string + */ + private void appendTagKeywordsToOutputString() { + if (hasTagKeywords()) { + currentStringWidth = 0; + outputString.append(TAG_KEYWORDS_HEADING); + for (String keyword: record.getKeywordSet(KeywordType.IncludeTags)) { + ensureStringWidthWithinLimit(); + appendIncludedKeyword(keyword); + recalculateCurrentStringWidth(keyword); + } + for (String keyword: record.getKeywordSet(KeywordType.ExcludeTags)) { + ensureStringWidthWithinLimit(); + appendExcludedKeyword(keyword); + recalculateCurrentStringWidth(keyword); + } + } + } + + /** + * Starts outputString on a newline if its width is too long. + */ + private void ensureStringWidthWithinLimit() { + if (currentStringWidth >= MAX_STRING_WIDTH) { + outputString.append("\n"); + currentStringWidth = 0; + } + } + + private boolean hasTagKeywords() { + return !record.getKeywordSet(KeywordType.IncludeTags).isEmpty() + || !record.getKeywordSet(KeywordType.ExcludeTags).isEmpty(); + } + + private boolean hasNameKeywords() { + return !record.getKeywordSet(KeywordType.IncludeNames).isEmpty() + || !record.getKeywordSet(KeywordType.ExcludeNames).isEmpty(); + } + + private void appendIncludedKeyword(String keyword) { + String prefixedKeyword = String.format(PREFIXED_KEYWORD_FORMAT, INCLUDE_PREFIX, keyword); + outputString.append(prefixedKeyword); + } + + private void appendExcludedKeyword(String keyword) { + String prefixedKeyword = String.format(PREFIXED_KEYWORD_FORMAT, EXCLUDE_PREFIX, keyword); + outputString.append(prefixedKeyword); + } + + private void recalculateCurrentStringWidth(String keyword) { + if (keyword != null) { + currentStringWidth += keyword.length() + EXTRA_PADDING_LENGTH; + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e83..af7d0062c9e7 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -11,12 +11,15 @@ import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; + import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; import seedu.address.model.tag.Tag; /** @@ -42,9 +45,11 @@ public AddCommand parse(String args) throws ParseException { Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Skill skill = new Skill("BLANK SKILL"); // add command does not allow adding skills straight away + SkillLevel skillLevel = new SkillLevel(0); // TODO add skill functionality Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, skill, skillLevel, tagList); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddSkillCommandParser.java b/src/main/java/seedu/address/logic/parser/AddSkillCommandParser.java new file mode 100644 index 000000000000..0e6c1b4e2e7b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddSkillCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILL; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.AddSkillCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Skill; + +/** + * Parses input arguments and creates a new {@code AddSkillCommand} object + */ +public class AddSkillCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code AddSkillCommand} + * and returns a {@code AddSkillCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddSkillCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SKILL); + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddSkillCommand.MESSAGE_USAGE), ive); + } + String skill = argMultimap.getValue(PREFIX_SKILL).orElse(""); + return new AddSkillCommand(index, new Skill(skill)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddSkillLevelCommandParser.java b/src/main/java/seedu/address/logic/parser/AddSkillLevelCommandParser.java new file mode 100644 index 000000000000..c08c663ee2b7 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddSkillLevelCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SKILLLEVEL; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.AddSkillLevelCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; + +/** + * Parses input arguments and creates a new {@code AddSkillLevelCommand} object + */ +public class AddSkillLevelCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the {@code AddSkillLevelCommand} + * and returns a {@code AddSkillLevelCommand} object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddSkillLevelCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SKILLLEVEL, PREFIX_SKILL); + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddSkillLevelCommand.MESSAGE_USAGE), ive); + } + Skill skill = new Skill(argMultimap.getValue(PREFIX_SKILL).orElse("")); + SkillLevel level = new SkillLevel(Integer.parseInt(argMultimap.getValue(PREFIX_SKILLLEVEL).orElse(""))); + + return new AddSkillLevelCommand(index, skill, level); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b7d57f5db86a..dfd75d2a685a 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -6,9 +6,15 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import seedu.address.logic.LoginManager; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddSkillCommand; +import seedu.address.logic.commands.AddSkillLevelCommand; +import seedu.address.logic.commands.BudgetCalculationCommand; +import seedu.address.logic.commands.BudgetCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CreateAccountCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; @@ -16,9 +22,12 @@ import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoginUserIdPasswordRoleCommand; import seedu.address.logic.commands.RedoCommand; import seedu.address.logic.commands.SelectCommand; import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.UndoFindCommand; +import seedu.address.logic.commands.ViewClubBudgetsCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -47,6 +56,14 @@ public Command parseCommand(String userInput) throws ParseException { final String commandWord = matcher.group("commandWord"); final String arguments = matcher.group("arguments"); switch (commandWord) { + case LoginUserIdPasswordRoleCommand.COMMAND_WORD: + return new LoginUserIdPasswordRoleCommandParser().parse(arguments); + + case CreateAccountCommand.COMMAND_WORD: + if (!LoginManager.getIsCurrentlyTesting()) { + LoginManager.setIsCurrentlyCreatingAccount(true); + } + return new CreateAccountCommandParser().parse(arguments); case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); @@ -84,9 +101,26 @@ public Command parseCommand(String userInput) throws ParseException { case RedoCommand.COMMAND_WORD: return new RedoCommand(); + case AddSkillCommand.COMMAND_WORD: + return new AddSkillCommandParser().parse(arguments); + + case AddSkillLevelCommand.COMMAND_WORD: + return new AddSkillLevelCommandParser().parse(arguments); + + case UndoFindCommand.COMMAND_WORD: + return new UndoFindCommand(); + + case BudgetCommand.COMMAND_WORD: + return new BudgetCommandParser().parse(arguments); + + case BudgetCalculationCommand.COMMAND_WORD: + return new BudgetCalculationCommandParser().parse(arguments); + + case ViewClubBudgetsCommand.COMMAND_WORD: + return new ViewClubBudgetsCommandParser().parse(arguments); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } } - } diff --git a/src/main/java/seedu/address/logic/parser/BudgetCalculationCommandParser.java b/src/main/java/seedu/address/logic/parser/BudgetCalculationCommandParser.java new file mode 100644 index 000000000000..9cc7323425d2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/BudgetCalculationCommandParser.java @@ -0,0 +1,45 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TOTAL_BUDGET; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.BudgetCalculationCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.clubbudget.TotalBudget; + +/** + * Parses input arguments and creates a new BudgetCalculationCommand object + */ +public class BudgetCalculationCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the BudgetCalculationCommand + * and returns an BudgetCalculationCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public BudgetCalculationCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TOTAL_BUDGET); + + if (!arePrefixesPresent(argMultimap, PREFIX_TOTAL_BUDGET) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + BudgetCalculationCommand.MESSAGE_USAGE)); + } + + TotalBudget totalBudget = ParserUtil.parseTotalBudget(argMultimap.getValue(PREFIX_TOTAL_BUDGET).get()); + + return new BudgetCalculationCommand(totalBudget); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/BudgetCommandParser.java b/src/main/java/seedu/address/logic/parser/BudgetCommandParser.java new file mode 100644 index 000000000000..55772ec1e3cf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/BudgetCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CLUB_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EXPECTED_TURNOUT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NUMBER_OF_EVENTS; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.BudgetCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.budgetelements.ClubName; +import seedu.address.model.budgetelements.ExpectedTurnout; +import seedu.address.model.budgetelements.NumberOfEvents; + +/** + * Parses input arguments and creates a new BudgetCommand object + */ +public class BudgetCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the BudgetCommand + * and returns an BudgetCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public BudgetCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_CLUB_NAME, PREFIX_EXPECTED_TURNOUT, PREFIX_NUMBER_OF_EVENTS); + + if (!arePrefixesPresent(argMultimap, PREFIX_CLUB_NAME, PREFIX_EXPECTED_TURNOUT, PREFIX_NUMBER_OF_EVENTS) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, BudgetCommand.MESSAGE_USAGE)); + } + + ClubName clubname = ParserUtil.parseClubName(argMultimap.getValue(PREFIX_CLUB_NAME).get()); + ExpectedTurnout expectedturnout = ParserUtil.parseExpectedTurnout(argMultimap.getValue( + PREFIX_EXPECTED_TURNOUT).get()); + NumberOfEvents numberofevents = ParserUtil.parseNumberOfEvents(argMultimap.getValue( + PREFIX_NUMBER_OF_EVENTS).get()); + + ClubBudgetElements club = new ClubBudgetElements(clubname, expectedturnout, numberofevents); + + return new BudgetCommand(club); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..bbdf880333b6 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,10 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_SKILL = new Prefix("s/"); + public static final Prefix PREFIX_SKILLLEVEL = new Prefix("l/"); + public static final Prefix PREFIX_CLUB_NAME = new Prefix("c/"); + public static final Prefix PREFIX_EXPECTED_TURNOUT = new Prefix("t/"); + public static final Prefix PREFIX_NUMBER_OF_EVENTS = new Prefix("e/"); + public static final Prefix PREFIX_TOTAL_BUDGET = new Prefix("b/"); } diff --git a/src/main/java/seedu/address/logic/parser/CreateAccountCommandParser.java b/src/main/java/seedu/address/logic/parser/CreateAccountCommandParser.java new file mode 100644 index 000000000000..7ff3b90fbd56 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CreateAccountCommandParser.java @@ -0,0 +1,81 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.ParserUtil.parseUserId; +import static seedu.address.logic.parser.ParserUtil.parseUserPassword; +import static seedu.address.logic.parser.ParserUtil.parseUserRole; + +import java.io.UnsupportedEncodingException; +import java.util.StringTokenizer; + +import seedu.address.logic.commands.CreateAccountCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.login.LoginDetails; +import seedu.address.model.login.UserId; +import seedu.address.model.login.UserPassword; +import seedu.address.model.login.UserRole; + + +/** + * Parses input arguments and creates a new CreateAccountCommand object + */ +public class CreateAccountCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CreateAccountCommand + * and returns an CreateAccountCommand object for execution. + * @throws ParseException if the user input does not conform to the expected format + */ + public CreateAccountCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateAccountCommand.MESSAGE_USAGE)); + } + + String[] keywords = trimmedArgs.split("\\s+"); + LoginDetails details = extractLoginDetailsFromInput(keywords, args); + return new CreateAccountCommand(details); + } + + /** + * Constructs and returns a LoginDetails object from the input of user account credentials + * @param keywords string array representation of login user input + * @param args from input of user account credentials + * @return LoginDetails object constructed from parsed user input consisting of userId, userPassword and userRole + * @throws ParseException if the user input does not conform to the expected format + */ + public LoginDetails extractLoginDetailsFromInput(String[] keywords, String args) throws ParseException { + if (keywords.length != 3) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateAccountCommand.MESSAGE_USAGE)); + } else { + StringTokenizer st = new StringTokenizer(args); + UserId userId = null; + UserPassword userPassword = null; + UserRole userRole = null; + for (int i = 1; st.hasMoreTokens(); i++) { + if (i == 2) { + try { + userId = parseUserId(st.nextToken()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } else if (i == 3) { + try { + userPassword = parseUserPassword(st.nextToken()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } else if (i == 4) { + try { + userRole = parseUserRole(st.nextToken()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + } + return new LoginDetails(userId, userPassword, userRole); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index b186a967cb94..1b9779b1a9af 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -2,17 +2,25 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindNameSubCommand; +import seedu.address.logic.commands.FindTagSubCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object */ public class FindCommandParser implements Parser { + public static final String TAG_OPTION_STRING = "\\tag"; + public static final String EXCLUDE_OPTION_STRING = "\\exclude"; + /** * Parses the given {@code String} of arguments in the context of the FindCommand * and returns an FindCommand object for execution. @@ -20,14 +28,66 @@ public class FindCommandParser implements Parser { */ public FindCommand parse(String args) throws ParseException { String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + String[] keywords = trimmedArgs.split("\\s+"); + List keywordsList = new ArrayList(Arrays.asList(keywords)); + + if (!hasValidInputFormat(keywordsList)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + if (isExcludeTagSearch(keywordsList)) { + removesFirstTwoItemsFromKeywordsList(keywordsList); + return new FindTagSubCommand(new TagContainsKeywordsPredicate(keywordsList), true); + } else if (isIncludeTagSearch(keywordsList)) { + keywordsList.remove(0); + return new FindTagSubCommand(new TagContainsKeywordsPredicate(keywordsList)); + } else if (isExcludePersonSearch(keywordsList)) { + keywordsList.remove(0); + return new FindNameSubCommand(new NameContainsKeywordsPredicate(keywordsList), true); + } else { + return new FindNameSubCommand(new NameContainsKeywordsPredicate(keywordsList)); + } + } - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + private boolean isExcludePersonSearch(List keywordsList) { + return keywordsList.get(0).equals(EXCLUDE_OPTION_STRING); } + private boolean isIncludeTagSearch(List keywordsList) { + return keywordsList.size() >= 2 && keywordsList.get(0).equals(TAG_OPTION_STRING) + && !keywordsList.get(1).equals(EXCLUDE_OPTION_STRING); + } + + private boolean isExcludeTagSearch(List keywordsList) { + return keywordsList.size() >= 2 && keywordsList.get(0).equals(TAG_OPTION_STRING) + && keywordsList.get(1).equals(EXCLUDE_OPTION_STRING); + } + + /** + * Returns false if the user's input is in the incorrect format + */ + private boolean hasValidInputFormat(List keywordsList) { + if (keywordsList.get(0).equals(TAG_OPTION_STRING) && keywordsList.size() == 1) { + return false; + } else if (keywordsList.get(0).equals(TAG_OPTION_STRING) + && keywordsList.get(1).equals(EXCLUDE_OPTION_STRING) && keywordsList.size() == 2) { + return false; + } else if (keywordsList.get(0).equals(EXCLUDE_OPTION_STRING) && keywordsList.size() == 1) { + return false; + } + return true; + } + + private void removesFirstTwoItemsFromKeywordsList(List keywordsList) { + assert keywordsList.size() >= 2; + keywordsList.remove(1); + keywordsList.remove(0); + } } diff --git a/src/main/java/seedu/address/logic/parser/LoginUserIdPasswordRoleCommandParser.java b/src/main/java/seedu/address/logic/parser/LoginUserIdPasswordRoleCommandParser.java new file mode 100644 index 000000000000..802e47c58a43 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LoginUserIdPasswordRoleCommandParser.java @@ -0,0 +1,97 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.login.UserRole.MESSAGE_USERROLE_CONSTRAINTS; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import seedu.address.logic.LoginManager; +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.commands.LoginUserIdPasswordRoleCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.login.UserIdContainsKeywordsPredicate; +import seedu.address.model.login.UserPasswordContainsKeywordsPredicate; +import seedu.address.model.login.UserRoleContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new LoginUserIdCommand object + */ +public class LoginUserIdPasswordRoleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoginUserIdPasswordRoleCommand + * and returns an LoginUserIdPasswordRoleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public LoginCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + + String[] keywords = trimmedArgs.split("\\s+"); + if (keywords.length < 3) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } else if (keywords.length > 3) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, LoginCommand.MESSAGE_USAGE)); + } + String encryptedLoginId = null; + String encryptedLoginPassword = null; + String encryptedLoginRole = null; + try { + encryptedLoginId = Base64.getEncoder().encodeToString(keywords[0].getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + try { + encryptedLoginPassword = Base64.getEncoder().encodeToString(keywords[1].getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + try { + encryptedLoginRole = Base64.getEncoder().encodeToString(keywords[2].getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + List keywordsList = new ArrayList<>(Arrays.asList(encryptedLoginId, + encryptedLoginPassword, encryptedLoginRole)); + return setRoleReturnLoginCommandObject(keywords, keywordsList); + } + + /** + * Sets the conditions for certain roles and returns a LoginUserIdPasswordRoleCommand object. + * @param keywords string array representation of user login input + * @param keywordsList array list representation of user login input + * @return a LoginUserIdPasswordRoleCommand object + */ + public LoginUserIdPasswordRoleCommand setRoleReturnLoginCommandObject( + String[] keywords, List keywordsList) throws ParseException { + switch(keywords[2]) { + case "member": + LoginManager.setIsMember(true); + LoginManager.setIsPresident(false); + LoginManager.setIsTreasurer(false); + break; + case "president": + LoginManager.setIsPresident(true); + LoginManager.setIsMember(false); + LoginManager.setIsTreasurer(false); + break; + case "treasurer": + LoginManager.setIsTreasurer(true); + LoginManager.setIsMember(false); + LoginManager.setIsPresident(false); + break; + default: + throw new ParseException(MESSAGE_USERROLE_CONSTRAINTS); + } + return new LoginUserIdPasswordRoleCommand(new UserIdContainsKeywordsPredicate(keywordsList), + new UserPasswordContainsKeywordsPredicate(keywordsList), + new UserRoleContainsKeywordsPredicate(keywordsList)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d6551ad8e3ff..d3ad7d7d7f88 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -1,6 +1,7 @@ package seedu.address.logic.parser; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -12,5 +13,5 @@ public interface Parser { * Parses {@code userInput} into a command and returns it. * @throws ParseException if {@code userInput} does not conform the expected format */ - T parse(String userInput) throws ParseException; + T parse(String userInput) throws ParseException, CommandException; } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..f75b6c2d0f1a 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,13 +2,22 @@ import static java.util.Objects.requireNonNull; +import java.io.UnsupportedEncodingException; import java.util.Collection; import java.util.HashSet; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.LoginManager; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.budgetelements.ClubName; +import seedu.address.model.budgetelements.ExpectedTurnout; +import seedu.address.model.budgetelements.NumberOfEvents; +import seedu.address.model.clubbudget.TotalBudget; +import seedu.address.model.login.UserId; +import seedu.address.model.login.UserPassword; +import seedu.address.model.login.UserRole; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -22,6 +31,51 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + /** + * Parses a {@code String userId} into a {@code UserId}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code userId} is invalid. + */ + public static UserId parseUserId(String userId) throws ParseException, UnsupportedEncodingException { + requireNonNull(userId); + String trimmeduserId = userId.trim(); + if (!UserId.isValidUserId(trimmeduserId) && !LoginManager.getIsCurrentlyTesting()) { + throw new ParseException(UserId.MESSAGE_USERID_CONSTRAINTS); + } + return new UserId(trimmeduserId); + } + + /** + * Parses a {@code String userPassword} into a {@code UserPassword}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code userPassword} is invalid. + */ + public static UserPassword parseUserPassword(String userPassword) throws ParseException, + UnsupportedEncodingException { + requireNonNull(userPassword); + String trimmeduserPassword = userPassword.trim(); + if (!UserPassword.isValidUserPassword(trimmeduserPassword) && !LoginManager.getIsCurrentlyTesting()) { + throw new ParseException(UserPassword.MESSAGE_USERPASSWORD_CONSTRAINTS); + } + return new UserPassword(trimmeduserPassword); + } + + /** + * Parses a {@code String userRole} into a {@code UserRole}. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the given {@code userRole} is invalid. + */ + public static UserRole parseUserRole(String userRole) throws ParseException, UnsupportedEncodingException { + requireNonNull(userRole); + String trimmeduserRole = userRole.trim(); + if (!UserRole.isValidUserRole(trimmeduserRole) && !LoginManager.getIsCurrentlyTesting()) { + throw new ParseException(UserRole.MESSAGE_USERROLE_CONSTRAINTS); + } + return new UserRole(trimmeduserRole); + } + /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. @@ -121,4 +175,64 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String clubname} into a {@code ClubName}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code clubname} is invalid. + */ + public static ClubName parseClubName(String clubname) throws ParseException { + requireNonNull(clubname); + String trimmedClubName = clubname.trim(); + if (!ClubName.isValidClubName(trimmedClubName)) { + throw new ParseException(ClubName.MESSAGE_CLUB_NAME_CONSTRAINTS); + } + return new ClubName(trimmedClubName); + } + + /** + * Parses a {@code String expectedturnout} into a {@code ExpectedTurnout}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code expectedturnout} is invalid. + */ + public static ExpectedTurnout parseExpectedTurnout(String expectedturnout) throws ParseException { + requireNonNull(expectedturnout); + String trimmedExpectedTurnout = expectedturnout.trim(); + if (!ExpectedTurnout.isValidExpectedTurnout(trimmedExpectedTurnout)) { + throw new ParseException(ExpectedTurnout.MESSAGE_EXPECTED_TURNOUT_CONSTRAINTS); + } + return new ExpectedTurnout(trimmedExpectedTurnout); + } + + /** + * Parses a {@code String numberofevents} into a {@code NumberOfEvents}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code numberofevents} is invalid. + */ + public static NumberOfEvents parseNumberOfEvents(String numberofevents) throws ParseException { + requireNonNull(numberofevents); + String trimmedNumberOfEvents = numberofevents.trim(); + if (!NumberOfEvents.isValidNumberOfEvents(trimmedNumberOfEvents)) { + throw new ParseException(NumberOfEvents.MESSAGE_NUMBER_OF_EVENTS_CONSTRAINTS); + } + return new NumberOfEvents(trimmedNumberOfEvents); + } + + /** + * Parses a {@code int totalBudget} into a {@code int}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code totalBudget} is invalid. + */ + public static TotalBudget parseTotalBudget(String totalBudget) throws ParseException { + requireNonNull(totalBudget); + String trimmedTotalBudget = totalBudget.trim(); + if (!TotalBudget.isValidTotalBudget(trimmedTotalBudget)) { + throw new ParseException(TotalBudget.MESSAGE_TOTAL_BUDGET_CONSTRAINTS); + } + return new TotalBudget(trimmedTotalBudget); + } } diff --git a/src/main/java/seedu/address/logic/parser/ViewClubBudgetsCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewClubBudgetsCommandParser.java new file mode 100644 index 000000000000..38b7a6404c75 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewClubBudgetsCommandParser.java @@ -0,0 +1,46 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_CLUB_NAME; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.ViewClubBudgetsCommand; + +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.budgetelements.ClubName; + +/** + * Parses input arguments and creates a new ViewClubBudgetsCommand object + */ +public class ViewClubBudgetsCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ViewClubBudgetsCommand + * and returns a ViewClubBudgetsCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewClubBudgetsCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_CLUB_NAME); + + if (!arePrefixesPresent(argMultimap, PREFIX_CLUB_NAME) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + ViewClubBudgetsCommand.MESSAGE_USAGE)); + } + + ClubName clubName = ParserUtil.parseClubName(argMultimap.getValue(PREFIX_CLUB_NAME).get()); + + return new ViewClubBudgetsCommand(clubName); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 7f85c8b9258b..70dffeade4c1 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -5,6 +5,10 @@ import java.util.List; import javafx.collections.ObservableList; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.budgetelements.UniqueClubsList; +import seedu.address.model.clubbudget.FinalClubBudget; +import seedu.address.model.clubbudget.UniqueClubBudgetList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; @@ -27,6 +31,16 @@ public class AddressBook implements ReadOnlyAddressBook { persons = new UniquePersonList(); } + private final UniqueClubsList clubs; + { + clubs = new UniqueClubsList(); + } + + private final UniqueClubBudgetList clubBudgets; + { + clubBudgets = new UniqueClubBudgetList(); + } + public AddressBook() {} /** @@ -93,6 +107,39 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Returns true if a club with the same identity as {@code club} exists in the address book. + */ + public boolean hasClub(ClubBudgetElements club) { + requireNonNull(club); + return clubs.contains(club); + } + + /** + * Adds a club to the address book. + * The club must not already exist in the address book. + */ + public void addClub(ClubBudgetElements c) { + clubs.add(c); + } + + /** + * Returns true if a club budget with the same identity as {@code clubBudget} exists in the address book. + */ + public boolean hasClubBudget(FinalClubBudget clubBudget) { + requireNonNull(clubBudget); + return clubBudgets.contains(clubBudget); + } + + /** + * Adds a club budget to the address book. + * The club budget must not already exist in the address book. + */ + public void addClubBudget(FinalClubBudget c) { + clubBudgets.add(c); + } + + //// util methods @Override @@ -106,6 +153,14 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getClubsList() { + return clubs.asUnmodifiableObservableList(); } + + @Override + public ObservableList getClubBudgetsList() { + return clubBudgets.asUnmodifiableObservableList(); } + @Override public boolean equals(Object other) { return other == this // short circuit if same object diff --git a/src/main/java/seedu/address/model/LoginBook.java b/src/main/java/seedu/address/model/LoginBook.java new file mode 100644 index 000000000000..ed2c08224bca --- /dev/null +++ b/src/main/java/seedu/address/model/LoginBook.java @@ -0,0 +1,91 @@ +package seedu.address.model; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.model.login.LoginDetails; +import seedu.address.model.login.UniqueAccountList; + +/** + * Wraps all data at the login-book level + * Duplicates are not allowed (by .isSameAccount comparison) + */ +public class LoginBook implements ReadOnlyLoginBook { + + private final UniqueAccountList accounts; + + public LoginBook() { + accounts = new UniqueAccountList(); + } + + /** + * Creates a LoginBook using the LoginDetails in the {@code toBeCopied} + */ + public LoginBook(ReadOnlyLoginBook toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the account list with {@code loginDetails}. + * {@code loginDetails} must not contain duplicate login details. + */ + public void setLoginDetails(List accounts) { + this.accounts.setLoginDetails(accounts); + } + + /** + * Resets the existing data of this {@code LoginBook} with {@code newData}. + */ + public void resetData(ReadOnlyLoginBook newData) { + requireNonNull(newData); + + setLoginDetails(newData.getLoginDetailsList()); + } + + //// login-level operations + + /** + * Returns true if an account with the same credentials as {@code loginDetails} exists in the login book. + */ + public boolean hasAccount(LoginDetails loginDetails) { + requireNonNull(loginDetails); + return accounts.contains(loginDetails); + } + + /** + * Adds an account to the login book. + * The account must not already exist in the login book. + */ + public void createAccount(LoginDetails l) { + accounts.add(l); + } + + //// util methods + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof LoginBook // instanceof handles nulls + && accounts.equals(((LoginBook) other).accounts)); + } + + @Override + public String toString() { + return accounts.asUnmodifiableObservableList().size() + " accounts"; + } + + @Override + public int hashCode() { + return accounts.hashCode(); + } + + @Override + public ObservableList getLoginDetailsList() { + return accounts.asUnmodifiableObservableList(); + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..467da6311728 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,9 +1,16 @@ package seedu.address.model; +import java.util.List; import java.util.function.Predicate; import javafx.collections.ObservableList; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.clubbudget.FinalClubBudget; +import seedu.address.model.login.LoginDetails; import seedu.address.model.person.Person; +import seedu.address.model.searchhistory.KeywordType; +import seedu.address.model.searchhistory.ReadOnlyKeywordsRecord; +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; /** * The API of the Model component. @@ -12,9 +19,28 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_ACCOUNTS = unused -> true; + + Predicate PREDICATE_SHOW_ALL_CLUBS = unused -> true; + + Predicate PREDICATE_SHOW_ALL_CLUB_BUDGETS = unused -> true; + /** + * Creates an account for address book. + * The account must not already exist in the address book. + */ + void createAccount(LoginDetails loginDetails); + + /** + * Returns true if an account with the same user ID as {@code account} exists in the address book. + */ + boolean hasAccount(LoginDetails credentials); + /** Clears existing backing model and replaces with the provided new data. */ void resetData(ReadOnlyAddressBook newData); + /** Returns the LoginBook */ + ReadOnlyLoginBook getLoginBook(); + /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); @@ -42,14 +68,31 @@ public interface Model { */ void updatePerson(Person target, Person editedPerson); + /** Returns an unmodifiable view of the filtered login details list */ + ObservableList getFilteredLoginDetailsList(); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * Returns an unmodifiable view of the filtered clubs list */ + ObservableList getFilteredClubsList(); + + /** + * Returns an unmodifiable view of the filtered club budgets list */ + ObservableList getFilteredClubBudgetsList(); + + /** + * Updates the filter of the filtered login details list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredLoginDetailsList(Predicate predicate); + + /** + * Updates the filter of the filtered club budgets list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ - void updateFilteredPersonList(Predicate predicate); + void updateFilteredClubBudgetsList(Predicate predicate); /** * Returns true if the model has previous address book states to restore. @@ -75,4 +118,51 @@ public interface Model { * Saves the current address book state for undo/redo. */ void commitAddressBook(); + + /** + * Returns true if a club with the same identity as {@code club} exists in the address book. + */ + boolean hasClub(ClubBudgetElements club); + + /** + * Adds the given club. + * @code club} must not already exist in the address book. + */ + void addClub(ClubBudgetElements club); + + /** + * Returns true if a club budget with the same identity as {@code clubBudget} exists in the address book. + */ + boolean hasClubBudget(FinalClubBudget clubBudget); + + /** + * Adds the given club budget. + * @code clubBudget} must not already exist in the address book. + */ + void addClubBudget(FinalClubBudget clubBudget); + + /** + * Reverts model's filtered person list and search history to previous state. + */ + void revertLastSearch() throws EmptyHistoryException; + + /** + * Updates model's filtered person list and search history to the next state. + */ + void executeSearch(Predicate predicate); + + /** + * Resets all search history and returns filtered person list to initial state. + */ + void resetSearchHistoryToInitialState(); + + /** + * Records keywords into history. + */ + void recordKeywords(KeywordType type, List keywords); + + /** + * Returns only the readable version of KeywordsRecord. + */ + ReadOnlyKeywordsRecord getReadOnlyKeywordsRecord(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..54dd5833168b 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -12,7 +13,16 @@ 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.LoginBookChangedEvent; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.clubbudget.FinalClubBudget; +import seedu.address.model.login.LoginDetails; import seedu.address.model.person.Person; +import seedu.address.model.searchhistory.KeywordType; +import seedu.address.model.searchhistory.KeywordsRecord; +import seedu.address.model.searchhistory.ReadOnlyKeywordsRecord; +import seedu.address.model.searchhistory.SearchHistoryManager; +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; /** * Represents the in-memory model of the address book data. @@ -20,32 +30,48 @@ public class ModelManager extends ComponentManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private final SearchHistoryManager searchHistoryManager = new SearchHistoryManager<>(); + private final KeywordsRecord keywordsRecord = new KeywordsRecord(); + private final VersionedLoginBook versionedLoginBook; private final VersionedAddressBook versionedAddressBook; private final FilteredList filteredPersons; + private final FilteredList filteredLoginDetails; + private final FilteredList filteredClubs; + private final FilteredList filteredClubBudgets; /** * Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { + public ModelManager(ReadOnlyLoginBook loginBook, ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { super(); - requireAllNonNull(addressBook, userPrefs); + requireAllNonNull(loginBook, addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + versionedLoginBook = new VersionedLoginBook(loginBook); versionedAddressBook = new VersionedAddressBook(addressBook); filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + filteredClubs = new FilteredList<>(versionedAddressBook.getClubsList()); + filteredLoginDetails = new FilteredList<>(versionedLoginBook.getLoginDetailsList()); + filteredClubBudgets = new FilteredList<>(versionedAddressBook.getClubBudgetsList()); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new LoginBook(), new AddressBook(), new UserPrefs()); } @Override public void resetData(ReadOnlyAddressBook newData) { + resetSearchHistoryToInitialState(); versionedAddressBook.resetData(newData); indicateAddressBookChanged(); } + @Override + public ReadOnlyLoginBook getLoginBook() { + return versionedLoginBook; + } + @Override public ReadOnlyAddressBook getAddressBook() { return versionedAddressBook; @@ -56,6 +82,23 @@ private void indicateAddressBookChanged() { raise(new AddressBookChangedEvent(versionedAddressBook)); } + private void indicateLoginBookChanged() { + raise(new LoginBookChangedEvent(versionedLoginBook)); + } + + @Override + public void createAccount(LoginDetails details) { + versionedLoginBook.createAccount(details); + updateFilteredLoginDetailsList(PREDICATE_SHOW_ALL_ACCOUNTS); + indicateLoginBookChanged(); + } + + @Override + public boolean hasAccount(LoginDetails details) { + requireNonNull(details); + return versionedLoginBook.hasAccount(details); + } + @Override public boolean hasPerson(Person person) { requireNonNull(person); @@ -71,18 +114,58 @@ public void deletePerson(Person target) { @Override public void addPerson(Person person) { versionedAddressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + resetSearchHistoryToInitialState(); indicateAddressBookChanged(); } @Override public void updatePerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - versionedAddressBook.updatePerson(target, editedPerson); indicateAddressBookChanged(); } + //=========== Submitting Budget Data ===================================================================== + + @Override + public boolean hasClub(ClubBudgetElements club) { + requireNonNull(club); + return versionedAddressBook.hasClub(club); + } + + @Override + public void addClub(ClubBudgetElements club) { + versionedAddressBook.addClub(club); + indicateAddressBookChanged(); + } + + //=========== Filtered Account List Accessors ============================================================= + + @Override + public ObservableList getFilteredLoginDetailsList() { + return FXCollections.unmodifiableObservableList(filteredLoginDetails); + } + + @Override + public void updateFilteredLoginDetailsList(Predicate predicate) { + requireNonNull(predicate); + filteredLoginDetails.setPredicate(predicate); + } + + //=========== Getting Final Budget ===================================================================== + + @Override + public boolean hasClubBudget(FinalClubBudget clubBudget) { + requireNonNull(clubBudget); + return versionedAddressBook.hasClubBudget(clubBudget); + } + + @Override + public void addClubBudget(FinalClubBudget clubBudget) { + versionedAddressBook.addClubBudget(clubBudget); + indicateAddressBookChanged(); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -94,10 +177,32 @@ public ObservableList getFilteredPersonList() { return FXCollections.unmodifiableObservableList(filteredPersons); } + //=========== Filtered Clubs List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code ClubBudgetElements} backed by the internal list of + * {@code versionedAddressBook} + */ @Override - public void updateFilteredPersonList(Predicate predicate) { + public ObservableList getFilteredClubsList() { + return FXCollections.unmodifiableObservableList(filteredClubs); + } + + //=========== Filtered Club Budgets List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code FinalClubBudget} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public ObservableList getFilteredClubBudgetsList() { + return FXCollections.unmodifiableObservableList(filteredClubBudgets); + } + + @Override + public void updateFilteredClubBudgetsList(Predicate predicate) { requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + filteredClubBudgets.setPredicate(predicate); } //=========== Undo/Redo ================================================================================= @@ -129,6 +234,43 @@ public void commitAddressBook() { versionedAddressBook.commit(); } + //=========== Persons Search Pruning ==================================================== + @Override + public void revertLastSearch() throws EmptyHistoryException { + Predicate predicate = searchHistoryManager.revertLastSearch(); + if (predicate != null) { + filteredPersons.setPredicate(predicate); + } else { + filteredPersons.setPredicate(PREDICATE_SHOW_ALL_PERSONS); + } + keywordsRecord.undoKeywordsHistory(); + } + + @Override + public void executeSearch(Predicate predicate) { + requireNonNull(predicate); + Predicate updatedPredicate = searchHistoryManager.executeNewSearch(predicate); + filteredPersons.setPredicate(updatedPredicate); + } + + @Override + public void resetSearchHistoryToInitialState() { + searchHistoryManager.clearSearchHistory(); + filteredPersons.setPredicate(PREDICATE_SHOW_ALL_PERSONS); + keywordsRecord.clearKeywordsHistory(); + } + + @Override + public void recordKeywords(KeywordType type, List keywords) { + requireAllNonNull(keywords); + keywordsRecord.recordKeywords(type, keywords); + } + + @Override + public ReadOnlyKeywordsRecord getReadOnlyKeywordsRecord() { + return keywordsRecord; + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -144,7 +286,8 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; return versionedAddressBook.equals(other.versionedAddressBook) - && filteredPersons.equals(other.filteredPersons); + && filteredPersons.equals(other.filteredPersons) + && searchHistoryManager.equals(other.searchHistoryManager) + && keywordsRecord.equals(other.keywordsRecord); } - } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a290..366eca2c23d2 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,8 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.budgetelements.ClubBudgetElements; +import seedu.address.model.clubbudget.FinalClubBudget; import seedu.address.model.person.Person; /** @@ -13,5 +15,7 @@ public interface ReadOnlyAddressBook { * This list will not contain any duplicate persons. */ ObservableList getPersonList(); + ObservableList getClubsList(); + ObservableList getClubBudgetsList(); } diff --git a/src/main/java/seedu/address/model/ReadOnlyLoginBook.java b/src/main/java/seedu/address/model/ReadOnlyLoginBook.java new file mode 100644 index 000000000000..47b8b65f01a5 --- /dev/null +++ b/src/main/java/seedu/address/model/ReadOnlyLoginBook.java @@ -0,0 +1,16 @@ +package seedu.address.model; + +import javafx.collections.ObservableList; +import seedu.address.model.login.LoginDetails; + +/** + * Unmodifiable view of the login book + */ +public interface ReadOnlyLoginBook { + + /** + * Returns an unmodifiable view of the accounts list. + * This list will not contain any duplicate accounts. + */ + ObservableList getLoginDetailsList(); +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 980b2b388852..d9a96320f7aa 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -13,6 +13,7 @@ public class UserPrefs { private GuiSettings guiSettings; private Path addressBookFilePath = Paths.get("data" , "addressbook.xml"); + private Path loginBookFilePath = Paths.get("data" , "loginbook.xml"); public UserPrefs() { setGuiSettings(500, 500, 0, 0); @@ -30,6 +31,10 @@ public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } + public Path getLoginBookFilePath() { + return loginBookFilePath; + } + public Path getAddressBookFilePath() { return addressBookFilePath; } diff --git a/src/main/java/seedu/address/model/VersionedLoginBook.java b/src/main/java/seedu/address/model/VersionedLoginBook.java new file mode 100644 index 000000000000..3d5d6a9c5dac --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedLoginBook.java @@ -0,0 +1,41 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code LoginBook} that keeps track of its own history. + */ +public class VersionedLoginBook extends LoginBook { + + private final List loginBookStateList; + private int currentStatePointer; + + public VersionedLoginBook(ReadOnlyLoginBook initialState) { + super(initialState); + + loginBookStateList = new ArrayList<>(); + loginBookStateList.add(new LoginBook(initialState)); + currentStatePointer = 0; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedLoginBook)) { + return false; + } + + VersionedLoginBook otherVersionedLoginBook = (VersionedLoginBook) other; + + // state check + return super.equals(otherVersionedLoginBook) + && loginBookStateList.equals(otherVersionedLoginBook.loginBookStateList) + && currentStatePointer == otherVersionedLoginBook.currentStatePointer; + } +} diff --git a/src/main/java/seedu/address/model/budgetelements/ClubBudgetElements.java b/src/main/java/seedu/address/model/budgetelements/ClubBudgetElements.java new file mode 100644 index 000000000000..eb62e9c3255a --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/ClubBudgetElements.java @@ -0,0 +1,96 @@ +package seedu.address.model.budgetelements; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +/** + * Represents the budget elements of a club in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class ClubBudgetElements { + + // Identity fields + private final ClubName clubname; + + // Data fields + private final ExpectedTurnout expectedturnout; + private final NumberOfEvents numberofevents; + + + /** + * Every field must be present and not null. + */ + public ClubBudgetElements(ClubName clubname, ExpectedTurnout expectedturnout, NumberOfEvents numberofevents) { + requireAllNonNull(clubname, expectedturnout, numberofevents); + this.clubname = clubname; + this.expectedturnout = expectedturnout; + this.numberofevents = numberofevents; + } + + public ClubName getClubName() { + return clubname; + } + + public ExpectedTurnout getExpectedTurnout() { + return expectedturnout; + } + + public NumberOfEvents getNumberOfEvents() { + return numberofevents; + } + + + /** + * Returns true if both clubs have the same name + * This defines a weaker notion of equality between two persons. + */ + + + public boolean isSameClub(ClubBudgetElements otherClubBudgetElements) { + if (otherClubBudgetElements == this) { + return true; + } + + return otherClubBudgetElements != null + && otherClubBudgetElements.getClubName().equals(getClubName()); + } + + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ClubBudgetElements)) { + return false; + } + + ClubBudgetElements otherClubBudgetElements = (ClubBudgetElements) other; + return otherClubBudgetElements.getClubName().equals(getClubName()); + } + + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(clubname, expectedturnout, numberofevents); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClubName()) + .append(" Expected Turnout: ") + .append(getExpectedTurnout()) + .append(" Number of Events: ") + .append(getNumberOfEvents()); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/address/model/budgetelements/ClubName.java b/src/main/java/seedu/address/model/budgetelements/ClubName.java new file mode 100644 index 000000000000..57c9c3eb60fb --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/ClubName.java @@ -0,0 +1,60 @@ +package seedu.address.model.budgetelements; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Club's name in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidClubName(String)} + */ + +public class ClubName { + + public static final String MESSAGE_CLUB_NAME_CONSTRAINTS = + "Club names should only contain alphanumeric characters and it should not be blank"; + + /* + * The first character of the club name must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + + public static final String CLUB_NAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + + public final String clubName; + + /** + * Constructs a {@code Name}. + * + * @param clubname A valid club name. + */ + public ClubName(String clubname) { + requireNonNull(clubname); + checkArgument(isValidClubName(clubname), MESSAGE_CLUB_NAME_CONSTRAINTS); + clubName = clubname; + } + + /** + * Returns true if a given string is a valid club name. + */ + public static boolean isValidClubName(String test) { + return test.matches(CLUB_NAME_VALIDATION_REGEX); + } + + @Override + public String toString() { + return clubName; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ClubName // instanceof handles nulls + && clubName.equals(((ClubName) other).clubName)); // state check + } + + @Override + public int hashCode() { + return clubName.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/budgetelements/ExpectedTurnout.java b/src/main/java/seedu/address/model/budgetelements/ExpectedTurnout.java new file mode 100644 index 000000000000..f0b52f103105 --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/ExpectedTurnout.java @@ -0,0 +1,53 @@ +package seedu.address.model.budgetelements; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the expected number of attendees for every event a club is planning to have, in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidExpectedTurnout(String)} + */ + +public class ExpectedTurnout { + + public static final String MESSAGE_EXPECTED_TURNOUT_CONSTRAINTS = + "Expected turnout should only contain numbers, and it can be any number including zero"; + public static final String EXPECTED_TURNOUT_VALIDATION_REGEX = "\\d{1,}"; + public final String value; + + /** + * Constructs a {@code ExpectedTurnout}. + * + * @param expectedturnout A valid phone number. + */ + public ExpectedTurnout(String expectedturnout) { + requireNonNull(expectedturnout); + checkArgument(isValidExpectedTurnout(expectedturnout), MESSAGE_EXPECTED_TURNOUT_CONSTRAINTS); + value = expectedturnout; + } + + /** + * Returns true if a given string is a valid phone number. + */ + public static boolean isValidExpectedTurnout(String test) { + return test.matches(EXPECTED_TURNOUT_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpectedTurnout // instanceof handles nulls + && value.equals(((ExpectedTurnout) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/budgetelements/NumberOfEvents.java b/src/main/java/seedu/address/model/budgetelements/NumberOfEvents.java new file mode 100644 index 000000000000..72cae71ef34c --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/NumberOfEvents.java @@ -0,0 +1,53 @@ +package seedu.address.model.budgetelements; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the number of events a club is planning to have, in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidNumberOfEvents(String)} + */ + +public class NumberOfEvents { + + public static final String MESSAGE_NUMBER_OF_EVENTS_CONSTRAINTS = + "Number of events should only contain numbers, and it can be any number including zero"; + public static final String NUMBER_OF_EVENTS_VALIDATION_REGEX = "\\d{1,}"; + public final String value; + + /** + * Constructs a {@code NumberOfEvents}. + * + * @param numberofevents A valid phone number. + */ + public NumberOfEvents(String numberofevents) { + requireNonNull(numberofevents); + checkArgument(isValidNumberOfEvents(numberofevents), MESSAGE_NUMBER_OF_EVENTS_CONSTRAINTS); + value = numberofevents; + } + + /** + * Returns true if a given string is a valid phone number. + */ + public static boolean isValidNumberOfEvents(String test) { + return test.matches(NUMBER_OF_EVENTS_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NumberOfEvents // instanceof handles nulls + && value.equals(((NumberOfEvents) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/budgetelements/UniqueClubsList.java b/src/main/java/seedu/address/model/budgetelements/UniqueClubsList.java new file mode 100644 index 000000000000..a303df76c3fe --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/UniqueClubsList.java @@ -0,0 +1,85 @@ +package seedu.address.model.budgetelements; + +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.budgetelements.exceptions.DuplicateClubException; + +/** + * A list of clubs that enforces uniqueness between its elements and does not allow nulls. + * A club is considered unique by comparing using {@code ClubBudgetElements#isSameClub(ClubBudgetElements)}. As such + * adding of clubs uses ClubBudgetElements#isSameClub(ClubBudgetElements) for equality so as to ensure that the club + * being added is unique in terms of identity in the UniqueClubsList. + * + * Supports a minimal set of list operations. + * + * @see ClubBudgetElements#isSameClub(ClubBudgetElements) + */ +public class UniqueClubsList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent club as the given argument. + */ + public boolean contains(ClubBudgetElements toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameClub); + } + + /** + * Adds a club to the list. + * The club must not already exist in the list. + */ + public void add(ClubBudgetElements toAdd) { + requireNonNull(toAdd); + + if (contains(toAdd)) { + throw new DuplicateClubException(); + } + + internalList.add(toAdd); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueClubsList // instanceof handles nulls + && internalList.equals(((UniqueClubsList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code persons} contains only unique persons. + */ + private boolean clubsAreUnique(List clubs) { + for (int i = 0; i < clubs.size() - 1; i++) { + for (int j = i + 1; j < clubs.size(); j++) { + if (clubs.get(i).isSameClub(clubs.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/budgetelements/exceptions/DuplicateClubException.java b/src/main/java/seedu/address/model/budgetelements/exceptions/DuplicateClubException.java new file mode 100644 index 000000000000..0c67c1fcd15f --- /dev/null +++ b/src/main/java/seedu/address/model/budgetelements/exceptions/DuplicateClubException.java @@ -0,0 +1,11 @@ +package seedu.address.model.budgetelements.exceptions; + +/** + * Signals that the operation will result in duplicate Clubs (Clubs are considered duplicates if they have the same + * identity). + */ +public class DuplicateClubException extends RuntimeException { + public DuplicateClubException() { + super("Operation would result in duplicate clubs"); + } +} diff --git a/src/main/java/seedu/address/model/clubbudget/FinalClubBudget.java b/src/main/java/seedu/address/model/clubbudget/FinalClubBudget.java new file mode 100644 index 000000000000..e43450af2c1d --- /dev/null +++ b/src/main/java/seedu/address/model/clubbudget/FinalClubBudget.java @@ -0,0 +1,86 @@ +package seedu.address.model.clubbudget; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.budgetelements.ClubName; + +/** + * Represents the final allocated budget of a club in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class FinalClubBudget { + // Identity fields + private final ClubName clubname; + + // Data fields + private final int allocatedBudget; + + /** + * Every field must be present and not null. + */ + public FinalClubBudget(ClubName clubname, int allocatedBudget) { + requireAllNonNull(clubname, allocatedBudget); + this.clubname = clubname; + this.allocatedBudget = allocatedBudget; + } + + public ClubName getClubName() { + return clubname; + } + + public int getAllocatedBudget() { + return allocatedBudget; + } + + /** + * Returns true if both persons of the same name have at least one other identity field that is the same. + * This defines a weaker notion of equality between two persons. + */ + + + public boolean isSameFinalClubBudget(FinalClubBudget otherFinalClubBudget) { + if (otherFinalClubBudget == this) { + return true; + } + + return otherFinalClubBudget != null + && otherFinalClubBudget.getClubName().equals(getClubName()); + } + + /** + * Returns true if both clubs have the same identity fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FinalClubBudget)) { + return false; + } + + FinalClubBudget otherFinalClubBudget = (FinalClubBudget) other; + return otherFinalClubBudget.getClubName().equals(getClubName()); + } + + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(clubname, allocatedBudget); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClubName()) + .append(" Final allocated budget: ") + .append(getAllocatedBudget()); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/address/model/clubbudget/TotalBudget.java b/src/main/java/seedu/address/model/clubbudget/TotalBudget.java new file mode 100644 index 000000000000..3ebead75b660 --- /dev/null +++ b/src/main/java/seedu/address/model/clubbudget/TotalBudget.java @@ -0,0 +1,51 @@ +package seedu.address.model.clubbudget; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents the Total Budget made available by the NUSSU Exco in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidTotalBudget(String)} + */ + +public class TotalBudget { + + public static final String MESSAGE_TOTAL_BUDGET_CONSTRAINTS = "Total budget should only contain numbers " + + "including zero. It should be given in SGD"; + public static final String TOTAL_BUDGET_VALIDATION_REGEX = "\\d{1,}"; + public final String value; + + /** + * Constructs a {@code TotalBudget}. + * @param totalBudget A valid total budget. + */ + public TotalBudget(String totalBudget) { + requireNonNull(totalBudget); + checkArgument(isValidTotalBudget(totalBudget), MESSAGE_TOTAL_BUDGET_CONSTRAINTS); + value = totalBudget; + } + + /** + * Returns true if a given string is a valid phone number. + */ + public static boolean isValidTotalBudget(String test) { + return test.matches(TOTAL_BUDGET_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TotalBudget // instanceof handles nulls + && value.equals(((TotalBudget) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/clubbudget/UniqueClubBudgetList.java b/src/main/java/seedu/address/model/clubbudget/UniqueClubBudgetList.java new file mode 100644 index 000000000000..032b635d0d5c --- /dev/null +++ b/src/main/java/seedu/address/model/clubbudget/UniqueClubBudgetList.java @@ -0,0 +1,82 @@ +package seedu.address.model.clubbudget; + +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * A list of final club budgets that enforces uniqueness between its elements and does not allow nulls. + * A club budget is considered unique by comparing using {@code FinalClubBudget#isSameFinalClubBudget(FinalClubBudge)}. + * + * Supports a minimal set of list operations. + * + * @see FinalClubBudget#isSameFinalClubBudget(FinalClubBudget) + */ +public class UniqueClubBudgetList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent club as the given argument. + */ + public boolean contains(FinalClubBudget toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameFinalClubBudget); + } + + /** + * Adds a club budget to the list. + * The club must not already exist in the list. + */ + public void add(FinalClubBudget toAdd) { + requireNonNull(toAdd); + + if (contains(toAdd)) { + //throw new DuplicateFinalClubBudgetException(); + } + + internalList.add(toAdd); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueClubBudgetList // instanceof handles nulls + && internalList.equals(((UniqueClubBudgetList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code persons} contains only clubs' budgets. + */ + private boolean clubsAreUnique(List clubBudgets) { + for (int i = 0; i < clubBudgets.size() - 1; i++) { + for (int j = i + 1; j < clubBudgets.size(); j++) { + if (clubBudgets.get(i).isSameFinalClubBudget(clubBudgets.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/clubbudget/exceptions/DuplicateFinalClubBudgetException.java b/src/main/java/seedu/address/model/clubbudget/exceptions/DuplicateFinalClubBudgetException.java new file mode 100644 index 000000000000..736ab3db8f93 --- /dev/null +++ b/src/main/java/seedu/address/model/clubbudget/exceptions/DuplicateFinalClubBudgetException.java @@ -0,0 +1,11 @@ +package seedu.address.model.clubbudget.exceptions; + +/** + * Signals that the operation will result in duplicate Club Budgets (Club budgets are considered duplicates if they have + * the same identity). + */ +public class DuplicateFinalClubBudgetException extends RuntimeException { + public DuplicateFinalClubBudgetException() { + super("Operation would result in duplicate club budgets"); + } +} diff --git a/src/main/java/seedu/address/model/login/LoginDetails.java b/src/main/java/seedu/address/model/login/LoginDetails.java new file mode 100644 index 000000000000..79f569815f2e --- /dev/null +++ b/src/main/java/seedu/address/model/login/LoginDetails.java @@ -0,0 +1,81 @@ +package seedu.address.model.login; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +/** + * Represents user's login credentials. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class LoginDetails { + private final UserId userId; + private final UserPassword userPassword; + private final UserRole userRole; + + /** + * Every field must be present and not null. + */ + public LoginDetails(UserId userId, UserPassword userPassword, UserRole userRole) { + requireAllNonNull(userId, userPassword, userRole); + this.userId = userId; + this.userPassword = userPassword; + this.userRole = userRole; + } + + public UserId getUserId() { + return userId; + } + + public UserPassword getUserPassword() { + return userPassword; + } + + public UserRole getUserRole() { + return userRole; + } + + /** + * Returns true if and only if both accounts are of the same user ID. + */ + public boolean isSameAccount(LoginDetails otherAccount) { + if (otherAccount == this) { + return true; + } + + return otherAccount != null && otherAccount.getUserId().equals(getUserId()); + } + + /** + * Returns true if both accounts have the same user ID. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof LoginDetails)) { + return false; + } + + LoginDetails otherAccount = (LoginDetails) other; + return otherAccount.getUserId().equals(getUserId()); + } + + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(userId, userPassword, userRole); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getUserId()) + .append(getUserPassword()) + .append(getUserRole()); + return builder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/login/UniqueAccountList.java b/src/main/java/seedu/address/model/login/UniqueAccountList.java new file mode 100644 index 000000000000..49cf04a05f80 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UniqueAccountList.java @@ -0,0 +1,96 @@ +package seedu.address.model.login; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.login.exceptions.DuplicateAccountException; + +/** + * A list of accounts that enforces uniqueness between its elements and does not allow nulls. + * An account is considered unique by comparing using {@code LoginDetails#isSameAccount(LoginDetails)}. + * As such, adding and updating of accounts uses LoginDetails#isSameAccount(LoginDetails) + * for equality so as to ensure that the account being added or updated is unique in terms of details + * in the UniqueAccountList. However, the removal of an account uses LoginDetails#equals(Object) so + * as to ensure that the account with exactly the same user ID and password will be removed. + * + * Supports a minimal set of list operations. + * + * @see LoginDetails#isSameAccount(LoginDetails) + */ +public class UniqueAccountList implements Iterable { + + private final ObservableList internalLoginList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent account as the given argument. + */ + public boolean contains(LoginDetails toCheck) { + requireNonNull(toCheck); + return internalLoginList.stream().anyMatch(toCheck::isSameAccount); + } + + /** + * Adds an account to the list. + * The account must not already exist in the list. + */ + public void add(LoginDetails toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateAccountException(); + } + internalLoginList.add(toAdd); + } + + /** + * Replaces the contents of this list with {@code loginDetails}. + * {@code loginDetails} must not contain duplicate accounts. + */ + public void setLoginDetails(List loginDetails) { + requireAllNonNull(loginDetails); + if (!loginDetailsAreUnique(loginDetails)) { + throw new DuplicateAccountException(); + } + + internalLoginList.setAll(loginDetails); + } + + @Override + public Iterator iterator() { + return internalLoginList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueAccountList // instanceof handles nulls + && internalLoginList.equals(((UniqueAccountList) other).internalLoginList)); + } + + @Override + public int hashCode() { + return internalLoginList.hashCode(); + } + + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalLoginList); + } + + /** + * Returns true if {@code loginDetails} contains only unique accounts. + */ + private boolean loginDetailsAreUnique(List loginDetails) { + for (int i = 0; i < loginDetails.size() - 1; i++) { + for (int j = i + 1; j < loginDetails.size(); j++) { + if (loginDetails.get(i).isSameAccount(loginDetails.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/login/UserId.java b/src/main/java/seedu/address/model/login/UserId.java new file mode 100644 index 000000000000..57497427b911 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserId.java @@ -0,0 +1,65 @@ +package seedu.address.model.login; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.io.UnsupportedEncodingException; +import java.util.Base64; + +import seedu.address.logic.LoginManager; + +/** + * Represents an account's user ID in the login book. + * Guarantees: immutable; is valid as declared in {@link #isValidUserId(String)} + */ +public class UserId { + + public static final String MESSAGE_USERID_CONSTRAINTS = "User ID should only contain 9 alphanumeric characters, " + + " with the first and last character, upper-case only alphabets, " + + "7 characters in between, all integers, and it should not be blank and not have any spaces"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String USERID_VALIDATION_REGEX = "[A-Z][0-9]{7}[A-Z]"; + + public final String fullUserId; + + /** + * Constructs a {@code UserId}. + * + * @param id A valid userid. + */ + public UserId(String id) throws UnsupportedEncodingException { + requireNonNull(id); + if (LoginManager.getIsCurrentlyCreatingAccount() && !LoginManager.getIsCurrentlyTesting()) { + checkArgument(isValidUserId(id), MESSAGE_USERID_CONSTRAINTS); + } + if (LoginManager.getIsCurrentlyCreatingAccount()) { + fullUserId = Base64.getEncoder().encodeToString(id.getBytes("utf-8")); + } else { + fullUserId = id; + } + + } + + /** + * Returns true if a given string is a valid user ID. + */ + public static boolean isValidUserId(String test) { + return test.matches(USERID_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserId // instanceof handles nulls + && fullUserId.equals(((UserId) other).fullUserId)); // state check + } + + @Override + public int hashCode() { + return fullUserId.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/login/UserIdContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/login/UserIdContainsKeywordsPredicate.java new file mode 100644 index 000000000000..a0f3fd979bd4 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserIdContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.login; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code LoginDetails}'s {@code UserId} matches the user ID given. + */ +public class UserIdContainsKeywordsPredicate implements Predicate { + + private List keywords; + + public UserIdContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(LoginDetails loginDetails) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(loginDetails.getUserId().fullUserId, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserIdContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((UserIdContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/login/UserPassword.java b/src/main/java/seedu/address/model/login/UserPassword.java new file mode 100644 index 000000000000..b58005f184f2 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserPassword.java @@ -0,0 +1,62 @@ +package seedu.address.model.login; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.io.UnsupportedEncodingException; +import java.util.Base64; + +import seedu.address.logic.LoginManager; + +/** + * Represents an account's user password in the login book. + * Guarantees: immutable; is valid as declared in {@link #isValidUserPassword(String)} + */ +public class UserPassword { + public static final String MESSAGE_USERPASSWORD_CONSTRAINTS = "User password is case-sensitive, and it " + + "should not be blank and not have any spaces"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String USERPASSWORD_VALIDATION_REGEX = "[\\S]*"; + + public final String fullUserPassword; + + /** + * Constructs a {@code UserPassword}. + * + * @param pass A valid user password. + */ + public UserPassword(String pass) throws UnsupportedEncodingException { + requireNonNull(pass); + if (LoginManager.getIsCurrentlyCreatingAccount() && !LoginManager.getIsCurrentlyTesting()) { + checkArgument(isValidUserPassword(pass), MESSAGE_USERPASSWORD_CONSTRAINTS); + } + if (LoginManager.getIsCurrentlyCreatingAccount()) { + fullUserPassword = Base64.getEncoder().encodeToString(pass.getBytes("utf-8")); + } else { + fullUserPassword = pass; + } + } + + /** + * Returns true if a given string is a valid user password. + */ + public static boolean isValidUserPassword(String test) { + return test.matches(USERPASSWORD_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserPassword // instanceof handles nulls + && fullUserPassword.equals(((UserPassword) other).fullUserPassword)); // state check + } + + @Override + public int hashCode() { + return fullUserPassword.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/login/UserPasswordContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/login/UserPasswordContainsKeywordsPredicate.java new file mode 100644 index 000000000000..d0cf5b90a9d2 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserPasswordContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.login; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code LoginDetails}'s {@code UserPassword} matches the input password given. + */ +public class UserPasswordContainsKeywordsPredicate implements Predicate { + + private List keywords; + + public UserPasswordContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(LoginDetails loginDetails) { + return keywords.stream().anyMatch(keyword -> + StringUtil.containsWordCheckCase(loginDetails.getUserPassword().fullUserPassword, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserPasswordContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((UserPasswordContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/login/UserRole.java b/src/main/java/seedu/address/model/login/UserRole.java new file mode 100644 index 000000000000..69b9619fd3ff --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserRole.java @@ -0,0 +1,63 @@ +package seedu.address.model.login; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.io.UnsupportedEncodingException; +import java.util.Base64; + +import seedu.address.logic.LoginManager; + +/** + * Represents an account's user role in the login book. + * Guarantees: immutable; is valid as declared in {@link #isValidUserRole(String)} + */ +public class UserRole { + + public static final String MESSAGE_USERROLE_CONSTRAINTS = "User role must be either member, president " + + "or treasurer."; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String USERROLE_VALIDATION_REGEX = "\\b(member|president|treasurer)\\b"; + + public final String fullUserRole; + + /** + * Constructs a {@code UserRole}. + * + * @param role A valid userrole. + */ + public UserRole(String role) throws UnsupportedEncodingException { + requireNonNull(role); + if (LoginManager.getIsCurrentlyCreatingAccount() && !LoginManager.getIsCurrentlyTesting()) { + checkArgument(isValidUserRole(role), MESSAGE_USERROLE_CONSTRAINTS); + } + if (LoginManager.getIsCurrentlyCreatingAccount()) { + fullUserRole = Base64.getEncoder().encodeToString(role.getBytes("utf-8")); + } else { + fullUserRole = role; + } + } + + /** + * Returns true if a given string is a valid user role. + */ + public static boolean isValidUserRole(String test) { + return test.matches(USERROLE_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserRole // instanceof handles nulls + && fullUserRole.equals(((UserRole) other).fullUserRole)); // state check + } + + @Override + public int hashCode() { + return fullUserRole.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/login/UserRoleContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/login/UserRoleContainsKeywordsPredicate.java new file mode 100644 index 000000000000..f90d4a72f7a8 --- /dev/null +++ b/src/main/java/seedu/address/model/login/UserRoleContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.login; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code LoginDetails}'s {@code UserRole} matches the input role given. + */ +public class UserRoleContainsKeywordsPredicate implements Predicate { + + private List keywords; + + public UserRoleContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(LoginDetails loginDetails) { + return keywords.stream().anyMatch(keyword -> + StringUtil.containsWordIgnoreCase(loginDetails.getUserRole().fullUserRole, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UserRoleContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((UserRoleContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/login/exceptions/DuplicateAccountException.java b/src/main/java/seedu/address/model/login/exceptions/DuplicateAccountException.java new file mode 100644 index 000000000000..ea68298dbb6b --- /dev/null +++ b/src/main/java/seedu/address/model/login/exceptions/DuplicateAccountException.java @@ -0,0 +1,11 @@ +package seedu.address.model.login.exceptions; + +/** + * Signals that the operation will result in duplicate LoginDetails (LoginDetails are considered duplicates if they + * have the same user ID). + */ +public class DuplicateAccountException extends RuntimeException { + public DuplicateAccountException() { + super("Operation would result in duplicate accounts"); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index c9b5868427ca..6313aed3b1d8 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -1,8 +1,13 @@ package seedu.address.model.person; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + import java.util.List; import java.util.function.Predicate; +import com.google.common.collect.ImmutableList; + import seedu.address.commons.util.StringUtil; /** @@ -15,6 +20,12 @@ public NameContainsKeywordsPredicate(List keywords) { this.keywords = keywords; } + public List getLowerCaseKeywords() { + return keywords.stream() + .map(String::toLowerCase) + .collect(collectingAndThen(toList(), ImmutableList::copyOf)); + } + @Override public boolean test(Person person) { return keywords.stream() diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 557a7a60cd51..3382551223b8 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -22,17 +22,22 @@ public class Person { // Data fields private final Address address; + private final Skill skill; + private final SkillLevel skillLevel; private final Set tags = new HashSet<>(); /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { + public Person(Name name, Phone phone, Email email, Address address, Skill skill, + SkillLevel skillLevel, Set tags) { requireAllNonNull(name, phone, email, address, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.skill = skill; + this.skillLevel = skillLevel; this.tags.addAll(tags); } @@ -52,6 +57,13 @@ public Address getAddress() { return address; } + public Skill getSkill() { + return skill; + } + + public SkillLevel getSkillLevel() { + return skillLevel; } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -66,7 +78,7 @@ public Set getTags() { */ public boolean isSamePerson(Person otherPerson) { if (otherPerson == this) { - return true; + return true; //memory when you compare between 2 objects } return otherPerson != null diff --git a/src/main/java/seedu/address/model/person/Skill.java b/src/main/java/seedu/address/model/person/Skill.java new file mode 100644 index 000000000000..45f8d17c3a3b --- /dev/null +++ b/src/main/java/seedu/address/model/person/Skill.java @@ -0,0 +1,27 @@ +package seedu.address.model.person; +import static java.util.Objects.requireNonNull; +/** + * Represents a Person's skill in the address book. + * Guarantees: immutable; is always valid + */ +public class Skill { + public final String value; + public Skill(String skill) { + requireNonNull(skill); + value = skill; + } + @Override + public String toString() { + return value; + } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Skill // instanceof handles nulls + && value.equals(((Skill) other).value)); // state check + } + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/SkillLevel.java b/src/main/java/seedu/address/model/person/SkillLevel.java new file mode 100644 index 000000000000..f0baf22551cd --- /dev/null +++ b/src/main/java/seedu/address/model/person/SkillLevel.java @@ -0,0 +1,33 @@ +package seedu.address.model.person; + +// import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Skill's Level in the address book. + * Guarantees: immutable; is always valid + */ + +public class SkillLevel { + + public final int skillLevel; + + public SkillLevel(int skillLevel) { + this.skillLevel = skillLevel; + } + + public boolean isValidSkillLevel() { + return skillLevel >= 0 && skillLevel <= 100; + } + + @Override + public String toString() { + return Integer.toString(skillLevel); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SkillLevel // instanceof handles nulls + && skillLevel == ((SkillLevel) other).skillLevel); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java new file mode 100644 index 000000000000..c075542b39df --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java @@ -0,0 +1,61 @@ +package seedu.address.model.person; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.collect.ImmutableList; + +import seedu.address.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + public List getLowerCaseKeywords() { + return keywords.stream() + .map(String::toLowerCase) + .collect(collectingAndThen(toList(), ImmutableList::copyOf)); + } + + @Override + public boolean test(Person person) { + HashSet tagsAsString = retrievePersonTagsAsString(person); + for (String s:keywords) { + if (tagsAsString.contains(s.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Returns a HashSet of String representing a {@code Person}'s Tags + */ + private HashSet retrievePersonTagsAsString(Person person) { + HashSet tagsAsString = new HashSet<>(); + Set tags = person.getTags(); + for (Tag tag:tags) { + tagsAsString.add(tag.toSearchString().toLowerCase()); + } + return tagsAsString; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/searchhistory/KeywordType.java b/src/main/java/seedu/address/model/searchhistory/KeywordType.java new file mode 100644 index 000000000000..f4e135bab1d9 --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/KeywordType.java @@ -0,0 +1,8 @@ +package seedu.address.model.searchhistory; + +/** + * A set of pre-defined keyword types used in searching + */ +public enum KeywordType { + IncludeNames, ExcludeNames, IncludeTags, ExcludeTags; +} diff --git a/src/main/java/seedu/address/model/searchhistory/KeywordsBundle.java b/src/main/java/seedu/address/model/searchhistory/KeywordsBundle.java new file mode 100644 index 000000000000..cb1e4ab33606 --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/KeywordsBundle.java @@ -0,0 +1,40 @@ +package seedu.address.model.searchhistory; + +import java.util.List; + +/** + * A utility class to store the list of keywords and its type. + */ +public class KeywordsBundle { + protected KeywordType type; + protected List keywords; + + public KeywordsBundle (KeywordType type, List keywords) { + assert type != null; + assert keywords != null; + this.type = type; + this.keywords = keywords; + } + + public KeywordType getType() { + return type; + } + + public List getKeywords() { + return keywords; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof KeywordsBundle)) { + return false; + } + + KeywordsBundle other = (KeywordsBundle) obj; + return type == other.type && keywords.equals(other.keywords); + } +} diff --git a/src/main/java/seedu/address/model/searchhistory/KeywordsHistoryStack.java b/src/main/java/seedu/address/model/searchhistory/KeywordsHistoryStack.java new file mode 100644 index 000000000000..ef452570e3fd --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/KeywordsHistoryStack.java @@ -0,0 +1,71 @@ +package seedu.address.model.searchhistory; + +import java.util.EmptyStackException; +import java.util.List; +import java.util.Stack; + +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; + +/** + * Represents the in-app memory of all keywords and their types that were previously executed. + */ +public class KeywordsHistoryStack { + + protected Stack keywordsBundlesStack = new Stack<>(); + + /** + * Creates a new KeywordsBundle object according to the parameter and add it + * to the top of the stack + */ + public void push(KeywordType type, List keywords) { + if (type == null || keywords == null) { + return; + } + KeywordsBundle bundle = new KeywordsBundle(type, keywords); + keywordsBundlesStack.push(bundle); + } + + /** + * Adds a new KeywordsBundle object to the top of the stack + */ + public void push(KeywordsBundle bundle) { + if (bundle == null) { + return; + } + keywordsBundlesStack.push(bundle); + } + /** + * Returns a KeywordBundle object that was at the top of the stack + * and removes it from the stack. + * @throws EmptyHistoryException If search history is empty before revert. + */ + public KeywordsBundle pop() throws EmptyHistoryException { + try { + return keywordsBundlesStack.pop(); + } catch (EmptyStackException e) { + throw new EmptyHistoryException(); + } + } + + public void clear() { + keywordsBundlesStack.clear(); + } + + public boolean isEmpty() { + return keywordsBundlesStack.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof KeywordsHistoryStack)) { + return false; + } + + KeywordsHistoryStack other = (KeywordsHistoryStack) obj; + return keywordsBundlesStack.equals(other.keywordsBundlesStack); + } +} diff --git a/src/main/java/seedu/address/model/searchhistory/KeywordsRecord.java b/src/main/java/seedu/address/model/searchhistory/KeywordsRecord.java new file mode 100644 index 000000000000..3937372b8a92 --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/KeywordsRecord.java @@ -0,0 +1,99 @@ +package seedu.address.model.searchhistory; + +import java.util.HashMap; +import java.util.List; +import java.util.SortedSet; + +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; + +/** + * Represents the in-app memory of all keywords relevant to the current Persons search. + */ +public class KeywordsRecord implements ReadOnlyKeywordsRecord { + + protected HashMap map = new HashMap<>(); + + protected KeywordsHistoryStack historyStack = new KeywordsHistoryStack(); + + public KeywordsRecord() { + prepareKeywordSets(); + } + + /** + * Clears all keywords in memory. + */ + public void clearKeywordsHistory() { + map.clear(); + historyStack.clear(); + prepareKeywordSets(); + } + + /** + * Resets in-app memory of keywords back to its previous state. + */ + public void undoKeywordsHistory() throws EmptyHistoryException { + KeywordsBundle bundle = historyStack.pop(); + KeywordsSet set = map.get(bundle.getType()); + set.removeKeywordsFromSet(bundle.getKeywords()); + } + + /** + * Records keywords according to their type. + * + * @param type KeywordType. + * @param keywords list of words to be stored in memory. + */ + public void recordKeywords (KeywordType type, List keywords) { + if (type == null || keywords == null) { + return; + } + KeywordsSet set = map.get(type); + set.addKeywordsToSet(keywords); + updateHistoryStack(type, keywords); + } + + private void updateHistoryStack(KeywordType type, List keywords) { + assert type != null; + assert keywords != null; + historyStack.push(type, keywords); + } + + @Override + public SortedSet getKeywordSet(KeywordType type) { + KeywordsSet set = map.get(type); + return set.getUniqueKeywordsSet(); + } + + private void prepareKeywordSets() { + for (KeywordType type: KeywordType.values()) { + map.put(type, new KeywordsSet()); + } + } + + /** + * Returns true if no keywords are stored. + */ + public boolean isEmpty() { + for (KeywordsSet set: map.values()) { + if (!set.isEmpty()) { + return false; + } + } + return historyStack.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + + if (!(obj instanceof KeywordsRecord)) { + return false; + } + + KeywordsRecord other = (KeywordsRecord) obj; + return map.equals(other.map) && historyStack.equals(other.historyStack); + } +} diff --git a/src/main/java/seedu/address/model/searchhistory/KeywordsSet.java b/src/main/java/seedu/address/model/searchhistory/KeywordsSet.java new file mode 100644 index 000000000000..5f1fdf986733 --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/KeywordsSet.java @@ -0,0 +1,53 @@ +package seedu.address.model.searchhistory; + +import java.util.List; +import java.util.SortedSet; + +import com.google.common.collect.TreeMultiset; + +/** + * Represents the storage of keywords. + */ +public class KeywordsSet { + private TreeMultiset treeMultiSet; + + public KeywordsSet(TreeMultiset set) { + treeMultiSet = set; + } + + KeywordsSet() { + treeMultiSet = TreeMultiset.create(); + } + + void addKeywordsToSet(List keywords) { + treeMultiSet.addAll(keywords); + } + + void removeKeywordsFromSet(List keywords) { + for (String keyword : keywords) { + treeMultiSet.remove(keyword); + } + } + + boolean isEmpty() { + return treeMultiSet.isEmpty(); + } + + SortedSet getUniqueKeywordsSet() { + return treeMultiSet.elementSet(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof KeywordsSet)) { + return false; + } + + KeywordsSet other = (KeywordsSet) obj; + return treeMultiSet.equals(other.treeMultiSet); + } +} diff --git a/src/main/java/seedu/address/model/searchhistory/ReadOnlyKeywordsRecord.java b/src/main/java/seedu/address/model/searchhistory/ReadOnlyKeywordsRecord.java new file mode 100644 index 000000000000..5c3d178b85ae --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/ReadOnlyKeywordsRecord.java @@ -0,0 +1,10 @@ +package seedu.address.model.searchhistory; + +import java.util.SortedSet; + +/** + * An interface for KeywordsRecord that only allows the retrieval of data. + */ +public interface ReadOnlyKeywordsRecord { + SortedSet getKeywordSet(KeywordType type); +} diff --git a/src/main/java/seedu/address/model/searchhistory/SearchHistoryManager.java b/src/main/java/seedu/address/model/searchhistory/SearchHistoryManager.java new file mode 100644 index 000000000000..00ca51f778df --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/SearchHistoryManager.java @@ -0,0 +1,96 @@ +package seedu.address.model.searchhistory; + +import java.util.EmptyStackException; +import java.util.Stack; +import java.util.function.Predicate; + +import seedu.address.model.searchhistory.exceptions.EmptyHistoryException; + +/** + * Represents in-memory model for Predicates containing system search logic. + */ +public class SearchHistoryManager { + + protected Stack> searchHistoryStack = new Stack<>(); + private final Predicate emptyStackPredicate = predicate -> true; + + /** Returns Predicate at top of search history stack if stack is non-empty. + * If search history stack is empty, a predicate that defaults to true is returned. + * @return a Predicate containing the system search logic. + */ + private Predicate retrievePredicateAtTopOfStack() { + if (!searchHistoryStack.empty()) { + return searchHistoryStack.peek(); + } else { + return emptyStackPredicate; + } + } + + /** Updates system search logic to its next state given a Predicate containing user-defined search logic. + * @param newPredicate a Predicate containing user-defined search logic. + **/ + private void addNewPredicateToStack(Predicate newPredicate) { + if (newPredicate == null) { + return; + } + if (searchHistoryStack.isEmpty()) { + searchHistoryStack.push(newPredicate); + } else { + Predicate predicate = retrievePredicateAtTopOfStack(); + Predicate updatedPredicate = predicate.and(newPredicate); + searchHistoryStack.push(updatedPredicate); + } + } + + private void removeLastPredicateFromStack() { + searchHistoryStack.pop(); + } + + /** Returns a Predicate containing system search logic after reverting to its previous state. + * If search history is empty after revert, a predicate that defaults to true is returned. + * @return a Predicate containing the system search logic after reverting. + * @throws EmptyHistoryException If search history is empty before revert. + */ + public Predicate revertLastSearch() throws EmptyHistoryException { + try { + removeLastPredicateFromStack(); + } catch (EmptyStackException e) { + throw new EmptyHistoryException(); + } + return retrievePredicateAtTopOfStack(); + } + + /** Returns a Predicate containing system search logic given a user-defined search logic. + * @param predicate a Predicate containing the user-defined search logic. + * @return a Predicate containing the system search logic. + **/ + public Predicate executeNewSearch(Predicate predicate) { + addNewPredicateToStack(predicate); + return retrievePredicateAtTopOfStack(); + } + + /** + * Returns true if search history is empty. + */ + public boolean isEmpty() { + return searchHistoryStack.empty(); + } + + public void clearSearchHistory() { + searchHistoryStack.clear(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof SearchHistoryManager)) { + return false; + } + + SearchHistoryManager other = (SearchHistoryManager) obj; + return searchHistoryStack.size() == other.searchHistoryStack.size(); + } +} diff --git a/src/main/java/seedu/address/model/searchhistory/exceptions/EmptyHistoryException.java b/src/main/java/seedu/address/model/searchhistory/exceptions/EmptyHistoryException.java new file mode 100644 index 000000000000..a142c4837f73 --- /dev/null +++ b/src/main/java/seedu/address/model/searchhistory/exceptions/EmptyHistoryException.java @@ -0,0 +1,6 @@ +package seedu.address.model.searchhistory.exceptions; + +/** + * Signals that an illegal operation has been performed on an empty search history + */ +public class EmptyHistoryException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index 8cdff2773ac9..b6d84647ed43 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -51,4 +51,11 @@ public String toString() { return '[' + tagName + ']'; } + /** + * Format state as text for searching. + */ + public String toSearchString() { + return tagName; + } + } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..dee1666392e7 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,45 +1,76 @@ package seedu.address.model.util; +import java.io.UnsupportedEncodingException; import java.util.Arrays; +import java.util.Base64; import java.util.Set; import java.util.stream.Collectors; import seedu.address.model.AddressBook; +import seedu.address.model.LoginBook; import seedu.address.model.ReadOnlyAddressBook; + +import seedu.address.model.ReadOnlyLoginBook; +import seedu.address.model.login.LoginDetails; +import seedu.address.model.login.UserId; +import seedu.address.model.login.UserPassword; +import seedu.address.model.login.UserRole; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Skill; + +import seedu.address.model.person.SkillLevel; import seedu.address.model.tag.Tag; /** - * Contains utility methods for populating {@code AddressBook} with sample data. + * Contains utility methods for populating {@code AddressBook} and {@code LoginBook} with sample data. */ public class SampleDataUtil { + public static final Skill EMPTY_SKILL = new Skill(""); + public static final SkillLevel EMPTY_SKILLLEVEL = new SkillLevel(0); public static Person[] getSamplePersons() { 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 Geylang Street 29, #06-40"), new Skill("Photography"), new SkillLevel(90), 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"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new Skill("Videography"), new SkillLevel(50), getTagSet("colleagues", "friends")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), new Skill("Design"), new SkillLevel(45), getTagSet("neighbours")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), new Skill("Photoshop"), new SkillLevel(50), getTagSet("family")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), + new Address("Blk 47 Tampines Street 20, #17-35"), new Skill("Stage Managing"), new SkillLevel(30), getTagSet("classmates")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), + new Address("Blk 45 Aljunied Street 85, #11-31"), new Skill("Public Speaking"), new SkillLevel(80), getTagSet("colleagues")) }; } + public static LoginDetails getSampleLoginDetails() { + LoginDetails loginDetails = null; + String encryptedLoginId = null; + String encryptedLoginPassword = null; + String encryptedLoginRole = null; + try { + encryptedLoginId = Base64.getEncoder().encodeToString("A1234567M".getBytes("utf-8")); + encryptedLoginPassword = Base64.getEncoder().encodeToString("zaq1xsw2cde3".getBytes("utf-8")); + encryptedLoginRole = Base64.getEncoder().encodeToString("president".getBytes("utf-8")); + loginDetails = new LoginDetails(new UserId(encryptedLoginId), + new UserPassword(encryptedLoginPassword), new UserRole(encryptedLoginRole)); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return loginDetails; + } + public static ReadOnlyAddressBook getSampleAddressBook() { AddressBook sampleAb = new AddressBook(); for (Person samplePerson : getSamplePersons()) { @@ -48,6 +79,12 @@ public static ReadOnlyAddressBook getSampleAddressBook() { return sampleAb; } + public static ReadOnlyLoginBook getSampleLoginBook() { + LoginBook sampleLb = new LoginBook(); + sampleLb.createAccount(getSampleLoginDetails()); + return sampleLb; + } + /** * Returns a tag set containing the list of strings given. */ diff --git a/src/main/java/seedu/address/storage/LoginBookStorage.java b/src/main/java/seedu/address/storage/LoginBookStorage.java new file mode 100644 index 000000000000..4000cc105a57 --- /dev/null +++ b/src/main/java/seedu/address/storage/LoginBookStorage.java @@ -0,0 +1,44 @@ +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.ReadOnlyLoginBook; + +/** + * Represents a storage for {@link seedu.address.model.LoginBook}. + */ +public interface LoginBookStorage { + + /** + * Returns the file path of the data file. + */ + Path getLoginBookFilePath(); + + /** + * Returns LoginBook data as a {@link ReadOnlyLoginBook}. + * 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 readLoginBook() throws DataConversionException, IOException; + + /** + * @see #getLoginBookFilePath() + */ + Optional readLoginBook(Path loginFilePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyLoginBook} to the storage. + * @param loginBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveLoginBook(ReadOnlyLoginBook loginBook) throws IOException; + + /** + * @see #saveLoginBook(ReadOnlyLoginBook) + */ + void saveLoginBook(ReadOnlyLoginBook loginBook, Path loginFilePath) throws IOException; +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 28791127999b..762d2ca983df 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,15 +5,17 @@ import java.util.Optional; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.LoginBookChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLoginBook; import seedu.address.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends LoginBookStorage, AddressBookStorage, UserPrefsStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -21,19 +23,36 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override void saveUserPrefs(UserPrefs userPrefs) throws IOException; + @Override + Path getLoginBookFilePath(); + @Override Path getAddressBookFilePath(); + @Override + Optional readLoginBook() throws DataConversionException, IOException; + @Override Optional readAddressBook() throws DataConversionException, IOException; + @Override + void saveLoginBook(ReadOnlyLoginBook loginBook) throws IOException; + @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + /** + * Saves the current version of the Login Book to the hard disk. + * Creates the data file if it is missing. + * Raises {@link DataSavingExceptionEvent} if there was an error during saving. + */ + void handleLoginBookChangedEvent(LoginBookChangedEvent abce); + /** * Saves the current version of the Address Book to the hard disk. * Creates the data file if it is missing. * Raises {@link DataSavingExceptionEvent} if there was an error during saving. */ void handleAddressBookChangedEvent(AddressBookChangedEvent abce); + } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index b0df908a76a7..cc30d6794f9a 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -10,9 +10,11 @@ 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.LoginBookChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyLoginBook; import seedu.address.model.UserPrefs; /** @@ -21,12 +23,15 @@ public class StorageManager extends ComponentManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private LoginBookStorage loginBookStorage; private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(LoginBookStorage loginBookStorage, AddressBookStorage addressBookStorage, + UserPrefsStorage userPrefsStorage) { super(); + this.loginBookStorage = loginBookStorage; this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; } @@ -48,6 +53,45 @@ public void saveUserPrefs(UserPrefs userPrefs) throws IOException { userPrefsStorage.saveUserPrefs(userPrefs); } + // ================ LoginBook methods ============================== + + @Override + public Path getLoginBookFilePath() { + return loginBookStorage.getLoginBookFilePath(); + } + + @Override + public Optional readLoginBook() throws DataConversionException, IOException { + return readLoginBook(loginBookStorage.getLoginBookFilePath()); + } + + @Override + public Optional readLoginBook(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return loginBookStorage.readLoginBook(filePath); + } + + @Override + public void saveLoginBook(ReadOnlyLoginBook loginBook) throws IOException { + saveLoginBook(loginBook, loginBookStorage.getLoginBookFilePath()); + } + + @Override + public void saveLoginBook(ReadOnlyLoginBook loginBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + loginBookStorage.saveLoginBook(loginBook, filePath); + } + + @Override + @Subscribe + public void handleLoginBookChangedEvent(LoginBookChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); + try { + saveLoginBook(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } // ================ AddressBook methods ============================== diff --git a/src/main/java/seedu/address/storage/XmlAccount.java b/src/main/java/seedu/address/storage/XmlAccount.java new file mode 100644 index 000000000000..a89f09258f23 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAccount.java @@ -0,0 +1,99 @@ +package seedu.address.storage; + +import java.io.UnsupportedEncodingException; +import java.util.Objects; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.login.LoginDetails; +import seedu.address.model.login.UserId; +import seedu.address.model.login.UserPassword; +import seedu.address.model.login.UserRole; + +/** + * JAXB-friendly version of the Account. + */ +public class XmlAccount { + + public static final String MISSING_ACCOUNT_FIELD_MESSAGE_FORMAT = "LoginDetail's %s field is missing!"; + + @XmlElement(required = true) + private String userId; + @XmlElement(required = true) + private String userPassword; + @XmlElement(required = true) + private String userRole; + + /** + * Constructs an XmlAccount. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAccount() {} + + /** + * Constructs an {@code XmlAccount} with the given account details. + */ + public XmlAccount(String userId, String userPassword, String userRole) { + this.userId = userId; + this.userPassword = userPassword; + this.userRole = userRole; + } + + /** + * Converts a given LoginDetails into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAccount + */ + public XmlAccount(LoginDetails source) { + userId = source.getUserId().fullUserId; + userPassword = source.getUserPassword().fullUserPassword; + userRole = source.getUserRole().fullUserRole; + + } + + /** + * Converts this jaxb-friendly account object into the model's LoginDetails object. + * + * @throws IllegalValueException if there were any data constraints violated in the account + */ + public LoginDetails toModelType() throws IllegalValueException, UnsupportedEncodingException { + UserId modelUserId; + UserPassword modelUserPassword; + UserRole modelUserRole; + if (userId == null) { + throw new IllegalValueException(String.format(MISSING_ACCOUNT_FIELD_MESSAGE_FORMAT, + UserId.class.getSimpleName())); + } + modelUserId = new UserId(userId); + + if (userPassword == null) { + throw new IllegalValueException(String.format(MISSING_ACCOUNT_FIELD_MESSAGE_FORMAT, + UserPassword.class.getSimpleName())); + } + modelUserPassword = new UserPassword(userPassword); + if (userRole == null) { + throw new IllegalValueException(String.format(MISSING_ACCOUNT_FIELD_MESSAGE_FORMAT, + UserRole.class.getSimpleName())); + } + modelUserRole = new UserRole(userRole); + + return new LoginDetails(modelUserId, modelUserPassword, modelUserRole); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAccount)) { + return false; + } + + XmlAccount otherLoginDetails = (XmlAccount) other; + return Objects.equals(userId, otherLoginDetails.userId) + && Objects.equals(userPassword, otherLoginDetails.userPassword) + && Objects.equals(userRole, otherLoginDetails.userRole); + } +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java index c03785e5700f..84e02f8e9992 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java @@ -15,6 +15,8 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Skill; +import seedu.address.model.person.SkillLevel; import seedu.address.model.tag.Tag; /** @@ -32,6 +34,10 @@ public class XmlAdaptedPerson { private String email; @XmlElement(required = true) private String address; + @XmlElement(required = true) + private String skill; + @XmlElement(required = true) + private String skillLevel; @XmlElement private List tagged = new ArrayList<>(); @@ -65,6 +71,8 @@ public XmlAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + skill = source.getSkill().value; + skillLevel = Integer.toString(source.getSkillLevel().skillLevel); tagged = source.getTags().stream() .map(XmlAdaptedTag::new) .collect(Collectors.toList()); @@ -113,8 +121,11 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + final Skill modelSkill = new Skill(skill); + final SkillLevel modelSkillLevel = new SkillLevel(Integer.parseInt(skillLevel)); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelSkill, modelSkillLevel, modelTags); } @Override diff --git a/src/main/java/seedu/address/storage/XmlLoginBookStorage.java b/src/main/java/seedu/address/storage/XmlLoginBookStorage.java new file mode 100644 index 000000000000..04a641c110b8 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlLoginBookStorage.java @@ -0,0 +1,79 @@ +package seedu.address.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.ReadOnlyLoginBook; + +/** + * A class to access LoginBook data stored as an xml file on the hard disk. + */ +public class XmlLoginBookStorage implements LoginBookStorage { + + private static final Logger logger = LogsCenter.getLogger(XmlLoginBookStorage.class); + + private Path loginFilePath; + + public XmlLoginBookStorage(Path loginFilePath) { + this.loginFilePath = loginFilePath; + } + + public Path getLoginBookFilePath() { + return loginFilePath; + } + + @Override + public Optional readLoginBook() throws DataConversionException, IOException { + return readLoginBook(loginFilePath); + } + + /** + * Similar to {@link #readLoginBook()} + * @param loginFilePath location of the data. Cannot be null + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readLoginBook(Path loginFilePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(loginFilePath); + + if (!Files.exists(loginFilePath)) { + logger.info("LoginBook file " + loginFilePath + " not found"); + return Optional.empty(); + } + + XmlSerializableLoginBook xmlLoginBook = XmlLoginFileStorage.loadDataFromSaveFile(loginFilePath); + try { + return Optional.of(xmlLoginBook.toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + loginFilePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveLoginBook(ReadOnlyLoginBook loginBook) throws IOException { + saveLoginBook(loginBook, loginFilePath); + } + + /** + * Similar to {@link #saveLoginBook(ReadOnlyLoginBook)} + * @param loginFilePath location of the data. Cannot be null + */ + public void saveLoginBook(ReadOnlyLoginBook loginBook, Path loginFilePath) throws IOException { + requireNonNull(loginBook); + requireNonNull(loginFilePath); + + FileUtil.createIfMissing(loginFilePath); + XmlLoginFileStorage.saveDataToFile(loginFilePath, new XmlSerializableLoginBook(loginBook)); + } +} diff --git a/src/main/java/seedu/address/storage/XmlLoginFileStorage.java b/src/main/java/seedu/address/storage/XmlLoginFileStorage.java new file mode 100644 index 000000000000..1bd7bbe12944 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlLoginFileStorage.java @@ -0,0 +1,38 @@ +package seedu.address.storage; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import javax.xml.bind.JAXBException; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.XmlUtil; + +/** + * Stores loginbook data in an XML file + */ +public class XmlLoginFileStorage { + /** + * Saves the given loginbook data to the specified file. + */ + public static void saveDataToFile(Path file, XmlSerializableLoginBook loginBook) + throws FileNotFoundException { + try { + XmlUtil.saveDataToFile(file, loginBook); + } catch (JAXBException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } + + /** + * Returns login book in the file or an empty login book + */ + public static XmlSerializableLoginBook loadDataFromSaveFile(Path file) throws DataConversionException, + FileNotFoundException { + try { + return XmlUtil.getDataFromFile(file, XmlSerializableLoginBook.class); + } catch (JAXBException e) { + throw new DataConversionException(e); + } + } +} diff --git a/src/main/java/seedu/address/storage/XmlSerializableLoginBook.java b/src/main/java/seedu/address/storage/XmlSerializableLoginBook.java new file mode 100644 index 000000000000..ead1e3172ea0 --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlSerializableLoginBook.java @@ -0,0 +1,76 @@ +package seedu.address.storage; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.LoginBook; +import seedu.address.model.ReadOnlyLoginBook; +import seedu.address.model.login.LoginDetails; + +/** + * An Immutable LoginBook that is serializable to XML format + */ +@XmlRootElement(name = "loginbook") +public class XmlSerializableLoginBook { + + public static final String MESSAGE_DUPLICATE_ACCOUNT = "LoginDetails list contains duplicate account(s)."; + @XmlElement + private List accounts; + + /** + * Creates an empty XmlSerializableLoginBook. + * This empty constructor is required for marshalling. + */ + public XmlSerializableLoginBook() { + accounts = new ArrayList<>(); + } + + /** + * Conversion + */ + public XmlSerializableLoginBook(ReadOnlyLoginBook src) { + this(); + accounts.addAll(src.getLoginDetailsList().stream().map(XmlAccount::new).collect(Collectors.toList())); + } + + /** + * Converts this loginbook into the model's {@code LoginBook} object. + * + * @throws IllegalValueException if there were any data constraints violated or duplicates in the + * {@code XmlAccount}. + */ + public LoginBook toModelType() throws IllegalValueException { + LoginBook loginBook = new LoginBook(); + for (XmlAccount l : accounts) { + LoginDetails loginDetails = null; + try { + loginDetails = l.toModelType(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + if (loginBook.hasAccount(loginDetails)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_ACCOUNT); + } + loginBook.createAccount(loginDetails); + } + return loginBook; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializableLoginBook)) { + return false; + } + return accounts.equals(((XmlSerializableLoginBook) other).accounts); + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 3d7aaded5640..82867b347c52 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -99,15 +99,16 @@ private void replaceText(String text) { * Handles the Enter button pressed event. */ @FXML - private void handleCommandEntered() { + void handleCommandEntered() { try { CommandResult commandResult = logic.execute(commandTextField.getText()); + logger.info("Result: " + commandResult.feedbackToUser); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); + initHistory(); historySnapshot.next(); // process result of the command commandTextField.setText(""); - logger.info("Result: " + commandResult.feedbackToUser); - raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); } catch (CommandException | ParseException e) { initHistory(); diff --git a/src/main/java/seedu/address/ui/LoginWindow.java b/src/main/java/seedu/address/ui/LoginWindow.java new file mode 100644 index 000000000000..6619f707110c --- /dev/null +++ b/src/main/java/seedu/address/ui/LoginWindow.java @@ -0,0 +1,30 @@ +package seedu.address.ui; + +import javax.swing.JOptionPane; + +import seedu.address.logic.Logic; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Provides the UI for the login process. + */ +class LoginWindow { + + LoginWindow() {} + + /** + * method to kick-start the login process. + */ + void initializeLogin(Logic logic) { + String loginInput = JOptionPane.showInputDialog("Please login first by entering login credentials:"); + if (loginInput == null) { + System.exit(0); + } + try { + logic.execute(loginInput); + } catch (CommandException | ParseException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..01430e525fe9 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -18,6 +18,7 @@ import seedu.address.commons.events.ui.ExitAppRequestEvent; import seedu.address.commons.events.ui.ShowHelpRequestEvent; import seedu.address.logic.Logic; +import seedu.address.logic.LoginManager; import seedu.address.model.UserPrefs; /** @@ -115,24 +116,36 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { }); } + /** + * Starts a pop-up window which prompts the user for input of login credentials. + */ + private void initializeLoginProcess() { + LoginWindow loginWindow = new LoginWindow(); + loginWindow.initializeLogin(logic); + } + /** * Fills up all the placeholders of this window. */ void fillInnerParts() { + do { + initializeLoginProcess(); + } while(!(LoginManager.getIsLoginSuccessful())); + + CommandBox commandBox = new CommandBox(logic); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + ResultDisplay resultDisplay = new ResultDisplay(); + resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + browserPanel = new BrowserPanel(); browserPlaceholder.getChildren().add(browserPanel.getRoot()); personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); - ResultDisplay resultDisplay = new ResultDisplay(); - resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - - CommandBox commandBox = new CommandBox(logic); - commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } void hide() { diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..cc4fc8f54663 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -37,6 +37,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label skillPair; + @FXML private FlowPane tags; public PersonCard(Person person, int displayedIndex) { @@ -47,6 +49,9 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + //skill.setText(person.getSkill().value); + //skillLevel.setText(Integer.toString(person.getSkillLevel().skillLevel)); // TODO: Is there a better way? + skillPair.setText(person.getSkill().value + ": " + Integer.toString(person.getSkillLevel().skillLevel)); person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/address/ui/Ui.java index e6a67fe8c027..7103ea3a5af1 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/address/ui/Ui.java @@ -1,6 +1,8 @@ package seedu.address.ui; import javafx.stage.Stage; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; /** * API of UI component @@ -8,7 +10,7 @@ public interface Ui { /** Starts the UI (and the App). */ - void start(Stage primaryStage); + void start(Stage primaryStage, Model model, CommandHistory history); /** Stops the UI. */ void stop(); diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 3fd3c17be156..638211500730 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -15,7 +15,9 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.CommandHistory; import seedu.address.logic.Logic; +import seedu.address.model.Model; import seedu.address.model.UserPrefs; /** @@ -45,7 +47,7 @@ public UiManager(Logic logic, Config config, UserPrefs prefs) { } @Override - public void start(Stage primaryStage) { + public void start(Stage primaryStage, Model model, CommandHistory history) { logger.info("Starting UI..."); //Set the application icon. diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad558..3d54ed3a76c6 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -31,6 +31,7 @@ diff --git a/src/test/data/XmlUtilTest/missingLoginDetailField.xml b/src/test/data/XmlUtilTest/missingLoginDetailField.xml new file mode 100644 index 000000000000..54d6071c416c --- /dev/null +++ b/src/test/data/XmlUtilTest/missingLoginDetailField.xml @@ -0,0 +1,6 @@ + + + + zaq1xsw2cde3 + member + diff --git a/src/test/data/XmlUtilTest/validAddressBook.xml b/src/test/data/XmlUtilTest/validAddressBook.xml index 6265778674d3..f24decbb42e6 100644 --- a/src/test/data/XmlUtilTest/validAddressBook.xml +++ b/src/test/data/XmlUtilTest/validAddressBook.xml @@ -5,53 +5,71 @@ 9482424 hans@example.com
4th street
+ Photography + 20 Ruth Mueller 87249245 ruth@example.com
81th street
+ A + 1
Heinz Kurz 95352563 heinz@example.com
wall street
+ R + 100
Cornelia Meier 87652533 cornelia@example.com
10th street
+ B + 2
Werner Meyer 9482224 werner@example.com
michegan ave
+ C + 3
Lydia Kunz 9482427 lydia@example.com
little tokyo
+ D + 4
Anna Best 9482442 anna@example.com
4th street
+ E + 5
Stefan Meier 8482424 stefan@example.com
little india
+ F + 6
Martin Mueller 8482131 hans@example.com
chicago ave
+ G + 7
diff --git a/src/test/data/XmlUtilTest/validLoginBook.xml b/src/test/data/XmlUtilTest/validLoginBook.xml new file mode 100644 index 000000000000..f140f6aa1e4b --- /dev/null +++ b/src/test/data/XmlUtilTest/validLoginBook.xml @@ -0,0 +1,13 @@ + + + + A1234567M + zaq1xsw2cde3 + member + + + A1234568M + 1qaz2wsx3edc + member + + diff --git a/src/test/data/XmlUtilTest/validLoginDetails.xml b/src/test/data/XmlUtilTest/validLoginDetails.xml new file mode 100644 index 000000000000..b7546473b6ae --- /dev/null +++ b/src/test/data/XmlUtilTest/validLoginDetails.xml @@ -0,0 +1,6 @@ + + + A1234567M + zaq1xsw2cde3 + member + diff --git a/src/test/java/guitests/guihandles/PersonCardHandle.java b/src/test/java/guitests/guihandles/PersonCardHandle.java index 1789735e49a8..396332528bd5 100644 --- a/src/test/java/guitests/guihandles/PersonCardHandle.java +++ b/src/test/java/guitests/guihandles/PersonCardHandle.java @@ -19,6 +19,8 @@ public class PersonCardHandle extends NodeHandle { private static final String ADDRESS_FIELD_ID = "#address"; private static final String PHONE_FIELD_ID = "#phone"; private static final String EMAIL_FIELD_ID = "#email"; + private static final String SKILL_FIELD_ID = "#skill"; + private static final String SKILLLEVEL_FIELD_ID = "#skillLevel"; private static final String TAGS_FIELD_ID = "#tags"; private final Label idLabel; @@ -26,6 +28,8 @@ public class PersonCardHandle extends NodeHandle { private final Label addressLabel; private final Label phoneLabel; private final Label emailLabel; + private final Label skillLabel; + private final Label skillLevelLabel; private final List