Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master

pull/5/head
Trivernis 5 years ago committed by Gitea
commit a9aa44ae3e

@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- delete handler for media to delete the corresponding file - delete handler for media to delete the corresponding file
- type for create post to know if it is a media or text post (media posts are invisible until a media file is uploaded) - type for create post to know if it is a media or text post (media posts are invisible until a media file is uploaded)
- reports and mutations to report posts and create reasons to report - reports and mutations to report posts and create reasons to report
- level entity
### Removed ### Removed
@ -51,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- config behaviour to use all files that reside in the ./config directory with the .toml format - config behaviour to use all files that reside in the ./config directory with the .toml format
- default response timeout from 2 minutes to 30 seconds - default response timeout from 2 minutes to 30 seconds
- cluster api to start workers with a 2 second delay each to avoid race conditions - cluster api to start workers with a 2 second delay each to avoid race conditions
- levels to be configured in the backend
### Fixed ### Fixed
@ -60,7 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- style issues - style issues
- graphql schema for denyRequest using the wrong parameters - graphql schema for denyRequest using the wrong parameters
- sendRequest allowing duplicates - sendRequest allowing duplicates
- upload throwing an error when the old picture doesn't exist
- extension of uploaded videos doesn't have a dot
## [0.9] - 2019-10-29 ## [0.9] - 2019-10-29

@ -98,7 +98,7 @@ export class UploadManager {
* @param extension * @param extension
*/ */
public async processAndStoreVideo(data: Buffer, extension: string): Promise<Media> { public async processAndStoreVideo(data: Buffer, extension: string): Promise<Media> {
const fileBasename = UploadManager.getCrypticFileName() + extension; const fileBasename = UploadManager.getCrypticFileName() + "." + extension;
await fsx.ensureDir(this.dataDir); await fsx.ensureDir(this.dataDir);
const filePath = path.join(this.dataDir, fileBasename); const filePath = path.join(this.dataDir, fileBasename);
await fsx.writeFile(filePath, data); await fsx.writeFile(filePath, data);

@ -67,12 +67,12 @@ namespace dataaccess {
models.Media, models.Media,
models.Report, models.Report,
models.ReportReason, models.ReportReason,
models.Level,
]); ]);
} catch (err) { } catch (err) {
globals.logger.error(err.message); globals.logger.error(err.message);
globals.logger.debug(err.stack); globals.logger.debug(err.stack);
} }
await databaseCleanup();
setInterval(databaseCleanup, config.get<number>("database.cleanupInterval") * 1000); setInterval(databaseCleanup, config.get<number>("database.cleanupInterval") * 1000);
} }

@ -0,0 +1,10 @@
import {BaseError} from "./BaseError";
/**
* An error that is thrown when the level already exists
*/
export class LevelAlreadyExistsError extends BaseError {
constructor(property: string) {
super(`A level with the property value '${property}' already exists`);
}
}

@ -0,0 +1,32 @@
import * as sqz from "sequelize";
import {Column, Model, NotNull, Table, Unique} from "sequelize-typescript";
/**
* A level of the ranking system
*/
@Table({underscored: true})
export class Level extends Model<Level> {
/**
* The name of the level
*/
@NotNull
@Unique
@Column({allowNull: false, type: sqz.STRING(64), unique: true})
public name: string;
/**
* The required points for the level
*/
@NotNull
@Unique
@Column({allowNull: false, unique: true})
public points: number;
/**
* Returns the number of the level as the number of the database entry
*/
public async levelNumber(): Promise<number> {
return Level.count({where: {points: {[sqz.Op.lte]: this.points}}});
}
}

