Skip to content

Commit

Permalink
Resolve #21: Implement system for flagging typo/suspect PBs
Browse files Browse the repository at this point in the history
  • Loading branch information
big213 committed May 23, 2021
1 parent 4ce913b commit 9a68fc4
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Knex from "knex";

export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable("personalBest", function (t) {
t.boolean("is_flagged").notNullable().defaultTo(false);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable("personalBest", function (t) {
t.dropColumn("is_flagged");
});
}
1 change: 1 addition & 0 deletions backend/functions/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export async function up(knex: Knex): Promise<void[]> {
table.integer("time_elapsed").nullable();
table.decimal("moves_count").nullable();
table.boolean("is_current").notNullable();
table.boolean("is_flagged").notNullable().defaultTo(false);
table.text("public_comments").nullable();
table.dateTime("created_at").notNullable().defaultTo(knex.fn.now());
table.dateTime("updated_at").nullable();
Expand Down
21 changes: 12 additions & 9 deletions backend/functions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ export type FilterByField<T> = {
/**Enum stored as a separate key value*/ userRole:
| "NORMAL"
| "NONE"
| "ADMIN";
| "ADMIN"
| "MODERATOR";
/**Enum stored as is*/ userPermission:
| "A_A"
| "user_x"
Expand All @@ -98,6 +99,7 @@ export type FilterByField<T> = {
| "user_create"
| "user_delete"
| "personalBest_create"
| "personalBest_flag"
| "product_create"
| "apiKey_create"
| "userUserFollowLink_get";
Expand Down Expand Up @@ -307,6 +309,7 @@ export type FilterByField<T> = {
"personalBestFilterByField/createdBy.userUserFollowLink/user.id": FilterByField<
Scalars["id"]
>;
"personalBestFilterByField/isFlagged": FilterByField<Scalars["boolean"]>;
personalBestFilterByObject: {
id?: InputTypes["personalBestFilterByField/id"];
"createdBy.id"?: InputTypes["personalBestFilterByField/createdBy.id"];
Expand All @@ -318,6 +321,7 @@ export type FilterByField<T> = {
isCurrent?: InputTypes["personalBestFilterByField/isCurrent"];
setSize?: InputTypes["personalBestFilterByField/setSize"];
"createdBy.userUserFollowLink/user.id"?: InputTypes["personalBestFilterByField/createdBy.userUserFollowLink/user.id"];
isFlagged?: InputTypes["personalBestFilterByField/isFlagged"];
};
personalBestPaginator: {
first?: Scalars["number"];
Expand All @@ -343,21 +347,15 @@ export type FilterByField<T> = {
publicComments?: Scalars["string"] | null;
};
updatePersonalBestFields: {
pbClass?: InputTypes["personalBestClass"];
event?: InputTypes["event"];
setSize?: Scalars["number"];
attemptsSucceeded?: Scalars["number"] | null;
attemptsTotal?: Scalars["number"] | null;
product?: InputTypes["product"] | null;
happenedOn?: Scalars["unixTimestamp"];
timeElapsed?: Scalars["number"] | null;
movesCount?: Scalars["number"] | null;
isFlagged?: Scalars["boolean"];
publicComments?: Scalars["string"] | null;
};
updatePersonalBest: {
item: InputTypes["personalBest"];
fields: InputTypes["updatePersonalBestFields"];
};
flagPersonalBest: { item: InputTypes["personalBest"] };
apiKey: { id?: Scalars["id"] };
"apiKeyFilterByField/id": FilterByField<Scalars["id"]>;
"apiKeyFilterByField/user.id": FilterByField<Scalars["id"]>;
Expand Down Expand Up @@ -710,6 +708,7 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
Args: undefined;
};
isCurrent: { Type: Scalars["boolean"]; Args: undefined };
isFlagged: { Type: Scalars["boolean"]; Args: undefined };
publicComments: { Type: Scalars["string"] | null; Args: undefined };
/**The numerical score rank of this PB given its event, pbClass, and setSize, among public PBs only*/ ranking: {
Type: Scalars["number"] | null;
Expand Down Expand Up @@ -808,6 +807,10 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
Type: PersonalBest;
Args: InputTypes["updatePersonalBest"];
};
flagPersonalBest: {
Type: PersonalBest;
Args: InputTypes["flagPersonalBest"];
};
getApiKey: { Type: ApiKey; Args: InputTypes["apiKey"] };
getApiKeyPaginator: {
Type: ApiKeyPaginator;
Expand Down
4 changes: 4 additions & 0 deletions backend/functions/src/schema/enums/userPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export class userPermissionEnum extends Enum {
"personalBest_create"
);

static readonly personalBest_flag = new userPermissionEnum(
"personalBest_flag"
);

static readonly product_create = new userPermissionEnum("product_create");

static readonly apiKey_create = new userPermissionEnum("apiKey_create");
Expand Down
1 change: 1 addition & 0 deletions backend/functions/src/schema/enums/userRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export class userRoleKenum extends Kenum {
static readonly NORMAL = new userRoleKenum("NORMAL", 1);
static readonly NONE = new userRoleKenum("NONE", 2);
static readonly ADMIN = new userRoleKenum("ADMIN", 3);
static readonly MODERATOR = new userRoleKenum("MODERATOR", 4);
}
6 changes: 6 additions & 0 deletions backend/functions/src/schema/helpers/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { userRoleKenum, userPermissionEnum } from "../enums";

export const userRoleToPermissionsMap = {
[userRoleKenum.ADMIN.name]: [userPermissionEnum.A_A],
[userRoleKenum.MODERATOR.name]: [
userPermissionEnum.personalBest_create,
userPermissionEnum.personalBest_flag,
userPermissionEnum.product_create,
userPermissionEnum.userUserFollowLink_get,
],
[userRoleKenum.NORMAL.name]: [
userPermissionEnum.personalBest_create,
userPermissionEnum.product_create,
Expand Down
29 changes: 29 additions & 0 deletions backend/functions/src/schema/models/personalBest/rootResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { PersonalBest } from "../../services";
import { generateBaseRootResolvers } from "../../core/helpers/rootResolver";
import {
GiraffeqlInputFieldType,
GiraffeqlInputType,
GiraffeqlInputTypeLookup,
GiraffeqlRootResolverType,
} from "giraffeql";

export default {
...generateBaseRootResolvers(PersonalBest, [
Expand All @@ -9,4 +15,27 @@ export default {
"create",
"update",
]),
flagPersonalBest: new GiraffeqlRootResolverType({
name: "flagPersonalBest",
restOptions: {
method: "post",
route: "/flagPersonalBest",
},
type: PersonalBest.typeDef,
allowNull: false,
args: new GiraffeqlInputFieldType({
required: true,
type: new GiraffeqlInputType({
name: "flagPersonalBest",
fields: {
item: new GiraffeqlInputFieldType({
type: PersonalBest.inputTypeDefLookup,
required: true,
}),
},
}),
}),
resolver: ({ req, args, query, fieldPath }) =>
PersonalBest.flagRecord({ req, args, query, fieldPath }),
}),
};
45 changes: 45 additions & 0 deletions backend/functions/src/schema/models/personalBest/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class PersonalBestService extends PaginatedService {
isCurrent: {},
setSize: {},
"createdBy.userUserFollowLink/user.id": {},
isFlagged: {},
};

uniqueKeyMap = {
Expand Down Expand Up @@ -489,6 +490,50 @@ export class PersonalBestService extends PaginatedService {
});
}

@permissionsCheck("flag")
async flagRecord({
req,
fieldPath,
args,
query,
data = {},
isAdmin = false,
}: ServiceFunctionInputs) {
// args should be validated already
const validatedArgs = <any>args;

const item = await this.lookupRecord(
[{ field: "id" }],
validatedArgs.item,
fieldPath
);

// convert any lookup/joined fields into IDs
await this.handleLookupArgs(validatedArgs.fields, fieldPath);

await Resolver.updateObjectType({
typename: this.typename,
id: item.id,
updateFields: {
isFlagged: true,
updatedAt: 1,
},
req,
fieldPath,
});

const returnData = await this.getRecord({
req,
args: { id: item.id },
query,
fieldPath,
isAdmin,
data,
});

return returnData;
}

@permissionsCheck("delete")
async deleteRecord({
req,
Expand Down
28 changes: 26 additions & 2 deletions backend/functions/src/schema/models/personalBest/typeDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
sqlOptions: {
field: "pb_class",
},
typeDefOptions: { addable: true, updateable: false },
}),
event: generateJoinableField({
service: Event,
allowNull: false,
typeDefOptions: { addable: true, updateable: false },
}),
setSize: generateIntegerField({
allowNull: false,
sqlOptions: {
field: "set_size",
},
typeDefOptions: { addable: true, updateable: false },
}),
score: generateIntegerField({
allowNull: false,
Expand All @@ -54,11 +57,13 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
allowNull: true,
description: "The number of successful attempts",
sqlOptions: { field: "attempts_succeeded" },
typeDefOptions: { addable: true, updateable: false },
}),
attemptsTotal: generateIntegerField({
allowNull: true,
description: "The total number of attempts",
sqlOptions: { field: "attempts_total" },
typeDefOptions: { addable: true, updateable: false },
}),
product: generateJoinableField({
service: Product,
Expand All @@ -68,22 +73,31 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
allowNull: false,
defaultValue: knex.fn.now(), // not really setting via DB. default is calculated manually in the createRecord function
sqlOptions: { field: "happened_on" },
typeDefOptions: { addable: true, updateable: false },
}),
timeElapsed: generateIntegerField({
allowNull: true,
description: "The amount of ms time elapsed for the pb attempt",
sqlOptions: { field: "time_elapsed" },
typeDefOptions: { addable: true, updateable: false },
}),
movesCount: generateDecimalField({
allowNull: true,
description: "The amount of moves used in the pb attempt",
sqlOptions: { field: "moves_count" },
typeDefOptions: { addable: true, updateable: false },
}),
isCurrent: generateBooleanField({
allowNull: false,
typeDefOptions: { addable: false, updateable: false },
sqlOptions: { field: "is_current" },
}),
isFlagged: generateBooleanField({
allowNull: false,
defaultValue: false,
typeDefOptions: { addable: false, updateable: true },
sqlOptions: { field: "is_flagged" },
}),
publicComments: generateTextField({
allowNull: true,
sqlOptions: {
Expand All @@ -101,11 +115,16 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
"pbClass.id",
"setSize",
"isCurrent",
"isFlagged",
"createdBy.isPublic",
],
async resolver({ parentValue, fieldPath }) {
// if not a current PB or user is not public, return null
if (!parentValue.isCurrent || !parentValue.createdBy.isPublic)
// if not a current PB or user is not public or isFlagged, return null
if (
!parentValue.isCurrent ||
!parentValue.createdBy.isPublic ||
parentValue.isFlagged
)
return null;

const resultsCount = await Resolver.countObjectType(
Expand Down Expand Up @@ -143,6 +162,11 @@ export default new GiraffeqlObjectType(<ObjectTypeDefinition>{
operator: "eq",
value: true,
},
{
field: "isFlagged",
operator: "eq",
value: false,
},
],
},
true
Expand Down
5 changes: 5 additions & 0 deletions frontend/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ export default {
operator: 'eq',
value: 1,
},
{
field: 'isFlagged',
operator: 'eq',
value: false,
},
],
}),
loginRequired: false,
Expand Down
38 changes: 38 additions & 0 deletions frontend/models/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { executeGiraffeql } from '~/services/giraffeql'
import { handleError } from '~/services/base'

export const flagPersonalBest = async (that, item) => {
try {
if (!that.$store.getters['auth/user']) {
throw new Error('Login required')
}

// check for moderator
if (
!['ADMIN', 'MODERATOR'].includes(that.$store.getters['auth/user'].role)
) {
throw new Error('Must be moderator to flag PBs')
}

await executeGiraffeql(that, {
flagPersonalBest: {
__args: {
item: {
id: item.id,
},
},
},
})

that.$notifier.showSnackbar({
message: `Flagged Personal Best`,
variant: 'success',
})

that.reset({
resetExpanded: false,
})
} catch (err) {
handleError(that, err)
}
}
Loading

0 comments on commit 9a68fc4

Please sign in to comment.