Add course lists to subjects with add/edit option

Added count() method for hasManyThrough relations in a slightly hacky way
This commit is contained in:
Norbi Peti 2022-03-10 19:23:18 +01:00
parent 2a7aa2a65a
commit 9f78639690
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
28 changed files with 350 additions and 80 deletions

View file

@ -0,0 +1,134 @@
import { Count, CountSchema, Filter, FilterExcludingWhere, repository, Where, } from '@loopback/repository';
import { del, get, getModelSchemaRef, param, patch, post, put, requestBody, response, } from '@loopback/rest';
import { Course } from '../models';
import { CourseRepository } from '../repositories';
export class CourseController {
constructor(
@repository(CourseRepository)
public courseRepository: CourseRepository,
) {
}
@post('/courses')
@response(200, {
description: 'Course model instance',
content: {'application/json': {schema: getModelSchemaRef(Course)}},
})
async create(
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Course, {
title: 'NewCourse',
exclude: ['id'],
}),
},
},
})
course: Omit<Course, 'id'>,
): Promise<Course> {
return this.courseRepository.create(course);
}
@get('/courses/count')
@response(200, {
description: 'Course model count',
content: {'application/json': {schema: CountSchema}},
})
async count(
@param.where(Course) where?: Where<Course>,
): Promise<Count> {
return this.courseRepository.count(where);
}
@get('/courses')
@response(200, {
description: 'Array of Course model instances',
content: {
'application/json': {
schema: {
type: 'array',
items: getModelSchemaRef(Course, {includeRelations: true}),
},
},
},
})
async find(
@param.filter(Course) filter?: Filter<Course>,
): Promise<Course[]> {
return this.courseRepository.find(filter);
}
@patch('/courses')
@response(200, {
description: 'Course PATCH success count',
content: {'application/json': {schema: CountSchema}},
})
async updateAll(
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Course, {partial: true}),
},
},
})
course: Course,
@param.where(Course) where?: Where<Course>,
): Promise<Count> {
return this.courseRepository.updateAll(course, where);
}
@get('/courses/{id}')
@response(200, {
description: 'Course model instance',
content: {
'application/json': {
schema: getModelSchemaRef(Course, {includeRelations: true}),
},
},
})
async findById(
@param.path.number('id') id: number,
@param.filter(Course, {exclude: 'where'}) filter?: FilterExcludingWhere<Course>
): Promise<Course> {
return this.courseRepository.findById(id, filter);
}
@patch('/courses/{id}')
@response(204, {
description: 'Course PATCH success',
})
async updateById(
@param.path.number('id') id: number,
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Course, {partial: true}),
},
},
})
course: Course,
): Promise<void> {
await this.courseRepository.updateById(id, course);
}
@put('/courses/{id}')
@response(204, {
description: 'Course PUT success',
})
async replaceById(
@param.path.number('id') id: number,
@requestBody() course: Course,
): Promise<void> {
await this.courseRepository.replaceById(id, course);
}
@del('/courses/{id}')
@response(204, {
description: 'Course DELETE success',
})
async deleteById(@param.path.number('id') id: number): Promise<void> {
await this.courseRepository.deleteById(id);
}
}

View file

@ -7,3 +7,4 @@ export * from './user-course.controller';
export * from './course-requirement.controller';
export * from './course-requirement.controller';
export * from './subject.controller';
export * from './course.controller';

View file