@ -18,8 +18,10 @@ export class Media extends Model<Media> {
*/ */
@BeforeDestroy @BeforeDestroy
public static async deleteMediaFile(instance: Media) { public static async deleteMediaFile(instance: Media) {
if (await fsx.pathExists(instance.path)) {
await fsx.unlink(instance.path); await fsx.unlink(instance.path);
} }
}
/** /**
* The api url for the media * The api url for the media

@ -1,5 +1,6 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import { import {
BeforeUpdate,
BelongsTo, BelongsTo,
BelongsToMany, BelongsToMany,
Column, Column,
@ -24,6 +25,7 @@ import {Friendship} from "./Friendship";
import {Group} from "./Group"; import {Group} from "./Group";
import {GroupAdmin} from "./GroupAdmin"; import {GroupAdmin} from "./GroupAdmin";
import {GroupMember} from "./GroupMember"; import {GroupMember} from "./GroupMember";
import {Level} from "./Level";
import {Media} from "./Media"; import {Media} from "./Media";
import {Post} from "./Post"; import {Post} from "./Post";
import {PostVote} from "./PostVote"; import {PostVote} from "./PostVote";
@ -35,6 +37,19 @@ import {Request, RequestType} from "./Request";
@Table({underscored: true}) @Table({underscored: true})
export class User extends Model<User> { export class User extends Model<User> {
/**
* A function that is called before the user is updated.
* It assigns the corresponding level to the user
* @param instance
*/
@BeforeUpdate
public static async assignLevel(instance: User) {
const level = await Level.findOne({where: {points: {[sqz.Op.lte]: instance.rankpoints}}, order: [["points", "desc"]]}) as Level;
if (level) {
instance.$set("rLevel", level);
}
}
/** /**
* The name of the user * The name of the user
*/ */
@ -99,13 +114,25 @@ export class User extends Model<User> {
@Column({defaultValue: false, allowNull: false}) @Column({defaultValue: false, allowNull: false})
public isAdmin: boolean; public isAdmin: boolean;
/**
* The level of the user
*/
@ForeignKey(() => Level)
@Column({allowNull: true})
public levelId: number;
/** /**
* The id of the media that is the users profile picture * The id of the media that is the users profile picture
*/ */
@ForeignKey(() => Media) @ForeignKey(() => Media)
@Column({allowNull: false}) @Column({allowNull: true})
public mediaId: number; public mediaId: number;
/**
* The level of the user
*/
@BelongsTo(() => Level)
public rLevel: Level;
/** /**
* The media of the user * The media of the user
@ -221,8 +248,8 @@ export class User extends Model<User> {
/** /**
* The level of the user which is the points divided by 100 * The level of the user which is the points divided by 100
*/ */
public get level(): number { public async level(): Promise<Level> {
return Math.ceil(this.getDataValue("rankpoints") / 100); return await this.$get("rLevel") as Level;
} }
/** /**

@ -16,3 +16,4 @@ export {BlacklistedPhrase} from "./BlacklistedPhrase";
export {Media} from "./Media"; export {Media} from "./Media";
export {Report} from "./Report"; export {Report} from "./Report";
export {ReportReason} from "./ReportReason"; export {ReportReason} from "./ReportReason";
export {Level} from "./Level";

@ -1,11 +1,13 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import sequelize from "sequelize";
import isEmail from "validator/lib/isEmail"; import isEmail from "validator/lib/isEmail";
import dataAccess from "../../lib/dataAccess"; import dataAccess from "../../lib/dataAccess";
import {BlacklistedError} from "../../lib/errors/BlacklistedError"; import {BlacklistedError} from "../../lib/errors/BlacklistedError";
import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError"; import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError";
import {HandleInUseError} from "../../lib/errors/HandleInUseError"; import {HandleInUseError} from "../../lib/errors/HandleInUseError";
import {InvalidEmailError} from "../../lib/errors/InvalidEmailError"; import {InvalidEmailError} from "../../lib/errors/InvalidEmailError";
import {LevelAlreadyExistsError} from "../../lib/errors/LevelAlreadyExistsError";
import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError"; import {NotAGroupAdminError} from "../../lib/errors/NotAGroupAdminError";
import {NotAnAdminError} from "../../lib/errors/NotAnAdminError"; import {NotAnAdminError} from "../../lib/errors/NotAnAdminError";
import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError"; import {NotTheGroupCreatorError} from "../../lib/errors/NotTheGroupCreatorError";
@ -15,7 +17,18 @@ import {ReportReasonNameAlreadyExistsError} from "../../lib/errors/ReportReasonN
import {ReportReasonNotFoundError} from "../../lib/errors/ReportReasonNotFoundError"; import {ReportReasonNotFoundError} from "../../lib/errors/ReportReasonNotFoundError";
import globals from "../../lib/globals"; import globals from "../../lib/globals";
import {InternalEvents} from "../../lib/InternalEvents"; import {InternalEvents} from "../../lib/InternalEvents";
import {Activity, BlacklistedPhrase, ChatMessage, ChatRoom, Event, Group, Post, Request, User} from "../../lib/models"; import {
Activity,
BlacklistedPhrase,
ChatMessage,
ChatRoom,
Event,
Group,
Level,
Post,
Request,
User,
} from "../../lib/models";
import {Report} from "../../lib/models"; import {Report} from "../../lib/models";
import {ReportReason} from "../../lib/models"; import {ReportReason} from "../../lib/models";
import {UploadManager} from "../../lib/UploadManager"; import {UploadManager} from "../../lib/UploadManager";
@ -147,6 +160,8 @@ export class MutationResolver extends BaseResolver {
await user.save(); await user.save();
return user.settings; return user.settings;
} catch (err) { } catch (err) {
globals.logger.warning(err.message);
globals.logger.debug(err.stack);
throw new GraphQLError("Invalid settings json."); throw new GraphQLError("Invalid settings json.");
} }
} }
@ -557,4 +572,26 @@ export class MutationResolver extends BaseResolver {
} }
return ReportReason.create({name, description}); return ReportReason.create({name, description});
} }
/**
* Creates a new level
* @param name
* @param levelNumber
* @param requiredPoints
* @param request
*/
public async createLevel({name, requiredPoints}: {name: string, requiredPoints: number}, request: any):
Promise<Level> {
this.ensureLoggedIn(request);
const user = await User.findByPk(request.session.userId);
if (!user.isAdmin) {
throw new NotAnAdminError();
}
const existingLevel = await Level.findOne({where: {[sequelize.Op.or]: [{name}, {points: requiredPoints}]}});
if (existingLevel) {
throw new LevelAlreadyExistsError(
existingLevel.name === name ? "name" : "points");
}
return Level.create({name, points: requiredPoints});
}
} }

