Browse Source

feat(Reports): custom issues, functionality for 'report per issue'

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 3 years ago
parent
commit
dc18712aae

+ 55 - 128
backend/logic/actions/reports.js

@@ -11,29 +11,6 @@ const SongsModule = moduleManager.modules.songs;
 const CacheModule = moduleManager.modules.cache;
 const ActivitiesModule = moduleManager.modules.activities;
 
-const reportableIssues = [
-	{
-		name: "Video",
-		reasons: ["Doesn't exist", "It's private", "It's not available in my country"]
-	},
-	{
-		name: "Title",
-		reasons: ["Incorrect", "Inappropriate"]
-	},
-	{
-		name: "Duration",
-		reasons: ["Skips too soon", "Skips too late", "Starts too soon", "Skips too late"]
-	},
-	{
-		name: "Artists",
-		reasons: ["Incorrect", "Inappropriate"]
-	},
-	{
-		name: "Thumbnail",
-		reasons: ["Incorrect", "Inappropriate", "Doesn't exist"]
-	}
-];
-
 CacheModule.runJob("SUB", {
 	channel: "report.resolve",
 	cb: reportId => {
@@ -62,25 +39,17 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
-			[
-				next => {
-					reportModel.find({ resolved: false }).sort({ released: "desc" }).exec(next);
-				}
-			],
+			[next => reportModel.find({ resolved: false }).sort({ released: "desc" }).exec(next)],
 			async (err, reports) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log("ERROR", "REPORTS_INDEX", `Indexing reports failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
+
 				this.log("SUCCESS", "REPORTS_INDEX", "Indexing reports successful.");
 				return cb({ status: "success", data: { reports } });
 			}
@@ -95,29 +64,18 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	findOne: isAdminRequired(async function findOne(session, reportId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
-		async.waterfall(
-			[
-				next => {
-					reportModel.findOne({ _id: reportId }).exec(next);
-				}
-			],
-			async (err, report) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
-					return cb({ status: "error", message: err });
-				}
-				this.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
-				return cb({ status: "success", data: { report } });
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
+		async.waterfall([next => reportModel.findOne({ _id: reportId }).exec(next)], async (err, report) => {
+			if (err) {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
+				return cb({ status: "error", message: err });
 			}
-		);
+
+			this.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
+			return cb({ status: "success", data: { report } });
+		});
 	}),
 
 	/**
@@ -128,13 +86,8 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	getReportsForSong: isAdminRequired(async function getReportsForSong(session, songId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -147,7 +100,7 @@ export default {
 				(_reports, next) => {
 					const reports = [];
 					for (let i = 0; i < _reports.length; i += 1) {
-						data.push(_reports[i]._id);
+						reports.push(_reports[i]._id);
 					}
 					next(null, reports);
 				}
@@ -158,6 +111,7 @@ export default {
 					this.log("ERROR", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
+
 				this.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
 				return cb({ status: "success", data: { reports } });
 			}
@@ -165,20 +119,15 @@ export default {
 	}),
 
 	/**
-	 * Resolves a report
+	 * Resolves a reported issue
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
 	 * @param {Function} cb - gets called with the result
 	 */
 	resolve: isAdminRequired(async function resolve(session, reportId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -204,11 +153,14 @@ export default {
 					);
 					return cb({ status: "error", message: err });
 				}
+
 				CacheModule.runJob("PUB", {
 					channel: "report.resolve",
 					value: reportId
 				});
+
 				this.log("SUCCESS", "REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
+
 				return cb({
 					status: "success",
 					message: "Successfully resolved Report"
@@ -221,18 +173,23 @@ export default {
 	 * Creates a new report
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {object} data - the object of the report data
+	 * @param {object} report - the object of the report data
+	 * @param {string} report.youtubeId - the youtube id of the song that is being reported
+	 * @param {Array} report.issues - all issues reported (custom or defined)
 	 * @param {Function} cb - gets called with the result
 	 */
-	create: isLoginRequired(async function create(session, data, cb) {
+	create: isLoginRequired(async function create(session, report, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 
+		const { youtubeId } = report;
+
+		// properties for every report issue that is saved to db
+		const template = {};
+
 		async.waterfall(
 			[
-				next => {
-					songModel.findOne({ youtubeId: data.youtubeId }).exec(next);
-				},
+				next => songModel.findOne({ youtubeId }).exec(next),
 
 				(song, next) => {
 					if (!song) return next("Song not found.");
@@ -245,53 +202,32 @@ export default {
 				(song, next) => {
 					if (!song) return next("Song not found.");
 
-					delete data.youtubeId;
-					data.song = {
+					template.song = {
 						_id: song._id,
 						youtubeId: song.youtubeId
 					};
 
-					for (let z = 0; z < data.issues.length; z += 1) {
-						if (reportableIssues.filter(issue => issue.name === data.issues[z].name).length > 0) {
-							for (let r = 0; r < reportableIssues.length; r += 1) {
-								if (
-									reportableIssues[r].reasons.every(
-										reason => data.issues[z].reasons.indexOf(reason) < -1
-									)
-								) {
-									return cb({
-										status: "error",
-										message: "Invalid data"
-									});
-								}
-							}
-						} else
-							return cb({
-								status: "error",
-								message: "Invalid data"
-							});
-					}
-
 					return next(null, { title: song.title, artists: song.artists, thumbnail: song.thumbnail });
 				},
 
 				(song, next) => {
-					const issues = [];
-
-					for (let r = 0; r < data.issues.length; r += 1) {
-						if (!data.issues[r].reasons.length <= 0) issues.push(data.issues[r]);
-					}
+					template.createdBy = session.userId;
+					template.createdAt = Date.now();
 
-					data.issues = issues;
+					return async.each(
+						report.issues,
+						(issue, next) => {
+							reportModel.create({ ...issue, ...template }, (err, value) => {
+								CacheModule.runJob("PUB", {
+									channel: "report.create",
+									value
+								});
 
-					next(null, song);
-				},
-
-				(song, next) => {
-					data.createdBy = session.userId;
-					data.createdAt = Date.now();
-
-					reportModel.create(data, (err, report) => next(err, report, song));
+								return next(err);
+							});
+						},
+						err => next(err, report, song)
+					);
 				}
 			],
 			async (err, report, song) => {
@@ -300,31 +236,22 @@ export default {
 					this.log(
 						"ERROR",
 						"REPORTS_CREATE",
-						`Creating report for "${data.song._id}" failed by user "${session.userId}". "${err}"`
+						`Creating report for "${template.song._id}" failed by user "${session.userId}". "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
 
-				CacheModule.runJob("PUB", {
-					channel: "report.create",
-					value: report
-				});
-
 				ActivitiesModule.runJob("ADD_ACTIVITY", {
-					userId: report.createdBy,
+					userId: template.createdBy,
 					type: "song__report",
 					payload: {
 						message: `Reported song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
-						youtubeId: data.song.youtubeId,
+						youtubeId: template.song.youtubeId,
 						thumbnail: song.thumbnail
 					}
 				});
 
-				this.log(
-					"SUCCESS",
-					"REPORTS_CREATE",
-					`User "${session.userId}" created report for "${data.youtubeId}".`
-				);
+				this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${youtubeId}".`);
 
 				return cb({
 					status: "success",

+ 1 - 10
backend/logic/db/index.js

@@ -11,7 +11,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	playlist: 4,
 	punishment: 1,
 	queueSong: 1,
-	report: 2,
+	report: 3,
 	song: 5,
 	station: 5,
 	user: 3
@@ -262,15 +262,6 @@ class _DBModule extends CoreClass {
 
 					this.schemas.playlist.index({ createdFor: 1, type: 1 }, { unique: true });
 
-					// Report
-					this.schemas.report
-						.path("description")
-						.validate(
-							description =>
-								!description || (isLength(description, 0, 400) && regex.ascii.test(description)),
-							"Invalid description."
-						);
-
 					if (config.get("skipDbDocumentsVersionCheck")) resolve();
 					else {
 						this.runJob("CHECK_DOCUMENT_VERSIONS", {}, null, -1)

+ 3 - 8
backend/logic/db/schemas/report.js

@@ -4,14 +4,9 @@ export default {
 		_id: { type: String, required: true },
 		youtubeId: { type: String, required: true }
 	},
-	description: { type: String },
-	issues: [
-		{
-			name: String,
-			reasons: Array
-		}
-	],
+	category: { type: String, enum: ["custom", "video", "title", "duration", "artists", "thumbnail"], required: true },
+	info: { type: String, required: true },
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 2, required: true }
+	documentVersion: { type: Number, default: 3, required: true }
 };

+ 3 - 3
frontend/src/components/Queue.vue

@@ -89,7 +89,7 @@
 			@click="openModal('manageStation') & showManageStationTab('search')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Add Song To Queue </span>
+			<span> Add Song To Queue </span>
 		</button>
 		<button
 			class="button is-primary tab-actionable-button"
@@ -99,7 +99,7 @@
 			@click="openModal('requestSong')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Request Song </span>
+			<span> Request Song </span>
 		</button>
 		<button
 			class="button is-primary tab-actionable-button disabled"
@@ -115,7 +115,7 @@
 			v-tippy="{ theme: 'info' }"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Add Song To Queue </span>
+			<span> Add Song To Queue </span>
 		</button>
 		<div
 			id="queue-locked"

+ 5 - 5
frontend/src/components/modals/ManageStationKris/index.vue

@@ -53,7 +53,7 @@
 									<i class="material-icons icon-with-button"
 										>play_arrow</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Resume Station
 									</span>
 								</button>
@@ -65,7 +65,7 @@
 									<i class="material-icons icon-with-button"
 										>pause</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Pause Station
 									</span>
 								</button>
@@ -78,7 +78,7 @@
 									<i class="material-icons icon-with-button"
 										>skip_next</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Force Skip
 									</span>
 								</button>
@@ -91,7 +91,7 @@
 									<i class="material-icons icon-with-button"
 										>settings</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Manage Station
 									</span>
 								</button> -->
@@ -191,7 +191,7 @@
 				@click="openModal('requestSong')"
 			>
 				<i class="material-icons icon-with-button">queue</i>
-				<span class="optional-desktop-only-text"> Request Song </span>
+				<span> Request Song </span>
 			</button>
 			<div v-if="isOwnerOrAdmin()" class="right">
 				<confirm @confirm="clearAndRefillStationQueue()">

+ 1 - 1
frontend/src/components/modals/ManageStationOwen/index.vue

@@ -175,7 +175,7 @@
 				@click="openModal('requestSong')"
 			>
 				<i class="material-icons icon-with-button">queue</i>
-				<span class="optional-desktop-only-text"> Request Song </span>
+				<span> Request Song </span>
 			</button>
 			<div v-if="isOwnerOrAdmin()" class="right">
 				<confirm @confirm="clearAndRefillStationQueue()">

+ 139 - 29
frontend/src/components/modals/Report.vue

@@ -10,11 +10,11 @@
 
 				<div class="columns is-multiline">
 					<div
-						v-for="issue in issues"
+						v-for="issue in predefinedIssues"
 						class="column is-half"
-						:key="issue.name"
+						:key="issue.category"
 					>
-						<label class="label">{{ issue.name }}</label>
+						<label class="label">{{ issue.category }}</label>
 
 						<p
 							v-for="reason in issue.reasons"
@@ -61,6 +61,64 @@
 					<!-- allow for multiple custom issues with plus/add button and then a input textbox -->
 					<!-- do away with textbox -->
 
+					<div class="column is-half">
+						<div id="custom-issues">
+							<div id="custom-issues-title">
+								<label class="label">Issues not listed</label>
+
+								<button
+									class="button tab-actionable-button "
+									content="Add an issue that isn't listed"
+									v-tippy="{ theme: 'info' }"
+									@click="customIssues.push('')"
+								>
+									<i class="material-icons icon-with-button"
+										>add</i
+									>
+									<span>
+										Add Custom Issue
+									</span>
+								</button>
+							</div>
+
+							<div
+								class="custom-issue control is-grouped input-with-button"
+								v-for="(issue, index) in customIssues"
+								:key="index"
+							>
+								<p class="control is-expanded">
+									<input
+										type="text"
+										class="input"
+										v-model="customIssues[index]"
+										placeholder="Provide information..."
+									/>
+								</p>
+								<p class="control">
+									<button
+										class="button is-danger"
+										content="Remove custom issue"
+										v-tippy="{ theme: 'info' }"
+										@click="customIssues.splice(index, 1)"
+									>
+										<i class="material-icons">
+											delete
+										</i>
+									</button>
+								</p>
+							</div>
+
+							<p
+								id="no-issues-listed"
+								v-if="customIssues.length <= 0"
+							>
+								<em>
+									Add any issues that aren't listed above.
+								</em>
+							</p>
+						</div>
+					</div>
+
 					<!--
 						<div class="column">
 						<p class="content-box-optional-helper">
@@ -110,21 +168,10 @@ export default {
 	components: { Modal, SongItem },
 	data() {
 		return {
-			report: {
-				resolved: false,
-				youtubeId: "",
-				description: "",
-				issues: [
-					{ name: "Video", reasons: [] },
-					{ name: "Title", reasons: [] },
-					{ name: "Duration", reasons: [] },
-					{ name: "Artists", reasons: [] },
-					{ name: "Thumbnail", reasons: [] }
-				]
-			},
-			issues: [
+			customIssues: [],
+			predefinedIssues: [
 				{
-					name: "Video",
+					category: "video",
 					reasons: [
 						{
 							enabled: false,
@@ -153,7 +200,7 @@ export default {
 					]
 				},
 				{
-					name: "Title",
+					category: "title",
 					reasons: [
 						{
 							enabled: false,
@@ -170,7 +217,7 @@ export default {
 					]
 				},
 				{
-					name: "Duration",
+					category: "duration",
 					reasons: [
 						{
 							enabled: false,
@@ -199,7 +246,7 @@ export default {
 					]
 				},
 				{
-					name: "Artists",
+					category: "artists",
 					reasons: [
 						{
 							enabled: false,
@@ -216,7 +263,7 @@ export default {
 					]
 				},
 				{
-					name: "Thumbnail",
+					category: "thumbnail",
 					reasons: [
 						{
 							enabled: false,
@@ -252,17 +299,40 @@ export default {
 			socket: "websockets/getSocket"
 		})
 	},
-	mounted() {
-		if (this.song !== null) this.report.youtubeId = this.song.youtubeId;
-	},
 	methods: {
 		create() {
-			// generate report from here (filter by enabled reasons)
+			const issues = [];
+
+			// any predefined issues that are enabled
+			this.predefinedIssues.forEach(category =>
+				category.reasons.forEach(reason => {
+					if (reason.enabled) {
+						const info =
+							reason.info === ""
+								? reason.reason
+								: `${reason.reason} - ${reason.info}`;
 
-			this.socket.dispatch("reports.create", this.report, res => {
-				new Toast(res.message);
-				if (res.status === "success") this.closeModal("report");
-			});
+						issues.push({ category: category.category, info });
+					}
+				})
+			);
+
+			// any custom issues
+			this.customIssues.forEach(issue =>
+				issues.push({ category: "custom", info: issue })
+			);
+
+			this.socket.dispatch(
+				"reports.create",
+				{
+					issues,
+					youtubeId: this.song.youtubeId
+				},
+				res => {
+					new Toast(res.message);
+					if (res.status === "success") this.closeModal("report");
+				}
+			);
 		},
 		...mapActions("modals/report", ["reportSong"]),
 		...mapActions("modalVisibility", ["closeModal"])
@@ -299,6 +369,10 @@ export default {
 	}
 }
 
+.label {
+	text-transform: capitalize;
+}
+
 .columns {
 	margin-left: unset;
 	margin-right: unset;
@@ -329,4 +403,40 @@ export default {
 		}
 	}
 }
+
+#custom-issues {
+	height: 100%;
+
+	#custom-issues-title {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin-bottom: 15px;
+
+		button {
+			padding: 3px 5px;
+			height: initial;
+		}
+
+		label {
+			margin: 0;
+		}
+	}
+
+	#no-issues-listed {
+		display: flex;
+		height: calc(100% - 32px - 15px);
+		align-items: center;
+		justify-content: center;
+	}
+
+	.custom-issue {
+		flex-direction: row;
+
+		input {
+			height: 36px;
+			margin: 0;
+		}
+	}
+}
 </style>

+ 8 - 21
frontend/src/components/modals/ViewReport.vue

@@ -39,30 +39,17 @@
 						}}
 					</span>
 					<br />
-					<span v-if="report.description">
-						<strong>Description:</strong>
-						{{ report.description }}
+					<span>
+						<strong>Category:</strong>
+						{{ report.category }}
+					</span>
+					<br />
+					<span>
+						<strong>Info:</strong>
+						{{ report.info }}
 					</span>
 				</div>
 			</article>
-			<table v-if="report.issues.length > 0" class="table is-narrow">
-				<thead>
-					<tr>
-						<td>Issue</td>
-						<td>Reasons</td>
-					</tr>
-				</thead>
-				<tbody>
-					<tr v-for="issue in report.issues" :key="issue.name">
-						<td>
-							<span>{{ issue.name }}</span>
-						</td>
-						<td>
-							<span>{{ issue.reasons }}</span>
-						</td>
-					</tr>
-				</tbody>
-			</table>
 		</template>
 		<template #footer v-if="report && report._id">
 			<a class="button is-primary" href="#" @click="resolve(report._id)">

+ 6 - 2
frontend/src/pages/Admin/tabs/Reports.vue

@@ -8,7 +8,8 @@
 						<td>Song ID</td>
 						<td>Author</td>
 						<td>Time of report</td>
-						<td>Description</td>
+						<td>Category</td>
+						<td>Info</td>
 						<td>Options</td>
 					</tr>
 				</thead>
@@ -41,7 +42,10 @@
 							>
 						</td>
 						<td>
-							<span>{{ report.description }}</span>
+							<span>{{ report.category }}</span>
+						</td>
+						<td>
+							<span>{{ report.info }}</span>
 						</td>
 						<td>
 							<a

+ 1 - 1
frontend/src/pages/Station/Sidebar/Playlists.vue

@@ -99,7 +99,7 @@
 			@click="openModal('createPlaylist')"
 		>
 			<i class="material-icons icon-with-button">create</i>
-			<span class="optional-desktop-only-text"> Create Playlist </span>
+			<span> Create Playlist </span>
 		</a>
 	</div>
 </template>

+ 1 - 1
frontend/src/pages/Station/Sidebar/Users.vue

@@ -58,7 +58,7 @@
 			@click="copyToClipboard()"
 		>
 			<i class="material-icons icon-with-button">share</i>
-			<span class="optional-desktop-only-text">
+			<span>
 				Share (copy to clipboard)
 			</span>
 		</button>

+ 4 - 4
frontend/src/pages/Station/index.vue

@@ -119,7 +119,7 @@
 									<i class="material-icons icon-with-button"
 										>play_arrow</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Resume Station
 									</span>
 								</button>
@@ -131,7 +131,7 @@
 									<i class="material-icons icon-with-button"
 										>pause</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Pause Station
 									</span>
 								</button>
@@ -144,7 +144,7 @@
 									<i class="material-icons icon-with-button"
 										>skip_next</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Force Skip
 									</span>
 								</button>
@@ -157,7 +157,7 @@
 									<i class="material-icons icon-with-button"
 										>settings</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Manage Station
 									</span>
 								</button>