user page redesign: add home, notifications pages

for #442
pull/671/head
Ryan Barrett 2023-10-10 14:55:27 -07:00
rodzic afa16d3864
commit c1e0a08f72
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
7 zmienionych plików z 97 dodań i 56 usunięć

Wyświetl plik

@ -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),

Wyświetl plik

@ -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/@<id>', 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'/<any({",".join(PROTOCOLS)}):protocol>/<id>/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'/<any({",".join(PROTOCOLS)}):protocol>/<id>/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'/<any({",".join(PROTOCOLS)}):protocol>/<id>/<any(followers,following):collection>')
@ -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)

Wyświetl plik

@ -0,0 +1,11 @@
{% extends "user_base.html" %}
{% set tab = "home" %}
{% block subtabs %}
<div class="row">
Subscribe:
<a href="{{ g.user.user_page_path('feed') }}">HTML</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
</div>
{% endblock subtabs %}

Wyświetl plik

@ -0,0 +1,11 @@
{% extends "user_base.html" %}
{% set tab = "notifications" %}
{% block subtabs %}
<div class="row">
Subscribe:
<a href="{{ g.user.user_page_path('feed') }}">HTML</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
</div>
{% endblock subtabs %}

Wyświetl plik

@ -9,7 +9,7 @@
&middot;
{% if g.user.LABEL != 'activitypub' %}
Bridged to
bridged to
<nobr title="Fediverse address">
<img class="logo" src="/static/fediverse_logo.svg">
{{ g.user.ap_address() }}
@ -24,8 +24,3 @@
{% endif %}
</div>
{% endblock subtabs %}
{#
{% block feed %}
{% endblock feed %}
#}

Wyświetl plik

@ -41,7 +41,7 @@
<!-- tabs -->
<div class="row tabs">
<a>
<a></a>
<a href="{{ g.user.user_page_path() }}"
{% if tab == 'profile' %}class="active-tab"{% endif %}
>👤 Profile</a><a
@ -51,17 +51,10 @@
href="{{ g.user.user_page_path('notifications') }}"
{% if tab == 'notifications' %}class="active-tab"{% endif %}
>🔔 Notifications</a>
<a>
<a></a>
</div>
{% block subtabs %}
<div class="row">
&middot; <a href="{{ g.user.user_page_path('feed') }}">HTML</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=atom') }}">Atom</a>
&middot; <a href="{{ g.user.user_page_path('feed?format=rss') }}">RSS</a>
&middot; <a href="/.well-known/webfinger?resource=acct:{{ g.user.ap_address() }}">Webfinger</a>
&middot; <a href="{{ g.user.ap_actor() }}">ActivityPub</a>
</div>
{% endblock subtabs %}
{% block feed %}

Wyświetl plik

@ -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):