@ -7,7 +7,18 @@ import {GroupNotFoundError} from "../../lib/errors/GroupNotFoundError";
import {NotAnAdminError} from "../../lib/errors/NotAnAdminError"; import {NotAnAdminError} from "../../lib/errors/NotAnAdminError";
import {RequestNotFoundError} from "../../lib/errors/RequestNotFoundError"; import {RequestNotFoundError} from "../../lib/errors/RequestNotFoundError";
import {UserNotFoundError} from "../../lib/errors/UserNotFoundError"; import {UserNotFoundError} from "../../lib/errors/UserNotFoundError";
import {Activity, BlacklistedPhrase, ChatRoom, Event, Group, Post, Report, Request, User} from "../../lib/models"; import {
Activity,
BlacklistedPhrase,
ChatRoom,
Event,
Group,
Level,
Post,
Report,
Request,
User
} from "../../lib/models";
import {BlacklistedResult} from "./BlacklistedResult"; import {BlacklistedResult} from "./BlacklistedResult";
import {MutationResolver} from "./MutationResolver"; import {MutationResolver} from "./MutationResolver";
import {SearchResult} from "./SearchResult"; import {SearchResult} from "./SearchResult";
@ -198,6 +209,15 @@ export class QueryResolver extends MutationResolver {
if (!user?.isAdmin) { if (!user?.isAdmin) {
throw new NotAnAdminError(); throw new NotAnAdminError();
} }
return Report.findAll({limit: first, offset}); return Report.findAll({limit: first, offset, order: [["id", "DESC"]]});
}
/**
* Returns the levels that are configured
* @param first
* @param offset
*/
public async getLevels({first, offset}: {first: number, offset: number}): Promise<Level[]> {
return Level.findAll({limit: first, offset, order: [["points", "ASC"]]});
} }
} }

@ -48,6 +48,9 @@ type Query {
"Returns all issued reports with pagination" "Returns all issued reports with pagination"
getReports(first: Int = 20, offset: Int = 0): [Report!]! @complexity(value: 1, multipliers: ["first"]) getReports(first: Int = 20, offset: Int = 0): [Report!]! @complexity(value: 1, multipliers: ["first"])
"Returns the levels configured in the backend"
getLevels(first: Int =20, offset: Int = 0): [Level!]! @complexity(value: 1, multipliers: ["first"])
} }
type Mutation { type Mutation {
@ -143,6 +146,9 @@ type Mutation {
"Creates a new report reason" "Creates a new report reason"
createReportReason(name: String!, description: String!): ReportReason createReportReason(name: String!, description: String!): ReportReason
"Creates a new level"
createLevel(name: String!, requiredPoints: Int!): Level!
} }
interface UserData { interface UserData {
@ -189,7 +195,7 @@ interface UserData {
points: Int! points: Int!
"the levels of the user depending on the points" "the levels of the user depending on the points"
level: Int! level: Level
} }
"represents a single user account" "represents a single user account"
@ -240,7 +246,7 @@ type User implements UserData{
eventCount: Int! eventCount: Int!
"the levels of the user depending on the points" "the levels of the user depending on the points"
level: Int! level: Level
} }
type Profile implements UserData { type Profile implements UserData {
@ -311,7 +317,7 @@ type Profile implements UserData {
points: Int! points: Int!
"the levels of the user depending on the points" "the levels of the user depending on the points"
level: Int! level: Level
"the custom settings for the frontend" "the custom settings for the frontend"
settings: String! settings: String!
@ -571,6 +577,22 @@ type ReportReason {
description: String! description: String!
} }
"A level of a user"
type Level {
"The level id"
id: ID!
"The name of the level"
name: String!
"The number of the level in the ranking"
levelNumber: Int!
"The points required for this level"
points: Int!
}
"represents the type of media" "represents the type of media"
enum MediaType { enum MediaType {
VIDEO VIDEO

Loading…
Cancel
Save