392 lines
16 KiB
JavaScript
392 lines
16 KiB
JavaScript
const mongoose = require("mongoose");
|
||
const Jimp = require("jimp");
|
||
const uuid = require("uuid/v4");
|
||
|
||
const Participation = require("./Participation");
|
||
const Rating = require("./Rating");
|
||
const Comment = require("./Comment");
|
||
let User;
|
||
|
||
const Schema = mongoose.Schema;
|
||
|
||
const Error_PaintingNotExist = new Error("尋找的目標畫作不存在。");
|
||
const Error_PaintingIsLocked = new Error("此畫作已被鎖定,無法對其做任何修改。");
|
||
const Error_PaintingHasFinished = new Error("此畫作已經完成,無法再做一次完成的動作。");
|
||
|
||
let PaintingSchema = Schema({
|
||
id: String,
|
||
links: String,
|
||
name: String,
|
||
description: String,
|
||
artist: String,
|
||
createdTime: {type : Date, default : Date.now },
|
||
lastModified: {type : Date, default : Date.now },
|
||
tags: [{type : String}],
|
||
activity: {type : Schema.Types.ObjectId, ref: "Participation"},
|
||
viewAuthority: Number,
|
||
totalScore: Number,
|
||
ratings: [{type : Schema.Types.ObjectId, ref: "Rating"}],
|
||
comments: [{type : Schema.Types.ObjectId, ref: "Comment"}],
|
||
isFinished: Boolean,
|
||
isLocked: Boolean
|
||
});
|
||
|
||
/**
|
||
* 交叉引入,由User來呼叫。
|
||
*/
|
||
PaintingSchema.statics.crossInitByUser = function () {
|
||
User = Object.freeze(require("./User"));
|
||
}
|
||
|
||
/**
|
||
* 建立新畫作的基本必要資料。
|
||
* @typedef NewPaintingData
|
||
* @prop {string} name 作品名稱。
|
||
* @prop {string} description 作品的敘述。
|
||
* @prop {string} artist 畫作的作者。即使用者名稱。
|
||
* @prop {string[]} tags 此畫作的標籤。
|
||
* @prop {number} viewAuthority 作品的訪問權限。0=公開;1=半公開;2=私人。
|
||
* @prop {boolean} isFinished 作品是否完成。
|
||
*/
|
||
/**
|
||
* 建立一個新的畫作。
|
||
* @param {NewPaintingData} data 用於建立新畫作的基本資料。
|
||
* @param {CallbackFunction} callback 回呼函式。成功時回呼畫作的_id。
|
||
*/
|
||
PaintingSchema.statics.createNewPainting = function (data, callback) {
|
||
let createdDate = new Date();
|
||
let paintingUUID = uuid();
|
||
let newPainting = this({
|
||
id : paintingUUID,
|
||
links : "/db/paintings/" + paintingUUID + ".png",
|
||
name : data.name,
|
||
description : data.description,
|
||
artist : data.artist,
|
||
createdTime : createdDate,
|
||
lastModified : createdDate,
|
||
tags : data.tags,
|
||
activity : null,
|
||
viewAuthority : data.viewAuthority,
|
||
totalScore : 0,
|
||
ratings : [],
|
||
comments : [],
|
||
isFinished : data.isFinished,
|
||
isLocked : false
|
||
});
|
||
newPainting.save((err, painting) => {
|
||
if (err)
|
||
callback(err, null);
|
||
else
|
||
callback(null, {_id: painting._id, id: paintingUUID, lastModified: createdDate});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 一個包含所有要更新的圖畫資訊的物件。
|
||
* @typedef NewPaintingInfo
|
||
* @prop {string} name 作品名稱
|
||
* @prop {string} description 作品的敘述。
|
||
* @prop {string[]} taglist 標籤清單。
|
||
* @prop {number} view_authority 作品的訪問權限。
|
||
* @prop {boolean} isFinished 作品是否完成。
|
||
*/
|
||
/**
|
||
* 透過傳入的圖畫id尋找目標的圖畫資料,以new_info來更新其資訊。
|
||
* @param {NewPaintingInfo} new_info 要更新目標圖畫資訊的物件。
|
||
* @param {string} id 目標圖畫資料的ID。
|
||
* @param {Jimp.Jimp} image Jimp影像物件。用來更新圖畫資料。
|
||
* @param {CallbackFunction} callback 回呼函式。若成功則回呼圖畫的_id與最後更新時間;其餘皆為錯誤。
|
||
*/
|
||
PaintingSchema.statics.UpdateInfoById = function (new_info, id, image, callback) {
|
||
// 以圖畫id來取得目標資料
|
||
this.findOne({"id": id}, (err, paintingDocs) => {
|
||
if (err) return callback(err, null);
|
||
if (!paintingDocs) return callback(Error_PaintingNotExist, null); // 若找不到畫作資料,則回呼錯誤
|
||
if (paintingDocs.isLocked) return callback(Error_PaintingIsLocked, null); // 若目標畫作為「鎖定」狀態,則回乎錯誤
|
||
let isFinished_before = paintingDocs.isFinished; // 取得更改之前的isFinished的狀態
|
||
let newLastModified = new Date(); // 更新「最後修改日期」的時間
|
||
|
||
// 若目前作品已完成 且 使用者還想再次「完成」此作品,則回報錯誤
|
||
if (isFinished_before && new_info.isFinished) return callback(Error_PaintingHasFinished, null);
|
||
|
||
// 更新資料
|
||
paintingDocs.name = new_info.name;
|
||
paintingDocs.description = new_info.description;
|
||
paintingDocs.tags = new_info.taglist;
|
||
paintingDocs.viewAuthority = new_info.view_authority;
|
||
|
||
// 「最後修改日期」是隨著圖畫內容是否更動而變的
|
||
if (!isFinished_before)
|
||
paintingDocs.lastModified = newLastModified;
|
||
|
||
// 一但完成作品之後,就不會再回到「沒有完成」的狀態了。
|
||
paintingDocs.isFinished = isFinished_before || new_info.isFinished;
|
||
|
||
// 將圖畫資料儲存後並回呼
|
||
paintingDocs.save((err) => {
|
||
if (err) return callback(err, null);
|
||
|
||
/**
|
||
* 幾本上針對「作品完成」狀態與動作有四種情況:
|
||
* 儲存前 儲存動作
|
||
* 1. 未完成 不完成 : 作品未完成情況下,執行一般的「儲存」動作。
|
||
* 2. 未完成 完成 : 作品未完成情況下,執行「完成圖畫」的動作。
|
||
* 3. 已完成 不完成 : 作品已完成情況下,執行一般的「儲存」動作。也就是不更改畫作影像,僅變更畫作的相關資訊。動作完成後,圖畫仍為「完成」狀態。
|
||
* 4. 已完成 完成 : 作品已完成情況下,執行「完成圖畫」的動作。這是不被允許的,一個作品完成之後就不能再「完成」第二次。
|
||
* 在上頭的程式碼 "if (isFinished_before && new_info.isFinished) ..." 中,已將第4種情況剔除,
|
||
* 因此以下拿 paintingDocs.isFinished 來判斷是否更新圖畫影像。
|
||
*/
|
||
if (!isFinished_before) {
|
||
// 將新的影像複寫在舊的影像之上
|
||
image.write("./db/paintings/" + paintingDocs.id + ".png", (err) => {
|
||
if (err) return callback(err, null);
|
||
callback(null, {_id: paintingDocs._id, lastModified: newLastModified});
|
||
});
|
||
}
|
||
else {
|
||
callback(null, {_id: paintingDocs._id, lastModified: newLastModified});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 確認使用者(user_id)對於畫作(paintingId)的訪問權限是否足夠。
|
||
* 若訪問權限足夠,回呼圖畫id;反之,則回呼false。
|
||
* @param {string} paintingId 畫作的Id。
|
||
* @param {string} user_id 使用者的_id。
|
||
* @param {CallbackFunction} callback 回呼函式。
|
||
*/
|
||
PaintingSchema.statics.CheckViewAuthority = function (paintingId, user_id, callback) {
|
||
this.findOne({"id": paintingId})
|
||
.select("id artist link viewAuthority")
|
||
.exec((err, paintingDocs) => {
|
||
if (err) return callback(err, null);
|
||
if (!paintingDocs) return callback(null, false); // 若尋找的圖畫Id不存在,則回乎false。
|
||
|
||
// 當訪問權限為「公開」時,則直接回呼
|
||
if (paintingDocs.viewAuthority == 0) {
|
||
callback(null, paintingId);
|
||
}
|
||
// 當訪問權限為「半公開」或「私人」時,且使用者有登入時,則近一步判斷。
|
||
else if (user_id){
|
||
// 取得圖畫作者的資料,判斷使用者是否正為圖畫作者 或 圖畫作者的好友。
|
||
User.findOne({"username": paintingDocs.artist}, "_id friendList", (err, artistDocs) => {
|
||
if (err) return callback(err, null);
|
||
let isArtist = user_id == artistDocs._id; // 使用者是否為圖畫作者
|
||
let isFriend = artistDocs.IsUsersFriend(user_id); // 使用者是否為圖畫作者的好友
|
||
let check = isArtist || (paintingDocs.viewAuthority == 1 && isFriend); // 只要是圖畫作者 或 圖畫作者的朋友且訪問權限為「半公開」,則可以瀏覽此圖畫。
|
||
let result = check ? paintingId : false; // 依 check 取得結果
|
||
callback(null, result);
|
||
});
|
||
}
|
||
// 若圖畫訪問權限不為「公開」,且使用者又沒登入,則回呼false。
|
||
else {
|
||
callback(null, false);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 透過畫作Id取得在「繪圖創作」頁面上所需要的畫作資料。
|
||
* @param {string} paintingId 指定的畫作Id。
|
||
* @param {CallbackFunction} 回呼函式。若成功找到,則將畫作資料回呼;若失敗則為null。
|
||
*/
|
||
PaintingSchema.statics.GetDrawingPageInfoById = function (paintingId, callback) {
|
||
this.findOne({ "id": paintingId })
|
||
.populate({ path: "activity", select: { "nthSeason": 1, "themeName": 1 } })
|
||
.exec((err, paintingDocs) => {
|
||
if (err) return callback(err, null);
|
||
if (!paintingDocs) return callback(null, null);
|
||
|
||
paintingDocs.paintingName = paintingDocs.name; // 差值需求表定義之中,與Painting資料表唯一不一樣的地方。 paintingName: paintingDocs.name
|
||
callback(null, paintingDocs);
|
||
}
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 判斷錯誤是否為「指定的畫作不存在」。
|
||
* @return {boolean} 檢查的結果。
|
||
*/
|
||
PaintingSchema.statics.IsError_PaintingNotExist = function (error) {
|
||
return error == Error_PaintingNotExist;
|
||
}
|
||
|
||
/**
|
||
* 判斷錯誤是否為「畫作已被鎖定無法更改」。
|
||
* @return {boolean} 檢查結果。
|
||
*/
|
||
PaintingSchema.statics.IsError_PaintingIsLocked = function (error) {
|
||
return error == Error_PaintingIsLocked;
|
||
}
|
||
|
||
/**
|
||
* 判斷錯誤是否為「畫作已完成無法做二次完成動作」
|
||
* @return {boolean} 檢查結果。
|
||
*/
|
||
PaintingSchema.statics.IsError_PaintingHasFinished = function (error) {
|
||
return error == Error_PaintingHasFinished;
|
||
}
|
||
|
||
/**
|
||
* 取得「找不到指定畫作」的錯誤。
|
||
* @return {Error} 錯誤 Error_PaintingNotExist 。
|
||
*/
|
||
PaintingSchema.statics.GetError_PaintingNotExist = function () {
|
||
return Error_PaintingNotExist;
|
||
}
|
||
|
||
/**
|
||
* 移除所有與圖畫有關聯的「留言(Comment)」、「評分(Rating)」與「參與活動(Participation)」
|
||
* @param {CallbackFunction} callback 回呼函式。
|
||
*/
|
||
PaintingSchema.methods.RemoveAllReferenceInfo = function (callback) {
|
||
Participation.remove({"_id": this.activity}, (err) => {
|
||
if (err) return callback(err, null);
|
||
|
||
Rating.remove({"_id": { $in: this.ratings } }, (err) => {
|
||
if (err) return callback(err, null);
|
||
|
||
Comment.remove({"_id": { $in: this.comments }}, (err) => {
|
||
if (err) return callback(err, null);
|
||
callback(null, true);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 尋找使用者(Username)對這幅畫作的評分。若沒有,則回傳0。
|
||
* 注意,畫作資料必須要對ratings欄位做Populate之後,此函式執行才會有正確的結果。
|
||
* @param {string} username 使用者名稱。
|
||
* @return {number} 目標使用者的評分分數。
|
||
*/
|
||
PaintingSchema.methods.FindRatingScoreByUsername = function (username) {
|
||
for (let rating of this.ratings) {
|
||
if (rating.username == username)
|
||
return rating.score;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 透過畫作Id尋找指定的畫作資料,將留言新增到其中。
|
||
* @param {string} paintingId 指定的畫作Id。
|
||
* @param {string} username 留言的使用者。
|
||
* @param {string} userPhotoURL 留言使用者的個人圖像。
|
||
* @param {string} comment 留言內容。
|
||
* @param {CallbackFunction} callback 回呼函式。
|
||
*/
|
||
PaintingSchema.statics.PushNewComment = function (paintingId, username, userPhotoURL, comment, callback) {
|
||
// 尋找目標畫作
|
||
this.findOne({"id": paintingId}, "comments", (err, paintingDocs) => {
|
||
if (err) return callback(err, null);
|
||
if (!paintingDocs) return callback(Error_PaintingNotExist, null);
|
||
|
||
// 定義留言資料
|
||
let newComment = {
|
||
username: username,
|
||
photo: userPhotoURL,
|
||
comment: comment,
|
||
time: new Date()
|
||
};
|
||
|
||
// 新增留言
|
||
Comment.createNewComment(newComment, (err, _id) => {
|
||
if (err) return callback(err, null);
|
||
|
||
// 將該項留言連結至目標畫作
|
||
paintingDocs.comments.push(_id);
|
||
paintingDocs.save((err) => {
|
||
if (err)
|
||
callback(err, null);
|
||
else
|
||
callback(null, _id);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 以使用者名稱(username)來尋找對應的評分(Rating)。
|
||
* 注意,必須要先連結(Populate)過ratings欄位之後,才能使用此函式。
|
||
* @param {string} username 目標使用者名稱。
|
||
* @return {Rating?} 評分資料。
|
||
*/
|
||
PaintingSchema.methods.FindRatingByUsername = function (username) {
|
||
for (let docs of this.ratings) {
|
||
if (docs.username == username)
|
||
return docs;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 更新totalScore欄位。注意,必須要先連結(Populate)過ratings欄位之後,才能使用此函式。
|
||
*/
|
||
PaintingSchema.methods.UpdateTotalScore = function () {
|
||
let sum = 0;
|
||
for (let docs of this.ratings) {
|
||
sum += docs.score;
|
||
}
|
||
this.totalScore = sum / this.ratings.length;
|
||
}
|
||
|
||
/**
|
||
* 透過畫作Id尋找指定的畫作,將評分分數更新到其上。
|
||
* @param {string} paintingId 指定的畫作Id。
|
||
* @param {string} username 評分的使用者名稱。
|
||
* @param {number} score 評分分數。
|
||
* @param {CallbackFunction} callback 回呼函式。
|
||
*/
|
||
PaintingSchema.statics.UpdateRatingById = function (paintingId, username, score, callback) {
|
||
// 以圖畫Id,尋找指定的畫作資料
|
||
this.findOne({"id": paintingId})
|
||
.select("ratings totalScore")
|
||
.populate("ratings")
|
||
.exec((err, paintingDocs) => {
|
||
if (err) return callback(err, null);
|
||
if (!paintingDocs) return callback(Error_PaintingNotExist, null);
|
||
let datas = { username: username, score: score }; // Rating建立新資料
|
||
|
||
// 檢查使用者在之前是否已有做過評分
|
||
let ratingDocs = paintingDocs.FindRatingByUsername(username);
|
||
|
||
// 若在先前已有評分過,則
|
||
if (ratingDocs) {
|
||
ratingDocs.score = score; // 更新評分分數
|
||
paintingDocs.UpdateTotalScore(); // 更新totalScore欄位
|
||
|
||
// 儲存評分資料
|
||
ratingDocs.save((err) => {
|
||
if (err) return callback(err, null);
|
||
|
||
// 儲存畫作資料
|
||
paintingDocs.save((err) => {
|
||
if (err) return callback(err, null);
|
||
callback(null, paintingDocs._id);
|
||
});
|
||
});
|
||
}
|
||
else {
|
||
// 創立一個新的評分
|
||
Rating.createNewRating(datas, (err, ratingDocs) => {
|
||
if (err) return callback(err, null);
|
||
paintingDocs.ratings.push(ratingDocs); // 將新的評分加入
|
||
paintingDocs.UpdateTotalScore(); // 更新總評分
|
||
|
||
// 儲存畫作資料
|
||
paintingDocs.save((err) => {
|
||
if (err) return callback(err, null);
|
||
callback(null, paintingDocs._id);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
module.exports = mongoose.model("Painting", PaintingSchema); |