diff --git a/.gitignore b/.gitignore index eab4c7db6a5..29b729090d3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ src/test/data/sandbox/ .DS_Store docs/_site/ docs/_markbind/logs/ +/htmlReport/ diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 026b52b8efd..9d8ae9e0106 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -397,19 +397,87 @@ The following diagram summarises what happens when a user executes a Sort comman other fields when it becomes supported. - Cons: Applicant#compareTo has to return different values depending on which descriptor has been chosen, causing some bugs when working with other Java functions as the order of Objects compared to each other is not meant to - change during runtime. + change during runtime. ##### Aspect: Command syntax - Alternative 1 (current choice): `sort d/ [valid field]` - - Pros: Simple and minimal text fields, with a single prefix required to enable sorting. - - Cons: Only able to sort in ascending order. + - Pros: Simple and minimal text fields, with a single prefix required to enable sorting. + - Cons: Only able to sort in ascending order. - Alternative 2: `sort d/ [valid field] o/ [a/d]` - - Pros: Able to sort in either ascending or descending order. - - Cons: Requires additional input from the user, slowing down the use of the command. + - Pros: Able to sort in either ascending or descending order. + - Cons: Requires additional input from the user, slowing down the use of the command. - Alternative 3: `sort d/ [valid field] o/ [a/d]` where `o/` is optional - - Pros: Retains the ability to sort in either order, but also the conciseness of Alternative 1. - - Cons: Users who are not aware of the `o/` feature may not use it. + - Pros: Retains the ability to sort in either order, but also the conciseness of Alternative 1. + - Cons: Users who are not aware of the `o/` feature may not use it. + +### Filter feature + +#### Implementation + +The filter feature works by updating the `Predicate` used in the `FilteredList` of `ModelManager`. Using +the predicate, minimal changes to the implementation of StaffSnap is required. + +To create a single predicate that is able to search and filter for multiple fields, a `CustomFilterPredicate` class is +created +It currently contains the following fields and is able to filter for applicants which match all specified fields. + +1. Name +2. Phone +3. Email +4. Position +5. Status +6. Less than score +7. Greater than score + +When `CustomFilterPredicate#test` is called, it will check if the specified fields are a substring of the same field of +the applicant, +returning true if all specified fields match, and false otherwise. + +#### Steps to trigger + +1. User opens the app +2. User enters `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where one or more of the prefixes can be specified to + be filtered by + +Once step 2 is complete, the GUI will update and refresh the applicant list with only applicants which match all +specified fields. +The following diagram summarises what happens when a user executes a Filter command: + + + +### Design considerations + +#### Aspect: How to filter applicants + +- Alternative 1 (current choice): use a custom predicate and FilteredList, **compare using strings** + - Pros: Current implementation of ModelManager already uses FilteredList, making a custom predicate an easy + extension. + The `CustomFilterPredicate` can easily be extended when more applicant fields are ready to expand the utility of + the + filter command. + - Cons: Comparison of predicate fields to applicant fields are done using string comparison. +- Alternative 2: use a custom predicate and FilteredList, **compare within field classes** + - Pros: Same as alternative 1, and definition of what is considered a match can be defined in the field class (e.g. + Name, Phone, etc). + - Cons: Will require greater complexity than alternative 1 in implementation. May be slower to integrate new classes + in the future. + +#### Aspect: Command syntax + +- Alternative 1: `filter n/ [Name] e/ [Email] p/ [Position] hp/ [Phone] s/ [Status] lts/ [Score] gts/ [Score]` + - Pros: Unambiguous and forces all fields to be entered, preventing errors. + - Cons: Users cannot filter by single fields. Requires more key presses to enter a filter command. +- Alternative 2: `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where only one term is allowed + - Pros: Quicker to key in command than alternative 1. + - Cons: Only allows users to filter by one field at a time, limiting utility of filter command. +- Alternative 3 (current choice): `filter [n/, e/, p/, hp/, s/, lts/, gts/] [term]`, where at least one term is required + and the others + are optional + - Pros: Provides flexibility in the filter command to filter by one or more fields, while still retaining the speed + of alternative 2 when few fields are required. + - Cons: Unfamiliar users may not know that fields can be optional anc continue to key in the full command at all + times. -------------------------------------------------------------------------------------------------------------------- diff --git a/docs/diagrams/FilterCommandActivityDiagram.puml b/docs/diagrams/FilterCommandActivityDiagram.puml new file mode 100644 index 00000000000..23c4622ad33 --- /dev/null +++ b/docs/diagrams/FilterCommandActivityDiagram.puml @@ -0,0 +1,42 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:User enters filter command syntax; +:ApplicantBookParser and FilterCommandParser +parse arguments; +if (At least 1 argument is provided) then (true) + :Parse provided fields; + if (Provided fields are valid) then (true) + :Create new CustomFilterPredicate + from specified fields; + :Create new FilterCommand + from CustomFilterPredicate; + :FilterCommand updates predicate used in + ModelManager with CustomFilterPredicate; + :Display success message and show updated list in GUI; + stop + else (false) + :Throw ParseException with + invalid command format + message and proper Filter + syntax; + :Display error message; + stop + endif + +else (false) +label 1 +label 2 +label 3 + +:Throw ParseException with + invalid command format + message and proper Filter + syntax; + :Display error message; + stop +endif + +@enduml + diff --git a/src/main/java/seedu/staffsnap/commons/util/StringUtil.java b/src/main/java/seedu/staffsnap/commons/util/StringUtil.java index 4d11a031385..3fe3b5730ce 100644 --- a/src/main/java/seedu/staffsnap/commons/util/StringUtil.java +++ b/src/main/java/seedu/staffsnap/commons/util/StringUtil.java @@ -31,13 +31,33 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { 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+"); + String[] wordsInPreppedSentence = sentence.split("\\s+"); return Arrays.stream(wordsInPreppedSentence) .anyMatch(sentenceWord -> sentenceWord.toLowerCase().contains(preppedWord.toLowerCase())); } + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case. + *
examples:
+     *       containsStringIgnoreCase("ABc def", "abc") == true
+     *       containsStringIgnoreCase("ABc def", "DEF") == true
+     *       containsStringIgnoreCase("ABc def", "AB") == true
+     *       containsStringIgnoreCase("ABc def", "acd") == false // "acd" is not a substring in "ABc def"
+     *       
+ * @param sentence a String that is not null + * @param word a String that is not empty and is not null + */ + public static boolean containsStringIgnoreCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + + return sentence.contains(word); + } /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/staffsnap/logic/commands/FilterCommand.java b/src/main/java/seedu/staffsnap/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..abdd2877232 --- /dev/null +++ b/src/main/java/seedu/staffsnap/logic/commands/FilterCommand.java @@ -0,0 +1,80 @@ +package seedu.staffsnap.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_GREATER_THAN_SCORE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_LESS_THAN_SCORE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_STATUS; + +import seedu.staffsnap.commons.util.ToStringBuilder; +import seedu.staffsnap.logic.Messages; +import seedu.staffsnap.model.Model; +import seedu.staffsnap.model.applicant.CustomFilterPredicate; + + + +/** + * Finds and lists all applicants in address book whose name contains any of the argument keywords. + * Keyword matching is case-insensitive. + */ +public class FilterCommand extends Command { + + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters all applicants who match the descriptor."; + public static final String MESSAGE_FAILURE = "Please add at least one field to filter by. " + + "Possible fields include:" + "\n" + + PREFIX_NAME + " [NAME], " + + PREFIX_EMAIL + " [EMAIL], " + + PREFIX_POSITION + " [POSITION], " + + PREFIX_PHONE + " [PHONE], " + + PREFIX_STATUS + " [STATUS], " + + PREFIX_LESS_THAN_SCORE + " [SCORE], " + + PREFIX_GREATER_THAN_SCORE + " [SCORE]"; + public static final String MESSAGE_SCORE_PARSE_FAILURE = "Score in lts/ or gts/ has to be a number with up to 1 " + + "decimal place"; + + private final CustomFilterPredicate predicate; + + public FilterCommand(CustomFilterPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredApplicantList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_APPLICANTS_LISTED_OVERVIEW, model.getFilteredApplicantList().size())); + } + + /** + * Checks if the two FilterCommand objects are equivalent, by comparing the equivalence of their predicates. + * @param other Other FilterCommand. + * @return true if equals, false if not equals. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return predicate.equals(otherFilterCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/staffsnap/logic/commands/SortCommand.java b/src/main/java/seedu/staffsnap/logic/commands/SortCommand.java index 71ef47e8d3f..bf9b37cf773 100644 --- a/src/main/java/seedu/staffsnap/logic/commands/SortCommand.java +++ b/src/main/java/seedu/staffsnap/logic/commands/SortCommand.java @@ -18,6 +18,7 @@ public class SortCommand extends Command { + "Parameters: " + PREFIX_DESCRIPTOR + "DESCRIPTOR "; public static final String MESSAGE_SUCCESS = "Sorted all Applicants"; + public static final String MESSAGE_FAILURE = "Please add a descriptor with d/ [name/phone]."; private final Descriptor descriptor; diff --git a/src/main/java/seedu/staffsnap/logic/commands/StatusCommand.java b/src/main/java/seedu/staffsnap/logic/commands/StatusCommand.java index 4d0d5e14293..3d714a0af8b 100644 --- a/src/main/java/seedu/staffsnap/logic/commands/StatusCommand.java +++ b/src/main/java/seedu/staffsnap/logic/commands/StatusCommand.java @@ -22,12 +22,15 @@ public class StatusCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the status of the applicant identified " + "by the index number used in the displayed applicant list.\n" - + "Parameters: INDEX (must be a positive integer) " + "STATUS [u(ndecided)/o(ffered)/r(ejected)]."; + + "Parameters: INDEX (must be a positive integer) " + "s/ [u(ndecided)/o(ffered)/r(ejected)]."; public static final String MESSAGE_EDIT_STATUS_SUCCESS = "Edited Applicant Status: %1$s"; public static final String MESSAGE_NO_STATUS = "Missing Status, please follow the following parameters." + "Parameters: INDEX (must be a positive integer) " - + "STATUS [u(ndecided)/o(ffered)/r(ejected)]."; + + "s/ [u(ndecided)/o(ffered)/r(ejected)]."; + public static final String MESSAGE_NO_INDEX = "Missing Index, please follow the following parameters." + + "Parameters: INDEX (must be a positive integer) " + + "s/ [u(ndecided)/o(ffered)/r(ejected)]."; private final Index index; diff --git a/src/main/java/seedu/staffsnap/logic/parser/ApplicantBookParser.java b/src/main/java/seedu/staffsnap/logic/parser/ApplicantBookParser.java index 4c821822f3d..4057fee7c36 100644 --- a/src/main/java/seedu/staffsnap/logic/parser/ApplicantBookParser.java +++ b/src/main/java/seedu/staffsnap/logic/parser/ApplicantBookParser.java @@ -18,6 +18,7 @@ import seedu.staffsnap.logic.commands.EditCommand; import seedu.staffsnap.logic.commands.EditInterviewCommand; import seedu.staffsnap.logic.commands.ExitCommand; +import seedu.staffsnap.logic.commands.FilterCommand; import seedu.staffsnap.logic.commands.FindCommand; import seedu.staffsnap.logic.commands.HelpCommand; import seedu.staffsnap.logic.commands.ListCommand; @@ -66,7 +67,7 @@ public Command parseCommand(String userInput) throws ParseException { isConfirmed = isConfirmedNext; isConfirmedNext = false; - switch (commandWord) { + switch (commandWord.toLowerCase()) { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); @@ -106,6 +107,9 @@ public Command parseCommand(String userInput) throws ParseException { case AddInterviewCommand.COMMAND_WORD: return new AddInterviewCommandParser().parse(arguments); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + case EditInterviewCommand.COMMAND_WORD: return new EditInterviewCommandParser().parse(arguments); diff --git a/src/main/java/seedu/staffsnap/logic/parser/CliSyntax.java b/src/main/java/seedu/staffsnap/logic/parser/CliSyntax.java index c06096ef1b0..a2333de653b 100644 --- a/src/main/java/seedu/staffsnap/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/staffsnap/logic/parser/CliSyntax.java @@ -14,5 +14,9 @@ public class CliSyntax { public static final Prefix PREFIX_INTERVIEW = new Prefix("i/"); public static final Prefix PREFIX_TYPE = new Prefix("t/"); public static final Prefix PREFIX_RATING = new Prefix("r/"); + public static final Prefix PREFIX_STATUS = new Prefix("s/"); + public static final Prefix PREFIX_LESS_THAN_SCORE = new Prefix("lts/"); + public static final Prefix PREFIX_GREATER_THAN_SCORE = new Prefix("gts/"); + } diff --git a/src/main/java/seedu/staffsnap/logic/parser/FilterCommandParser.java b/src/main/java/seedu/staffsnap/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..f73f075e627 --- /dev/null +++ b/src/main/java/seedu/staffsnap/logic/parser/FilterCommandParser.java @@ -0,0 +1,86 @@ +package seedu.staffsnap.logic.parser; + +import static seedu.staffsnap.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_GREATER_THAN_SCORE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_INTERVIEW; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_LESS_THAN_SCORE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_STATUS; + +import java.util.List; + +import seedu.staffsnap.logic.commands.FilterCommand; +import seedu.staffsnap.logic.parser.exceptions.ParseException; +import seedu.staffsnap.model.applicant.CustomFilterPredicate; +import seedu.staffsnap.model.applicant.Email; +import seedu.staffsnap.model.applicant.Name; +import seedu.staffsnap.model.applicant.Phone; +import seedu.staffsnap.model.applicant.Position; +import seedu.staffsnap.model.applicant.Status; +import seedu.staffsnap.model.interview.Interview; + + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * and returns a FilterCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_FAILURE)); + } + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_POSITION, PREFIX_INTERVIEW, PREFIX_STATUS, PREFIX_LESS_THAN_SCORE, + PREFIX_GREATER_THAN_SCORE); + + Name name = null; + Phone phone = null; + Email email = null; + Position position = null; + List interviewList = null; + Status status = null; + Double lessThanScore = null; + Double greaterThanScore = null; + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_POSITION, + PREFIX_STATUS, PREFIX_LESS_THAN_SCORE, PREFIX_GREATER_THAN_SCORE); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + } + if (argMultimap.getValue(PREFIX_POSITION).isPresent()) { + position = ParserUtil.parsePosition(argMultimap.getValue(PREFIX_POSITION).get()); + } + if (argMultimap.getValue(PREFIX_INTERVIEW).isPresent()) { + interviewList = ParserUtil.parseInterviews(argMultimap.getAllValues(PREFIX_INTERVIEW)); + } + if (argMultimap.getValue(PREFIX_STATUS).isPresent()) { + status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).get()); + } + if (argMultimap.getValue(PREFIX_LESS_THAN_SCORE).isPresent()) { + lessThanScore = ParserUtil.parseScore(argMultimap.getValue(PREFIX_LESS_THAN_SCORE).get()); + } + if (argMultimap.getValue(PREFIX_GREATER_THAN_SCORE).isPresent()) { + greaterThanScore = ParserUtil.parseScore(argMultimap.getValue(PREFIX_GREATER_THAN_SCORE).get()); + } + return new FilterCommand(new CustomFilterPredicate(name, phone, email, position, interviewList, status, + lessThanScore, greaterThanScore)); + } + +} diff --git a/src/main/java/seedu/staffsnap/logic/parser/ParserUtil.java b/src/main/java/seedu/staffsnap/logic/parser/ParserUtil.java index 7695bebd740..11842dbedd4 100644 --- a/src/main/java/seedu/staffsnap/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/staffsnap/logic/parser/ParserUtil.java @@ -9,12 +9,14 @@ import seedu.staffsnap.commons.core.index.Index; import seedu.staffsnap.commons.util.StringUtil; +import seedu.staffsnap.logic.commands.FilterCommand; import seedu.staffsnap.logic.parser.exceptions.ParseException; import seedu.staffsnap.model.applicant.Descriptor; import seedu.staffsnap.model.applicant.Email; import seedu.staffsnap.model.applicant.Name; import seedu.staffsnap.model.applicant.Phone; import seedu.staffsnap.model.applicant.Position; +import seedu.staffsnap.model.applicant.Status; import seedu.staffsnap.model.interview.Interview; import seedu.staffsnap.model.interview.Rating; @@ -213,4 +215,33 @@ public static Rating parseRating(String rating) throws ParseException { public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); } + + /** + * Parses a {@code String status} into a {@code Status}. + * + * @param status String representation of Status + * @return Status if successful, or null if no matching status is found. + */ + public static Status parseStatus(String status) { + requireNonNull(status); + return Status.findByName(status); + } + + /** + * Parses a {@code String score} into a {@code Double}. + * + * @param score String representation of score + * @return Double score which is the average rating of all interviews + * @throws ParseException if a NumberFormatException is caught + */ + public static Double parseScore(String score) throws ParseException { + requireNonNull(score); + Double result; + try { + result = new Double(score); + } catch (NumberFormatException e) { + throw new ParseException(FilterCommand.MESSAGE_SCORE_PARSE_FAILURE); + } + return result; + } } diff --git a/src/main/java/seedu/staffsnap/logic/parser/SortCommandParser.java b/src/main/java/seedu/staffsnap/logic/parser/SortCommandParser.java index e296f8e2bfa..ceaccfa4e5b 100644 --- a/src/main/java/seedu/staffsnap/logic/parser/SortCommandParser.java +++ b/src/main/java/seedu/staffsnap/logic/parser/SortCommandParser.java @@ -24,7 +24,7 @@ public SortCommand parse(String args) throws ParseException { if (!arePrefixesPresent(argMultimap, PREFIX_DESCRIPTOR) || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_FAILURE)); } argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_DESCRIPTOR); diff --git a/src/main/java/seedu/staffsnap/logic/parser/StatusCommandParser.java b/src/main/java/seedu/staffsnap/logic/parser/StatusCommandParser.java index 87448f002fc..7fd899b1547 100644 --- a/src/main/java/seedu/staffsnap/logic/parser/StatusCommandParser.java +++ b/src/main/java/seedu/staffsnap/logic/parser/StatusCommandParser.java @@ -1,6 +1,7 @@ package seedu.staffsnap.logic.parser; import static seedu.staffsnap.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.staffsnap.logic.parser.CliSyntax.PREFIX_STATUS; import seedu.staffsnap.commons.core.index.Index; import seedu.staffsnap.logic.commands.StatusCommand; @@ -18,15 +19,24 @@ public class StatusCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public StatusCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_USAGE)); + } ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args); + ArgumentTokenizer.tokenize(args, PREFIX_STATUS); - if (argMultimap.getPreamble().length() <= 2) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_USAGE)); + Index index = null; + Status status = null; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_NO_INDEX)); + } + if (argMultimap.getValue(PREFIX_STATUS).isPresent()) { + status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).get()); } - String[] splitString = argMultimap.getPreamble().split("\\s+"); - Index index = ParserUtil.parseIndex(splitString[0]); - Status status = Status.findByName(splitString[1]); if (status == null) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_NO_STATUS)); } diff --git a/src/main/java/seedu/staffsnap/model/applicant/Applicant.java b/src/main/java/seedu/staffsnap/model/applicant/Applicant.java index b6fb8a263ed..583dd461897 100644 --- a/src/main/java/seedu/staffsnap/model/applicant/Applicant.java +++ b/src/main/java/seedu/staffsnap/model/applicant/Applicant.java @@ -215,4 +215,18 @@ public Status getStatus() { public void setStatus(Status status) { this.status = status; } + + /** + * Get the score of an Applicant + * @return Double score of Applicant + */ + public Double getScore() { + List interviews = getInterviews(); + Double totalScore = 0.; + for (Interview interview: interviews) { + totalScore += new Double(interview.rating.value); + } + Double averageScore = totalScore / interviews.size(); + return averageScore; + } } diff --git a/src/main/java/seedu/staffsnap/model/applicant/CustomFilterPredicate.java b/src/main/java/seedu/staffsnap/model/applicant/CustomFilterPredicate.java new file mode 100644 index 00000000000..fbfa6ae530f --- /dev/null +++ b/src/main/java/seedu/staffsnap/model/applicant/CustomFilterPredicate.java @@ -0,0 +1,123 @@ +package seedu.staffsnap.model.applicant; + +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +import seedu.staffsnap.commons.util.StringUtil; +import seedu.staffsnap.commons.util.ToStringBuilder; +import seedu.staffsnap.model.interview.Interview; + + +/** + * Custom predicate to be used to filter applicants + */ +public class CustomFilterPredicate implements Predicate { + + // Identity fields + private final Name name; + private final Phone phone; + // Data fields + private final Email email; + private final Position position; + private final List interviews; + private final Status status; + private final Double lessThanScore; + private final Double greaterThanScore; + + /** + * Constructor for CustomFilterPredicate + * + * @param name Name of applicant + * @param phone Phone number of applicant + * @param email Email address of applicant + * @param position Position applied for by applicant + * @param interviews Interviews applicant has to go through + */ + public CustomFilterPredicate(Name name, Phone phone, Email email, Position position, List interviews, + Status status, Double lessThanScore, Double greaterThanScore) { + this.name = name; + this.phone = phone; + this.email = email; + this.position = position; + this.interviews = interviews; + this.status = status; + this.lessThanScore = lessThanScore; + this.greaterThanScore = greaterThanScore; + } + + /** + * @param applicant the applicant to be tested. + * @return true if applicant matches all specified fields in the predicate + */ + @Override + public boolean test(Applicant applicant) { + if (this.name != null) { + if (!StringUtil.containsStringIgnoreCase(applicant.getName().toString(), this.name.toString())) { + return false; + } + } + if (this.phone != null) { + if (!StringUtil.containsWordIgnoreCase(applicant.getPhone().toString(), this.phone.toString())) { + return false; + } + } + if (this.email != null) { + if (!StringUtil.containsWordIgnoreCase(applicant.getEmail().toString(), this.email.toString())) { + return false; + } + } + if (this.position != null) { + if (!StringUtil.containsWordIgnoreCase(applicant.getPosition().toString(), this.position.toString())) { + return false; + } + } + if (this.status != null) { + if (applicant.getStatus() != this.status) { + return false; + } + } + if (this.lessThanScore != null) { + if (applicant.getScore() >= this.lessThanScore) { + return false; + } + } + if (this.greaterThanScore != null) { + if (applicant.getScore() <= this.greaterThanScore) { + return false; + } + } + return true; + } + + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("email", email) + .add("position", position) + .add("interviews", interviews) + .add("status", status).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomFilterPredicate that = (CustomFilterPredicate) o; + return Objects.equals(name, that.name) && Objects.equals(phone, that.phone) && Objects.equals(email, + that.email) && Objects.equals(position, that.position) && Objects.equals(interviews, that.interviews) + && Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(name, phone, email, position, interviews); + } +} diff --git a/src/test/java/seedu/staffsnap/logic/commands/FilterCommandTest.java b/src/test/java/seedu/staffsnap/logic/commands/FilterCommandTest.java new file mode 100644 index 00000000000..935bcff524a --- /dev/null +++ b/src/test/java/seedu/staffsnap/logic/commands/FilterCommandTest.java @@ -0,0 +1,133 @@ +package seedu.staffsnap.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.staffsnap.logic.Messages.MESSAGE_APPLICANTS_LISTED_OVERVIEW; +import static seedu.staffsnap.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.staffsnap.testutil.TypicalApplicants.ALICE; +import static seedu.staffsnap.testutil.TypicalApplicants.BENSON; +import static seedu.staffsnap.testutil.TypicalApplicants.CARL; +import static seedu.staffsnap.testutil.TypicalApplicants.DANIEL; +import static seedu.staffsnap.testutil.TypicalApplicants.ELLE; +import static seedu.staffsnap.testutil.TypicalApplicants.FIONA; +import static seedu.staffsnap.testutil.TypicalApplicants.getTypicalApplicantBook; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.staffsnap.model.Model; +import seedu.staffsnap.model.ModelManager; +import seedu.staffsnap.model.UserPrefs; +import seedu.staffsnap.model.applicant.CustomFilterPredicate; +import seedu.staffsnap.model.applicant.Email; +import seedu.staffsnap.model.applicant.Name; +import seedu.staffsnap.model.applicant.Phone; +import seedu.staffsnap.model.applicant.Position; +import seedu.staffsnap.model.applicant.Status; +import seedu.staffsnap.model.interview.Interview; + + +class FilterCommandTest { + + private Model model = new ModelManager(getTypicalApplicantBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalApplicantBook(), new UserPrefs()); + + @Test + public void equals() { + Name name1 = BENSON.getName(); + Phone phone1 = BENSON.getPhone(); + Email email1 = BENSON.getEmail(); + Position position1 = BENSON.getPosition(); + List interviewList1 = BENSON.getInterviews(); + Status status1 = BENSON.getStatus(); + + Name name2 = CARL.getName(); + Phone phone2 = CARL.getPhone(); + Email email2 = CARL.getEmail(); + Position position2 = CARL.getPosition(); + List interviewList2 = CARL.getInterviews(); + Status status2 = CARL.getStatus(); + + CustomFilterPredicate firstPredicate = new CustomFilterPredicate(name1, phone1, email1, position1, + interviewList1, status1, null, null); + CustomFilterPredicate secondPredicate = new CustomFilterPredicate(name2, phone2, email2, position2, + interviewList2, status2, null, null); + + FilterCommand filterFirstCommand = new FilterCommand(firstPredicate); + FilterCommand filterSecondCommand = new FilterCommand(secondPredicate); + + // same object -> returns true + assertTrue(filterFirstCommand.equals(filterFirstCommand)); + + // same values -> returns true + FilterCommand findFirstCommandCopy = new FilterCommand(firstPredicate); + assertTrue(filterFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(filterFirstCommand.equals(1)); + + // null -> returns false + assertFalse(filterFirstCommand.equals(null)); + + // different applicant -> returns false + assertFalse(filterFirstCommand.equals(filterSecondCommand)); + } + + @Test + public void execute_zeroKeywords_allApplicantsFound() { + String expectedMessage = String.format(MESSAGE_APPLICANTS_LISTED_OVERVIEW, 8); + CustomFilterPredicate predicate = new CustomFilterPredicate(null, null, null, null, null, null, null, null); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredApplicantList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(expectedModel.getFilteredApplicantList(), model.getFilteredApplicantList()); + } + + @Test + public void execute_partialName_multipleApplicantsFound() { + String expectedMessage = String.format(MESSAGE_APPLICANTS_LISTED_OVERVIEW, 4); + CustomFilterPredicate predicate = new CustomFilterPredicate(new Name("a"), null, null, null, null, null, + null, null); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredApplicantList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ALICE, CARL, DANIEL, FIONA), model.getFilteredApplicantList()); + } + + @Test + public void execute_multipleKeywords_singleApplicantFound() { + String expectedMessage = String.format(MESSAGE_APPLICANTS_LISTED_OVERVIEW, 1); + CustomFilterPredicate predicate = new CustomFilterPredicate(FIONA.getName(), FIONA.getPhone(), null, null, + null, null, null, null); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredApplicantList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(FIONA), model.getFilteredApplicantList()); + } + + @Test + public void execute_multipleKeywords_zeroApplicantsFound() { + String expectedMessage = String.format(MESSAGE_APPLICANTS_LISTED_OVERVIEW, 0); + CustomFilterPredicate predicate = new CustomFilterPredicate(ALICE.getName(), BENSON.getPhone(), + CARL.getEmail(), DANIEL.getPosition(), ELLE.getInterviews(), FIONA.getStatus(), null, null); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredApplicantList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + System.out.println(model.getFilteredApplicantList()); + assertEquals(Arrays.asList(), model.getFilteredApplicantList()); + } + + + @Test + public void toStringMethod() { + CustomFilterPredicate predicate = new CustomFilterPredicate(FIONA.getName(), null, null, null, null, null, + null, null); + FilterCommand findCommand = new FilterCommand(predicate); + String expected = FilterCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, findCommand.toString()); + } + +} diff --git a/src/test/java/seedu/staffsnap/logic/parser/ApplicantBookParserTest.java b/src/test/java/seedu/staffsnap/logic/parser/ApplicantBookParserTest.java index b54bfef802d..2b131e24189 100644 --- a/src/test/java/seedu/staffsnap/logic/parser/ApplicantBookParserTest.java +++ b/src/test/java/seedu/staffsnap/logic/parser/ApplicantBookParserTest.java @@ -15,10 +15,12 @@ import seedu.staffsnap.logic.commands.AddCommand; //import seedu.staffsnap.logic.commands.ClearCommand; +import seedu.staffsnap.logic.commands.AddInterviewCommand; import seedu.staffsnap.logic.commands.DeleteCommand; import seedu.staffsnap.logic.commands.EditCommand; import seedu.staffsnap.logic.commands.EditCommand.EditApplicantDescriptor; import seedu.staffsnap.logic.commands.ExitCommand; +import seedu.staffsnap.logic.commands.FilterCommand; import seedu.staffsnap.logic.commands.FindCommand; import seedu.staffsnap.logic.commands.HelpCommand; import seedu.staffsnap.logic.commands.ListCommand; @@ -51,8 +53,9 @@ public void parseCommand_clear() throws Exception { @Test public void parseCommand_delete() throws Exception { - DeleteCommand command = (DeleteCommand) parser.parseCommand( - DeleteCommand.COMMAND_WORD + " " + INDEX_FIRST_APPLICANT.getOneBased()); + DeleteCommand command = + (DeleteCommand) parser.parseCommand(DeleteCommand.COMMAND_WORD + " " + + INDEX_FIRST_APPLICANT.getOneBased()); assertEquals(new DeleteCommand(INDEX_FIRST_APPLICANT), command); } @@ -60,11 +63,10 @@ public void parseCommand_delete() throws Exception { public void parseCommand_edit() throws Exception { Applicant applicant = new ApplicantBuilder().build(); EditApplicantDescriptor descriptor = new EditApplicantDescriptorBuilder(applicant).build(); - EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD - + " " - + INDEX_FIRST_APPLICANT.getOneBased() - + " " - + ApplicantUtil.getEditApplicantDescriptorDetails(descriptor)); + EditCommand command = + (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " + + INDEX_FIRST_APPLICANT.getOneBased() + " " + + ApplicantUtil.getEditApplicantDescriptorDetails(descriptor)); assertEquals(new EditCommand(INDEX_FIRST_APPLICANT, descriptor), command); } @@ -77,8 +79,9 @@ public void parseCommand_exit() throws Exception { @Test public void parseCommand_find() throws Exception { List keywords = Arrays.asList("foo", "bar", "baz"); - FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + FindCommand command = + (FindCommand) parser.parseCommand(FindCommand.COMMAND_WORD + " " + keywords.stream() + .collect(Collectors.joining(" "))); assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); } @@ -99,10 +102,21 @@ public void parseCommand_sort() throws Exception { assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + "d/ name") instanceof SortCommand); } + @Test + public void parseCommand_filter() throws Exception { + assertTrue(parser.parseCommand(FilterCommand.COMMAND_WORD + " " + "d/ name") instanceof FilterCommand); + } + + @Test + public void parseCommand_addi() throws Exception { + assertTrue(parser.parseCommand(AddInterviewCommand.COMMAND_WORD + " 1 " + "t/ technical") + instanceof AddInterviewCommand); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { - assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), () - -> parser.parseCommand("")); + assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + HelpCommand.MESSAGE_USAGE), () -> parser.parseCommand("")); } @Test diff --git a/src/test/java/seedu/staffsnap/logic/parser/FilterCommandParserTest.java b/src/test/java/seedu/staffsnap/logic/parser/FilterCommandParserTest.java new file mode 100644 index 00000000000..b06b06a0453 --- /dev/null +++ b/src/test/java/seedu/staffsnap/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,59 @@ +package seedu.staffsnap.logic.parser; + + +import static seedu.staffsnap.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.staffsnap.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.staffsnap.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.staffsnap.logic.commands.FilterCommand; +import seedu.staffsnap.model.applicant.CustomFilterPredicate; +import seedu.staffsnap.model.applicant.Email; +import seedu.staffsnap.model.applicant.Name; +import seedu.staffsnap.model.applicant.Phone; +import seedu.staffsnap.model.applicant.Position; +import seedu.staffsnap.model.applicant.Status; + +class FilterCommandParserTest { + + private static final String MESSAGE_INVALID_FORMAT = String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterCommand.MESSAGE_FAILURE); + private FilterCommandParser parser = new FilterCommandParser(); + + + @Test + void parse_missingParts_failure() { + assertParseFailure(parser, " ", MESSAGE_INVALID_FORMAT); + } + + @Test + void parse_nameOnly_success() { + assertParseSuccess(parser, " n/ Name", new FilterCommand(new CustomFilterPredicate(new Name("Name"), null, + null, null, null, null, null, null))); + } + + @Test + void parse_emailOnly_success() { + assertParseSuccess(parser, " e/ test@test.com", new FilterCommand(new CustomFilterPredicate(null, null, + new Email("test@test.com"), null, null, null, null, null))); + } + + @Test + void parse_positionOnly_success() { + assertParseSuccess(parser, " p/ Software Engineer", new FilterCommand(new CustomFilterPredicate(null, null, + null, new Position("Software Engineer"), null, null, null, null))); + } + + @Test + void parse_phoneOnly_success() { + assertParseSuccess(parser, " hp/ 98765432", new FilterCommand(new CustomFilterPredicate(null, new Phone( + "98765432"), null, null, null, null, null, null))); + } + + @Test + void parse_statusOnly_success() { + assertParseSuccess(parser, " s/ o", new FilterCommand(new CustomFilterPredicate(null, null, null, null, + null, Status.OFFERED, null, null))); + } +} diff --git a/src/test/java/seedu/staffsnap/logic/parser/StatusCommandParserTest.java b/src/test/java/seedu/staffsnap/logic/parser/StatusCommandParserTest.java index 3878691487f..5c057cc7126 100644 --- a/src/test/java/seedu/staffsnap/logic/parser/StatusCommandParserTest.java +++ b/src/test/java/seedu/staffsnap/logic/parser/StatusCommandParserTest.java @@ -19,7 +19,7 @@ class StatusCommandParserTest { @Test void parse_validArgs_returnsStatusCommand() { StatusCommand expectedStatusCommand = new StatusCommand(Index.fromOneBased(1), Status.OFFERED); - assertParseSuccess(parser, "1 o", expectedStatusCommand); + assertParseSuccess(parser, "1 s/ o", expectedStatusCommand); } @Test @@ -29,11 +29,12 @@ void parse_invalidArgs_throwsParseException() { @Test void parse_missingStatus_throwsParseException() { - assertParseFailure(parser, "1", String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "1", String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_NO_STATUS)); } @Test void parse_missingIndex_throwsParseException() { - assertParseFailure(parser, "o", String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatusCommand.MESSAGE_USAGE)); + assertParseFailure(parser, "s/ o", String.format(MESSAGE_INVALID_COMMAND_FORMAT, + StatusCommand.MESSAGE_NO_INDEX)); } }