Add /user/followers and /user/followees pages

This commit is contained in:
Alessio 2023-12-26 23:16:51 -06:00
parent dd68ee1fce
commit 3117a1364c
11 changed files with 106 additions and 41 deletions

View File

@ -18,5 +18,5 @@ func (app *Application) Lists(w http.ResponseWriter, r *http.Request) {
where is_followed = 1`)
panic_if(err)
app.buffered_render_basic_page(w, "tpl/list.tpl", users)
app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{Title: "Offline Follows", Users: users})
}

View File

@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"strings"
"context"
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
@ -151,3 +152,19 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
app.buffered_render_tweet_page(w, "tpl/tweet_detail.tpl", data)
}
type key string
const TWEET_KEY = key("tweet")
func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context {
return context.WithValue(ctx, TWEET_KEY, tweet)
}
func get_tweet_from_context(ctx context.Context) scraper.Tweet {
tweet, is_ok := ctx.Value(TWEET_KEY).(scraper.Tweet)
if !is_ok {
panic("Tweet not found in context")
}
return tweet
}

View File

@ -1,6 +1,7 @@
package webserver
import (
"fmt"
"errors"
"net/http"
"strings"
@ -49,6 +50,15 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
panic_if(app.Profile.DownloadUserContentFor(&user))
}
if len(parts) > 1 && parts[1] == "followers" {
app.UserFollowers(w, r, user)
return
}
if len(parts) > 1 && parts[1] == "followees" {
app.UserFollowees(w, r, user)
return
}
if r.URL.Query().Has("scrape") {
if app.IsScrapingDisabled {
app.InfoLog.Printf("Would have scraped: %s", r.URL.Path)
@ -118,3 +128,21 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
app.buffered_render_tweet_page(w, "tpl/user_feed.tpl", data)
}
}
type ListData struct {
Title string
Users []scraper.User
}
func (app *Application) UserFollowees(w http.ResponseWriter, r *http.Request, user scraper.User) {
app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{
Title: fmt.Sprintf("Followed by @%s", user.Handle),
Users: app.Profile.GetFollowees(user.ID),
})
}
func (app *Application) UserFollowers(w http.ResponseWriter, r *http.Request, user scraper.User) {
app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{
Title: fmt.Sprintf("Followers of @%s", user.Handle),
Users: app.Profile.GetFollowers(user.ID),
})
}

View File

@ -2,7 +2,6 @@ package webserver
import (
"bytes"
"context"
"fmt"
"html/template"
"io"
@ -249,19 +248,3 @@ func cursor_to_query_params(c persistence.Cursor) string {
result.Set("sort-order", c.SortOrder.String())
return result.Encode()
}
type key string
const TWEET_KEY = key("tweet")
func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context {
return context.WithValue(ctx, TWEET_KEY, tweet)
}
func get_tweet_from_context(ctx context.Context) scraper.Tweet {
tweet, is_ok := ctx.Value(TWEET_KEY).(scraper.Tweet)
if !is_ok {
panic("Tweet not found in context")
}
return tweet
}

View File

@ -79,7 +79,7 @@ func TestUserFeed(t *testing.T) {
root, err := html.Parse(resp.Body)
require.NoError(err)
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | @Cernovich")
assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
assert.Len(tweet_nodes, 7)
@ -119,7 +119,7 @@ func TestUserFeedWithCursor(t *testing.T) {
root, err := html.Parse(resp.Body)
require.NoError(err)
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | @Cernovich")
assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
assert.Len(tweet_nodes, 2)
@ -181,6 +181,34 @@ func TestUserFeedLikesTab(t *testing.T) {
assert.Len(tweets, 4)
}
// Followers and followees
// -----------------------
func TestUserFollowers(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followers", nil))
require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body)
require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 2)
}
func TestUserFollowees(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followees", nil))
require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body)
require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 1)
}
// Timeline page
// -------------
@ -194,7 +222,7 @@ func TestTimeline(t *testing.T) {
root, err := html.Parse(resp.Body)
require.NoError(err)
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Timeline")
assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
assert.Len(tweet_nodes, 18)
@ -210,7 +238,7 @@ func TestTimelineWithCursor(t *testing.T) {
root, err := html.Parse(resp.Body)
require.NoError(err)
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Timeline")
assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
assert.Len(tweet_nodes, 10)
@ -245,7 +273,7 @@ func TestSearch(t *testing.T) {
root, err := html.Parse(resp.Body)
require.NoError(err)
title_node := cascadia.Query(root, selector("title"))
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Search")
assert.Equal(title_node.FirstChild.Data, "Search | Offline Twitter")
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
assert.Len(tweet_nodes, 1)

View File

@ -3,7 +3,7 @@
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Offline Twitter | {{template "title" .}}</title>
<title>{{template "title" .}} | Offline Twitter</title>
<link rel='stylesheet' href='/static/styles.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<link rel='stylesheet' href='/static/vendor/fonts.css'>

View File

@ -1,5 +1,5 @@
{{define "title"}}Followed Users{{end}}
{{define "title"}}{{.Title}}{{end}}
{{define "main"}}
{{template "list" .}}
{{template "list" .Users}}
{{end}}

View File

@ -37,14 +37,14 @@
</div>
<div class="followers-followees-container row">
<div class="followers-container">
<a href="/{{$user.Handle}}/followers" class="followers-container unstyled-link">
<span class="followers-count">{{$user.FollowersCount}}</span>
<span class="followers-label">followers</span>
</div>
<div class="followees-container">
</a>
<a href="/{{$user.Handle}}/followees" class="followers-container unstyled-link">
<span class="following-label">is following</span>
<span class="following-count">{{$user.FollowingCount}}</span>
</div>
</a>
<div class="spacer"></div>

View File

@ -39,18 +39,26 @@ func (p Profile) IsXFollowingY(follower_id UserID, followee_id UserID) bool {
return rows.Next() // true if there is a row, false otherwise
}
func (p Profile) GetFollowers(followee_id UserID) []UserID {
var ret []UserID
err := p.DB.Select(&ret, `select follower_id from follows where followee_id = ?`, followee_id)
func (p Profile) GetFollowers(followee_id UserID) []User {
var ret []User
err := p.DB.Select(&ret, `
select `+USERS_ALL_SQL_FIELDS+`
from users
where id in (select follower_id from follows where followee_id = ?)
`, followee_id)
if err != nil {
panic(err)
}
return ret
}
func (p Profile) GetFollowees(follower_id UserID) []UserID {
var ret []UserID
err := p.DB.Select(&ret, `select followee_id from follows where follower_id = ?`, follower_id)
func (p Profile) GetFollowees(follower_id UserID) []User {
var ret []User
err := p.DB.Select(&ret, `
select `+USERS_ALL_SQL_FIELDS+`
from users
where id in (select followee_id from follows where follower_id = ?)
`, follower_id)
if err != nil {
panic(err)
}

View File

@ -34,11 +34,11 @@ func TestSaveAndLoadFollows(t *testing.T) {
// Save and reload it
profile.SaveAsFolloweesList(follower.ID, trove)
new_followee_ids := profile.GetFollowees(follower.ID)
new_followees := profile.GetFollowees(follower.ID)
assert.Len(new_followee_ids, len(followee_ids))
for _, id := range new_followee_ids {
_, is_ok := trove.Users[id]
assert.Len(new_followees, len(followee_ids))
for _, followee := range new_followees {
_, is_ok := trove.Users[followee.ID]
assert.True(is_ok)
}
}

View File

@ -421,7 +421,8 @@ create index if not exists index_follows_followee_id on follows (followee_id);
create index if not exists index_follows_follower_id on follows (follower_id);
insert into follows values
(1, 1178839081222115328, 1488963321701171204),
(2, 1032468021485293568, 1488963321701171204);
(2, 1032468021485293568, 1488963321701171204),
(3, 1488963321701171204, 1240784920831762433);
create table fake_user_sequence(latest_fake_id integer not null);