1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2025-01-07 18:30:26 +00:00

add feed grouping (#61)

This commit is contained in:
Elias Bomberger 2021-10-25 17:51:36 -04:00
parent 924b011e06
commit 114e18bcbd
5 changed files with 165 additions and 69 deletions

View File

@ -1,7 +1,9 @@
package history package history
import ( import (
"errors"
"fmt" "fmt"
"net/url"
"strings" "strings"
"time" "time"
@ -10,9 +12,7 @@ import (
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
) )
var groupPeriod, _ = time.ParseDuration("30m") func recentChangesFeed(opts FeedOptions) *feeds.Feed {
func recentChangesFeed() *feeds.Feed {
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: "Recent changes", Title: "Recent changes",
Link: &feeds.Link{Href: cfg.URL}, Link: &feeds.Link{Href: cfg.URL},
@ -21,46 +21,101 @@ func recentChangesFeed() *feeds.Feed {
Updated: time.Now(), Updated: time.Now(),
} }
revs := RecentChanges(30) revs := RecentChanges(30)
groups := groupRevisionsByPeriod(revs, groupPeriod) groups := opts.grouping.Group(revs)
for _, grp := range groups { for _, grp := range groups {
item := grp.feedItem() item := grp.feedItem(opts)
feed.Add(&item) feed.Add(&item)
} }
return feed return feed
} }
// RecentChangesRSS creates recent changes feed in RSS format. // RecentChangesRSS creates recent changes feed in RSS format.
func RecentChangesRSS() (string, error) { func RecentChangesRSS(opts FeedOptions) (string, error) {
return recentChangesFeed().ToRss() return recentChangesFeed(opts).ToRss()
} }
// RecentChangesAtom creates recent changes feed in Atom format. // RecentChangesAtom creates recent changes feed in Atom format.
func RecentChangesAtom() (string, error) { func RecentChangesAtom(opts FeedOptions) (string, error) {
return recentChangesFeed().ToAtom() return recentChangesFeed(opts).ToAtom()
} }
// RecentChangesJSON creates recent changes feed in JSON format. // RecentChangesJSON creates recent changes feed in JSON format.
func RecentChangesJSON() (string, error) { func RecentChangesJSON(opts FeedOptions) (string, error) {
return recentChangesFeed().ToJSON() return recentChangesFeed(opts).ToJSON()
} }
func (grp revisionGroup) feedItem() feeds.Item { type revisionGroup []Revision
func newRevisionGroup(rev Revision) revisionGroup {
return revisionGroup([]Revision{rev})
}
func (grp *revisionGroup) addRevision(rev Revision) {
*grp = append(*grp, rev)
}
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
var (
currentYear int
currentMonth time.Month
)
for _, rev := range revs {
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
currentYear = rev.Time.Year()
currentMonth = rev.Time.Month()
res = append(res, newRevisionGroup(rev))
} else {
res[len(res)-1].addRevision(rev)
}
}
return res
}
// groupRevisionsByPeriodFromNow groups close-together revisions.
// If two revisions happened within period of each other, they are put in the same group.
func groupRevisionsByPeriod(revs []Revision, period time.Duration) (res []revisionGroup) {
if len(revs) == 0 {
return res
}
currTime := revs[0].Time
res = append(res, newRevisionGroup(revs[0]))
for _, rev := range revs[1:] {
if currTime.Sub(rev.Time) < period {
res[len(res)-1].addRevision(rev)
} else {
res = append(res, newRevisionGroup(rev))
}
currTime = rev.Time
}
return res
}
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
return feeds.Item{ return feeds.Item{
Title: grp.title(), Title: grp.title(opts.groupOrder),
Author: grp.author(), Author: grp.author(),
Id: grp[0].Hash, Id: grp[len(grp)-1].Hash,
Description: grp.descriptionForFeed(), Description: grp.descriptionForFeed(opts.groupOrder),
Created: grp[len(grp)-1].Time, // earliest revision Created: grp[len(grp)-1].Time, // earliest revision
Updated: grp[0].Time, // latest revision Updated: grp[0].Time, // latest revision
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()}, Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
} }
} }
func (grp revisionGroup) title() string { func (grp revisionGroup) title(order FeedGroupOrder) string {
var message string
switch order {
case NewToOld:
message = grp[0].Message
case OldToNew:
message = grp[len(grp)-1].Message
}
if len(grp) == 1 { if len(grp) == 1 {
return grp[0].Message return message
} else { } else {
return fmt.Sprintf("%d edits (%s, ...)", len(grp), grp[0].Message) return fmt.Sprintf("%d edits (%s, ...)", len(grp), message)
} }
} }
@ -75,10 +130,85 @@ func (grp revisionGroup) author() *feeds.Author {
return &feeds.Author{Name: author} return &feeds.Author{Name: author}
} }
func (grp revisionGroup) descriptionForFeed() string { func (grp revisionGroup) descriptionForFeed(order FeedGroupOrder) string {
builder := strings.Builder{} builder := strings.Builder{}
for _, rev := range grp { switch order {
builder.WriteString(rev.descriptionForFeed()) case NewToOld:
for _, rev := range grp {
builder.WriteString(rev.descriptionForFeed())
}
case OldToNew:
for i := len(grp) - 1; i >= 0; i-- {
builder.WriteString(grp[i].descriptionForFeed())
}
} }
return builder.String() return builder.String()
} }
type FeedOptions struct {
grouping FeedGrouping
groupOrder FeedGroupOrder
}
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
grouping, err := parseFeedGrouping(query)
if err != nil {
return FeedOptions{}, err
}
groupOrder, err := parseFeedGroupOrder(query)
if err != nil {
return FeedOptions{}, err
}
return FeedOptions{grouping, groupOrder}, nil
}
type FeedGrouping interface {
Group([]Revision) []revisionGroup
}
func parseFeedGrouping(query url.Values) (FeedGrouping, error) {
if query.Get("period") == "" {
return NormalFeedGrouping{}, nil
} else {
period, err := time.ParseDuration(query.Get("period"))
if err != nil {
return nil, err
}
return PeriodFeedGrouping{Period: period}, nil
}
}
type NormalFeedGrouping struct{}
func (NormalFeedGrouping) Group(revs []Revision) (res []revisionGroup) {
for _, rev := range revs {
res = append(res, newRevisionGroup(rev))
}
return res
}
type PeriodFeedGrouping struct {
Period time.Duration
}
func (g PeriodFeedGrouping) Group(revs []Revision) (res []revisionGroup) {
return groupRevisionsByPeriod(revs, g.Period)
}
type FeedGroupOrder int
const (
NewToOld FeedGroupOrder = iota
OldToNew FeedGroupOrder = iota
)
func parseFeedGroupOrder(query url.Values) (FeedGroupOrder, error) {
switch query.Get("order") {
case "oldtonew":
return OldToNew, nil
case "newtoold":
case "":
return NewToOld, nil
}
return 0, errors.New("unknown order")
}

