Added Story model, added rpdata-as2storyimport command
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package story
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var chapterCollection *mgo.Collection
|
||||
|
||||
// A Chapter is a part of a story.
|
||||
type Chapter struct {
|
||||
ID string `bson:"_id"`
|
||||
StoryID string `bson:"storyId"`
|
||||
Title string `bson:"title"`
|
||||
Author string `bson:"author"`
|
||||
Source string `bson:"source"`
|
||||
CreatedDate time.Time `bson:"createdDate"`
|
||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
||||
EditedDate time.Time `bson:"editedDate"`
|
||||
}
|
||||
|
||||
// Edit edits a chapter, and updates EditedDate. While many Edit functions cheat if there's nothing to
|
||||
// change, this functill will due to EditedDate.
|
||||
func (chapter *Chapter) Edit(title, source *string, fictionalDate *time.Time) error {
|
||||
now := time.Now()
|
||||
changes := bson.M{"editedDate": now}
|
||||
changed := *chapter
|
||||
changed.EditedDate = now
|
||||
|
||||
if title != nil && *title != chapter.Title {
|
||||
changes["title"] = *title
|
||||
changed.Title = *title
|
||||
}
|
||||
if source != nil && *source != chapter.Source {
|
||||
changes["source"] = *source
|
||||
changed.Source = *source
|
||||
}
|
||||
if fictionalDate != nil && *fictionalDate != chapter.FictionalDate {
|
||||
changes["fictionalDate"] = *fictionalDate
|
||||
changed.FictionalDate = *fictionalDate
|
||||
}
|
||||
|
||||
err := chapterCollection.UpdateId(chapter.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*chapter = changed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a chapter.
|
||||
func (chapter *Chapter) Remove() error {
|
||||
return chapterCollection.RemoveId(chapter.ID)
|
||||
}
|
||||
|
||||
// makeChapterID makes a random chapter ID that's 24 characters long
|
||||
func makeChapterID() string {
|
||||
result := "SC"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 24 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:24]
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
chapterCollection = db.C("story.chapters")
|
||||
|
||||
chapterCollection.EnsureIndexKey("storyId")
|
||||
chapterCollection.EnsureIndexKey("author")
|
||||
chapterCollection.EnsureIndexKey("createdDate")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package story
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/internal/store"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
)
|
||||
|
||||
var storyCollection *mgo.Collection
|
||||
|
||||
// ErrTagAlreadyExists is an error returned by Story.AddTag
|
||||
var ErrTagAlreadyExists = errors.New("Tag already exists")
|
||||
|
||||
// ErrTagNotExists is an error returned by Story.RemoveTag
|
||||
var ErrTagNotExists = errors.New("Tag does not exist")
|
||||
|
||||
// A Story is user content that does not have a wiki-suitable format. Documents, new stories, short stories, and so on.
|
||||
// The story model is a container for multiple chapters this time, in contrast to the previous version.
|
||||
type Story struct {
|
||||
ID string `bson:"_id"`
|
||||
Author string `bson:"author"`
|
||||
Name string `bson:"name"`
|
||||
Category string `bson:"category"`
|
||||
Open bool `bson:"open"`
|
||||
Listed bool `bson:"listed"`
|
||||
Tags []Tag `bson:"tags"`
|
||||
CreatedDate time.Time `bson:"createdDate"`
|
||||
FictionalDate time.Time `bson:"fictionalDate,omitempty"`
|
||||
UpdatedDate time.Time `bson:"updatedDate"`
|
||||
}
|
||||
|
||||
// AddTag adds a tag to the story. It returns ErrTagAlreadyExists if the tag is already there
|
||||
func (story *Story) AddTag(tag Tag) error {
|
||||
for i := range story.Tags {
|
||||
if story.Tags[i].Equal(tag) {
|
||||
return ErrTagAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$push": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
story.Tags = append(story.Tags, tag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag to the story. It returns ErrTagNotExists if the tag does not exist.
|
||||
func (story *Story) RemoveTag(tag Tag) error {
|
||||
index := -1
|
||||
for i := range story.Tags {
|
||||
if story.Tags[i].Equal(tag) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return ErrTagNotExists
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$pull": bson.M{"tags": tag}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
story.Tags = append(story.Tags[:index], story.Tags[index+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit edits the story, reflecting the new values in the story's struct values. If nothing will be
|
||||
// changed, it will silently return without a database roundtrip.
|
||||
func (story *Story) Edit(name, category *string, listed, open *bool, fictionalDate *time.Time) error {
|
||||
changes := bson.M{}
|
||||
changed := *story
|
||||
|
||||
if name != nil && *name == story.Name {
|
||||
changes["name"] = *name
|
||||
changed.Name = *name
|
||||
}
|
||||
if category != nil && *category == story.Category {
|
||||
changes["category"] = *category
|
||||
changed.Name = *category
|
||||
}
|
||||
if listed != nil && *listed == story.Listed {
|
||||
changes["listed"] = *listed
|
||||
changed.Listed = *listed
|
||||
}
|
||||
if open != nil && *open == story.Open {
|
||||
changes["open"] = *open
|
||||
changed.Open = *open
|
||||
}
|
||||
if fictionalDate != nil && *fictionalDate == story.FictionalDate {
|
||||
changes["fictionalDate"] = *fictionalDate
|
||||
changed.FictionalDate = *fictionalDate
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := storyCollection.UpdateId(story.ID, bson.M{"$set": changes})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*story = changed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the story from the database
|
||||
func (story *Story) Remove() error {
|
||||
return storyCollection.RemoveId(story.ID)
|
||||
}
|
||||
|
||||
// AddChapter adds a chapter to the story. This does not enforce the `Open` setting, but it will log a warning if it
|
||||
// occurs
|
||||
func (story *Story) AddChapter(title, author, source string, createdDate, finctionalDate time.Time) (Chapter, error) {
|
||||
if !story.Open && author != story.Author {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: AddChapter is breaking Open rules (story.id=%#+v, story.name=%#+v, chapter.author=%#+v, chapter.title=%#+v)", story.ID, story.Name, author, title)
|
||||
}
|
||||
|
||||
chapter := Chapter{
|
||||
ID: makeChapterID(),
|
||||
StoryID: story.ID,
|
||||
Title: title,
|
||||
Author: author,
|
||||
Source: source,
|
||||
CreatedDate: createdDate,
|
||||
FictionalDate: finctionalDate,
|
||||
EditedDate: createdDate,
|
||||
}
|
||||
|
||||
err := chapterCollection.Insert(chapter)
|
||||
if err != nil {
|
||||
return Chapter{}, err
|
||||
}
|
||||
|
||||
if createdDate.After(story.UpdatedDate) {
|
||||
if err := storyCollection.UpdateId(story.ID, bson.M{"$set": bson.M{"updatedDate": createdDate}}); err == nil {
|
||||
story.UpdatedDate = createdDate
|
||||
}
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
||||
|
||||
// New creates a new story.
|
||||
func New(name, author, category string, listed, open bool, tags []Tag, createdDate, fictionalDate time.Time) (Story, error) {
|
||||
story := Story{
|
||||
ID: makeStoryID(),
|
||||
Name: name,
|
||||
Author: author,
|
||||
Category: category,
|
||||
Listed: listed,
|
||||
Open: open,
|
||||
Tags: tags,
|
||||
CreatedDate: createdDate,
|
||||
FictionalDate: fictionalDate,
|
||||
UpdatedDate: createdDate,
|
||||
}
|
||||
|
||||
err := storyCollection.Insert(story)
|
||||
if err != nil {
|
||||
return Story{}, err
|
||||
}
|
||||
|
||||
return story, nil
|
||||
}
|
||||
|
||||
// makeStoryID makes a random story ID that's 16 characters long
|
||||
func makeStoryID() string {
|
||||
result := "S"
|
||||
offset := 0
|
||||
data := make([]byte, 32)
|
||||
|
||||
rand.Read(data)
|
||||
for len(result) < 16 {
|
||||
result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
|
||||
offset += 8
|
||||
|
||||
if offset >= 32 {
|
||||
rand.Read(data)
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
return result[:16]
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
storyCollection = db.C("story.stories")
|
||||
|
||||
storyCollection.EnsureIndexKey("tags")
|
||||
storyCollection.EnsureIndexKey("author")
|
||||
storyCollection.EnsureIndexKey("updatedDate")
|
||||
storyCollection.EnsureIndexKey("fictionalDate")
|
||||
storyCollection.EnsureIndexKey("listed")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package story
|
||||
|
||||
// A Tag associates a story with other content, like other stories, logs and more.
|
||||
type Tag struct {
|
||||
Kind string `bson:"kind"`
|
||||
Name string `bson:"name"`
|
||||
}
|
||||
|
||||
// Equal returns true if the tags match one another.
|
||||
func (tag *Tag) Equal(other Tag) bool {
|
||||
return tag.Kind == other.Kind && tag.Name == other.Name
|
||||
}
|
||||
Reference in New Issue
Block a user