From c1e0a08f720c9735c3c4de7b2a8a53322e0fb6f8 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Tue, 10 Oct 2023 14:55:27 -0700 Subject: [PATCH] user page redesign: add home, notifications pages for #442 --- models.py | 17 +++++---- pages.py | 70 ++++++++++++++++++++++-------------- templates/home.html | 11 ++++++ templates/notifications.html | 11 ++++++ templates/profile.html | 7 +--- templates/user_base.html | 11 ++---- tests/test_pages.py | 26 +++++++++----- 7 files changed, 97 insertions(+), 56 deletions(-) create mode 100644 templates/home.html create mode 100644 templates/notifications.html diff --git a/models.py b/models.py index 7d74840..f40c58b 100644 --- a/models.py +++ b/models.py @@ -957,7 +957,7 @@ class Follower(ndb.Model): filter_prop == g.user.key, ).order(-Follower.updated) - followers, before, after = fetch_page(query, Follower) + followers, before, after = fetch_page(query, Follower, by=Follower.updated) users = ndb.get_multi(f.from_ if collection == 'followers' else f.to for f in followers) User.load_multi(u for u in users if u) @@ -968,12 +968,11 @@ class Follower(ndb.Model): return followers, before, after -def fetch_page(query, model_class): +def fetch_page(query, model_class, by=None): """Fetches a page of results from a datastore query. Uses the ``before`` and ``after`` query params (if provided; should be - ISO8601 timestamps) and the queried model class's ``updated`` property to - identify the page to fetch. + ISO8601 timestamps) and the ``by`` property to identify the page to fetch. Populates a ``log_url_path`` property on each result entity that points to a its most recent logged request. @@ -981,6 +980,8 @@ def fetch_page(query, model_class): Args: query (google.cloud.ndb.query.Query) model_class (class) + by (ndb.model.Property): paging property, eg :attr:`models.Object.updated` + or :attr:`models.Object.created` Returns: (list of Object or Follower, str, str) tuple: (results, new_before, @@ -988,6 +989,8 @@ def fetch_page(query, model_class): ``before`` and ``after`` to fetch the previous and next pages, respectively """ + assert by + # if there's a paging param ('before' or 'after'), update query with it # TODO: unify this with Bridgy's user page def get_paging_param(param): @@ -1006,11 +1009,11 @@ def fetch_page(query, model_class): if before and after: error("can't handle both before and after") elif after: - query = query.filter(model_class.updated >= after).order(model_class.updated) + query = query.filter(by >= after).order(by) elif before: - query = query.filter(model_class.updated < before).order(-model_class.updated) + query = query.filter(by < before).order(-by) else: - query = query.order(-model_class.updated) + query = query.order(-by) query_iter = query.iter() results = sorted(itertools.islice(query_iter, 0, PAGE_SIZE), diff --git a/pages.py b/pages.py index cc65a0b..630209c 100644 --- a/pages.py +++ b/pages.py @@ -9,7 +9,7 @@ from flask import g, render_template, request from google.cloud.ndb import tasklets from google.cloud.ndb.query import AND, OR from google.cloud.ndb.stats import KindStat -from granary import as1, as2, atom, microformats2, rss +from granary import as1, atom, microformats2, rss import humanize from oauth_dropins.webutil import flask_util, logs, util from oauth_dropins.webutil.flask_util import error, flash, redirect @@ -42,6 +42,9 @@ def load_user(protocol, id): :class:`werkzeug.exceptions.HTTPException` on error or redirect """ assert id + if protocol == 'ap' and not id.startswith('@'): + id = '@' + id + cls = PROTOCOLS[protocol] g.user = cls.get_by_id(id) @@ -95,15 +98,12 @@ def web_user_redirects(**kwargs): # WARNING: this overrides the /ap/... actor URL route in activitypub.py, *only* # for handles with leading @ character. be careful when changing this route! @app.get(f'/ap/@', defaults={'protocol': 'ap'}) -def user(protocol, id): - if protocol == 'ap' and not id.startswith('@'): - id = '@' + id - +def profile(protocol, id): load_user(protocol, id) query = Object.query(OR(Object.users == g.user.key, Object.notify == g.user.key)) - objects, before, after = fetch_objects(query) + objects, before, after = fetch_objects(query, by=Object.updated) followers = Follower.query(Follower.to == g.user.key, Follower.status == 'active')\ @@ -115,15 +115,27 @@ def user(protocol, id): .count(limit=FOLLOWERS_UI_LIMIT) following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}' - return render_template( - 'profile.html', - follow_url=request.values.get('url'), - logs=logs, - util=util, - address=request.args.get('address'), - g=g, - **locals(), - ) + return render_template('profile.html', logs=logs, util=util, g=g, **locals()) + + +@app.get(f'///home') +def home(protocol, id): + load_user(protocol, id) + + query = Object.query(Object.feed == g.user.key) + objects, before, after = fetch_objects(query, by=Object.created) + + return render_template('home.html', logs=logs, util=util, g=g, **locals()) + + +@app.get(f'///notifications') +def notifications(protocol, id): + load_user(protocol, id) + + query = Object.query(Object.notify == g.user.key) + objects, before, after = fetch_objects(query, by=Object.updated) + + return render_template('notifications.html', logs=logs, util=util, g=g, **locals()) @app.get(f'///') @@ -134,10 +146,10 @@ def followers_or_following(protocol, id, collection): return render_template( f'{collection}.html', address=request.args.get('address'), - as2=as2, + follow_url=request.values.get('url'), g=g, util=util, - **locals() + **locals(), ) @@ -149,13 +161,13 @@ def feed(protocol, id): load_user(protocol, id) - objects = Object.query(OR(Object.feed == g.user.key, - # backward compatibility - AND(Object.users == g.user.key, - Object.labels == 'feed'))) \ - .order(-Object.created) \ - .fetch(PAGE_SIZE) - activities = [obj.as1 for obj in objects if not obj.deleted] + query = Object.query(Object.feed == g.user.key) + # .order(-Object.created) \ + # .fetch(PAGE_SIZE) + # activities = [obj.as1 for obj in objects if not obj.deleted] + + objects, _, _ = fetch_objects(query, by=Object.created) + activities = [obj.as1 for obj in objects] # hydrate authors, actors, objects from stored Objects fields = 'author', 'actor', 'object' @@ -230,7 +242,7 @@ def bridge_user(): return render_template('bridge_user.html') -def fetch_objects(query): +def fetch_objects(query, by=None): """Fetches a page of :class:`models.Object` entities from a datastore query. Wraps :func:`models.fetch_page` and adds attributes to the returned @@ -238,16 +250,22 @@ def fetch_objects(query): Args: query (ndb.Query) + by (ndb.model.Property): either :attr:`models.Object.updated` or + :attr:`models.Object.created` Returns: (list of models.Object, str, str) tuple: (results, new ``before`` query param, new ``after`` query param) to fetch the previous and next pages, respectively """ - objects, new_before, new_after = fetch_page(query, Object) + assert by is Object.updated or by is Object.created + objects, new_before, new_after = fetch_page(query, Object, by=by) # synthesize human-friendly content for objects for i, obj in enumerate(objects): + if obj.deleted: + continue + obj_as1 = obj.as1 inner_obj = as1.get_object(obj_as1) diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..6d46da8 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,11 @@ +{% extends "user_base.html" %} +{% set tab = "home" %} + +{% block subtabs %} +
+ Subscribe: + HTML + · Atom + · RSS +
+{% endblock subtabs %} diff --git a/templates/notifications.html b/templates/notifications.html new file mode 100644 index 0000000..a5e251d --- /dev/null +++ b/templates/notifications.html @@ -0,0 +1,11 @@ +{% extends "user_base.html" %} +{% set tab = "notifications" %} + +{% block subtabs %} +
+ Subscribe: + HTML + · Atom + · RSS +
+{% endblock subtabs %} diff --git a/templates/profile.html b/templates/profile.html index 6fb41d0..9bc5bf2 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -9,7 +9,7 @@ · {% if g.user.LABEL != 'activitypub' %} - Bridged to + bridged to {{ g.user.ap_address() }} @@ -24,8 +24,3 @@ {% endif %} {% endblock subtabs %} - -{# -{% block feed %} -{% endblock feed %} -#} diff --git a/templates/user_base.html b/templates/user_base.html index a18ec0d..cb3acb6 100644 --- a/templates/user_base.html +++ b/templates/user_base.html @@ -41,7 +41,7 @@ {% block subtabs %} -
- · HTML - · Atom - · RSS - · Webfinger - · ActivityPub -
{% endblock subtabs %} {% block feed %} diff --git a/tests/test_pages.py b/tests/test_pages.py index b658164..c389e99 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -44,8 +44,8 @@ class PagesTest(TestCase): self.assert_equals(200, got.status_code) def test_user_fake(self): - self.make_user('foo.com', cls=Fake) - got = self.client.get('/fa/foo.com') + self.make_user('fake:foo', cls=Fake) + got = self.client.get('/fa/fake:foo') self.assert_equals(200, got.status_code) def test_user_page_handle(self): @@ -171,9 +171,19 @@ class PagesTest(TestCase): self.assertIn('@follow@stored', body) self.assertIn('@me@plus.google.com', body) + def test_home_fake(self): + self.make_user('fake:foo', cls=Fake) + got = self.client.get('/fa/fake:foo/home') + self.assert_equals(200, got.status_code) + + def test_home_objects(self): + self.add_objects() + got = self.client.get('/web/user.com/home') + self.assert_equals(200, got.status_code) + def test_followers_fake(self): - self.make_user('foo.com', cls=Fake) - got = self.client.get('/fa/foo.com/followers') + self.make_user('fake:foo', cls=Fake) + got = self.client.get('/fa/fake:foo/followers') self.assert_equals(200, got.status_code) def test_followers_empty(self): @@ -216,8 +226,8 @@ class PagesTest(TestCase): self.assertNotIn('class="follower', got.get_data(as_text=True)) def test_following_fake(self): - self.make_user('foo.com', cls=Fake) - got = self.client.get('/fa/foo.com/following') + self.make_user('fake:foo', cls=Fake) + got = self.client.get('/fa/fake:foo/following') self.assert_equals(200, got.status_code) def test_following_user_not_found(self): @@ -247,8 +257,8 @@ class PagesTest(TestCase): self.assert_equals('/web/user.com/feed', got.headers['Location']) def test_feed_fake(self): - self.make_user('foo.com', cls=Fake) - got = self.client.get('/fa/foo.com/feed') + self.make_user('fake:foo', cls=Fake) + got = self.client.get('/fa/fake:foo/feed') self.assert_equals(200, got.status_code) def test_feed_html_empty(self):