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);