Add /user/followers and /user/followees pages
This commit is contained in:
parent
dd68ee1fce
commit
3117a1364c
@ -18,5 +18,5 @@ func (app *Application) Lists(w http.ResponseWriter, r *http.Request) {
|
|||||||
where is_followed = 1`)
|
where is_followed = 1`)
|
||||||
panic_if(err)
|
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})
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
"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)
|
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
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -49,6 +50,15 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
panic_if(app.Profile.DownloadUserContentFor(&user))
|
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 r.URL.Query().Has("scrape") {
|
||||||
if app.IsScrapingDisabled {
|
if app.IsScrapingDisabled {
|
||||||
app.InfoLog.Printf("Would have scraped: %s", r.URL.Path)
|
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)
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ package webserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@ -249,19 +248,3 @@ func cursor_to_query_params(c persistence.Cursor) string {
|
|||||||
result.Set("sort-order", c.SortOrder.String())
|
result.Set("sort-order", c.SortOrder.String())
|
||||||
return result.Encode()
|
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
|
|
||||||
}
|
|
||||||
|
@ -79,7 +79,7 @@ func TestUserFeed(t *testing.T) {
|
|||||||
root, err := html.Parse(resp.Body)
|
root, err := html.Parse(resp.Body)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
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"))
|
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||||
assert.Len(tweet_nodes, 7)
|
assert.Len(tweet_nodes, 7)
|
||||||
@ -119,7 +119,7 @@ func TestUserFeedWithCursor(t *testing.T) {
|
|||||||
root, err := html.Parse(resp.Body)
|
root, err := html.Parse(resp.Body)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
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"))
|
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||||
assert.Len(tweet_nodes, 2)
|
assert.Len(tweet_nodes, 2)
|
||||||
@ -181,6 +181,34 @@ func TestUserFeedLikesTab(t *testing.T) {
|
|||||||
assert.Len(tweets, 4)
|
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
|
// Timeline page
|
||||||
// -------------
|
// -------------
|
||||||
|
|
||||||
@ -194,7 +222,7 @@ func TestTimeline(t *testing.T) {
|
|||||||
root, err := html.Parse(resp.Body)
|
root, err := html.Parse(resp.Body)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
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"))
|
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||||
assert.Len(tweet_nodes, 18)
|
assert.Len(tweet_nodes, 18)
|
||||||
@ -210,7 +238,7 @@ func TestTimelineWithCursor(t *testing.T) {
|
|||||||
root, err := html.Parse(resp.Body)
|
root, err := html.Parse(resp.Body)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
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"))
|
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||||
assert.Len(tweet_nodes, 10)
|
assert.Len(tweet_nodes, 10)
|
||||||
@ -245,7 +273,7 @@ func TestSearch(t *testing.T) {
|
|||||||
root, err := html.Parse(resp.Body)
|
root, err := html.Parse(resp.Body)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
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"))
|
tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||||
assert.Len(tweet_nodes, 1)
|
assert.Len(tweet_nodes, 1)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<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='stylesheet' href='/static/styles.css'>
|
||||||
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
|
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
|
||||||
<link rel='stylesheet' href='/static/vendor/fonts.css'>
|
<link rel='stylesheet' href='/static/vendor/fonts.css'>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{{define "title"}}Followed Users{{end}}
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
{{template "list" .}}
|
{{template "list" .Users}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -37,14 +37,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="followers-followees-container row">
|
<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-count">{{$user.FollowersCount}}</span>
|
||||||
<span class="followers-label">followers</span>
|
<span class="followers-label">followers</span>
|
||||||
</div>
|
</a>
|
||||||
<div class="followees-container">
|
<a href="/{{$user.Handle}}/followees" class="followers-container unstyled-link">
|
||||||
<span class="following-label">is following</span>
|
<span class="following-label">is following</span>
|
||||||
<span class="following-count">{{$user.FollowingCount}}</span>
|
<span class="following-count">{{$user.FollowingCount}}</span>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
@ -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
|
return rows.Next() // true if there is a row, false otherwise
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Profile) GetFollowers(followee_id UserID) []UserID {
|
func (p Profile) GetFollowers(followee_id UserID) []User {
|
||||||
var ret []UserID
|
var ret []User
|
||||||
err := p.DB.Select(&ret, `select follower_id from follows where followee_id = ?`, followee_id)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Profile) GetFollowees(follower_id UserID) []UserID {
|
func (p Profile) GetFollowees(follower_id UserID) []User {
|
||||||
var ret []UserID
|
var ret []User
|
||||||
err := p.DB.Select(&ret, `select followee_id from follows where follower_id = ?`, follower_id)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -34,11 +34,11 @@ func TestSaveAndLoadFollows(t *testing.T) {
|
|||||||
|
|
||||||
// Save and reload it
|
// Save and reload it
|
||||||
profile.SaveAsFolloweesList(follower.ID, trove)
|
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))
|
assert.Len(new_followees, len(followee_ids))
|
||||||
for _, id := range new_followee_ids {
|
for _, followee := range new_followees {
|
||||||
_, is_ok := trove.Users[id]
|
_, is_ok := trove.Users[followee.ID]
|
||||||
assert.True(is_ok)
|
assert.True(is_ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
create index if not exists index_follows_follower_id on follows (follower_id);
|
||||||
insert into follows values
|
insert into follows values
|
||||||
(1, 1178839081222115328, 1488963321701171204),
|
(1, 1178839081222115328, 1488963321701171204),
|
||||||
(2, 1032468021485293568, 1488963321701171204);
|
(2, 1032468021485293568, 1488963321701171204),
|
||||||
|
(3, 1488963321701171204, 1240784920831762433);
|
||||||
|
|
||||||
|
|
||||||
create table fake_user_sequence(latest_fake_id integer not null);
|
create table fake_user_sequence(latest_fake_id integer not null);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user