Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Филтри за сигналите в преброителния център #60

Merged
merged 8 commits into from
Jun 27, 2021
49 changes: 49 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,54 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
String: {
message: 'Use string instead',
fixWith: 'string',
},
Boolean: {
message: 'Use boolean instead',
fixWith: 'boolean',
},
Number: {
message: 'Use number instead',
fixWith: 'number',
},
Symbol: {
message: 'Use symbol instead',
fixWith: 'symbol',
},

Function: {
message: [
'The `Function` type accepts any function-like value.',
'It provides no type safety when calling the function, which can be a common source of bugs.',
'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.',
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.',
].join('\n'),
},

// object typing
Object: {
message: [
'The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.',
'- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.',
'- If you want a type meaning "any value", you probably want `unknown` instead.',
].join('\n'),
},
'{}': {
message: [
'`{}` actually means "any non-nullish value".',
'- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.',
'- If you want a type meaning "any value", you probably want `unknown` instead.',
].join('\n'),
},
object: false,
},
},
],
},
};
3 changes: 2 additions & 1 deletion src/i18n/bg/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"BROADCAST_PENDING": "В изчакване",
"BROADCAST_PROCESSING": "Обработва се",
"BROADCAST_PUBLISHED": "Публикувано",
"BROADCAST_DISCARDED": "Отхвърлено"
"BROADCAST_DISCARDED": "Отхвърлено",
"OBJECT_ACCEPTED": "Прието"
}
3 changes: 2 additions & 1 deletion src/i18n/en/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"BROADCAST_PENDING": "Pending",
"BROADCAST_PROCESSING": "Processing",
"BROADCAST_PUBLISHED": "Published",
"BROADCAST_DISCARDED": "Discarded"
"BROADCAST_DISCARDED": "Discarded",
"OBJECT_ACCEPTED": "Accepted"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddTownsCountryMunicipalityIndices1624337501383
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create index "towns_country_id_key" on "towns" ("country_id");
create index "towns_municipality_id_key" on "towns" ("country_id");
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
drop index "towns_municipality_id_key";
drop index "towns_country_id_key";
`);
}
}
2 changes: 1 addition & 1 deletion src/sections/api/town-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class IsTownExistsConstraint implements ValidatorConstraintInterface {
}

export function IsTownExists(validationOptions?: ValidationOptions) {
return function (town: TownDto, propertyName: string) {
return function (town: TownDto | object, propertyName: string) {
registerDecorator({
target: town.constructor,
propertyName: propertyName,
Expand Down
2 changes: 1 addition & 1 deletion src/sections/entities/town.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class Town {
readonly id: number;

@Column()
readonly code: number;
code: number;

@Column()
readonly name: string;
Expand Down
1 change: 1 addition & 0 deletions src/sections/sections.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { CityRegionsRepository } from './entities/cityRegions.repository';
MunicipalitiesRepository,
CountriesRepository,
CityRegionsRepository,
TownsRepository,
],
controllers: [
SectionsController,
Expand Down
2 changes: 1 addition & 1 deletion src/users/api/organization-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class IsOrganizationExistsConstraint
}

export function IsOrganizationExists(validationOptions?: ValidationOptions) {
return function (object: OrganizationDto, propertyName: string) {
return function (object: OrganizationDto | object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
Expand Down
6 changes: 3 additions & 3 deletions src/users/api/user-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export class IsUserExistsConstraint implements ValidatorConstraintInterface {
}

defaultMessage?(): string {
return `User with $property "$value" does not exist!`;
return `User with ID "$value" does not exist!`;
}
}

export function IsUserExists(validationOptions?: ValidationOptions) {
return function (user: UserDto, propertyName: string) {
return function (target: UserDto | object, propertyName: string) {
registerDecorator({
target: user.constructor,
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
Expand Down
33 changes: 33 additions & 0 deletions src/utils/ulid-constraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';

@ValidatorConstraint({ async: true })
@Injectable()
export class IsULIDConstraint implements ValidatorConstraintInterface {
async validate(id?: string): Promise<boolean> {
return typeof id === 'string' && /[A-Z0-9]{26}/.test(id);
}

defaultMessage?(context: ValidationArguments): string {
console.debug(context.object);
return `${context.property} identifier is not an ULID`;
}
}

export function IsULID(validationOptions?: ValidationOptions) {
return function (entity: object, propertyName: string) {
registerDecorator({
target: entity.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsULIDConstraint,
});
};
}
4 changes: 3 additions & 1 deletion src/violations/api/violation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export class ViolationDto {
@Expose({ groups: ['read', 'create'] })
@Type(() => TownDto)
@Transform(
({ value: id }) => plainToClass(TownDto, { id }, { groups: ['create'] }),
({ value: id }) =>
plainToClass(TownDto, { code: id }, { groups: ['create'] }),
{ groups: ['create'] },
)
@IsNotEmpty({ groups: ['create'] })
Expand Down Expand Up @@ -111,6 +112,7 @@ export class ViolationDto {
groups: ['create'],
},
);
violation.town.code = this.town.id;

let sortPosition = 1;
violation.pictures = (violation.pictures || []).map(
Expand Down
55 changes: 47 additions & 8 deletions src/violations/api/violations-filters.dto.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import { Optional } from '@nestjs/common';
import { Type } from 'class-transformer';
import {
IsBooleanString,
IsInt,
IsNumberString,
IsOptional,
Length,
} from 'class-validator';
import { IsTownExists } from 'src/sections/api/town-exists.constraint';
import { IsOrganizationExists } from 'src/users/api/organization-exists.constraint';
import { IsUserExists } from 'src/users/api/user-exists.constraint';
import { PageDTO } from 'src/utils/page.dto';
import { IsULID } from 'src/utils/ulid-constraint';
import { ViolationStatus } from '../entities/violation.entity';

export class ViolationsFilters extends PageDTO {
@Optional()
@IsOptional()
@IsULID()
@IsUserExists()
assignee: string;

@Optional()
@IsOptional()
@Length(1, 9)
section: string;

@Optional()
@IsOptional()
status: ViolationStatus;

@Optional()
author: string;
@IsOptional()
@IsNumberString()
@Length(2, 2)
electionRegion: string;

@Optional()
@IsOptional()
@IsNumberString()
@Length(2, 2)
municipality: string;

@IsOptional()
@IsNumberString()
@Length(2, 2)
country: string;

@IsOptional()
@IsTownExists()
town: number;

@Optional()
@IsOptional()
@IsNumberString()
@Length(2, 2)
cityRegion: string;

@IsOptional()
@IsInt()
@Type(() => Number)
@IsOrganizationExists()
organization: number;

@IsOptional()
@IsBooleanString()
published: boolean;
}
2 changes: 1 addition & 1 deletion src/violations/api/violations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ViolationsController {
@Get()
@HttpCode(200)
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: Ability) => ability.can(Action.Read, Violation))
@CheckPolicies((ability: Ability) => ability.can(Action.Manage, Violation))
@UsePipes(new ValidationPipe({ transform: true }))
async index(
@Query() query: ViolationsFilters,
Expand Down
71 changes: 47 additions & 24 deletions src/violations/entities/violations.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { User } from '../../users/entities';
import { Violation } from './violation.entity';
import { ViolationUpdateType } from './violation-update.entity';
import { ViolationsFilters } from '../api/violations-filters.dto';
import { TownsRepository } from 'src/sections/entities/towns.repository';

@Injectable()
export class ViolationsRepository {
constructor(
@InjectRepository(Violation) private readonly repo: Repository<Violation>,
private readonly townsRepo: TownsRepository,
) {}

findOneOrFail(id: string): Promise<Violation> {
Expand All @@ -32,11 +34,14 @@ export class ViolationsRepository {
): SelectQueryBuilder<Violation> {
const qb = this.repo.createQueryBuilder('violation');

qb.leftJoinAndSelect('violation.section', 'section');
qb.innerJoinAndSelect('violation.town', 'town');
qb.innerJoinAndSelect('violation.updates', 'update');
qb.innerJoinAndSelect('update.actor', 'actor');
qb.innerJoinAndSelect('actor.organization', 'organization');
qb.innerJoinAndSelect('violation.updates', 'update_send');
qb.andWhere('update_send.type = :update', {
update: ViolationUpdateType.SEND,
});
qb.innerJoinAndSelect('update_send.actor', 'sender');
qb.innerJoinAndSelect('sender.organization', 'organization');
qb.innerJoinAndSelect('violation.updates', 'updates');
qb.leftJoinAndSelect('violation.pictures', 'picture');

if (filters.assignee) {
Expand All @@ -47,44 +52,62 @@ export class ViolationsRepository {
}

if (filters.section) {
qb.innerJoinAndSelect('violation.section', 'section');
qb.andWhere('section.id LIKE :section', {
section: `${filters.section}%`,
});
} else {
qb.leftJoinAndSelect('violation.section', 'section');
}

if (filters.status) {
qb.andWhere('violation.status = :status', { status: filters.status });
}

if (filters.town) {
qb.andWhere('town.code = :town', { town: filters.town });
}

if (filters.author || filters.organization) {
qb.innerJoin('violation.updates', 'update_send');
qb.andWhere('update_send.type = :update', {
update: ViolationUpdateType.SEND,
if (filters.electionRegion) {
qb.innerJoin('town.municipality', 'municipality');
qb.innerJoin('municipality.electionRegions', 'electionRegions');
qb.andWhere('electionRegions.code = :electionRegion', {
electionRegion: filters.electionRegion,
});

if (filters.author) {
qb.andWhere('update_send.actor_id = :author', {
author: filters.author,
if (filters.municipality) {
qb.andWhere('municipality.code = :municipality', {
municipality: filters.municipality,
});
}

if (filters.organization) {
qb.innerJoin('update_send.actor', 'sender');
qb.innerJoin('sender.organization', 'organization');
qb.andWhere('organization.id = :organization', {
organization: filters.organization,
if (filters.country) {
qb.innerJoin('town.country', 'country');
qb.andWhere('country.code = :country', { country: filters.country });
}

if (filters.town) {
qb.andWhere('town.code = :town', {
town: filters.town,
});
}
}

if (filters.status) {
qb.andWhere('violation.status = :status', { status: filters.status });
}

if (filters.published) {
qb.andWhere('violation.isPublished = :published', {
published: filters.published,
});
}

if (filters.organization) {
qb.andWhere('organization.id = :organization', {
organization: filters.organization,
});
}

return qb;
}

async save(violation: Violation): Promise<Violation> {
if (violation.town && !violation.town.id && violation.town.code) {
violation.town = await this.townsRepo.findOneByCode(violation.town.code);
}
await this.repo.save(violation);

return this.findOneOrFail(violation.id);
Expand Down
Loading