// We never update drafts, we always insert a new one. // A draft will have the next fields: id, type, created, up_to_date, parent_content/draft_content/none/both, data. // The saved input resides in the data. func SaveDraft(db *mgo.Database, content_rules map[string]interface{}, inp map[string][]string) (bson.ObjectId, error) { for i, _ := range content_rules { content_rules[i] = 1 } content_rules["type"] = "must" content_rules["id"] = "must" // Content id. content_rules["draft_id"] = "must" // Draft id, no empty if we save draft from a draft, empty if draft is saved from a content. dat, err := extract.New(content_rules).Extract(inp) if err != nil { return "", err } live_id_s := dat["id"].(string) draft_id_s := dat["draft_id"].(string) ins := m{ "created": time.Now().Unix(), "data": dat, "type": dat["type"], } var parent, root bson.ObjectId if len(draft_id_s) > 0 { // Coming from a draft. draft_id := patterns.ToIdWithCare(draft_id_s) var last_version bson.ObjectId parent, root, last_version, err = basic.GetDraftParent(db, "contents", draft_id) if parent == "" || root == "" { return "", fmt.Errorf("Coming from draft but still can not extract parent or root.") } // Has no version of it is a draft/descendant of a draft which connects to no saved content. if last_version != "" { ins["draft_of_version"] = last_version } } else if len(live_id_s) > 0 { // Coming from a content. live_id := patterns.ToIdWithCare(live_id_s) // This way draft saves coming from any versions will be saved to the version pointed to by the head anyway... parent, root, err = basic.GetParentTroughContent(db, "contents", live_id) // If we have a live content saved, we must have a version to point to too. if parent == "" || root == "" { return "", fmt.Errorf("Coming from content but still can not extract parent or root.") } ins["draft_of_version"] = parent } if err != nil { return "", err } if len(live_id_s) > 0 { live_id := patterns.ToIdWithCare(live_id_s) ins["draft_of"] = live_id // Just so we can display the content id for saving content immediately from a draft. } draft_id := bson.NewObjectId() if parent != "" { // If there is a parent there is a root... ins["-parent"] = parent ins["root"] = root } else { ins["root"] = draft_id } // Note: we dont store the last version here, building the draft will be harder. ins["_id"] = draft_id err = db.C(Cname + Draft_collection_postfix).Insert(ins) return draft_id, err }
func (v *V) editDraft(typ, id string) (interface{}, error) { hasid := len(id) > 0 uni := v.uni uni.Dat["is_draft"] = true if hasid { built, err := content_model.BuildDraft(uni.Db, typ, id) if err != nil { return nil, err } d := built["data"].(map[string]interface{}) if _, draft_of_sg := built["draft_of"]; draft_of_sg { uni.Dat["content_parent"] = true fresher, err := content_model.IsDraftUpToDate(uni.Db, built, d) if err != nil { return nil, err } uni.Dat["up_to_date"] = fresher uni.Dat["op"] = "update" } else { // It's possible that it has no parent at all, then it is a fresh new draft, first version. uni.Dat["op"] = "insert" } resolver.ResolveOne(uni.Db, d, nil) uni.Dat["content"] = d timeline, err := content_model.DraftTimeline(uni.Db, patterns.ToIdWithCare(id)) if err != nil { return nil, err } uni.Dat["timeline"] = timeline uni.Dat["draft"] = built return d, nil } uni.Dat["op"] = "insert" return map[string]interface{}{}, nil }
func (a *A) allowsContent(op string) (bson.ObjectId, string, error) { uni := a.uni var typ string if op == "insert" { typ = uni.Req.Form["type"][0] // See TODO below. } else { content_id := patterns.ToIdWithCare(uni.Req.Form["id"][0]) // TODO: Don't let it panic if id does not exists, return descriptive error message. _typ, err := content_model.TypeOf(uni.Db, content_id) if err != nil { return "", "", err } typ = _typ } auth_opts, ignore := user.AuthOpts(uni, "content.types."+typ, op) if ignore { return "", "", fmt.Errorf("Auth options should not be ignored.") } err, _ := user.AuthAction(uni, auth_opts) if err != nil { return "", "", err } uid_i, has_uid := jsonp.Get(uni.Dat, "_user._id") if !has_uid { return "", "", fmt.Errorf("Can't %v content, you have no id.", op) } uid := uid_i.(bson.ObjectId) user_level := scut.Ulev(uni.Dat["_user"]) allowed_err := content_model.CanModifyContent(uni.Db, uni.Req.Form, 300, uid, user_level) if allowed_err != nil { return "", "", allowed_err } return uid, typ, nil }
// Implementation of versioning is in basic.InudVersion. func ChangeHead(db *mgo.Database, ev ifaces.Event, inp map[string][]string, non_versioned_fields []string) error { rule := map[string]interface{}{ "version_id": "must", "id": "must", } dat, err := extract.New(rule).Extract(inp) if err != nil { return err } version_id_str := basic.StripId(dat["version_id"].(string)) version_id := bson.ObjectIdHex(version_id_str) var v interface{} err = db.C(Cname + "_version").Find(bson.M{"_id": version_id}).One(&v) if err != nil { return err } revert_to := v.(bson.M) id := patterns.ToIdWithCare(dat["id"].(string)) for _, v := range non_versioned_fields { delete(revert_to, v) } revert_to["points_to"] = revert_to["_id"] delete(revert_to, "id") return db.C(Cname).Update(bson.M{"_id": id}, bson.M{"$set": revert_to}) }
// Checks if unvotes are approved at all. Returns error if not. // Checks if vote option is legal. // Checks if user indeed voted to that option. Return error if not. // Decreases the counter field of the given vote option, and pulls the user_id from the field of the given vote option. func Unvote(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string) error { can_unvote, has_cu := action["can_unvote"] if !has_cu || can_unvote.(bool) == false { return fmt.Errorf("Can't unvote.") } dat, collection, _, input_vote, err := sharedProc(action, inp) if err != nil { return err } doc_id := patterns.ToIdWithCare(dat["document_id"].(string)) user_id := user["_id"].(bson.ObjectId) q := m{"_id": doc_id, input_vote: user_id} count, err := db.C(collection).Find(q).Count() if err != nil { return err } if count != 1 { return fmt.Errorf("Can't unvote a doc which you haven't vote on yet.") } q = m{"_id": doc_id} upd := m{ "$inc": m{ input_vote + "_count": -1, }, "$pull": m{ input_vote: user_id, }, } return db.C(collection).Update(q, upd) }
// Queries a draft and rebuilds it. Queries its parent too, and merges it with the input fields saved in "data". // The returned draft will be like a simple draft in the database, but in the data field it will contain fields of the parent plus the fresher saved input data. // draft_typ example: blog_draft // Only func BuildDraft(db *mgo.Database, draft_typ, draft_id_str string) (map[string]interface{}, error) { draft_id := patterns.ToIdWithCare(draft_id_str) q := m{"_id": draft_id} var v interface{} err := db.C(Cname + Draft_collection_postfix).Find(q).One(&v) if err != nil { return nil, err } draft := basic.Convert(v).(map[string]interface{}) typ_i, has_typ := draft["type"] if !has_typ { return nil, fmt.Errorf("Draft has no type.") } typ, is_str := typ_i.(string) if !is_str { return nil, fmt.Errorf("Draft type is not a string.") } if typ != draft_typ { return nil, fmt.Errorf("Draft type is not the expected one: %v instead if %v.", typ, draft_typ) } parent, err := GetParent(db, "contents", draft) if err != nil { return nil, err } draft["data"] = mergeWithParent(draft["data"].(map[string]interface{}), parent) return draft, nil }
// Example: // { // "type": "choose_child", // "c": "contents", // "choose_fieldname": "accepted", // This field will be set to true when the child document is chosen. // "parent_fieldname": "parent", // The name of the field in which the parent's id resides. // "max_choices": 1 // Optional, defaults to 1. Describes how many chosen children can exists per parent. // } // // Gets chosen document to find out parent. // Gets parent document, checks if user is the author of it. Return error if not. // Sets choose_fieldname to the number of already chosen children + 1 in the chosen document. // Increases "has_" + choose_fieldname by 1 in parent document. func ChooseChild(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string) error { choose_fieldname := action["choose_fieldname"].(string) parent_fieldname := action["parent_fieldname"].(string) coll := action["c"].(string) max_choices := 1 mc, has_mc := action["max_choices"] if has_mc { max_choices = int(mc.(float64)) } rule := m{ "chosen_doc_id": "must", } dat, err := extract.New(rule).Extract(inp) if err != nil { return err } user_id := user["_id"].(bson.ObjectId) chosen_doc_id := patterns.ToIdWithCare(dat["chosen_doc_id"]) parent_id, auth_err := IsAuthor(db, coll, parent_fieldname, user_id, chosen_doc_id) if auth_err != nil { return err } current_count, exc_err := Exceeded(db, coll, parent_fieldname, choose_fieldname, parent_id, max_choices) if exc_err != nil { return err } err = MarkAsChosen(db, coll, choose_fieldname, chosen_doc_id, current_count) if err != nil { return err } return IncrementParent(db, coll, choose_fieldname, parent_id, 1) }
// RespondContent could be called "insert child content". // Example: // { // "type": "respond", // "parent_fieldname": "parent", // "counter_fieldname": "replies", // "can_delete": true, // Optional, defaults to false // "delete_unless": {"$exists": "accepted"} // Optional, possible to delete response, unless this query on the response evaluates to true. // "update_parent_on_delete_if_child": [[{"$exists":"accepted"}, // Optional, run this update query on parent. // {"$unset": {"has_accepted": 1}}], ...] // // } // // If the extract module could convert a string to bson.ObjectId we would not need this. // // Checks if parent exists. Returns error if not. // Inserts new content with the id of the parent. // Increments field named "counter_fieldname" in parent. func RespondContent(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string, response_content_type_options map[string]interface{}) error { parent_fieldname := action["parent_fieldname"].(string) counter_fieldname := action["counter_fieldname"].(string) rcto := response_content_type_options parent, has_parent := inp["response_parent"] if !has_parent { return fmt.Errorf("No parent given.") } parent_id := patterns.ToIdWithCare(parent) q := m{"_id": parent_id} count, err := db.C("contents").Find(q).Count() if err != nil { return err } if count == 0 { return fmt.Errorf("Can't find parent with id %v.", parent_id) } fixval := m{parent_fieldname: parent_id} _, err = content_model.InsertWithFix(db, nil, rcto, inp, user["_id"].(bson.ObjectId), fixval) if err != nil { return err } upd := m{ "$inc": m{ counter_fieldname: 1, }, } return db.C("contents").Update(q, upd) }
// You don't actually edit anything on a past version... func (v *V) editVersion(typ, id string) (interface{}, error) { uni := v.uni uni.Dat["is_version"] = true version_id := patterns.ToIdWithCare(id) version, err := content_model.FindVersion(uni.Db, version_id) if err != nil { return nil, err } resolver.ResolveOne(uni.Db, version, nil) timeline, err := content_model.DraftTimeline(uni.Db, version_id) if err != nil { return nil, err } uni.Dat["timeline"] = timeline uni.Dat["op"] = "update" uni.Dat["content"] = version return nil, nil }
// Vote options are mutually exclusive. // Example: // { // "type": "vote", // "c": "contents", // "doc_type": "blog", // Optional // "can_unvote": true, // Optional, handled as false if unset or set and false. // "vote_options": ["like", "dislike"] // } // // Checks if the vote option sent from UI is legal. // Makes sure user have not voted yet, pushes user_id into a field named equal to a member of "vote_options", and increments a field named equal to a member of "vote_options" + "_count". func Vote(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string) error { doc_type, has_dt := action["doc_typ"] dat, collection, vote_options, input_vote, err := sharedProc(action, inp) if err != nil { return err } doc_id := patterns.ToIdWithCare(dat["document_id"].(string)) user_id := user["_id"].(bson.ObjectId) q := generateVoteQuery(doc_id, user_id, vote_options) if has_dt { q["type"] = doc_type.(string) } upd := m{ "$addToSet": m{ input_vote: user_id, }, "$inc": m{ input_vote + "_count": 1, }, } return db.C(collection).Update(q, upd) }
// We must somehow forbid the deletion of a given ContentResponse, because we have to do these actions at deletion too. // TODO: we have to find a solution to this question. // Update has no problems, no additional processing needs to be done when updating some fields like title, content, etc. // See https://github.com/opesun/hypecms/issues/29 // (Or operate with events/hooks?) // // Checks if it is possible to delete a response. // Checks if the response satisfies the "delete_unless" query, returns error if it does. // Deletes response. // Decrements parent "counter_fieldname" field by one. // Does "update_parent_on_delete_if_child" optional operation. func DeleteContentResponse(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string) error { can_delete := false if can_delete_i, has := action["can_delete"]; has { can_delete = can_delete_i.(bool) } if !can_delete { return fmt.Errorf("Can't delete response.") } response_id := patterns.ToIdWithCare(inp["respone_id"][0]) delete_unless, has_du := action["delete_unless"] if has_du { sat, err := patterns.Satisfies(db, "contents", response_id, delete_unless.(map[string]interface{})) if err != nil { return err } if sat { return fmt.Errorf("Can't delete response, satisfies \"unless\" query.") } } //counter_fieldname := action["counter_fieldname"].(string) //parent_update, has_pu := action["update_parent"] return nil }
// Gets chosen document to find out parent. // Gets parent document, checks if user is the author of it. Return error if not. // Unsets choose_fieldname in chosen document. // Decreases "has_" + choose_fieldname by 1 in parent document. func UnchooseChild(db *mgo.Database, user, action map[string]interface{}, inp map[string][]string) error { choose_fieldname := action["choose_fieldname"].(string) parent_fieldname := action["parent_fieldname"].(string) coll := action["c"].(string) return nil rule := m{ "chosen_doc_id": "must", } dat, err := extract.New(rule).Extract(inp) if err != nil { return err } user_id := user["_id"].(bson.ObjectId) chosen_doc_id := patterns.ToIdWithCare(dat["chosen_doc_id"]) parent_id, auth_err := IsAuthor(db, coll, parent_fieldname, user_id, chosen_doc_id) if auth_err != nil { return auth_err } err = UnmarkAsChosen(db, coll, choose_fieldname, chosen_doc_id) if err != nil { return err } return IncrementParent(db, coll, choose_fieldname, parent_id, -1) }
func PullTagFromAll(db *mgo.Database, tag_id string) error { return patterns.PullFromAll(db, Tag_cname, Tag_fieldname, patterns.ToIdWithCare(tag_id)) }