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:
parent
924b011e06
commit
114e18bcbd
172
history/feed.go
172
history/feed.go
@ -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")
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
Loading…
Reference in New Issue
Block a user