@ -1,24 +1,6 @@
import {
Count,
CountSchema,
Filter,
repository,
Where,
} from '@loopback/repository';
import {
del,
get,
getModelSchemaRef,
getWhereSchemaFor,
param,
patch,
post,
requestBody,
} from '@loopback/rest';
import {
Subject,
Course,
} from '../models';
import { Count, CountSchema, Filter, repository, Where, } from '@loopback/repository';
import { del, get, getModelSchemaRef, getWhereSchemaFor, param, patch, post, requestBody, response, } from '@loopback/rest';
import { Course, Subject, } from '../models';
import { SubjectRepository } from '../repositories';
export class SubjectCourseController {
@ -46,6 +28,18 @@ export class SubjectCourseController {
return this.subjectRepository.courses(id).find(filter);
}
@get('/subjects/{id}/courses/count')
@response(200, {
description: 'Course model count',
content: {'application/json': {schema: CountSchema}},
})
async count(
@param.path.number('id') id: number,
@param.where(Course) where?: Where<Course>,
): Promise<Count> {
return this.subjectRepository.courses(id).count(where);
}
@post('/subjects/{id}/courses', {
responses: {
'200': {

View file

@ -0,0 +1,29 @@
import { constrainWhere, Count, DefaultHasManyRepository, Entity, EntityCrudRepository, Where } from '@loopback/repository';
import { Options } from '@loopback/repository/src/common-types';
import { HasManyRepository } from '@loopback/repository/src/relations/has-many/has-many.repository';
import { InclusionResolver } from '@loopback/repository/src/relations/relation.types';
export interface CustomHasManyRepositoryFactory<Target extends Entity,
ForeignKeyType,
> {
/**
* Invoke the function to obtain HasManyRepository.
*/
(fkValue: ForeignKeyType): CustomHasManyRepository<Target>;
/**
* Use `resolver` property to obtain an InclusionResolver for this relation.
*/
inclusionResolver: InclusionResolver<Entity, Target>;
}
export interface CustomHasManyRepository<Target extends Entity> extends HasManyRepository<Target> {
count(where?: Where<Target>, options?: Options): Promise<Count>;
}
export class DefaultCustomHasManyRepository<TEntity extends Entity, TID, TRepository extends EntityCrudRepository<TEntity, TID>> extends DefaultHasManyRepository<TEntity, TID, TRepository> implements CustomHasManyRepository<TEntity> {
async count(where?: Where<TEntity>, options?: Options) {
const targetRepository = await this.getTargetRepository();
return targetRepository.count(constrainWhere(where, this.constraint), options);
}
}

View file

@ -1,21 +1,27 @@
import { inject, Getter } from '@loopback/core';
import { DefaultCrudRepository, repository, HasManyRepositoryFactory } from '@loopback/repository';
import { Getter, inject } from '@loopback/core';
import { DataObject, DefaultCrudRepository, repository } from '@loopback/repository';
import { DatabaseDataSource } from '../datasources';
import { Subject, SubjectRelations, Course } from '../models';
import { Course, Subject, SubjectRelations } from '../models';
import { UserRepository } from './user.repository';
import { CourseRepository } from './course.repository';
import { CustomHasManyRepositoryFactory, DefaultCustomHasManyRepository } from './CustomHasManyRepository';
export class SubjectRepository extends DefaultCrudRepository<Subject,
typeof Subject.prototype.id,
SubjectRelations> {
public readonly courses: HasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
public readonly courses: CustomHasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
constructor(
@inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('UserRepository') protected userRepositoryGetter: Getter<UserRepository>, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter<CourseRepository>,
) {
super(Subject, dataSource);
this.courses = this.createHasManyRepositoryFactoryFor('courses', courseRepositoryGetter,);
this.registerInclusionResolver('courses', this.courses.inclusionResolver);
const origCourses = this.createHasManyRepositoryFactoryFor('courses', courseRepositoryGetter,);
this.registerInclusionResolver('courses', origCourses.inclusionResolver);
const courses = function(fkValue: number | undefined) {
return new DefaultCustomHasManyRepository(courseRepositoryGetter, {subject_id: fkValue} as DataObject<Course>);
};
courses.inclusionResolver = origCourses.inclusionResolver;
this.courses = courses;
}
}

View file

@ -59,11 +59,10 @@ export class SzakdolgozatUserService implements UserService<User, Credentials> {
//function to find user by id
async findUserById(id: number): Promise<User & UserWithRelations> {
const userNotfound = 'invalid user';
const foundUser = await this.userRepository.findById(id);
if (!foundUser) {
throw new HttpErrors.Unauthorized(userNotfound);
throw new HttpErrors.Unauthorized('invalid user');
}
return foundUser;
}

View file

@ -1,14 +1,24 @@
# Szakdolgozat
Egy webalkalmazás, amely nyomonköveti egy-egy kurzus követelményeinek teljesitését oktatók és hallgatók számára.
## Adatok
### Kurzus
Egy kurzus egy adott tárgy egy adott félévben egy adott csoporttal.
## Szerepkörök
Csak bejelentkezett felhasználók férhetnek hozzá bármilyen adathoz. A saját adataikat mindig tudják módositani.
### Admin
* Teljes jogosultsága van az adatokhoz, kivéve a felhasználók adatait
* Hozzá tud rendelni más felhasználókat szerepkörökhöz egy-egy kurzus kapcsán
### Hallgató
* Az adott kurzushoz tartozó adatokat csak megtekinteni tudja
### Oktató

View file

@ -1,8 +1,4 @@
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"hosting": {
"public": "dist/Szakdolgozat",
"ignore": [

View file

@ -1,4 +0,0 @@
{
"indexes": [],
"fieldOverrides": []
}

View file

@ -1,29 +0,0 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function sameUser(user) {
return request.auth != null && request.auth.uid == user;
}
function getUserData() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
}
//Felhasználói adatok kezelése
match /users/{user} {
allow read, write: if sameUser(user) && request.auth.uid == request.resource.data.author_uid;
}
//Adminisztrátoroknak mindent lehet
match /data/{document=**} {
allow get, list, create, update, delete: if auth.token.admin;
}
//Diákok megnézhetik a tárgy adatait
match /data/subjects/{subject=**} {
allow get, list: if request.auth.uid in resource.data.students;
}
//Az oktatók módosithatják a követelményeket
match /data/subjects/{subject}/requirements/{requirement=**} {
allow read, write: if request.auth.uid in resource.data.teachers;
}
}
}

View file

@ -0,0 +1,6 @@
import { Model } from './model';
export class Course extends Model {
semester: string;
subjectId: number;
}

View file

@ -18,6 +18,10 @@ export class EditComponent<T extends Model> implements OnInit {
@Input() apiPath: string;
@Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[];
@Input() itemType: Type<T>;
/**
* Beküldés előtt extra adat hozzáadása
*/
@Input() beforeSubmit: (item: T) => Partial<T>;
formGroup: FormGroup;
constructor(private api: ApiService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) {
@ -48,13 +52,14 @@ export class EditComponent<T extends Model> implements OnInit {
async submit(): Promise<void> {
this.isLoading = true;
const value = Object.assign({}, this.formGroup.value, this.beforeSubmit(this.item) ?? {});
try {
if (this.item && !this.creating) {
await this.api.request('patch', this.apiPath + '/' + this.item.id, this.formGroup.value);
await this.api.request('patch', this.apiPath + '/' + this.item.id, value);
} else {
await this.api.request('post', this.apiPath, this.formGroup.value);
await this.api.request('post', this.apiPath, value);
}
await this.router.navigate(this.route.parent.snapshot.url.map(segment => segment.path));
await this.router.navigate(['..'], {relativeTo: this.route});
} catch (e) {
alert(e.message);
}

View file

@ -2,5 +2,6 @@
<button mat-raised-button color="primary" (click)="newItem()">Hozzáadás</button>
</div>
<app-table [items]="items" (pageChange)="handlePageChange($event)" [paginationData]="paginationData" [columns]="columns"
[loading]="loading" (editItem)="editItem($event)">
[loading]="loading" (editItem)="editItem($event)" [customActions]="customActions"
[allowEditing]="allowEditing">
</app-table>

View file

@ -16,6 +16,8 @@ export class ListComponent<T extends Model> implements OnInit {
@Input() itemType: Type<T>;
@Input() columns: { title: string, prop: keyof T }[];
@Input() allowNew = false;
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
@Input() allowEditing = true;
paginationData: PaginationData = {};
items: T[] = [];

View file

@ -12,6 +12,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
@ -31,7 +32,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
MatProgressSpinnerModule,
ReactiveFormsModule,
MatInputModule,
MatCheckboxModule
MatCheckboxModule,
MatTooltipModule
]
})
export class SharedComponentsModule {

View file

@ -18,15 +18,18 @@
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef style="width: 2rem"></th>
<th mat-header-cell *matHeaderCellDef [style.width]="2.5 * (1 + customActions.length) + 'rem'"></th>
<td mat-cell *matCellDef="let item">
<button mat-icon-button color="primary" (click)="editItem.emit(item)">
<button *ngIf="allowEditing" mat-icon-button color="primary" (click)="editItem.emit(item)">
<mat-icon>edit</mat-icon>
</button>
<button *ngFor="let action of customActions" mat-icon-button color="primary" (click)="action.action(item)">
<mat-icon [matTooltip]="action.label">{{ action.icon }}</mat-icon>
</button>
</td>
<tr mat-header-row *matHeaderRowDef="getPropNames()"></tr>
<tr mat-row *matRowDef="let row; columns: getPropNames()"></tr>
</ng-container>
<tr mat-header-row *matHeaderRowDef="getPropNames()"></tr>
<tr mat-row *matRowDef="let row; columns: getPropNames()"></tr>
</table>
</div>
<mat-paginator

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PaginationData } from '../../utility/pagination-data';
import { PageEvent } from '@angular/material/paginator';
@ -14,6 +14,8 @@ export class TableComponent<T> implements OnInit {
@Input() loading = false;
@Input() columns: { title: string, prop: string }[] = [];
@Input() paginationData: PaginationData = {page: 1, limit: 10};
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
@Input() allowEditing = true;
@Output() pageChange = new EventEmitter<PageEvent>();
@Output() editItem = new EventEmitter<T>();

View file

@ -0,0 +1,3 @@
<app-edit #edit [itemType]="itemType" apiPath="/courses" [beforeSubmit]="beforeSubmit" [fields]="[
{title: 'Félév', name: 'semester'}
]"></app-edit>

View file

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CourseEditComponent } from './course-edit.component';
describe('CourseEditComponent', () => {
let component: CourseEditComponent;
let fixture: ComponentFixture<CourseEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CourseEditComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CourseEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { Course } from '../../../../model/course.model';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-course-edit',
templateUrl: './course-edit.component.html',
styleUrls: ['./course-edit.component.css']
})
export class CourseEditComponent implements OnInit {
itemType = Course;
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
}
}

View file

@ -0,0 +1,3 @@
<app-list [apiPath]="'/subjects/'+subjectId+'/courses'" [itemType]="itemType" allowNew="true" [columns]="[
{prop: 'semester', title: 'Félév'}
]"></app-list>

View file

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CourseListComponent } from './course-list.component';
describe('CoursesComponent', () => {
let component: CourseListComponent;
let fixture: ComponentFixture<CourseListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CourseListComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CourseListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Course } from '../../../../model/course.model';
@Component({
selector: 'app-courses',
templateUrl: './course-list.component.html',
styleUrls: ['./course-list.component.css']
})
export class CourseListComponent implements OnInit {
subjectId: string;
itemType = Course;
constructor(route: ActivatedRoute) {
this.subjectId = route.snapshot.params.subjectId;
}
ngOnInit(): void {
}
}

View file

@ -1,4 +1,6 @@
<app-list apiPath="/subjects" [itemType]="itemType" allowNew="true" [columns]="[
{title: 'Név', prop: 'name'},
{title: 'Leirás', prop: 'description'}
]"></app-list>
]" [customActions]="[
{icon: 'chevron_right', label: 'Kurzusok', action: listCourses}
]"></app-list>

View file

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Subject } from '../../model/subject.model';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-subject-list',
@ -9,9 +10,14 @@ import { Subject } from '../../model/subject.model';
export class SubjectListComponent implements OnInit {
itemType = Subject;
constructor() { }
constructor(private route: ActivatedRoute, private router: Router) {
}
ngOnInit(): void {
}
listCourses = (subject: Subject): void => {
this.router.navigate([subject.id, 'courses'], {relativeTo: this.route});
};
}

View file

@ -5,14 +5,22 @@ import { SubjectEditComponent } from './subject-edit/subject-edit.component';
import { SharedComponentsModule } from '../shared-components/shared-components.module';
import { RouterModule, Routes } from '@angular/router';
import { RouteData } from '../app-routing.module';
import { CourseListComponent } from './subject-edit/courses/course-list/course-list.component';
import { CourseEditComponent } from './subject-edit/courses/course-edit/course-edit.component';
const routes: Routes = [
{path: '', component: SubjectListComponent, data: {title: 'Tárgyak'} as RouteData},
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}}
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}},
{
path: ':subjectId/courses', children: [
{path: ':id', component: CourseEditComponent, data: {title: 'Szerkesztés'} as RouteData},
{path: '', component: CourseListComponent, data: {title: 'Kurzusok'}}
]
}
];
@NgModule({
declarations: [SubjectListComponent, SubjectEditComponent],
declarations: [SubjectListComponent, SubjectEditComponent, CourseListComponent, CourseEditComponent],
imports: [
CommonModule,
SharedComponentsModule,