diff --git a/classes/AlertView/Standard.php b/classes/AlertView/Standard.php index d55170d073..dcc4f7729c 100644 --- a/classes/AlertView/Standard.php +++ b/classes/AlertView/Standard.php @@ -25,7 +25,9 @@ public function display() { $this->checkInput(); $this->searchForConstituenciesAndMembers(); - if (!sizeof($this->data['errors']) && ($this->data['keyword'] || $this->data['pid'])) { + if ($this->data['step'] || $this->data['addword']) { + $this->processStep(); + } elseif (!$this->data['results'] == 'changes-abandoned' && !sizeof($this->data['errors']) && $this->data['submitted'] && ($this->data['keyword'] || $this->data['pid'])) { $this->addAlert(); } @@ -37,6 +39,7 @@ public function display() { return $this->data; } + # This only happens if we have an alert and want to do something to it. private function processAction() { $token = get_http_var('t'); $alert = $this->alert->check_token($token); @@ -48,7 +51,8 @@ private function processAction() { $success = $this->confirmAlert($token); if ($success) { $this->data['results'] = 'alert-confirmed'; - $this->data['criteria'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($this->alert->criteria); + $this->data['criteria'] = $this->alert->criteria; + $this->data['display_criteria'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($this->alert->criteria); } } elseif ($action == 'Suspend') { $success = $this->suspendAlert($token); @@ -70,6 +74,8 @@ private function processAction() { if ($success) { $this->data['results'] = 'all-alerts-deleted'; } + } elseif ($action == 'Abandon') { + $this->data['results'] = 'changes-abandoned'; } if (!$success) { $this->data['results'] = 'alert-fail'; @@ -79,6 +85,50 @@ private function processAction() { $this->data['alert'] = $alert; } + # Process a screen in the alert creation wizard + private function processStep() { + # fetch a list of suggested terms. Need this for the define screen so we can filter out the suggested terms + # and not show them if the user goes back + if (($this->data['step'] == 'review' || $this->data['step'] == 'define') && !$this->data['shown_related']) { + $suggestions = []; + foreach ($this->data['keywords'] as $word) { + $terms = $this->alert->get_related_terms($word); + $terms = array_diff($terms, $this->data['keywords']); + if ($terms && count($terms)) { + $suggestions = array_merge($suggestions, $terms); + } + } + + if (count($suggestions) > 0) { + $this->data['step'] = 'add_vector_related'; + $this->data['suggestions'] = $suggestions; + } + # confirm the alert. Handles both creating and editing alerts + } elseif ($this->data['step'] == 'confirm') { + $success = true; + # if there's already an alert assume we are editing it and user must be logged in + if ($this->data['alert']) { + $success = $this->updateAlert($this->data['alert']['id'], $this->data); + if ($success) { + # reset all the data to stop anything getting confused + $this->data['results'] = 'alert-confirmed'; + $this->data['step'] = ''; + $this->data['pid'] = ''; + $this->data['alertsearch'] = ''; + $this->data['pc'] = ''; + $this->data['members'] = false; + $this->data['constituencies'] = []; + } else { + $this->data['results'] = 'alert-fail'; + $this->data['step'] = 'review'; + } + } else { + $success = $this->addAlert(); + $this->data['step'] = ''; + } + } + } + private function getBasicData() { global $this_page; @@ -93,12 +143,102 @@ private function getBasicData() { $this->data["email"] = trim(get_http_var("email")); $this->data['email_verified'] = false; } + + $this->data['token'] = get_http_var('t'); + $this->data['step'] = trim(get_http_var("step")); + $this->data['mp_step'] = trim(get_http_var("mp_step")); + $this->data['addword'] = trim(get_http_var("addword")); + $this->data['this_step'] = trim(get_http_var("this_step")); + $this->data['shown_related'] = get_http_var('shown_related'); + $this->data['match_all'] = get_http_var('match_all') == 'on'; $this->data['keyword'] = trim(get_http_var("keyword")); - $this->data['pid'] = trim(get_http_var("pid")); + $this->data['search_section'] = ''; $this->data['alertsearch'] = trim(get_http_var("alertsearch")); + $this->data['mp_search'] = trim(get_http_var("mp_search")); + $this->data['pid'] = trim(get_http_var("pid")); $this->data['pc'] = get_http_var('pc'); - $this->data['submitted'] = get_http_var('submitted') || $this->data['pid'] || $this->data['keyword']; - $this->data['token'] = get_http_var('t'); + $this->data['submitted'] = get_http_var('submitted') || $this->data['pid'] || $this->data['keyword'] || $this->data['step']; + + if ($this->data['addword'] || $this->data['step']) { + $alert = $this->alert->check_token($this->data['token']); + + $criteria = ''; + if ($alert) { + $criteria = $alert['criteria']; + } + + $this->data['alert'] = $alert; + + $this->data['alert_parts'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($criteria, true); + + $existing_rep = ''; + if (isset($this->data['alert_parts']['spokenby'])) { + $existing_rep = $this->data['alert_parts']['spokenby'][0]; + } + + $existing_section = ''; + if (count($this->data['alert_parts']['sections'])) { + $existing_section = $this->data['alert_parts']['sections'][0]; + } + + if ($this->data['alert_parts']['match_all']) { + $this->data['match_all'] = true; + } + + $words = get_http_var('words', $this->data['alert_parts']['words'], true); + + $this->data['words'] = []; + $this->data['keywords'] = []; + foreach ($words as $word) { + if (trim($word) != '') { + $this->data['keywords'][] = $word; + $this->data['words'][] = $this->wrap_phrase_in_quotes($word); + } + } + + $add_all_related = get_http_var('add_all_related'); + $this->data['add_all_related'] = $add_all_related; + $this->data['skip_keyword_terms'] = []; + + $selected_related_terms = get_http_var('selected_related_terms', [], true); + $this->data['selected_related_terms'] = $selected_related_terms; + + if ($this->data['step'] !== 'define') { + if ($add_all_related) { + $this->data['selected_related_terms'] = []; + $related_terms = get_http_var('related_terms', [], true); + foreach ($related_terms as $term) { + $this->data['skip_keyword_terms'][] = $term; + $this->data['keywords'][] = $term; + $this->data['words'][] = $this->wrap_phrase_in_quotes($term); + } + } else { + $this->data['skip_keyword_terms'] = $selected_related_terms; + foreach ($selected_related_terms as $term) { + $this->data['keywords'][] = $term; + $this->data['words'][] = $this->wrap_phrase_in_quotes($term); + } + } + } + $this->data['exclusions'] = trim(get_http_var("exclusions", implode('', $this->data['alert_parts']['exclusions']))); + $this->data['representative'] = trim(get_http_var("representative", $existing_rep)); + + $this->data['search_section'] = trim(get_http_var("search_section", $existing_section)); + + $separator = ' OR '; + if ($this->data['match_all']) { + $separator = ' '; + } + $this->data['keyword'] = implode($separator, $this->data['words']); + if ($this->data['exclusions']) { + $this->data['keyword'] = '(' . $this->data['keyword'] . ') -' . $this->data["exclusions"]; + } + + $this->data['results'] = ''; + + $this->getSearchSections(); + } # XXX probably should do something here if $alertsearch is set + $this->data['sign'] = get_http_var('sign'); $this->data['site'] = get_http_var('site'); $this->data['message'] = ''; @@ -106,6 +246,36 @@ private function getBasicData() { $ACTIONURL = new \MySociety\TheyWorkForYou\Url($this_page); $ACTIONURL->reset(); $this->data['actionurl'] = $ACTIONURL->generate(); + + } + + private function wrap_phrase_in_quotes($phrase) { + if (strpos($phrase, ' ') > 0) { + $phrase = '"' . trim($phrase, '"') . '"'; + } + + return $phrase; + } + + private function getRecentResults($text) { + global $SEARCHENGINE; + $se = new \SEARCHENGINE($text); + $this->data['search_result_count'] = $se->run_count(0, 10); + $se->run_search(0, 1, 'date'); + } + + private function getSearchSections() { + $this->data['sections'] = []; + if ($this->data['search_section']) { + foreach (explode(' ', $this->data['search_section']) as $section) { + $this->data['sections'][] = \MySociety\TheyWorkForYou\Utility\Alert::sectionToTitle($section); + } + } + } + + private function updateAlert($token) { + $success = $this->alert->update($token, $this->data); + return $success; } private function checkInput() { @@ -113,6 +283,12 @@ private function checkInput() { $errors = []; + # these are the initial screens and so cannot have any errors as we've not submitted + if (!$this->data['submitted'] || $this->data['step'] == 'define' || $this->data['mp_step'] == 'mp_alert') { + $this->data['errors'] = $errors; + return; + } + // Check each of the things the user has input. // If there is a problem with any of them, set an entry in the $errors array. // This will then be used to (a) indicate there were errors and (b) display @@ -131,6 +307,9 @@ private function checkInput() { } $text = $this->data['alertsearch']; + if ($this->data['mp_search']) { + $text = $this->data['mp_search']; + } if (!$text) { $text = $this->data['keyword']; } @@ -156,10 +335,48 @@ private function checkInput() { } private function searchForConstituenciesAndMembers() { - // Do the search - if ($this->data['alertsearch']) { - $this->data['members'] = \MySociety\TheyWorkForYou\Utility\Search::searchMemberDbLookupWithNames($this->data['alertsearch'], true); - [$this->data['constituencies'], $this->data['valid_postcode']] = \MySociety\TheyWorkForYou\Utility\Search::searchConstituenciesByQuery($this->data['alertsearch']); + if ($this->data['results'] == 'changes-abandoned') { + $this->data['members'] = false; + return; + } + + $text = $this->data['alertsearch']; + if ($this->data['mp_search']) { + $text = $this->data['mp_search']; + } + $errors = []; + if ($text != '') { + //$members_from_pids = array_values(\MySociety\TheyWorkForYou\Utility\Search::membersForIDs($this->data['alertsearch'])); + $members_from_names = []; + $names_from_pids = array_values(\MySociety\TheyWorkForYou\Utility\Search::speakerNamesForIDs($text)); + foreach ($names_from_pids as $name) { + $members_from_names = array_merge($members_from_names,\MySociety\TheyWorkForYou\Utility\Search::searchMemberDbLookupWithNames($name)); + } + $members_from_words = \MySociety\TheyWorkForYou\Utility\Search::searchMemberDbLookupWithNames($text, true); + $this->data['members'] = array_merge($members_from_words, $members_from_names); + [$this->data['constituencies'], $this->data['valid_postcode']] = \MySociety\TheyWorkForYou\Utility\Search::searchConstituenciesByQuery($text, false); + } elseif ($this->data['pid']) { + $MEMBER = new \MEMBER(['person_id' => $this->data['pid']]); + $this->data['members'] = [[ + "person_id" => $MEMBER->person_id, + "given_name" => $MEMBER->given_name, + "family_name" => $MEMBER->family_name, + "house" => $MEMBER->house_disp, + "title" => $MEMBER->title, + "lordofname" => $MEMBER->lordofname, + "constituency" => $MEMBER->constituency, + ]]; + } elseif (isset($this->data['representative']) && $this->data['representative'] != '') { + $this->data['members'] = \MySociety\TheyWorkForYou\Utility\Search::searchMemberDbLookupWithNames($this->data['representative'], true); + + $member_count = count($this->data['members']); + if ($member_count == 0) { + $errors["representative"] = gettext("No matching representative found"); + } elseif ($member_count > 1) { + $errors["representative"] = gettext("Multiple matching representatives found, please select one."); + } else { + $this->data['pid'] = $this->data['members'][0]['person_id']; + } } else { $this->data['members'] = []; } @@ -167,24 +384,42 @@ private function searchForConstituenciesAndMembers() { # If the above search returned one result for constituency # search by postcode, use it immediately if (isset($this->data['constituencies']) && count($this->data['constituencies']) == 1 && $this->data['valid_postcode']) { - $MEMBER = new \MEMBER(['constituency' => $this->data['constituencies'][0], 'house' => 1]); + $MEMBER = new \MEMBER(['constituency' => array_values($this->data['constituencies'])[0], 'house' => 1]); $this->data['pid'] = $MEMBER->person_id(); - $this->data['pc'] = $this->data['alertsearch']; + $this->data['pc'] = $text; unset($this->data['constituencies']); - $this->data['alertsearch'] = ''; } if (isset($this->data['constituencies'])) { $cons = []; foreach ($this->data['constituencies'] as $constituency) { try { - $MEMBER = new \MEMBER(['constituency' => $constituency, 'house' => 1]); + $MEMBER = new \MEMBER(['constituency' => $constituency]); $cons[$constituency] = $MEMBER; } catch (\MySociety\TheyWorkForYou\MemberException $e) { // do nothing } } $this->data['constituencies'] = $cons; + if (count($cons) == 1) { + $cons = array_values($cons); + $this->data['pid'] = $cons[0]->person_id(); + } + } + + if ($this->data['alertsearch'] && !$this->data['mp_step'] && ($this->data['pid'] || $this->data['members'] || $this->data['constituencies'])) { + if (count($this->data['members']) == 1) { + $this->data['pid'] = $this->data['members'][0]['person_id']; + } + $this->data['mp_step'] = 'mp_alert'; + $this->data['mp_search'] = $this->data['alertsearch']; + $this->data['alertsearch'] = ''; + } + + if (count($this->data["errors"]) > 0) { + $this->data["errors"] = array_merge($this->data["errors"], $errors); + } else { + $this->data["errors"] = $errors; } } @@ -203,8 +438,12 @@ private function addAlert() { $success = $this->alert->add($this->data, $confirm); if ($success > 0 && !$confirm) { + $this->data['step'] = ''; + $this->data['mp_step'] = ''; $result = 'alert-added'; } elseif ($success > 0) { + $this->data['step'] = ''; + $this->data['mp_step'] = ''; $result = 'alert-confirmation'; } elseif ($success == -2) { // we need to make sure we know that the person attempting to sign up @@ -230,7 +469,8 @@ private function addAlert() { $this->data['pc'] = ''; $this->data['results'] = $result; - $this->data['criteria'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($this->alert->criteria); + $this->data['criteria'] = $this->alert->criteria; + $this->data['display_criteria'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($this->alert->criteria); } @@ -303,13 +543,55 @@ private function formatSearchMemberData() { } private function setUserData() { + if (!isset($this->data['criteria'])) { + $criteria = $this->data['keyword']; + if (!$this->data['match_all']) { + $has_or = strpos($criteria, ' OR ') !== false; + $missing_braces = strpos($criteria, '(') === false; + + if ($has_or && $missing_braces) { + $criteria = "($criteria)"; + } + } + if ($this->data['search_section']) { + $criteria .= " section:" . $this->data['search_section']; + } + if ($this->data['pid']) { + $criteria .= " speaker:" . $this->data['pid']; + } + $this->getRecentResults($criteria); + + $this->data['criteria'] = $criteria; + $this->data['display_criteria'] = \MySociety\TheyWorkForYou\Utility\Alert::prettifyCriteria($criteria); + } + if ($this->data['results'] == 'changes-abandoned') { + $this->data['members'] = false; + $this->data['alertsearch'] = ''; + } + + if ($this->data['alertsearch'] && !(isset($this->data['mistakes']['postcode_and']) || $this->data['members'] || $this->data['pid'])) { + $this->data['step'] = 'define'; + $this->data['words'] = [$this->data['alertsearch']]; + $this->data['keywords'] = [$this->data['alertsearch']]; + $this->data['exclusions'] = ''; + $this->data['representative'] = ''; + } elseif ($this->data['alertsearch'] && ($this->data['members'] || $this->data['pid'])) { + $this->data['mp_step'] = 'mp_alert'; + $this->data['mp_search'] = [$this->data['alertsearch']]; + } elseif ($this->data['members'] && $this->data['mp_step'] == 'mp_search') { + $this->data['mp_step'] = ''; + } + $this->data['current_mp'] = false; $this->data['alerts'] = []; $this->data['keyword_alerts'] = []; $this->data['speaker_alerts'] = []; + $this->data['spoken_alerts'] = []; $this->data['own_member_alerts'] = []; $this->data['all_keywords'] = []; + $this->data['own_mp_criteria'] = ''; $own_mp_criteria = ''; + if ($this->data['email_verified']) { if ($this->user->postcode()) { $current_mp = new \MEMBER(['postcode' => $this->user->postcode()]); @@ -317,18 +599,65 @@ private function setUserData() { $this->data['current_mp'] = $current_mp; $own_mp_criteria = sprintf('speaker:%s', $current_mp->person_id()); } + $own_mp_criteria = $current_mp->full_name(); + $this->data['own_mp_criteria'] = $own_mp_criteria; } $this->data['alerts'] = \MySociety\TheyWorkForYou\Utility\Alert::forUser($this->data['email']); foreach ($this->data['alerts'] as $alert) { - if (array_key_exists('words', $alert)) { - $this->data['all_keywords'][] = implode(' ', $alert['words']); - $this->data['keyword_alerts'][] = $alert; - } elseif (array_key_exists('spokenby', $alert) and sizeof($alert['spokenby']) == 1 and $alert['spokenby'][0] == $own_mp_criteria) { + if (array_key_exists('spokenby', $alert) and sizeof($alert['spokenby']) == 1 and $alert['spokenby'][0] == $own_mp_criteria) { $this->data['own_member_alerts'][] = $alert; - } else { - $this->data['spoken_alerts'][] = $alert; + } elseif (array_key_exists('spokenby', $alert)) { + if (!array_key_exists($alert['spokenby'][0], $this->data['spoken_alerts'])) { + $this->data['spoken_alerts'][$alert['spokenby'][0]] = []; + } + $this->data['spoken_alerts'][$alert['spokenby'][0]][] = $alert; } } + foreach ($this->data['alerts'] as $alert) { + $term = implode(' ', $alert['words']); + $add = true; + if (array_key_exists('spokenby', $alert)) { + $add = false; + } elseif (array_key_exists($term, $this->data['spoken_alerts'])) { + $add = false; + $this->data['all_keywords'][] = $term; + $this->data['spoken_alerts'][$term][] = $alert; + } elseif ($term == $own_mp_criteria) { + $add = false; + $this->data['all_keywords'][] = $term; + $this->data['own_member_alerts'][] = $alert; + } elseif (\MySociety\TheyWorkForYou\Utility\Search::searchMemberDbLookupWithNames($term, true)) { + if (!array_key_exists($term, $this->data['spoken_alerts'])) { + $this->data['spoken_alerts'][$term] = []; + } + $add = false; + # need to add this to make it consistent so the front end know where to get the name + $alert['spokenby'] = [$term]; + $this->data['all_keywords'][] = $term; + $this->data['spoken_alerts'][$term][] = $alert; + } + if ($add) { + $this->data['all_keywords'][] = $term; + $this->data['keyword_alerts'][] = $alert; + } + } + } else { + if ($this->data['alertsearch'] && $this->data['pc']) { + $this->data['mp_step'] = 'mp_alert'; + } + } + if (count($this->data['alerts'])) { + $this->data['delete_token'] = $this->data['alerts'][0]['token']; + } + if ($this->data['addword'] != '' || ($this->data['step'] && count($this->data['errors']) > 0)) { + $this->data["step"] = get_http_var('this_step'); + } else { + $this->data['this_step'] = ''; + } + + $this->data["search_term"] = $this->data['alertsearch']; + if ($this->data['mp_search']) { + $this->data["search_term"] = $this->data['mp_search']; } } } diff --git a/tests/AlertsPageTest.php b/tests/AlertsPageTest.php index 6d62a781a7..91a2b15f24 100644 --- a/tests/AlertsPageTest.php +++ b/tests/AlertsPageTest.php @@ -15,6 +15,10 @@ private function fetch_page($vars) { return $this->base_fetch_page($vars, 'alert'); } + private function get_page($vars = []) { + return $this->base_fetch_page_user($vars, '1.fbb689a0c092f5534b929d302db2c8a9', 'alert'); + } + public function testFetchPage() { $page = $this->fetch_page([]); $this->assertStringContainsString('TheyWorkForYou Email Alerts', $page); @@ -22,12 +26,18 @@ public function testFetchPage() { public function testKeywordOnly() { $page = $this->fetch_page([ 'alertsearch' => 'elephant']); - $this->assertStringContainsString('Receive alerts when [elephant] is mentioned', $page); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('fetch_page([ 'alertsearch' => 'speaker:2']); + $this->assertStringContainsString('Mrs Test Current-MP', $page); } public function testPostCodeOnly() { $page = $this->fetch_page([ 'alertsearch' => 'SE17 3HE']); - $this->assertStringContainsString('when Mrs Test Current-MP', $page); + $this->assertStringContainsString('Mrs Test Current-MP', $page); } public function testPostCodeWithKeyWord() { @@ -49,4 +59,73 @@ public function testPostcodeAndKeywordWithNoSittingMP() { $this->assertStringContainsString('You have used a postcode and something else', $page); $this->assertStringNotContainsString('Did you mean to get alerts for when your MP', $page); } + + public function testBasicKeyWordAlertsCreation() { + $page = $this->fetch_page([ 'step' => 'define']); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('fetch_page([ 'step' => 'review', 'email' => 'test@example.org', 'words[]' => 'fish']); + $this->assertStringContainsString('Review Your Alert', $page); + $this->assertStringContainsString('fetch_page([ 'step' => 'confirm', 'email' => 'test@example.org', 'words[]' => 'fish']); + $this->assertStringContainsString('We’re nearly done', $page); + $this->assertStringContainsString('You should receive an email shortly', $page); + } + + public function testMultipleKeyWordAlertsCreation() { + $page = $this->fetch_page([ 'step' => 'define']); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('fetch_page([ 'step' => 'review', 'email' => 'test@example.org', 'words[]' => ['fish', 'salmon']]); + $this->assertStringContainsString('Review Your Alert', $page); + $this->assertStringContainsString('assertStringContainsString('fetch_page([ 'step' => 'confirm', 'email' => 'test@example.org', 'words[]' => ['fish', 'salmon']]); + $this->assertStringContainsString('You should receive an email shortly', $page); + } + + public function testMultipleKeyWordAlertsCreationLoggedIn() { + $page = $this->get_page(['step' => 'define']); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('get_page([ 'step' => 'review', 'words[]' => ['fish', 'salmon']]); + $this->assertStringContainsString('Review Your Alert', $page); + $this->assertStringContainsString('assertStringContainsString('get_page([ 'step' => 'confirm', 'words[]' => ['fish', 'salmon']]); + $this->assertStringContainsString('You will now receive email alerts on any day when [fish salmon] is mentioned in parliament', $page); + } + + public function testKeyWordAndSectionAlertsCreationLoggedIn() { + $page = $this->get_page(['step' => 'define']); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('get_page(['step' => 'review', 'words[]' => 'fish', 'search_section' => 'debates']); + $this->assertStringContainsString('Review Your Alert', $page); + $this->assertStringContainsString('get_page(['step' => 'confirm', 'words[]' => 'fish', 'search_section' => 'debates']); + $this->assertStringContainsString('You will now receive email alerts on any day when [fish] is mentioned in House of Commons debates', $page); + } + + public function testKeyWordAndSpeakerAlertsCreationLoggedIn() { + $page = $this->get_page(['step' => 'define']); + $this->assertStringContainsString('What word or phrase would you like to recieve alerts about', $page); + $this->assertStringContainsString('get_page(['step' => 'review', 'words[]' => 'fish', 'representative' => 'Mrs Test Current-MP']); + $this->assertStringContainsString('Review Your Alert', $page); + $this->assertStringContainsString('assertStringContainsString('get_page([ 'step' => 'confirm', 'words[]' => 'fish', 'representative' => 'Mrs Test Current-MP']); + $this->assertStringContainsString('You will now receive email alerts on any day when Mrs Test Current-MP mentions [fish] in parliament', $page); + } } diff --git a/tests/SearchTest.php b/tests/SearchTest.php index a99e0a6db3..0bd2e38fa7 100644 --- a/tests/SearchTest.php +++ b/tests/SearchTest.php @@ -121,7 +121,7 @@ public function testSearchPage() { public function testSearchPageMP() { $page = $this->fetch_page([ 'q' => 'Mary Smith' ]); $this->assertStringContainsString('Mary Smith', $page); - $this->assertStringContainsString('MP, Amber Valley', $page); + $this->assertMatchesRegularExpression('/MP *for Amber Valley/', $page); } /** @@ -169,9 +169,9 @@ public function testSearchPageMultipleCons() { $page = $this->fetch_page([ 'q' => 'Liverpool' ]); $this->assertStringContainsString('MPs in constituencies matching Liverpool', $page); $this->assertStringContainsString('Susan Brown', $page); - $this->assertStringContainsString('MP, Liverpool, Riverside', $page); + $this->assertMatchesRegularExpression('/MP *for Liverpool, Riverside/', $page); $this->assertStringContainsString('Andrew Jones', $page); - $this->assertStringContainsString('MP, Liverpool, Walton', $page); + $this->assertMatchesRegularExpression('/MP *for Liverpool, Walton/', $page); } /** diff --git a/tests/_fixtures/alertspage.xml b/tests/_fixtures/alertspage.xml index e237dec30d..b1c65e678c 100644 --- a/tests/_fixtures/alertspage.xml +++ b/tests/_fixtures/alertspage.xml @@ -150,6 +150,14 @@ + + 1 + Test + User + user@example.org + $2y$10$UNelQZqpPpO1jT.f7DLgeOdp.WBT81c5ECvOeTMFeQTBTyq3aCh8q + 1 + diff --git a/www/docs/js/main.js b/www/docs/js/main.js index 6dd194a9f0..9f319d38f9 100644 --- a/www/docs/js/main.js +++ b/www/docs/js/main.js @@ -343,6 +343,28 @@ $(function(){ if (!$('#options').data('advanced')) { $("#options").find(":input").attr("disabled", "disabled"); } + + $('#add-all').on('click', function(e) { + var $add_all = e.currentTarget; + var $selected_related = document.querySelectorAll('input[name="selected_related_terms[]"]'); + if ($add_all.checked) { + $selected_related.forEach(function(input) { + if (input.checked) { + input.setAttribute('data:was_checked', true); + } + input.checked = true; + input.setAttribute('disabled', true); + }); + } else { + $selected_related.forEach(function(input) { + if (!input.getAttribute('data:was_checked')) { + input.checked = false; + } + input.removeAttribute('data:was_checked'); + input.removeAttribute('disabled'); + }); + } + }); }); // Backwards-compatible functions for the click/submit trackers on MP pages @@ -423,6 +445,48 @@ function wrap_error($message){ return ''; } +function createAccordion(triggerSelector, contentSelector) { + var triggers = document.querySelectorAll(triggerSelector); + + triggers.forEach(function(trigger) { + var content = document.querySelector(trigger.getAttribute('href')); + + var openAccordion = function() { + content.style.maxHeight = content.scrollHeight + "px"; // Dynamically calculate height + content.setAttribute('aria-hidden', 'false'); + trigger.setAttribute('aria-expanded', 'true'); + }; + + var closeAccordion = function() { + content.style.maxHeight = null; // Collapse + content.setAttribute('aria-hidden', 'true'); + trigger.setAttribute('aria-expanded', 'false'); + }; + + trigger.addEventListener('click', function(e) { + e.preventDefault(); + + if (content.style.maxHeight) { + closeAccordion(); + } else { + openAccordion(); + } + }); + + // Accessibility + trigger.setAttribute('aria-controls', content.getAttribute('id')); + trigger.setAttribute('aria-expanded', 'false'); + content.setAttribute('aria-hidden', 'true'); + content.style.maxHeight = null; + }); +} + +// Initialize accordion when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + createAccordion('.accordion-button', '.accordion-content'); +}); + + $(function() { $('#how-often-annually').click(function() { diff --git a/www/docs/style/sass/_twfy-mixins.scss b/www/docs/style/sass/_twfy-mixins.scss index 05e5f4169c..5c7a50ce45 100644 --- a/www/docs/style/sass/_twfy-mixins.scss +++ b/www/docs/style/sass/_twfy-mixins.scss @@ -246,22 +246,29 @@ $weight_bold: 700; .button { background-color: $colour_primary; font-weight: $weight_semibold; - border: 0; + border: 1px solid $colour_primary; @include border-radius(3px); &:hover { background-color: $primary-color-700; } - &:focus { + &:focus-visible { background-color: $color-yellow; color: $body-font-color; } } + button { @extend .button; } +.button--outline { + border: 1px solid $colour_primary; + background-color: $white-text; + color: $colour_primary; +} + .secondary-button, .button--secondary { background-color: $colour_off_white; @@ -276,11 +283,23 @@ button { .button--red, .button--negative { background-color: $color_red; + border: 1px solid $color_red; &:hover { background-color: darken($color_red, 10%); } } +.button--outline-red { + color: $color_red; + border: 1px solid $color_red; + background-color: $white-text; + + &:hover { + color: $white-text; + background-color: $color_red; + } +} + .button--disabled, .button--disabled:hover { background-color: lighten($colour_off_white, 3%); diff --git a/www/docs/style/sass/app.scss b/www/docs/style/sass/app.scss index d356a39cd9..511351cd8c 100644 --- a/www/docs/style/sass/app.scss +++ b/www/docs/style/sass/app.scss @@ -62,10 +62,10 @@ @import url(https://fonts.googleapis.com/css2?family=Manrope:wght@700&family=Merriweather:wght@400;700&display=swap); /* Foundation Icons v 3.0 MIT License */ @font-face { - font-family: "foundation-icons"; - src: url("/style/foundation-icons/foundation-icons.woff") format("woff"); - font-weight: normal; - font-style: normal; + font-family: "foundation-icons"; + src: url("/style/foundation-icons/foundation-icons.woff") format("woff"); + font-weight: normal; + font-style: normal; } .fi-social-facebook:before, @@ -75,17 +75,24 @@ .fi-megaphone:before, .fi-pound:before, .fi-magnifying-glass:before, -.fi-heart:before +.fi-heart:before, +.fi-plus:before, +.fi-play:before, +.fi-pause:before, +.fi-trash:before, +.fi-page-edit:before, +.fi-x:before, +.fi-save:before { - font-family: "foundation-icons"; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - display: inline-block; - text-decoration: inherit; + font-family: "foundation-icons"; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + display: inline-block; + text-decoration: inherit; } // https://github.com/zurb/foundation-icon-fonts/blob/master/_foundation-icons.scss @@ -97,6 +104,13 @@ .fi-pound:before {content: "\f19a"} .fi-magnifying-glass:before {content: "\f16c"} .fi-heart:before { content: "\f159"; } +.fi-plus:before { content: "\f199"; } +.fi-play:before { content: "\f198"; } +.fi-pause:before { content: "\f191"; } +.fi-trash:before { content: "\f204"; } +.fi-page-edit:before { content: "\f184"; } +.fi-x:before { content: "\f217"; } +.fi-save:before { content: "\f1ac"; } html, body { @@ -129,13 +143,13 @@ h3 { } .pull-right { - @media (min-width: $medium-screen) { + @media (min-width: $medium-screen) { float: right; margin-left: 1em; } } .pull-left { - @media (min-width: $medium-screen) { + @media (min-width: $medium-screen) { float: left; margin-left: 1em; } @@ -166,12 +180,12 @@ ul { a { overflow-wrap: break-word; word-wrap: break-word; - + -webkit-hyphens: auto; - -moz-hyphens: auto; - -ms-hyphens: auto; - hyphens: auto; - + -moz-hyphens: auto; + -ms-hyphens: auto; + hyphens: auto; + color: $links; } @@ -198,7 +212,7 @@ a:focus { // for .button elements!! vertical-align: -0.4em; } - + &.tertiary { @include button-style($bg: $links); } @@ -231,6 +245,7 @@ form { @import "parts/panels"; @import "parts/promo-banner"; +@import "parts/accordion"; @import "pages/mp"; @import "pages/topics"; diff --git a/www/docs/style/sass/pages/_alert.scss b/www/docs/style/sass/pages/_alert.scss index 23aeab8d98..ff0e5b90ea 100644 --- a/www/docs/style/sass/pages/_alert.scss +++ b/www/docs/style/sass/pages/_alert.scss @@ -283,6 +283,11 @@ } } +.alert-section--message { + background-color: #FFFCD9; // very light yellow + padding: 1rem; +} + .alert-section--disambiguation { li { margin: 1em 0; diff --git a/www/docs/style/sass/parts/_accordion.scss b/www/docs/style/sass/parts/_accordion.scss new file mode 100644 index 0000000000..4c44111154 --- /dev/null +++ b/www/docs/style/sass/parts/_accordion.scss @@ -0,0 +1,280 @@ +.label { + background-color: #fff; + color: $primary-color; + padding: 0.25rem 0.5rem; + border-radius: 1rem; + font-size: 0.75rem; + + &--primary-light { + background-color: $primary-color-200; + color: $body-font-color; + } + + &--red { + background-color: lighten($color-red, 40%); + color: $body-font-color; + } +} + +.alert-page-header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + + button, h2 { + margin-bottom: 0; + margin-top: 0; + } + + .alert-page-header__button-group { + display: flex; + flex-direction: row; + gap: 0.5rem; + input { + margin-bottom: 0; + } + } +} + +.accordion { + margin-top: 2rem; +} + +.accordion-button { + width: 100%; + display: flex; + justify-content: space-between; + text-align: left; + padding: 0.5rem; + font-size: 1rem; + font-weight: 400; + cursor: pointer; + border: none; + color: $body-font-color; + background-color: $primary-color-200; + + &[aria-expanded="true"] { + background-color: lighten($primary-color-100, 6%); + color: $body-font-color; + border: 1px solid $primary-color; + & + .accordion-content{ + max-height: 1000px; + transition: max-height 0.3s ease; + } + + i { + transform: rotate(45deg); + } + } + +} + +.accordion-button--content { + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + gap: 0.75rem; + + .content-subtitle { + @extend .label; + } +} + +.accordion-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + padding-left: 1rem; + + .alert-controller-wrapper { + margin-bottom: 2rem; + form { + display: inline; + } + + button { + margin-bottom: 0; + span { + margin-right: 0.2rem; + } + } + + button.alert { + background-color: $color-red; + color: #fff; + } + } + + .add-remove-tool { + display: flex; + flex-direction: row; + + input { + margin: 0; + height: 40px; + } + + button { + max-width: 100px; + height: 40px; + } + } + + label { + font-size: inherit; + color: inherit; + } + + select { + max-width: 350px; + } +} + +.alert-page-alert-controls { + form { + display: inline; + } +} + +.keyword-list { + ul { + list-style: none; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; + margin-left: 0; + + li { + font-weight: bold; + i { + margin-left: 0.25rem; + } + } + + } +} + +.heading-with-bold-word { + font-weight: 400; + + span { + font-weight: bold; + } +} + +.alert-meta-info { + + .alert-meta-info-results { + display: flex; + flex-direction: row; + flex-wrap: wrap; + column-gap: 1rem; + row-gap: 1rem; + align-items: center; + margin-bottom: 1rem; + + .content-header-item { + border-radius: 0.5rem; + background-color: $primary-color-200; + padding: 1rem; + + dt { + font-size: 0.7rem; + text-transform: uppercase; + margin-bottom: 0; + } + + dd { + margin-bottom: 0; + font-size: 1.1rem; + } + } + } + +} + +button { + i { + margin-right: 0.15rem; + } +} + +.alert-page-section { + margin-bottom: 3rem; +} + +.alert-page-subsection { + margin-bottom: 2.5rem; + + .alert-page-subsection--subtitle { + margin-bottom: 0.5rem; + } + + :last-child { + margin-bottom: 0; + } +} + +.alert-page-option-label { + display: inline; + margin-left: 0.5em; +} + +.button.red { + background-color: $color-red; + color: #fff; + + &:hover { + background-color: darken($color-red, 15%); + } +} + +#create-alert-form { + label { + color: $body-font-color; + font-size: 1.1rem; + line-height: 1.2; + margin-bottom: 0.75rem; + } + + input[type="checkbox"], input[type="radio"] { + display: inline-block; + height: 1.5rem; + width: 1.5rem; + margin: 0 0.25rem 0 0; + vertical-align: middle; + + + label { + display: inline-block; + margin-bottom: 0; + line-height: 1.5rem; + vertical-align: middle; + } + } + + input[type="text"], select { + max-width: 400px; + height: 40px; + border-color: $body-font-color; + } + + .checkbox-wrapper { + display: flex; + flex-direction: row; + + input[type="checkbox"], input[type="radio"] { + flex-shrink: 0; + } + } + + .checkbox-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + } +} diff --git a/www/includes/easyparliament/alert.php b/www/includes/easyparliament/alert.php index 66493d36e8..226b6991e6 100644 --- a/www/includes/easyparliament/alert.php +++ b/www/includes/easyparliament/alert.php @@ -104,6 +104,43 @@ public function fetch($confirmed, $deleted) { return $data; } + public function get_related_terms($term) { + $q = $this->db->query("SELECT + search_suggestion + FROM vector_search_suggestions + WHERE search_term = :term", [ + ':term' => $term, + ]); + + $data = $q->fetchAll(); + $related = []; + foreach ($data as $d) { + $related[] = $d['search_suggestion']; + } + return $related; + } + + public function update($id, $details) { + $criteria = \MySociety\TheyWorkForYou\Utility\Alert::detailsToCriteria($details); + + $q = $this->db->query("SELECT * FROM alerts + WHERE alert_id = :id", [ + ':id' => $id, + ])->first(); + if ($q) { + $q = $this->db->query("UPDATE alerts SET deleted = 0, criteria = :criteria, confirmed = 1 + WHERE alert_id = :id", [ + ":criteria" => $criteria, + ":id" => $id, + ]); + + if ($q->success()) { + return 1; + } + } + return -1; + } + public function add($details, $confirmation_email = false, $instantly_confirm = true) { // Adds a new alert's info into the database. diff --git a/www/includes/easyparliament/templates/html/alert/_alert_form.php b/www/includes/easyparliament/templates/html/alert/_alert_form.php new file mode 100644 index 0000000000..91a39f82a6 --- /dev/null +++ b/www/includes/easyparliament/templates/html/alert/_alert_form.php @@ -0,0 +1,339 @@ +
+
+
+
+

+ + + + +

+ +
+ + + + +
+

+ + +
+ + + + + +
+ + +
+ + + + + + $word) { ?> + + + + + + +
+ +
+
+ > + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + 0) { ?> + + $member) { + $name = member_full_name($member['house'], $member['title'], $member['given_name'], $member['family_name'], $member['lordofname']); + if ($member['constituency']) { + $name .= ' (' . gettext($member['constituency']) . ')'; + } ?> + +
+ + +

+ + +
+ + + + +
+ + + + + + + + + + + + + +
+

+
+

+
    + +
  • + +
+
+ +

+ +
+ +
+ + + + + +
+
+ +
+ 0) { ?> +
+
+
+
+ + + +
+
+
30 May 2024
+
+ + + +
+ + + + +
+ + + + + + + + + + + + + + + + + +
+

+ +
+ +

:

+ +

:

+ +
    + +
  • + +
+
+ + +
+

:

+
    + +
  • + +
+
+ + +
+ 0) { ?> +

:

+
    + +
  • + +
+ +

+ +
+ + 0) { ?> +
+

+
    + +
  • + +
+
+ + + 0 || isset($lastmention)) { ?> +
+
+

See mentions for this alert

+ +
+ 0) { ?> +
+
+
+
+ + + +
+
+
30 May 2024
+
+ +
+ + +
+ + +
+ + + + + + +
+ +
+ +
+
+
+ +
+ +
+

+

+ phrase, be sure to put it in quotes. Also use quotes around a word to avoid stemming (where ‘horse’ would also match ‘horses’).') ?> +

+

+ one term per alert – if you wish to receive alerts on more than one thing, or for more than one person, simply fill in this form as many times as you need, or use boolean OR.') ?> +

+

+ horse or pony are mentioned in Parliament, please fill in this form once with the word horse and then again with the word pony (or you can put horse OR pony with the OR in capitals). Do not put horse, pony as that will only sign you up for alerts where both horse and pony are mentioned.') ?> +

+
+ +
+ +

+

+ +

+ +
    +
  • .
  • +
  • .
  • +
  • Managing email alerts, including how to stop or suspend them.'), 'https://www.mysociety.org/2014/09/04/how-to-manage-your-theyworkforyou-alerts/') ?>
  • +
      +
+ +
+
diff --git a/www/includes/easyparliament/templates/html/alert/_list_accordian.php b/www/includes/easyparliament/templates/html/alert/_list_accordian.php new file mode 100644 index 0000000000..fb1065f292 --- /dev/null +++ b/www/includes/easyparliament/templates/html/alert/_list_accordian.php @@ -0,0 +1,226 @@ +
+ $alert) { ?> +
+ + +
+ + +
+ +
+
+

Representative alerts

+
+
+ + +
+
+ +

+ + 0) { ?> +
+

+ +

+ + + +

Alert when is mentioned

+
+ + +
+ + + 0) { ?> +
+

+ + +
+ + + +
+
+

+ + +

+

+
+ + + + + + + + +
+
+ + + + +
+
+ + +

Alert when is mentioned

+
+ + +
+ +
+ +
diff --git a/www/includes/easyparliament/templates/html/alert/_mp_alert_form.php b/www/includes/easyparliament/templates/html/alert/_mp_alert_form.php new file mode 100644 index 0000000000..5746c14aed --- /dev/null +++ b/www/includes/easyparliament/templates/html/alert/_mp_alert_form.php @@ -0,0 +1,76 @@ + 0) { + $member_options = true; ?> +

%s speaks'), _htmlspecialchars($search_term)) ?>

+ + +
+ +

+ + + + +

+ + +

+ + + + + + + + +

+ +

+ + + + + + +

+ + + + + + + + + + + + + + + + + + +
+ diff --git a/www/includes/easyparliament/templates/html/alert/_own_mp_alerts.php b/www/includes/easyparliament/templates/html/alert/_own_mp_alerts.php new file mode 100644 index 0000000000..d8d51e2fdb --- /dev/null +++ b/www/includes/easyparliament/templates/html/alert/_own_mp_alerts.php @@ -0,0 +1,49 @@ + + +

+
+
+ + + + + + + + +
+
+ + + + +
+ +
+ + + +

Alert when is mentioned

+
+ + +
+ diff --git a/www/includes/easyparliament/templates/html/alert/index.php b/www/includes/easyparliament/templates/html/alert/index.php index 8f4deef191..12af929ee3 100644 --- a/www/includes/easyparliament/templates/html/alert/index.php +++ b/www/includes/easyparliament/templates/html/alert/index.php @@ -20,7 +20,7 @@

- +

@@ -83,7 +83,7 @@

- +

@@ -104,6 +104,11 @@

+ +

+

+ +

@@ -115,11 +120,22 @@ - +

+
+

+ +
+
+ + 0) || - ($alertsearch) + !$results && ( + $members || + (isset($constituencies) && count($constituencies) > 0) || + ($alertsearch) + ) ) { /* We need to disambiguate the user's instructions */ $member_options = false; @@ -129,7 +145,7 @@ -

%s speaks'), _htmlspecialchars($alertsearch)) ?>

+

%s speaks'), _htmlspecialchars($search_term)) ?>