diff options
-rw-r--r-- | README.md | 3 | ||||
-rwxr-xr-x | serve.py | 19 | ||||
-rw-r--r-- | tagrss.py | 62 | ||||
-rw-r--r-- | views/add_feed.tpl | 2 | ||||
-rw-r--r-- | views/hover_help.tpl | 1 | ||||
-rw-r--r-- | views/index.tpl | 35 | ||||
-rw-r--r-- | views/list_feeds.tpl | 4 | ||||
-rw-r--r-- | views/manage_feed.tpl | 2 | ||||
-rw-r--r-- | views/tag_hover_help.tpl | 1 |
9 files changed, 113 insertions, 16 deletions
@@ -14,10 +14,9 @@ See `LICENSE.txt` in the root of this repository for the text of the license. ## To do -* Add filtering by tag/feed +* Add JS to make the feed/tag input situation work like one would normally expect rather than like it's 1985. (Progressive enhancement, though.) * Do more user input validation * Handle more `requests` and `feedparser` error conditions -* Add logging * Add some reasonably high internal limit on tag count * Add support for authentication * Allow specifying update interval on a per-feed basis @@ -71,9 +71,20 @@ def index(): per_page: int = min(MAX_PER_PAGE_ENTRIES, int(bottle.request.query.get("per_page", DEFAULT_PER_PAGE_ENTRIES))) # type: ignore page_num = int(bottle.request.query.get("page_num", 1)) # type: ignore offset = (page_num - 1) * per_page + included_feeds_str: typing.Optional[str] = bottle.request.query.get("included_feeds", None) # type: ignore + included_feeds: typing.Optional[list[int]] = None + if included_feeds_str: + try: + included_feeds = [int(feed_id) for feed_id in included_feeds_str.split(" ")] + except ValueError: + pass + included_tags_str: typing.Optional[str] = bottle.request.query.get("included_tags", None) # type: ignore + included_tags: typing.Optional[list[str]] = None + if included_tags_str: + included_tags = parse_space_separated_tags(included_tags_str) with core_lock: - total_pages: int = max(1, math.ceil(core.get_entry_count() / per_page)) - entries = core.get_entries(limit=per_page, offset=offset) + total_pages: int = max(1, math.ceil(core.get_entry_count(included_feeds=included_feeds) / per_page)) + entries = core.get_entries(limit=per_page, offset=offset, included_feeds=included_feeds, included_tags=included_tags) return bottle.template( "index", entries=entries, @@ -82,6 +93,10 @@ def index(): total_pages=total_pages, per_page=per_page, max_per_page=MAX_PER_PAGE_ENTRIES, + included_feeds=included_feeds, + included_tags=included_tags, + included_feeds_str=included_feeds_str, + included_tags_str=included_tags_str, core=core, ) @@ -89,13 +89,34 @@ class TagRss: self.store_feed_entries(feed_id, parsed_feed, epoch_downloaded) def get_entries( - self, *, limit: int, offset: int = 0 + self, + *, + limit: int, + offset: int = 0, + included_feeds: typing.Optional[typing.Collection[int]] = None, + included_tags: typing.Optional[typing.Collection[str]] = None, ) -> list[dict[str, typing.Any]]: + where_clause: str = "" + if included_feeds or included_tags: + where_clause = "WHERE 1" + if included_feeds: + where_clause += f" AND feed_id IN ({','.join('?' * len(included_feeds))})" + if included_tags: + where_clause += ( + " AND feed_id IN (SELECT feed_id FROM feed_tags WHERE tag = ?)" + * len(included_tags) + ) with self.connection: resp = self.connection.execute( - "SELECT id, feed_id, title, link, epoch_published, epoch_updated FROM entries \ + f"SELECT id, feed_id, title, link, epoch_published, epoch_updated FROM entries \ + {where_clause} \ ORDER BY id DESC LIMIT ? OFFSET ?;", - (limit, offset), + ( + *(included_feeds if included_feeds else ()), + *(included_tags if included_tags else ()), + limit, + offset, + ), ).fetchall() entries = [] @@ -177,11 +198,36 @@ class TagRss: ) return feeds - def get_entry_count(self) -> int: - with self.connection: - return self.connection.execute("SELECT count from entry_count;").fetchone()[ - 0 - ] + def get_entry_count( + self, + *, + included_feeds: typing.Optional[typing.Collection[int]] = None, + included_tags: typing.Optional[typing.Collection[str]] = None, + ) -> int: + if not (included_feeds or included_tags): + with self.connection: + return self.connection.execute( + "SELECT count from entry_count;" + ).fetchone()[0] + else: + where_clause: str = "WHERE 1" + if included_feeds: + where_clause += ( + f" AND feed_id IN ({','.join('?' * len(included_feeds))})" + ) + if included_tags: + where_clause += ( + " AND feed_id IN (SELECT feed_id FROM feed_tags WHERE tag = ?)" + * len(included_tags) + ) + with self.connection: + return self.connection.execute( + f"SELECT COUNT(*) FROM entries {where_clause};", + ( + *(included_feeds if included_feeds else ()), + *(included_tags if included_tags else ()), + ), + ).fetchone()[0] def get_feed_count(self) -> int: with self.connection: diff --git a/views/add_feed.tpl b/views/add_feed.tpl index b08ea0e..0110bac 100644 --- a/views/add_feed.tpl +++ b/views/add_feed.tpl @@ -21,7 +21,7 @@ <br> <div class="side-by-side-help-container"> <input type="text" placeholder="Tags" name="tags"> - <span class="hover-help" tabindex="0" title="Space separated. Backslashes escape spaces.">🛈</span> + % include("tag_hover_help.tpl") </div> <input type="submit" value="Add"> </form> diff --git a/views/hover_help.tpl b/views/hover_help.tpl new file mode 100644 index 0000000..5d5eb97 --- /dev/null +++ b/views/hover_help.tpl @@ -0,0 +1 @@ +<span class="hover-help" tabindex="0" title="{{text}}">🛈</span> diff --git a/views/index.tpl b/views/index.tpl index acb6fd1..52e997f 100644 --- a/views/index.tpl +++ b/views/index.tpl @@ -51,6 +51,33 @@ <a href="/list_feeds" class="no-visited-indication">List feeds</a> </p> </nav> + <details {{"open" if (included_feeds or included_tags) else ""}}> + <summary>Filter</summary> + <form> + <div class="side-by-side-help-container"> + <label>Included feeds: + <input type="text" name="included_feeds" value="{{' '.join([str(feed_id) for feed_id in included_feeds]) if included_feeds else ''}}"> + </label> + % include("hover_help.tpl", text="Space-separated feed IDs.") + </div> + <div class="side-by-side-help-container"> + <label>Included tags: + <input type="text" name="included_tags" value="{{included_tags_str}}"> + </label> + % include("tag_hover_help.tpl") + </div> + <input type="submit" value="Filter"> + <input type="number" value="{{page_num}}" min="1" max="{{total_pages}}" name="page_num" style="display: none;"> + <input type="number" value="{{per_page}}" min="1" max="{{max_per_page}}" name="per_page" style="display: none;"> + </form> + <form> + <input type="text" name="included_feeds" value="" style="display: none;"> + <input type="text" name="included_tags" value="" style="display: none;"> + <input type="submit" value="Clear filters"> + <input type="number" value="{{page_num}}" min="1" max="{{total_pages}}" name="page_num" style="display: none;"> + <input type="number" value="{{per_page}}" min="1" max="{{max_per_page}}" name="per_page" style="display: none;"> + </form> + </details> <table> <thead> <tr> @@ -92,7 +119,7 @@ <td class="td-feed"> <div> <a href="/manage_feed?feed={{entry['feed_id']}}" class="no-visited-indication">⚙</a> - {{core.get_feed_title(entry["feed_id"])}} + {{core.get_feed_title(entry["feed_id"])}} <small>(</small>{{entry["feed_id"]}}<small>)</small> </div> </td> </tr> @@ -107,6 +134,12 @@ <input type="number" value="{{per_page}}" min="1" max="{{max_per_page}}" name="per_page"> </label> <input type="submit" value="Go"> + % if included_feeds: + <input type="text" name="included_feeds" value="{{included_feeds_str}}" style="display: none;"> + % end + % if included_tags: + <input type="text" name="included_feeds" value="{{included_tags_str}}" style="display: none;"> + % end </form> % include("footer.tpl") </body> diff --git a/views/list_feeds.tpl b/views/list_feeds.tpl index 6377289..3c60f06 100644 --- a/views/list_feeds.tpl +++ b/views/list_feeds.tpl @@ -13,6 +13,7 @@ <thead> <tr> <th>#</th> + <th>ID</th> <th>Feed</th> <th>Source</th> <th>Manage</th> @@ -22,7 +23,8 @@ % for i, feed in enumerate(feeds): <tr> <td>{{i + 1 + offset}}</td> - <td>{{feed["title"]}}</td> + <td>{{feed["id"]}}</td> + <td>{{feed["title"]}} (<a href="/?included_feeds={{feed['id']}}" class="no-visited-indication">filter</a>)</td> <td><a href="{{feed['source']}}" class="no-visited-indication">🔗</a></td> <td><a href="/manage_feed?feed={{feed['id']}}" class="no-visited-indication">⚙</a></td> </tr> diff --git a/views/manage_feed.tpl b/views/manage_feed.tpl index c110ad2..0ff127e 100644 --- a/views/manage_feed.tpl +++ b/views/manage_feed.tpl @@ -46,7 +46,7 @@ <label>Tags: <input type="text" name="tags" value="{{feed['serialised_tags']}}"> </label> - <span class="hover-help" tabindex="0" title="Space separated. Backslashes escape spaces.">🛈</span> + % include("tag_hover_help.tpl") </div> <input type="submit" value="Update" name="update_feed"> </form> diff --git a/views/tag_hover_help.tpl b/views/tag_hover_help.tpl new file mode 100644 index 0000000..9007e55 --- /dev/null +++ b/views/tag_hover_help.tpl @@ -0,0 +1 @@ +% include("hover_help.tpl", text="Space-separated. Backslashes escape spaces.") |