mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-18 22:52:50 +00:00
Add the "same" option for feeds.
Feeds items used to only be grouped if they had the same author. Now this can be configured to require the same author, message, both, or neither.
This commit is contained in:
parent
a7701bea0a
commit
d8c82855ff
@ -2,10 +2,38 @@
|
||||
Mycorrhiza Wiki has RSS, Atom, and JSON feeds to track the latest changes on the wiki.
|
||||
These feeds are linked on the [[/recent-changes | recent changes page]].
|
||||
|
||||
These feeds have some customization options:
|
||||
* **period** If the period option is set to a length of time (5m, 24h, etc.), edits by the same author that happen within this time of each other will be grouped into one item in the feed.
|
||||
* **order** Can be set to `old-to-now` (default) or `new-to-old`. This determines what order edits in groups will be shown in in your feed.
|
||||
## Options
|
||||
These feeds have options to combine related changes into groups:
|
||||
* {
|
||||
**period** Can be set to lengths of time like `5m`, `24h`, etc.
|
||||
Edits by the same author that happen within this time of each other can be grouped into one item in the feed.
|
||||
}
|
||||
* {
|
||||
**same** Can be set to `author`, `message`, or `none`.
|
||||
Edits will only be grouped together if they have the same author or message. By default, edits need to have the same author and message. If it is `none`, all edits can be grouped.
|
||||
}
|
||||
* {
|
||||
**order** Can be set to `old-to-now` (default) or `new-to-old`.
|
||||
This determines what order edits in groups will be shown in in your feed.
|
||||
}
|
||||
|
||||
If none of these options are set, changes will never be grouped.
|
||||
|
||||
## Examples
|
||||
URLs for feeds using these options look like this:
|
||||
* `/recent-changes-rss?period=1h`
|
||||
* `/recent-changes-atom?period=1h&order=old-to-new`
|
||||
* {
|
||||
`/recent-changes-rss?period=1h`
|
||||
Changes within one hour of each other with the same author and message will be grouped together.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?period=1h&order=new-to-old`
|
||||
Same as the last one, but the groups will be shown in the opposite order.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?period=1h&same=none`
|
||||
Changes within one hour of each other will be grouped together, even with different authors and messages.
|
||||
}
|
||||
* {
|
||||
`/recent-changes-atom?same=author&same=message`
|
||||
Changes with the same author and message will be grouped together no matter how much time passes between them.
|
||||
}
|
||||
|
264
history/feed.go
264
history/feed.go
@ -12,7 +12,7 @@ import (
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
const changeGroupMaxSize = 100
|
||||
const changeGroupMaxSize = 30
|
||||
|
||||
func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
||||
feed := &feeds.Feed{
|
||||
@ -22,7 +22,7 @@ func recentChangesFeed(opts FeedOptions) *feeds.Feed {
|
||||
Updated: time.Now(),
|
||||
}
|
||||
revs := newRecentChangesStream()
|
||||
groups := opts.grouping.Group(revs)
|
||||
groups := groupRevisions(revs, opts)
|
||||
for _, grp := range groups {
|
||||
item := grp.feedItem(opts)
|
||||
feed.Add(&item)
|
||||
@ -56,6 +56,18 @@ func (grp *revisionGroup) addRevision(rev Revision) {
|
||||
*grp = append(*grp, rev)
|
||||
}
|
||||
|
||||
// orderedIndex returns the ith revision in the group following the given order.
|
||||
func (grp *revisionGroup) orderedIndex(i int, order feedGroupOrder) *Revision {
|
||||
switch order {
|
||||
case newToOld:
|
||||
return &(*grp)[i]
|
||||
case oldToNew:
|
||||
return &(*grp)[len(*grp)-1-i]
|
||||
}
|
||||
// unreachable
|
||||
return nil
|
||||
}
|
||||
|
||||
func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
|
||||
var (
|
||||
currentYear int
|
||||
@ -73,24 +85,19 @@ func groupRevisionsByMonth(revs []Revision) (res []revisionGroup) {
|
||||
return res
|
||||
}
|
||||
|
||||
// groupRevisionsByPeriodFromNow groups close-together revisions and returns the first changeGroupMaxSize (30) groups.
|
||||
// If two revisions happened within period of each other, they are put in the same group.
|
||||
func groupRevisionsByPeriod(revs recentChangesStream, period time.Duration) (res []revisionGroup) {
|
||||
// groupRevisions groups revisions for a feed.
|
||||
// It returns the first changeGroupMaxSize (30) groups.
|
||||
// The grouping parameter determines when two revisions will be grouped.
|
||||
func groupRevisions(revs recentChangesStream, opts FeedOptions) (res []revisionGroup) {
|
||||
nextRev := revs.iterator()
|
||||
rev, empty := nextRev()
|
||||
if empty {
|
||||
return res
|
||||
}
|
||||
|
||||
currTime := rev.Time
|
||||
currGroup := newRevisionGroup(rev)
|
||||
for {
|
||||
rev, done := nextRev()
|
||||
if done {
|
||||
return append(res, currGroup)
|
||||
}
|
||||
|
||||
if currTime.Sub(rev.Time) < period && currGroup[0].Username == rev.Username {
|
||||
for rev, done := nextRev(); !done; rev, done = nextRev() {
|
||||
if opts.canGroup(currGroup, rev) {
|
||||
currGroup.addRevision(rev)
|
||||
} else {
|
||||
res = append(res, currGroup)
|
||||
@ -99,118 +106,213 @@ func groupRevisionsByPeriod(revs recentChangesStream, period time.Duration) (res
|
||||
}
|
||||
currGroup = newRevisionGroup(rev)
|
||||
}
|
||||
currTime = rev.Time
|
||||
}
|
||||
// no more revisions, haven't added the last group yet
|
||||
return append(res, currGroup)
|
||||
}
|
||||
|
||||
func (grp revisionGroup) feedItem(opts FeedOptions) feeds.Item {
|
||||
title, author := grp.titleAndAuthor(opts.order)
|
||||
return feeds.Item{
|
||||
Title: grp.title(opts.groupOrder),
|
||||
// groups for feeds should have the same author for all revisions
|
||||
Author: &feeds.Author{Name: grp[0].Username},
|
||||
Title: title,
|
||||
Author: author,
|
||||
Id: grp[len(grp)-1].Hash,
|
||||
Description: grp.descriptionForFeed(opts.groupOrder),
|
||||
Description: grp.descriptionForFeed(opts.order),
|
||||
Created: grp[len(grp)-1].Time, // earliest revision
|
||||
Updated: grp[0].Time, // latest revision
|
||||
Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// titleAndAuthor creates a title and author for a feed item.
|
||||
// If all messages and authors are the same (or there's just one rev), "message by author"
|
||||
// If all authors are the same, "num edits (first message, ...) by author"
|
||||
// Else (even if all messages are the same), "num edits (first message, ...)"
|
||||
func (grp revisionGroup) titleAndAuthor(order feedGroupOrder) (title string, author *feeds.Author) {
|
||||
allMessagesSame := true
|
||||
allAuthorsSame := true
|
||||
for _, rev := range grp[1:] {
|
||||
if rev.Message != grp[0].Message {
|
||||
allMessagesSame = false
|
||||
}
|
||||
if rev.Username != grp[0].Username {
|
||||
allAuthorsSame = false
|
||||
}
|
||||
if !allMessagesSame && !allAuthorsSame {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
author := grp[0].Username
|
||||
if len(grp) == 1 {
|
||||
return fmt.Sprintf("%s by %s", message, author)
|
||||
if allMessagesSame && allAuthorsSame {
|
||||
title = grp[0].Message
|
||||
} else {
|
||||
return fmt.Sprintf("%d edits by %s (%s, ...)", len(grp), author, message)
|
||||
title = fmt.Sprintf("%d edits (%s, ...)", len(grp), grp.orderedIndex(0, order).Message)
|
||||
}
|
||||
|
||||
if allAuthorsSame {
|
||||
title += fmt.Sprintf(" by %s", grp[0].Username)
|
||||
author = &feeds.Author{Name: grp[0].Username}
|
||||
} else {
|
||||
author = nil
|
||||
}
|
||||
|
||||
return title, author
|
||||
}
|
||||
|
||||
func (grp revisionGroup) descriptionForFeed(order FeedGroupOrder) string {
|
||||
func (grp revisionGroup) descriptionForFeed(order feedGroupOrder) string {
|
||||
builder := strings.Builder{}
|
||||
switch order {
|
||||
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())
|
||||
}
|
||||
for i := 0; i < len(grp); i++ {
|
||||
desc := grp.orderedIndex(i, order).descriptionForFeed()
|
||||
builder.WriteString(desc)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type feedOptionParserState struct {
|
||||
isAnythingSet bool
|
||||
conds []groupingCondition
|
||||
order feedGroupOrder
|
||||
}
|
||||
|
||||
// feedGrouping represents a set of conditions that must all be satisfied for revisions to be grouped.
|
||||
// If there are no conditions, revisions will never be grouped.
|
||||
type FeedOptions struct {
|
||||
grouping FeedGrouping
|
||||
groupOrder FeedGroupOrder
|
||||
conds []groupingCondition
|
||||
order feedGroupOrder
|
||||
}
|
||||
|
||||
func ParseFeedOptions(query url.Values) (FeedOptions, error) {
|
||||
grouping, err := parseFeedGrouping(query)
|
||||
parser := feedOptionParserState{}
|
||||
|
||||
err := parser.parseFeedGroupingPeriod(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
groupOrder, err := parseFeedGroupOrder(query)
|
||||
err = parser.parseFeedGroupingSame(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
err = parser.parseFeedGroupingOrder(query)
|
||||
if err != nil {
|
||||
return FeedOptions{}, err
|
||||
}
|
||||
return FeedOptions{grouping, groupOrder}, nil
|
||||
}
|
||||
|
||||
type FeedGrouping interface {
|
||||
Group(recentChangesStream) []revisionGroup
|
||||
}
|
||||
|
||||
func parseFeedGrouping(query url.Values) (FeedGrouping, error) {
|
||||
if query.Get("period") == "" {
|
||||
return NormalFeedGrouping{}, nil
|
||||
var conds []groupingCondition
|
||||
if parser.isAnythingSet {
|
||||
conds = parser.conds
|
||||
} else {
|
||||
// if no options are applied, do no grouping instead of using the default options
|
||||
conds = nil
|
||||
}
|
||||
return FeedOptions{conds: conds, order: parser.order}, nil
|
||||
}
|
||||
|
||||
func (parser *feedOptionParserState) parseFeedGroupingPeriod(query url.Values) error {
|
||||
if query["period"] != nil {
|
||||
parser.isAnythingSet = true
|
||||
period, err := time.ParseDuration(query.Get("period"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
return PeriodFeedGrouping{Period: period}, nil
|
||||
parser.conds = append(parser.conds, periodGroupingCondition{period})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (parser *feedOptionParserState) parseFeedGroupingSame(query url.Values) error {
|
||||
if same := query["same"]; same != nil {
|
||||
parser.isAnythingSet = true
|
||||
if len(same) == 1 && same[0] == "none" {
|
||||
// same=none adds no condition
|
||||
parser.conds = append(parser.conds, sameGroupingCondition{})
|
||||
return nil
|
||||
} else {
|
||||
// handle same=author, same=author&same=message, etc.
|
||||
cond := sameGroupingCondition{}
|
||||
for _, sameCond := range same {
|
||||
switch sameCond {
|
||||
case "author":
|
||||
if cond.author {
|
||||
return errors.New("set same=author twice")
|
||||
}
|
||||
cond.author = true
|
||||
case "message":
|
||||
if cond.message {
|
||||
return errors.New("set same=message twice")
|
||||
}
|
||||
cond.message = true
|
||||
default:
|
||||
return errors.New("unknown same option " + sameCond)
|
||||
}
|
||||
}
|
||||
parser.conds = append(parser.conds, cond)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// same defaults to both author and message
|
||||
// but this won't be applied if no grouping options are set
|
||||
parser.conds = append(parser.conds, sameGroupingCondition{author: true, message: true})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type NormalFeedGrouping struct{}
|
||||
|
||||
func (NormalFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) {
|
||||
for _, rev := range revs.next(changeGroupMaxSize) {
|
||||
res = append(res, newRevisionGroup(rev))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type PeriodFeedGrouping struct {
|
||||
Period time.Duration
|
||||
}
|
||||
|
||||
func (g PeriodFeedGrouping) Group(revs recentChangesStream) (res []revisionGroup) {
|
||||
return groupRevisionsByPeriod(revs, g.Period)
|
||||
}
|
||||
|
||||
type FeedGroupOrder int
|
||||
type feedGroupOrder int
|
||||
|
||||
const (
|
||||
NewToOld FeedGroupOrder = iota
|
||||
OldToNew FeedGroupOrder = iota
|
||||
newToOld feedGroupOrder = iota
|
||||
oldToNew feedGroupOrder = iota
|
||||
)
|
||||
|
||||
func parseFeedGroupOrder(query url.Values) (FeedGroupOrder, error) {
|
||||
switch query.Get("order") {
|
||||
case "", "old-to-new":
|
||||
return OldToNew, nil
|
||||
case "new-to-old":
|
||||
return NewToOld, nil
|
||||
func (parser *feedOptionParserState) parseFeedGroupingOrder(query url.Values) error {
|
||||
if order := query["order"]; order != nil {
|
||||
parser.isAnythingSet = true
|
||||
switch query.Get("order") {
|
||||
case "old-to-new":
|
||||
parser.order = oldToNew
|
||||
case "new-to-old":
|
||||
parser.order = newToOld
|
||||
default:
|
||||
return errors.New("unknown order option " + query.Get("order"))
|
||||
}
|
||||
} else {
|
||||
parser.order = oldToNew
|
||||
}
|
||||
return 0, errors.New("unknown order")
|
||||
return nil
|
||||
}
|
||||
|
||||
// canGroup determines whether a revision can be added to a group.
|
||||
func (opts FeedOptions) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
if len(opts.conds) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cond := range opts.conds {
|
||||
if !cond.canGroup(grp, rev) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type groupingCondition interface {
|
||||
canGroup(grp revisionGroup, rev Revision) bool
|
||||
}
|
||||
|
||||
// periodGroupingCondition will group two revisions if they are within period of each other.
|
||||
type periodGroupingCondition struct {
|
||||
period time.Duration
|
||||
}
|
||||
|
||||
func (cond periodGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
return grp[len(grp)-1].Time.Sub(rev.Time) < cond.period
|
||||
}
|
||||
|
||||
type sameGroupingCondition struct {
|
||||
author bool
|
||||
message bool
|
||||
}
|
||||
|
||||
func (c sameGroupingCondition) canGroup(grp revisionGroup, rev Revision) bool {
|
||||
return (!c.author || grp[0].Username == rev.Username) &&
|
||||
(!c.message || grp[0].Message == rev.Message)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user