diff --git a/backend/src/controllers/course.controller.ts b/backend/src/controllers/course.controller.ts new file mode 100644 index 0000000..35ef8b1 --- /dev/null +++ b/backend/src/controllers/course.controller.ts @@ -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, + ): Promise { + 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, + ): Promise { + 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, + ): Promise { + 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, + ): Promise { + 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 + ): Promise { + 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 { + 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 { + await this.courseRepository.replaceById(id, course); + } + + @del('/courses/{id}') + @response(204, { + description: 'Course DELETE success', + }) + async deleteById(@param.path.number('id') id: number): Promise { + await this.courseRepository.deleteById(id); + } +} diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index 05dbaf4..a84f1ce 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -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'; diff --git a/backend/src/controllers/subject-course.controller.ts b/backend/src/controllers/subject-course.controller.ts index fa97d4a..15a1e09 100644 --- a/backend/src/controllers/subject-course.controller.ts +++ b/backend/src/controllers/subject-course.controller.ts @@ -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, + ): Promise { + return this.subjectRepository.courses(id).count(where); + } + @post('/subjects/{id}/courses', { responses: { '200': { diff --git a/backend/src/repositories/CustomHasManyRepository.ts b/backend/src/repositories/CustomHasManyRepository.ts new file mode 100644 index 0000000..d35cff9 --- /dev/null +++ b/backend/src/repositories/CustomHasManyRepository.ts @@ -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 { + /** + * Invoke the function to obtain HasManyRepository. + */ + (fkValue: ForeignKeyType): CustomHasManyRepository; + + /** + * Use `resolver` property to obtain an InclusionResolver for this relation. + */ + inclusionResolver: InclusionResolver; +} + +export interface CustomHasManyRepository extends HasManyRepository { + count(where?: Where, options?: Options): Promise; +} + +export class DefaultCustomHasManyRepository> extends DefaultHasManyRepository implements CustomHasManyRepository { + async count(where?: Where, options?: Options) { + const targetRepository = await this.getTargetRepository(); + return targetRepository.count(constrainWhere(where, this.constraint), options); + } +} diff --git a/backend/src/repositories/subject.repository.ts b/backend/src/repositories/subject.repository.ts index 317109d..61fa41d 100644 --- a/backend/src/repositories/subject.repository.ts +++ b/backend/src/repositories/subject.repository.ts @@ -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 { - public readonly courses: HasManyRepositoryFactory; + public readonly courses: CustomHasManyRepositoryFactory; constructor( @inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('UserRepository') protected userRepositoryGetter: Getter, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter, ) { 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); + }; + courses.inclusionResolver = origCourses.inclusionResolver; + this.courses = courses; } } diff --git a/backend/src/services/user.service.ts b/backend/src/services/user.service.ts index 3a0883d..2333d8d 100644 --- a/backend/src/services/user.service.ts +++ b/backend/src/services/user.service.ts @@ -59,11 +59,10 @@ export class SzakdolgozatUserService implements UserService { //function to find user by id async findUserById(id: number): Promise { - 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; } diff --git a/frontend/README.md b/frontend/README.md index 880ba02..461a649 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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ó diff --git a/frontend/firebase.json b/frontend/firebase.json index 587a30f..095ea70 100644 --- a/frontend/firebase.json +++ b/frontend/firebase.json @@ -1,8 +1,4 @@ { - "firestore": { - "rules": "firestore.rules", - "indexes": "firestore.indexes.json" - }, "hosting": { "public": "dist/Szakdolgozat", "ignore": [ diff --git a/frontend/firestore.indexes.json b/frontend/firestore.indexes.json deleted file mode 100644 index 415027e..0000000 --- a/frontend/firestore.indexes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "indexes": [], - "fieldOverrides": [] -} diff --git a/frontend/firestore.rules b/frontend/firestore.rules deleted file mode 100644 index ec1f393..0000000 --- a/frontend/firestore.rules +++ /dev/null @@ -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; - } - } -} diff --git a/frontend/src/app/model/course.model.ts b/frontend/src/app/model/course.model.ts new file mode 100644 index 0000000..269cf15 --- /dev/null +++ b/frontend/src/app/model/course.model.ts @@ -0,0 +1,6 @@ +import { Model } from './model'; + +export class Course extends Model { + semester: string; + subjectId: number; +} diff --git a/frontend/src/app/shared-components/edit/edit.component.ts b/frontend/src/app/shared-components/edit/edit.component.ts index e529399..594fee0 100644 --- a/frontend/src/app/shared-components/edit/edit.component.ts +++ b/frontend/src/app/shared-components/edit/edit.component.ts @@ -18,6 +18,10 @@ export class EditComponent implements OnInit { @Input() apiPath: string; @Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[]; @Input() itemType: Type; + /** + * Beküldés előtt extra adat hozzáadása + */ + @Input() beforeSubmit: (item: T) => Partial; formGroup: FormGroup; constructor(private api: ApiService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) { @@ -48,13 +52,14 @@ export class EditComponent implements OnInit { async submit(): Promise { 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); } diff --git a/frontend/src/app/shared-components/list/list.component.html b/frontend/src/app/shared-components/list/list.component.html index c7b6146..625c728 100644 --- a/frontend/src/app/shared-components/list/list.component.html +++ b/frontend/src/app/shared-components/list/list.component.html @@ -2,5 +2,6 @@ + [loading]="loading" (editItem)="editItem($event)" [customActions]="customActions" + [allowEditing]="allowEditing"> diff --git a/frontend/src/app/shared-components/list/list.component.ts b/frontend/src/app/shared-components/list/list.component.ts index 5ee2bcf..b5b1803 100644 --- a/frontend/src/app/shared-components/list/list.component.ts +++ b/frontend/src/app/shared-components/list/list.component.ts @@ -16,6 +16,8 @@ export class ListComponent implements OnInit { @Input() itemType: Type; @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[] = []; diff --git a/frontend/src/app/shared-components/shared-components.module.ts b/frontend/src/app/shared-components/shared-components.module.ts index 96c78cb..e8bbde9 100644 --- a/frontend/src/app/shared-components/shared-components.module.ts +++ b/frontend/src/app/shared-components/shared-components.module.ts @@ -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 { diff --git a/frontend/src/app/shared-components/table/table.component.html b/frontend/src/app/shared-components/table/table.component.html index 1d760c5..dd2b7a4 100644 --- a/frontend/src/app/shared-components/table/table.component.html +++ b/frontend/src/app/shared-components/table/table.component.html @@ -18,15 +18,18 @@ - + - + - - + + 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(); @Output() editItem = new EventEmitter(); diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.css b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html new file mode 100644 index 0000000..b508328 --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.html @@ -0,0 +1,3 @@ + diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.spec.ts b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.spec.ts new file mode 100644 index 0000000..0295601 --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CourseEditComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CourseEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts new file mode 100644 index 0000000..4349875 --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-edit/course-edit.component.ts @@ -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 { + } + +} diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.css b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html new file mode 100644 index 0000000..31f7da6 --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.html @@ -0,0 +1,3 @@ + diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.spec.ts b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.spec.ts new file mode 100644 index 0000000..f70aa3c --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CourseListComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CourseListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts new file mode 100644 index 0000000..cd620a1 --- /dev/null +++ b/frontend/src/app/subjects/subject-edit/courses/course-list/course-list.component.ts @@ -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 { + } + +} diff --git a/frontend/src/app/subjects/subject-list/subject-list.component.html b/frontend/src/app/subjects/subject-list/subject-list.component.html index b09c1cf..2695271 100644 --- a/frontend/src/app/subjects/subject-list/subject-list.component.html +++ b/frontend/src/app/subjects/subject-list/subject-list.component.html @@ -1,4 +1,6 @@ + ]" [customActions]="[ + {icon: 'chevron_right', label: 'Kurzusok', action: listCourses} +]"> diff --git a/frontend/src/app/subjects/subject-list/subject-list.component.ts b/frontend/src/app/subjects/subject-list/subject-list.component.ts index 19df4c6..101af80 100644 --- a/frontend/src/app/subjects/subject-list/subject-list.component.ts +++ b/frontend/src/app/subjects/subject-list/subject-list.component.ts @@ -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}); + }; + } diff --git a/frontend/src/app/subjects/subjects.module.ts b/frontend/src/app/subjects/subjects.module.ts index 7d3afc4..13e51ec 100644 --- a/frontend/src/app/subjects/subjects.module.ts +++ b/frontend/src/app/subjects/subjects.module.ts @@ -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,