View File

@ -209,47 +209,3 @@ func PrimitiveDiffAtRevision(filepath, hash string) (string, error) {
} }
return out.String(), err return out.String(), err
} }
type revisionGroup []Revision
func newRevisionGroup(rev Revision) revisionGroup {
return revisionGroup([]Revision{rev})
}
func (grp *revisionGroup) addRevision(rev Revision) {
*grp = append(*grp, rev)
}
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
var (
currentYear int
currentMonth time.Month
)
for _, rev := range revs {
if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear {
currentYear = rev.Time.Year()
currentMonth = rev.Time.Month()
res = append(res, newRevisionGroup(rev))
} else {
res[len(res)-1].addRevision(rev)
}
}
return res
}
// groupRevisionsByPeriod groups revisions by how long ago they were.
// Revisions less than one period ago are placed in one group, revisions between one and two periods are in another, and so on.
func groupRevisionsByPeriod(revs []Revision, period time.Duration) (res []revisionGroup) {
now := time.Now()
currentPeriod := -1
for _, rev := range revs {
newPeriod := int(now.Sub(rev.Time).Seconds() / period.Seconds())
if newPeriod != currentPeriod {
currentPeriod = newPeriod
res = append(res, newRevisionGroup(rev))
} else {
res[len(res)-1].addRevision(rev)
}
}
return res
}

View File

@ -8,7 +8,7 @@ HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by t
{% if i > 0 %} {% if i > 0 %}
<span aria-hidden="true">, </span> <span aria-hidden="true">, </span>
{% endif %} {% endif %}
<a href="/hypha/{%s hyphaName %}">{%s hyphaName %}</a> <a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s hyphaName %}</a>
{% endfor %} {% endfor %}
{% endstripspace %} {% endstripspace %}
{% endfunc %} {% endfunc %}

View File

@ -39,7 +39,11 @@ func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:10 //line history/view.qtpl:10
} }
//line history/view.qtpl:10 //line history/view.qtpl:10
qw422016.N().S(`<a href="/hypha/`) qw422016.N().S(`<a href="/primitive-diff/`)
//line history/view.qtpl:11
qw422016.E().S(rev.Hash)
//line history/view.qtpl:11
qw422016.N().S(`/`)
//line history/view.qtpl:11 //line history/view.qtpl:11
qw422016.E().S(hyphaName) qw422016.E().S(hyphaName)
//line history/view.qtpl:11 //line history/view.qtpl:11

View File

@ -61,8 +61,14 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
} }
// genericHandlerOfFeeds is a helper function for the web feed handlers. // genericHandlerOfFeeds is a helper function for the web feed handlers.
func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func() (string, error), name string, contentType string) { func genericHandlerOfFeeds(w http.ResponseWriter, rq *http.Request, f func(history.FeedOptions) (string, error), name string, contentType string) {
if content, err := f(); err != nil { opts, err := history.ParseFeedOptions(rq.URL.Query())
var content string
if err == nil {
content, err = f(opts)
}
if err != nil {
w.Header().Set("Content-Type", "text/plain;charset=utf-8") w.Header().Set("Content-Type", "text/plain;charset=utf-8")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "An error while generating "+name+": "+err.Error()) fmt.Fprint(w, "An error while generating "+name+": "+err.Error())