forked from cmccabe/linkulator2
Merge branch 'search' of cmccabe/linkulator2 into master
This commit is contained in:
commit
2e05eb3408
34
data.py
34
data.py
|
@ -70,6 +70,16 @@ def parse_ignore_file() -> list:
|
|||
return ignore_names
|
||||
|
||||
|
||||
def get_parent_record(parent_id: str, link_data: list) -> list:
|
||||
"""given a parent ID, return the ID for the parent record or -1"""
|
||||
if parent_id == "":
|
||||
raise ValueError("parent_id cannot be empty")
|
||||
for record in link_data:
|
||||
if record[0] == parent_id.partition("+")[2]:
|
||||
return record
|
||||
raise KeyError("there's no parent record for the specified parent_id")
|
||||
|
||||
|
||||
class LinkData:
|
||||
"""Class that contains link_data, categories and categories count tables,
|
||||
plus methods to generate and update these items"""
|
||||
|
@ -173,3 +183,27 @@ class LinkData:
|
|||
cat_record["count"] += 1
|
||||
if cat_record["last_updated"] < timestamp:
|
||||
cat_record["last_updated"] = timestamp
|
||||
|
||||
def search(self, keyword: str) -> list:
|
||||
"""returns a unique list of link_data records for posts that contain
|
||||
the specified keyword"""
|
||||
if keyword == "":
|
||||
raise ValueError("a search keyword must be specified")
|
||||
|
||||
query = (record for record in self.link_data if keyword in record)
|
||||
|
||||
if query:
|
||||
search_results: set = set()
|
||||
for record in query:
|
||||
post_id = record[0]
|
||||
parent_id = record[3]
|
||||
if post_id:
|
||||
search_results.add(tuple(record))
|
||||
else:
|
||||
try:
|
||||
parent_record = get_parent_record(parent_id, self.link_data)
|
||||
except KeyError:
|
||||
continue
|
||||
search_results.add(tuple(parent_record))
|
||||
|
||||
return sorted(search_results, key=lambda x: x[0], reverse=True)
|
||||
|
|
|
@ -120,9 +120,59 @@ def print_thread_details(post_id) -> tuple:
|
|||
return parent_id, post_url
|
||||
|
||||
|
||||
def print_search_results(keyword: str, search_results: list):
|
||||
"""a view for the search results - prints results to screen"""
|
||||
print(
|
||||
"\nShowing results for {}\n\n{:>4s} {:<15s}{:<12s}{:<13s}".format(
|
||||
keyword, "ID#", "DATE", "AUTHOR", "DESC"
|
||||
)
|
||||
)
|
||||
|
||||
for display_index, record in enumerate(search_results, start=1):
|
||||
date = datetime.fromtimestamp(float(record[2])).strftime("%Y-%m-%d")
|
||||
author = record[1]
|
||||
desc = record[6]
|
||||
print("{:4d} {:<15s}{:<12s}{:<13s}".format(display_index, date, author, desc))
|
||||
|
||||
|
||||
## CONTROLS
|
||||
|
||||
|
||||
def search():
|
||||
"""Control for the search function"""
|
||||
while True:
|
||||
keyword = input("\nEnter your search (or leave empty to cancel):\n")
|
||||
if keyword == "":
|
||||
print("Search cancelled\n")
|
||||
return
|
||||
search_results = LinkData.search(keyword)
|
||||
if not search_results:
|
||||
print("No results found\n")
|
||||
return
|
||||
while True:
|
||||
print_search_results(keyword, search_results)
|
||||
option = input(
|
||||
"\nEnter a post ID to see its thread, {} to start a new search, {} to go back, or {} to quit: \n".format(
|
||||
style_text("s", "underline"),
|
||||
style_text("m", "underline"),
|
||||
style_text("q", "underline"),
|
||||
)
|
||||
).lower()
|
||||
if option == "q":
|
||||
graceful_exit()
|
||||
if option == "m":
|
||||
return
|
||||
if option == "s":
|
||||
break
|
||||
try:
|
||||
if 1 <= int(option) <= len(search_results):
|
||||
menu_view_thread_details(search_results[int(option) - 1][0])
|
||||
else:
|
||||
raise IndexError("Invalid post ID")
|
||||
except (KeyError, ValueError, IndexError):
|
||||
# Catch a Post ID that is not in the thread list or is not a number
|
||||
print("\n{}\n".format(style_text("Invalid entry", "bold")))
|
||||
|
||||
def view_link_in_browser(url):
|
||||
"""Attempts to view the specified URL in the configured browser"""
|
||||
if which(config.USER.browser) is None:
|
||||
|
@ -241,28 +291,6 @@ def post_link() -> int:
|
|||
return LinkData.add(record)
|
||||
|
||||
|
||||
def search(keyword):
|
||||
"""Search function - not yet complete"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
## PSEUDOCODE:
|
||||
## results_found = ""
|
||||
## for line in link_data:
|
||||
## if keyword in link_data[title] or keyword in link_data[category] or
|
||||
## keyword in link_data[comment]: ## results_found = "yes"
|
||||
## if line is parent post:
|
||||
## print line
|
||||
## elif line is reply:
|
||||
## get parentID
|
||||
## print(line[parentID])
|
||||
## if results_found == "":
|
||||
## print("No results found")
|
||||
## else:
|
||||
## next_step = input("Enter ID to view thread, "M" for main menu, or [Enter] to quit: ")
|
||||
## if next_step...
|
||||
|
||||
|
||||
def menu_view_categories():
|
||||
"""Displays list of categories, takes keyboard input and
|
||||
executes corresponding functions."""
|
||||
|
@ -270,8 +298,10 @@ def menu_view_categories():
|
|||
print_categories()
|
||||
|
||||
option = input(
|
||||
"\nEnter a category ID, {} to post a link, or {} to quit: ".format(
|
||||
style_text("p", "underline"), style_text("q", "underline")
|
||||
"\nEnter a category ID, {} to post a link, {} to search, or {} to quit: ".format(
|
||||
style_text("p", "underline"),
|
||||
style_text("s", "underline"),
|
||||
style_text("q", "underline"),
|
||||
)
|
||||
).lower()
|
||||
|
||||
|
@ -282,6 +312,9 @@ def menu_view_categories():
|
|||
if post_id >= 0:
|
||||
menu_view_thread_details(post_id)
|
||||
continue
|
||||
if option == "s":
|
||||
search()
|
||||
continue
|
||||
try:
|
||||
cat_index = categories[int(option) - 1]
|
||||
menu_view_category_details(cat_index)
|
||||
|
@ -298,8 +331,9 @@ def menu_view_category_details(cat_index):
|
|||
|
||||
option = input(
|
||||
"Enter a post ID to see its thread, {} to go back, {} to "
|
||||
"post a link, or {} to quit: ".format(
|
||||
"search, {} to post a link, or {} to quit: ".format(
|
||||
style_text("m", "underline"),
|
||||
style_text("s", "underline"),
|
||||
style_text("p", "underline"),
|
||||
style_text("q", "underline"),
|
||||
)
|
||||
|
@ -309,6 +343,9 @@ def menu_view_category_details(cat_index):
|
|||
graceful_exit()
|
||||
if option == "m":
|
||||
return
|
||||
if option == "s":
|
||||
search()
|
||||
continue
|
||||
if option == "p":
|
||||
post_id = post_link()
|
||||
if post_id >= 0:
|
||||
|
@ -324,10 +361,11 @@ def menu_view_category_details(cat_index):
|
|||
|
||||
def menu_view_thread_details(post_id):
|
||||
"""Displays thread details, handles related navigation menu"""
|
||||
option_text = "\nType {} to reply, {} to view in {}, {} to post a new link, {} to go back, or {} to quit: ".format(
|
||||
option_text = "\nType {} to reply, {} to view in {}, {} to search, {} to post a new link, {} to go back, or {} to quit: ".format(
|
||||
style_text("r", "underline"),
|
||||
style_text("b", "underline"),
|
||||
config.USER.browser,
|
||||
style_text("s", "underline"),
|
||||
style_text("p", "underline"),
|
||||
style_text("m", "underline"),
|
||||
style_text("q", "underline"),
|
||||
|
@ -344,6 +382,9 @@ def menu_view_thread_details(post_id):
|
|||
if option == "r":
|
||||
reply(parent_id)
|
||||
continue
|
||||
if option == "s":
|
||||
search()
|
||||
continue
|
||||
if option == "p":
|
||||
post_id = post_link()
|
||||
if post_id >= 0:
|
|
@ -1,11 +1,13 @@
|
|||
"""unit tests for the data module"""
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from time import time
|
||||
import data
|
||||
|
||||
|
||||
class TestDataHelperFunctions(unittest.TestCase):
|
||||
"""Tests that cover helper functions, mostly handling string validation"""
|
||||
|
||||
def test_wash_line(self):
|
||||
"""tests the data.wash_line function"""
|
||||
teststrings = [
|
||||
|
@ -104,6 +106,110 @@ class TestDataHelperFunctions(unittest.TestCase):
|
|||
data.process(item, "username{}".format(i)), teststrings_output[i]
|
||||
)
|
||||
|
||||
def test_get_parent_record(self):
|
||||
test_link_data = []
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
data.get_parent_record("", test_link_data)
|
||||
|
||||
|
||||
class TestLinkDataSearch(unittest.TestCase):
|
||||
@unittest.mock.patch.object(data.LinkData, "get")
|
||||
def test_search_exceptions(self, mock_get):
|
||||
"""ensures exceptions are raised"""
|
||||
link_data = data.LinkData()
|
||||
mock_get.assert_called()
|
||||
with self.assertRaises(ValueError):
|
||||
link_data.search("")
|
||||
|
||||
@unittest.mock.patch.object(data.LinkData, "get")
|
||||
def test_search(self, mock_get):
|
||||
"""tests search function"""
|
||||
link_data = data.LinkData()
|
||||
mock_get.assert_called()
|
||||
|
||||
link_data.link_data = [
|
||||
[
|
||||
"",
|
||||
"user1",
|
||||
"1576486443.8539028",
|
||||
"poster1+1576289662.7914467",
|
||||
"",
|
||||
"",
|
||||
"this comment contains a keyword regarding the keyword website",
|
||||
],
|
||||
[
|
||||
"",
|
||||
"user2",
|
||||
"1576486440.65404",
|
||||
"poster1+1576289662.7914467",
|
||||
"",
|
||||
"",
|
||||
"this is a reply to the site but does not contain the word",
|
||||
],
|
||||
[
|
||||
70,
|
||||
"poster1",
|
||||
"1576289662.7914467",
|
||||
"",
|
||||
"keyword",
|
||||
"http://keyword.com",
|
||||
"the keyword website",
|
||||
],
|
||||
[
|
||||
69,
|
||||
"poster2",
|
||||
"1576462584.2307518",
|
||||
"",
|
||||
"keyword",
|
||||
"gemini://keyword",
|
||||
"keyword site with no replies",
|
||||
],
|
||||
[68, "poster3", "1576462007.9509487", "", "a", "a", "key word"],
|
||||
[67, "poster4", "1576461366.5580268", "", "b", "b", "key.word"],
|
||||
[
|
||||
"",
|
||||
"user3",
|
||||
"1576376868.284987",
|
||||
"poster3+1576376644.2783155",
|
||||
"",
|
||||
"",
|
||||
"this is an orphaned reply but it contains the keyword",
|
||||
],
|
||||
[66, "keyword", "1576461366.5580268", "", "c", "c", "c"],
|
||||
[65, "poster6", "1576461367.5580268", "", "keyword", "c", "c"],
|
||||
[64, "poster7", "1576461368.5580268", "", "c", "keyword", "c"],
|
||||
[63, "poster8", "1576461369.5580268", "", "c", "c", "keyword"],
|
||||
[63, "poster9", "1576461370.5580268", "", "c", "c", "z8keyworddui3"],
|
||||
]
|
||||
|
||||
test_results = [
|
||||
(
|
||||
70,
|
||||
"poster1",
|
||||
"1576289662.7914467",
|
||||
"",
|
||||
"keyword",
|
||||
"http://keyword.com",
|
||||
"the keyword website",
|
||||
),
|
||||
(
|
||||
69,
|
||||
"poster2",
|
||||
"1576462584.2307518",
|
||||
"",
|
||||
"keyword",
|
||||
"gemini://keyword",
|
||||
"keyword site with no replies",
|
||||
),
|
||||
(66, "keyword", "1576461366.5580268", "", "c", "c", "c"),
|
||||
(65, "poster6", "1576461367.5580268", "", "keyword", "c", "c"),
|
||||
(64, "poster7", "1576461368.5580268", "", "c", "keyword", "c"),
|
||||
(63, "poster8", "1576461369.5580268", "", "c", "c", "keyword"),
|
||||
]
|
||||
|
||||
self.assertEqual(link_data.search("keyword"), test_results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tests for Linkulator views"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, call
|
||||
import linkulator
|
||||
|
||||
|
||||
class TestPrintSearchResults(unittest.TestCase):
|
||||
"""Tests covering the print_search_results function"""
|
||||
|
||||
@patch("builtins.print")
|
||||
def test_print_search_results(self, mock_print):
|
||||
test_keyword = "keyword"
|
||||
test_search_results = [
|
||||
(66, "keyword", "1576461366.5580268", "", "c", "c", "c"),
|
||||
(65, "poster6", "1576461367.5580268", "", "keyword", "c", "c"),
|
||||
(64, "poster7", "1576461368.5580268", "", "c", "keyword", "c"),
|
||||
(63, "poster8", "1576461369.5580268", "", "c", "c", "keyword"),
|
||||
]
|
||||
test_print_calls = [
|
||||
call(
|
||||
"\nShowing results for keyword\n\n ID# DATE AUTHOR DESC "
|
||||
),
|
||||
call(" 1 2019-12-16 keyword c \n"),
|
||||
call(" 2 2019-12-16 poster6 c \n"),
|
||||
call(" 3 2019-12-16 poster7 c \n"),
|
||||
call(" 4 2019-12-16 poster8 keyword \n"),
|
||||
]
|
||||
|
||||
linkulator.print_search_results(test_keyword, test_search_results)
|
||||
|
||||
self.assertEqual(
|
||||
mock_print.call_count, 5
|
||||
) # one count for title, 4 for the items)
|
||||
|
||||
self.assertListEqual(test_print_calls, mock_print.call_args_list)
|
Loading…
Reference in New Issue