More things

This commit is contained in:
2026-10-01 02:29:21 +02:00
parent e1df4d3ae1
commit 13b659662e
54 changed files with 4409 additions and 911 deletions

View File

@@ -0,0 +1,87 @@
import mongoose from "mongoose";
export const PROPOSAL_STATUSES = ["pending", "approved", "rejected"];
/* When a member saves the wiki the change does not land on it: it is held here
as a request for an admin to approve or reject. A request changes a page that
exists (`kind: "edit"`), or asks for one that does not yet (`kind: "create"`)
— an article, or a new language version of an existing one — and carries no
article until an admin's approval writes it. One pending request per article
and account — saving again while one waits revises it rather than stacking a
second copy, and the same draft a create editor holds revises itself the same
way. The account's name is kept beside its id so a request stays readable
after the account itself is closed. */
export const PROPOSAL_KINDS = ["edit", "create"];
const editProposalSchema = new mongoose.Schema(
{
// Whether the request rewrites a page or asks for a new one to exist.
kind: {
type: String,
enum: {
values: PROPOSAL_KINDS,
message: `Kind is one of: ${PROPOSAL_KINDS.join(", ")}.`,
},
default: "edit",
},
// The document the request wants to change, by id rather than slug: a
// retitle moves the slug out from under it. Null while the page is only
// being asked for; approval fills it in, so a decided request still names
// the article it brought about.
article: {
type: mongoose.Schema.Types.ObjectId,
ref: "Article",
default: null,
index: true,
},
// What a create asks the new page to be: the address it wants (the title
// slugified, or `<identity>-<language>` for a translation — the address is
// what tells one draft from another while both wait), and the identity of
// the article it joins as a version, if it translates one.
slug: { type: String, default: null, lowercase: true, trim: true },
baseSlug: { type: String, default: null, lowercase: true, trim: true },
// The text the editor was looking at — the other half of every diff on the
// dashboard, and how a reviewer spots an article that moved underneath.
// Empty for a creation: the page it asks for has nothing written on it.
base: {
title: { type: String, default: "" },
content: { type: String, default: "" },
tags: { type: [String], default: [] },
},
// The whole state being proposed, so approving is one application, not a patch
title: {
type: String,
required: [true, "Title is required"],
trim: true,
},
content: { type: String, default: "" },
tags: { type: [String], default: [] },
language: { type: String, default: "en", lowercase: true, trim: true },
status: {
type: String,
enum: {
values: PROPOSAL_STATUSES,
message: `Status is one of: ${PROPOSAL_STATUSES.join(", ")}.`,
},
default: "pending",
index: true,
},
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
createdByUsername: { type: String, default: "" },
decidedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", default: null },
decidedByUsername: { type: String, default: "" },
decidedAt: { type: Date, default: null },
},
{ timestamps: true }
);
// who has what waiting on which article — the filter the upsert saves through
editProposalSchema.index({ article: 1, createdBy: 1, status: 1 });
// ... and which address, for a request that names no article yet: the slug it
// asks for tells one member's waiting drafts apart from another's
editProposalSchema.index({ createdBy: 1, status: 1, kind: 1, slug: 1 });
const EditProposal = mongoose.model("EditProposal", editProposalSchema);
export default EditProposal;