diff --git a/LICENSE.txt b/LICENSE.txt index 1aeea9182e5a9e154f59375feafe22065e15f843..70887f54eb6c3f76d406273298ae06a77a510956 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -40,6 +40,7 @@ This software includes third party software components: - https://github.com/h2database/h2database - https://github.com/ben-manes/caffeine - https://github.com/MarcGiffing/bucket4j-spring-boot-starter +- https://github.com/commonmark/commonmark-java - https://github.com/angular/angular - https://github.com/angular-slider/ngx-slider @@ -67,6 +68,6 @@ This software includes third party software components: - https://github.com/mgechev/codelyzer - https://github.com/jasmine/jasmine - https://github.com/karma-runner/karma -- https://github.com/commonmark/commonmark-java +- https://github.com/angular-split/angular-split The licenses are included in the licenses folder. diff --git a/licenses/angular-split.txt b/licenses/angular-split.txt new file mode 100644 index 0000000000000000000000000000000000000000..b80e9973d4e8ff2f4cb7c0d4a07db0d30635d80e --- /dev/null +++ b/licenses/angular-split.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Bertrand Gaillard + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/subato-web/package-lock.json b/subato-web/package-lock.json index 2b5b3fa737e92b39d0e3fd48efe16e52351bed10..b72a0072435047601b28f9f6f02dd3d69d3386b0 100644 --- a/subato-web/package-lock.json +++ b/subato-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "subato-web", - "version": "2.0.0-beta1", + "version": "2.0.0-beta2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "subato-web", - "version": "2.0.0-beta1", + "version": "2.0.0-beta2", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -29,6 +29,7 @@ "@ngneat/cashew": "^3.0.0", "@tinymce/tinymce-angular": "^7.0.0", "angular-pipes": "^10.0.0", + "angular-split": "^14.1.0", "bootstrap": "4.6.1", "codemirror": "^5.65.2", "core-js": "2.5.1", @@ -4584,6 +4585,19 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/angular-split": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/angular-split/-/angular-split-14.1.0.tgz", + "integrity": "sha512-WJ7LUROpvuEy9r7/EHBO2TwrJXrHeB8PUeAWmM5gU1lCmfttAXcCGSCMMIuiCs/QNSsq8mR8rTMCvNs4Xeihfg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0", + "rxjs": ">=6.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -24123,6 +24137,14 @@ } } }, + "angular-split": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/angular-split/-/angular-split-14.1.0.tgz", + "integrity": "sha512-WJ7LUROpvuEy9r7/EHBO2TwrJXrHeB8PUeAWmM5gU1lCmfttAXcCGSCMMIuiCs/QNSsq8mR8rTMCvNs4Xeihfg==", + "requires": { + "tslib": "^2.0.0" + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/subato-web/package.json b/subato-web/package.json index 88c2e81f49d3d1e515812932b1047db0fc9c5172..64f75b73f0f2a6d6d627e9abf5a756c557ae83e8 100755 --- a/subato-web/package.json +++ b/subato-web/package.json @@ -1,6 +1,6 @@ { "name": "subato-web", - "version": "2.0.0-beta2", + "version": "2.0.0-beta3", "license": "MIT", "scripts": { "ng": "ng", @@ -43,6 +43,7 @@ "@ngneat/cashew": "^3.0.0", "@tinymce/tinymce-angular": "^7.0.0", "angular-pipes": "^10.0.0", + "angular-split": "^14.1.0", "bootstrap": "4.6.1", "codemirror": "^5.65.2", "core-js": "2.5.1", diff --git a/subato-web/src/app/app.component.ts b/subato-web/src/app/app.component.ts index 41b6ffdf5203307c08e918f8ab1077fda07bc8aa..8f75420564e34c3f820cd8cf35aaa2cbcf853684 100755 --- a/subato-web/src/app/app.component.ts +++ b/subato-web/src/app/app.component.ts @@ -66,9 +66,9 @@ export class AppComponent extends BaseComponent { } initApp(url: string): void { - combineLatest(this.authService.loadPrincipal(), this.appStateService.loadTerm()) + combineLatest(this.authService.loadPrincipal(), this.appStateService.loadTerm(), this.appStateService.loadConfig()) .pipe(takeUntil(this.unsubscribe)) - .subscribe(([principal, term]) => { + .subscribe(([principal, term, config]) => { this.appStateService.setInit(true); if (principal != null && this.appStateService.currentUrl == '/') { this.router.navigate(["/dashboard"]); diff --git a/subato-web/src/app/core/models/public-config.ts b/subato-web/src/app/core/models/public-config.ts new file mode 100644 index 0000000000000000000000000000000000000000..269a89b63da3ea2413593ad55acf63521576fa73 --- /dev/null +++ b/subato-web/src/app/core/models/public-config.ts @@ -0,0 +1,7 @@ +export class PublicConfig { + maxUploadSizeInMb: number; + + constructor(dto: any) { + this.maxUploadSizeInMb = dto.maxUploadSizeInMb; + } +} \ No newline at end of file diff --git a/subato-web/src/app/core/services/app.state.service.ts b/subato-web/src/app/core/services/app.state.service.ts index c5d4cb2ad88c2cf0b47cd0cf7946f383d2dc3ec6..0b9cc870f9fdb4006e37c253a2453c1b6ad06a96 100755 --- a/subato-web/src/app/core/services/app.state.service.ts +++ b/subato-web/src/app/core/services/app.state.service.ts @@ -10,7 +10,9 @@ import { Observable } from "rxjs/Observable"; import { filter, flatMap, map, take } from "rxjs/operators"; import { SUBATO_THEME } from "../core.module"; import { SubatoPrincipal } from "../models/principal/subato-principal"; +import { PublicConfig } from "../models/public-config"; import { AuthService } from "./auth.service"; +import { ConfigService } from "./config.service"; @Injectable({ @@ -40,11 +42,14 @@ export class AppStateService { courseId$: Observable<number> courseId: number + config: PublicConfig; + constructor( private router: Router, private termService: TermService, private authService: AuthService, private themeService: NbThemeService, + private configService: ConfigService, private notificationService: NotificationService ) { this._init = new BehaviorSubject<boolean>(false); @@ -115,6 +120,16 @@ export class AppStateService { return this.currentTerm; } + public loadConfig(): Observable<PublicConfig> { + return this.configService.getConfig() + .pipe( + map(config => { + this.config = config; + return config; + }) + ); + } + public loadTerm(): Observable<Term> { return this.termService.getAll() .pipe( diff --git a/subato-web/src/app/core/services/config.service.ts b/subato-web/src/app/core/services/config.service.ts new file mode 100755 index 0000000000000000000000000000000000000000..02581395146382d0fccb3acb0820e8e10f668b80 --- /dev/null +++ b/subato-web/src/app/core/services/config.service.ts @@ -0,0 +1,31 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { NbToastrService } from '@nebular/theme'; +import { HttpCacheManager, withCache } from '@ngneat/cashew'; +import { BackendError } from 'app/shared/models/backend-error'; +import { BaseService } from 'app/shared/services/base.service'; +import { environment } from 'environments/environment'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { PublicConfig } from '../models/public-config'; + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService extends BaseService { + + constructor(private http: HttpClient, private cacheManager: HttpCacheManager) { + super(); + } + + getConfig(): Observable<PublicConfig> { + return this.http.get<PublicConfig>(`${environment.backendUrl}/config`, { + context: withCache() + }).pipe( + catchError(e => { + throw BackendError.parse(e); + }), + map(conf => new PublicConfig(conf)) + ); + } +} \ No newline at end of file diff --git a/subato-web/src/app/core/styles/styles.scss b/subato-web/src/app/core/styles/styles.scss index 06588fc4415d301b95d4163c6d625b41c1830c7a..600b0ddf3c4ce25aa9e00e4ad46c2eae1b6032ef 100755 --- a/subato-web/src/app/core/styles/styles.scss +++ b/subato-web/src/app/core/styles/styles.scss @@ -111,6 +111,10 @@ } } + nb-accordion-item-body.no-padding .item-body { + padding: 0px !important; + } + table { tr.hover { &:hover { @@ -227,7 +231,7 @@ @supports (position: sticky) { position: sticky; top: 0; - z-index: 1020; + z-index: 991; } max-height: calc(100vh - 105px) !important; @@ -240,7 +244,7 @@ @supports (position: sticky) { position: sticky; top: 0; - z-index: 1020; + z-index: 991; } max-height: calc(100vh - 105px) !important; @@ -251,6 +255,7 @@ .sticky-top { max-height: calc(100vh - 105px) !important; top: 90px !important; + z-index: 991 !important; } .title { @@ -372,4 +377,57 @@ padding-left: 100px !important; padding-right: 100px !important; } + + nb-spinner { + z-index: 990 !important; + } + + .nb-tree-grid-row:nth-child(2n):not(:hover), .nb-tree-grid-row { + background: unset; + } + + .nb-tree-grid-row:hover { + background-color: var(--option-hover-background-color) !important; + color: var(--option-hover-text-color) !important; + } + + .nb-tree-grid-row:nth-child(2n):not(:hover).selected, .nb-tree-grid-row.selected { + //background-color: var(--button-filled-basic-background-color) !important; + background-color: var(--option-active-background-color); + color: var(--option-active-text-color); + } + + nb-tree-grid-row-toggle nb-icon { + color: var(--option-text-color) !important; + } + + as-split .as-split-gutter { + background-color: var(--background-basic-color-3) !important; + } + + .fullscreen { + width: 100vw; + height: 100vh !important; + position: fixed; + top: 0; + left: 0; + z-index: 999999; + } + + body.noscroll{ + position:fixed; + overflow:hidden; + } + + .badge { + font-size: 100% + } + + .badge-primary { + background-color: nb-theme(color-primary-500); + } + + solution-ide as-split-area { + overflow: hidden !important; + } } \ No newline at end of file diff --git a/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html b/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html index 32753125353d9640fb97788788c09e9046a63935..9f0322f3fae2c06a33c8a7344d84e1db11cb34ac 100644 --- a/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html +++ b/subato-web/src/app/features/courses/components/course-file-list/course-file-list.component.html @@ -23,11 +23,11 @@ <span *ngIf="file.description" class="subtitle text-muted">{{ file.description }} </span> </div> <div class="interaction"> - <a nbButton *ngIf="principal != null && principal.isAdmin(course.id)" [nbSpinner]="deletingFile == file.id" + <button nbButton *ngIf="principal != null && principal.isAdmin(course.id)" [disabled]="deletingFile == file.id" [nbSpinner]="deletingFile == file.id" (click)="$event.stopPropagation(); deleteFile(file)" [status]="'danger'"> <nb-icon icon="trash-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span> - </a> + </button> </div> </a> </nb-list-item> diff --git a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html index 4a52137cac484611bd86dc579172f40ca441f9d9..2f5cc5baf0ead3216c328869c3ef3635691ac3ed 100644 --- a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html +++ b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.html @@ -8,6 +8,7 @@ <div class="form-group"> <label for="file" class="label">Datei</label> <dropzone id="file" [file]="file" (fileChanged)="handleFile($event)"></dropzone> + <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small> </div> <div class="form-group"> <label for="description" class="label">Beschreibung</label> diff --git a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts index 97eea4d7a0fff11c3afed752fcaa7c65f6a4a52f..d0f0d68095d94af4e090a1418dcb7008276c29f4 100644 --- a/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts +++ b/subato-web/src/app/features/courses/components/course-file-upload/course-file-upload.component.ts @@ -1,6 +1,8 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; +import { PublicConfig } from 'app/core/models/public-config'; +import { AppStateService } from 'app/core/services/app.state.service'; import { BaseComponent } from 'app/shared/components/base/base.component'; import { BackendError } from 'app/shared/models/backend-error'; import { NotificationService } from 'app/shared/services/notification.service'; @@ -23,13 +25,17 @@ export class CourseFileUploadComponent extends BaseComponent implements OnInit { form: UntypedFormGroup; file: File; + config: PublicConfig; + constructor( private courseFileService: CourseFileService, + private appStateService: AppStateService, private router: Router, private route: ActivatedRoute, private notificationService: NotificationService, private fb: UntypedFormBuilder) { super(); + this.config = appStateService.config; } ngOnInit(): void { diff --git a/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html b/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html index 23a6648612ae4361ba7d3a74bfd43df258ea3d01..d1636c4aaf1ecf4b93012d4b5fa0a97d44a2ee32 100644 --- a/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html +++ b/subato-web/src/app/features/courses/components/module-multiselect/module-multiselect.component.html @@ -14,9 +14,9 @@ <span class="subtitle text-muted">{{ module.program.name }}</span> </div> <div class="interaction"> - <a nbButton (click)="remove(module)" [status]="'danger'"> + <button nbButton (click)="remove(module)" [status]="'danger'"> <nb-icon icon="close-outline" pack="eva"></nb-icon> - </a> + </button> </div> </nb-list-item> </nb-list> @@ -26,10 +26,10 @@ <div class="col-12 col-lg-6"> <div class="row mb-3"> <div class="col-12 d-flex" style="align-items: flex-start"> - <a nbButton class="mr-3" [disabled]="selectableModules.length == 0" (click)="addAll()" + <button nbButton class="mr-3" [disabled]="selectableModules.length == 0" (click)="addAll()" [status]="'primary'"> <nb-icon icon="plus-outline" pack="eva"></nb-icon> Alle - </a> + </button> <nb-form-field style="flex-grow: 1"> <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon> <input type="text" (ngModelChange)="filterModules($event)" [ngModel]="query" @@ -47,9 +47,9 @@ </nb-list-item> <nb-list-item *ngFor="let module of selectableModules"> <div class="interaction mr-3"> - <a nbButton (click)="add(module)" [status]="'primary'"> + <button nbButton (click)="add(module)" [status]="'primary'"> <nb-icon icon="plus-outline" pack="eva"></nb-icon> - </a> + </button> </div> <div class="content" style="flex-grow: 1"> <div class="title"> diff --git a/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html b/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html index cd099effc452d463d8a0c8026ff9c8276f339fba..5162cb69fd0e61bada08a7b8a1e72c5c5801a2de 100644 --- a/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html +++ b/subato-web/src/app/features/courses/pages/course-settings/course-settings.component.html @@ -11,16 +11,16 @@ </div> </div> <div style="align-self: center; gap: 15px" class="d-flex"> - <a *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'basic'" + <button *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'basic'" [nbSpinner]="reimportingCourse" [disabled]="reimportingCourse" (click)="reimport()"> <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">AoR Reimport</span> - </a> - <a *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'danger'" + </button> + <button *ngIf="principal && principal.isAdmin(course.id)" nbButton [status]="'danger'" [nbSpinner]="deletingCourse" [disabled]="deletingCourse" (click)="deleteCourse()"> <nb-icon icon="trash-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Kurs löschen</span> - </a> + </button> </div> </nb-card-header> </nb-card> diff --git a/subato-web/src/app/features/courses/pages/new-course/new-course.component.html b/subato-web/src/app/features/courses/pages/new-course/new-course.component.html index c90a2228aa7bf40cfa315aea29c783c0e2bccb68..bcc8c107bbbcb5e6551750e684cae1f6129653f0 100644 --- a/subato-web/src/app/features/courses/pages/new-course/new-course.component.html +++ b/subato-web/src/app/features/courses/pages/new-course/new-course.component.html @@ -23,21 +23,12 @@ <nb-stepper orientation="horizontal" [linear]="true"> <nb-step label="Vorlage" [stepControl]="templateForm"> <div class="step-container"> - <div class="buttons mb-4"> - <button nbButton disabled nbStepperPrevious> - <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon> - Zurück - </button> - <button nbButton nbStepperNext [disabled]="!templateForm.valid"> - Weiter - <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon> - </button> - </div> <nb-alert class="d-block"> Es kann ein bestehender Kurs als Vorlage angegeben werden. Aus diesem Kurs - werden dann bestimmte Daten kopiert. + werden dann bestimmte Daten kopiert. <br> - Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere Informationen dazu. + Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere + Informationen dazu. </nb-alert> <div class="form-group mb-3"> <nb-toggle [ngModel]="useTemplate" (checkedChange)="useTemplateChanged($event)"> @@ -50,21 +41,21 @@ message="Kurs muss ausgewählt werden"> </validation-hint> </div> - </div> - </nb-step> - - <nb-step label="Allgemein" [stepControl]="generalForm"> - <form class="step-container" [formGroup]="generalForm" ngNativeValidate> - <div class="buttons mb-4"> - <button nbButton nbStepperPrevious> + <div class="buttons mt-4"> + <button nbButton disabled nbStepperPrevious> <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon> Zurück </button> - <button nbButton nbStepperNext [disabled]="!generalForm.valid"> + <button nbButton nbStepperNext [disabled]="!templateForm.valid"> Weiter <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon> </button> </div> + </div> + </nb-step> + + <nb-step label="Allgemein" [stepControl]="generalForm"> + <form class="step-container" [formGroup]="generalForm" ngNativeValidate> <div class="row"> <div class="col-12 col-lg-6"> <div class="form-group"> @@ -91,25 +82,29 @@ </div> </div> </div> - </form> - </nb-step> - - <nb-step label="AoR"> - <div class="step-container"> - <div class="buttons mb-4"> + <div class="buttons mt-4"> <button nbButton nbStepperPrevious> <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon> Zurück </button> - <button nbButton (click)="save()" status="success" nbStepperNext> - <nb-icon icon="save-outline" pack="eva"></nb-icon> Erstellen + <button nbButton nbStepperNext [disabled]="!generalForm.valid"> + Weiter + <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon> </button> </div> + </form> + </nb-step> + + <nb-step label="AoR"> + <div class="step-container"> + <nb-alert class="d-block"> Kurse können durch die AoR-Integration mit Modulen aus den - Informatikstudiengängen verknüpft werden. Dies ermöglicht den Import von Veranstaltungen und deren Teilnehmer. + Informatikstudiengängen verknüpft werden. Dies ermöglicht den Import von + Veranstaltungen und deren Teilnehmer. <br> - Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere Informationen dazu. + Siehe <a [href]="courseManualUrl" target="_blank">Dokumentation</a> für weitere + Informationen dazu. </nb-alert> <nb-alert *ngIf="permanent" class="d-block" accent="warning">Du hast ausgewählt, @@ -117,11 +112,23 @@ der Kurs permanent ist. Teilnehmer, Veranstaltungen und Berechtigungen werden bei - permanenten Kursen nicht importiert. Eine Verknüpfung kann aber aufgrund der Kategorisierung in der Kursliste dennoch sinnvoll + permanenten Kursen nicht importiert. Eine Verknüpfung kann aber aufgrund der + Kategorisierung in der Kursliste dennoch sinnvoll sein.</nb-alert> <ngx-module-multiselect [selectedModules]="selectedModules" (modulesChanged)="modulesChanged($event)"></ngx-module-multiselect> + + <div class="buttons mt-4"> + <button nbButton nbStepperPrevious> + <nb-icon icon="arrowhead-left-outline" pack="eva"></nb-icon> + Zurück + </button> + <button nbButton [nbSpinner]="saving" (click)="save()" [disabled]="saving" + status="success" nbStepperNext> + <nb-icon icon="save-outline" pack="eva"></nb-icon> Erstellen + </button> + </div> </div> </nb-step> </nb-stepper> diff --git a/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts b/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts index c6d20b0b12c08c0381b04675fdc15388c8ebe2d3..16ad19c18f68e38cc61716a9a98ea629df4c22f1 100644 --- a/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts +++ b/subato-web/src/app/features/courses/pages/new-course/new-course.component.ts @@ -37,6 +37,8 @@ export class NewCourseComponent extends FormComponent implements OnInit { templateCourse: Course | null; selectedModules: ProgramModule[] = []; + saving: boolean = false; + constructor( private route: ActivatedRoute, private router: Router, @@ -107,6 +109,8 @@ export class NewCourseComponent extends FormComponent implements OnInit { } save() { + this.saving = true; + let dto: CreateCourseDto = { name: this.generalForm.get('name').value, shortName: this.generalForm.get('shortName').value, @@ -121,6 +125,17 @@ export class NewCourseComponent extends FormComponent implements OnInit { .subscribe(course => { this.notificationService.showSuccess("Kurs erfolgreich erstellt"); this.router.navigate(['course', course.id]); + this.saving = false; + }, error => { + this.saving = false; + + if (BackendError.isBackendError(error)) { + this.notificationService.showError(error.format( + "Fehler bei Erstellung des Kurses", + )); + } else { + throw error; + } }); } } diff --git a/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts b/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts index 9e84f0e34b5c9b4c6fc9c8a66911f6ed6a5b0716..c4946e41ef1b3424d8a75727541a727c7291e4d9 100644 --- a/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts +++ b/subato-web/src/app/features/exercises/components/eclipse-import/eclipse-import.component.ts @@ -23,6 +23,6 @@ export class EclipseImportComponent implements OnInit { } generateImportLink() { - this.importLink = `eclipse+command://de.hsrm.sls.subato.eclipse.core.commands.createTask?de.hsrm.sls.subato.eclipse.core.commands.createTask.parameter.exerciseId=${this.taskInstance.exercise.id}&de.hsrm.sls.subato.eclipse.core.commands.createTask.parameter.taskId=${this.taskInstance.task.id}`; + this.importLink = `eclipse+command://de.hsrm.sls.subato.eclipse.core.commands.importTask?de.hsrm.sls.subato.eclipse.core.commands.importTask.parameter.exerciseId=${this.taskInstance.exercise.id}&de.hsrm.sls.subato.eclipse.core.commands.importTask.parameter.taskId=${this.taskInstance.task.id}`; } } diff --git a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html index cc72c19d852090a75a2006a6992f70e7c4448742..fa06dd75ec9af4ee5a33682f670990a24c9fcb1c 100644 --- a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html +++ b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.html @@ -1,9 +1,14 @@ <div *ngIf="showFilter" class="row mt-3"> - <div class="col-12"> - <nb-toggle [ngModel]="hideInvisible" (ngModelChange)="applyFilter($event)">Unsichtbare Übungen + <div class="col-12 d-flex" style="justify-content: space-between; flex-wrap: wrap;gap: 15px;"> + <nb-form-field style="flex-grow: 1; max-width: 600px"> + <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon> + <input type="text" (ngModelChange)="onQueryChange($event)" [ngModel]="query" nbInput fullWidth> + </nb-form-field> + <nb-toggle [ngModel]="hideInvisible" (ngModelChange)="onHideInvisibleChanged($event)" style="min-width: 300px;">Unsichtbare Übungen ausblenden</nb-toggle> </div> </div> + <div class="row mt-3"> <div class="col-12"> <div> @@ -37,7 +42,9 @@ <nb-icon icon="question-mark-circle-outline"></nb-icon> </span> <span> - <a *ngIf="exerciseProgress.submission" [routerLink]="['/', 'submissions', exerciseProgress.submission.id]">{{ exerciseProgress.exercise.name }}</a> + <a *ngIf="exerciseProgress.submission" + [routerLink]="['/', 'submissions', exerciseProgress.submission.id]">{{ exerciseProgress.exercise.name + }}</a> <span *ngIf="!exerciseProgress.submission">{{ exerciseProgress.exercise.name }}</span> </span> </th> @@ -66,10 +73,11 @@ [class.success]="exerciseProgress.submission && exerciseProgress.submission.passed == true" [class.danger]="exerciseProgress.submission && exerciseProgress.submission.passed == false" [class.basic]="exerciseProgress.submission && exerciseProgress.submission.passed == null" - *ngFor="let taskInstance of exerciseProgress.taskInstances" - > + *ngFor="let taskInstance of exerciseProgress.taskInstances"> <td> - <a *ngIf="hasSolution(exerciseProgress, taskInstance)" [routerLink]="['/', 'submissions', exerciseProgress.submission.id]" [queryParams]="{ taskId: taskInstance.task.id }">{{ taskInstance.task.name }}</a> + <a *ngIf="hasSolution(exerciseProgress, taskInstance)" + [routerLink]="['/', 'submissions', exerciseProgress.submission.id]" + [queryParams]="{ taskId: taskInstance.task.id }">{{ taskInstance.task.name }}</a> <span *ngIf="!hasSolution(exerciseProgress, taskInstance)">{{ taskInstance.task.name }}</span> </td> <td class="fit center"> diff --git a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts index 04e92265a402318c93dad1c94620ad452893e60a..aea71a6449db6749cd8d65c89c12f6e0ac9446ec 100644 --- a/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts +++ b/subato-web/src/app/features/exercises/components/exercise-progress/exercise-progress.component.ts @@ -2,31 +2,47 @@ import { Component, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TaskInstance } from '../../models/task-instance'; import { ExerciseProgress } from '../../models/exercise-progress'; +import { Subject } from 'rxjs'; +import { BaseComponent } from 'app/shared/components/base/base.component'; +import { takeUntil, debounceTime } from 'rxjs/operators'; @Component({ selector: 'ngx-exercise-progress', templateUrl: './exercise-progress.component.html', styleUrls: ['./exercise-progress.component.scss'] }) -export class ExerciseProgressComponent implements OnInit { +export class ExerciseProgressComponent extends BaseComponent implements OnInit { @Input() _exerciseProgresses: ExerciseProgress[] = []; filteredExerciseProgresses: ExerciseProgress[] = []; @Input() showFilter: boolean; hideInvisible = true; + query: string; + private queryChanged: Subject<string> = new Subject<string>(); + @Input() set exerciseProgresses(val: ExerciseProgress[]) { this._exerciseProgresses = val; - console.log(this.hideInvisible); - this.applyFilter(this.hideInvisible); + this.applyFilter(); } get exerciseProgresses(): ExerciseProgress[] { return this._exerciseProgresses; } - constructor(private router: Router) { } + constructor(private router: Router) { + super(); + } ngOnInit(): void { + this.queryChanged + .pipe( + takeUntil(this.unsubscribe), + debounceTime(500), + ) + .subscribe(query => { + this.query = query; + this.applyFilter(); + }); } navigateToSubmission(exerciseProgress: ExerciseProgress) { @@ -50,10 +66,25 @@ export class ExerciseProgressComponent implements OnInit { return exerciseResult.submission?.pointRatings.find(r => r.taskInstance.task.id == taskInstance.task.id); } - applyFilter(val: boolean) { + onHideInvisibleChanged(val: boolean) { this.hideInvisible = val; + this.applyFilter(); + } + + onQueryChange(query) { + this.queryChanged.next(query); + } + + applyFilter() { this.filteredExerciseProgresses = this.hideInvisible ? this.exerciseProgresses.filter(r => !this.hideInvisible || r.exercise.visible) : this.exerciseProgresses; + + this.filteredExerciseProgresses = this.query + ? this.filteredExerciseProgresses.filter( + r => r.exercise.name.toLowerCase().indexOf(this.query.toLowerCase()) !== -1 || + r.taskInstances.some(ti => ti.task.name.toLocaleLowerCase().indexOf(this.query.toLowerCase()) !== -1) + ) + : this.filteredExerciseProgresses; } } diff --git a/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html b/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html index 21d3120a63a36f5a28f1d6e0bca0f868e84af5d4..bb7493e1f150ed5f16dfee763bc0a38408deeb1d 100644 --- a/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html +++ b/subato-web/src/app/features/exercises/components/exercise-task-selection/exercise-task-selection.component.html @@ -23,9 +23,9 @@ <nb-list *ngIf="!error"> <nb-list-item *ngFor="let task of selectableTasks"> <div class="mr-3"> - <a nbButton [status]="'primary'" (click)="selectTask(task)"> + <button nbButton [status]="'primary'" (click)="selectTask(task)"> <nb-icon icon="plus-outline" pack="eva"></nb-icon> - </a> + </button> </div> <div style="flex-grow: 1"> <div class="title"> diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2a85b84e95d34a5765b5afbf16c253925a2bb0ac --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.html @@ -0,0 +1,14 @@ +<nb-alert class="h-100 mb-0" style="flex-direction: row; + align-items: center;justify-content: space-between;" [status]="numerator != 0 ? status : 'basic'"> + <div style="display: flex; align-items: baseline; flex-direction: column; gap: 5px;"> + <div style="display: flex; flex-direction:row; flex-wrap: wrap; align-items: baseline; gap: 5px;"> + <div style="font-size: 34px;"> + {{ numerator }} + </div> + <div>/ {{ denominator }} Übungen</div> + </div> + <div class="details"> + <nb-icon class="mr-1" style="flex-shrink: 0" [icon]="icon"></nb-icon> <strong>{{ label }}</strong> + </div> + </div> +</nb-alert> \ No newline at end of file diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e242b3a76526ab0af4e42c7cddbca2ebf5c69a02 --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.scss @@ -0,0 +1,7 @@ +.details { + flex: 1 1 auto; + flex-direction: column; + justify-content: center; + height: 100%; + border-left: 1px solid transparent; + } \ No newline at end of file diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4672d3e23b62be3046d6e3933fcc95ca5167c91a --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionInfoBannerComponent } from './submission-info-banner.component'; + +describe('SubmissionInfoBannerComponent', () => { + let component: SubmissionInfoBannerComponent; + let fixture: ComponentFixture<SubmissionInfoBannerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SubmissionInfoBannerComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SubmissionInfoBannerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f18864bec577847672aaa7fdd4f8a4b3998c4aa8 --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-info-banner/submission-info-banner.component.ts @@ -0,0 +1,20 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'ngx-submission-info-banner', + templateUrl: './submission-info-banner.component.html', + styleUrls: ['./submission-info-banner.component.scss'] +}) +export class SubmissionInfoBannerComponent implements OnInit { + @Input() status: string; + @Input() numerator: number; + @Input() denominator: number; + @Input() label: string; + @Input() icon: string; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..8b288b9a2c1039f54fa0ff8d996f9e3d3c832334 --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.html @@ -0,0 +1,17 @@ +<div class="row"> + <div class="col-12 col-xl-4 mb-3"> + <ngx-submission-info-banner status="success" icon="checkmark-circle-2-outline" + [numerator]="exerciseProgressesResult.submissionsProgress.passedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length" + label="bestanden"></ngx-submission-info-banner> + </div> + <div class="col-12 col-xl-4 mb-3"> + <ngx-submission-info-banner status="danger" icon="close-circle-outline" + [numerator]="exerciseProgressesResult.submissionsProgress.notPassedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length" + label="nicht bestanden"></ngx-submission-info-banner> + </div> + <div class="col-12 col-xl-4 mb-3"> + <ngx-submission-info-banner status="primary" icon="question-mark-circle-outline" + [numerator]="exerciseProgressesResult.submissionsProgress.undecidedTotal" [denominator]="exerciseProgressesResult.exerciseProgresses.length" + label="ausstehend"></ngx-submission-info-banner> + </div> + </div> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.scss b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.scss similarity index 100% rename from subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.scss rename to subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.scss diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e73783dffdebc24aa4ecfec8904e7cb299b9317 --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionProgressContainerComponent } from './submission-progress-container.component'; + +describe('SubmissionProgressContainerComponent', () => { + let component: SubmissionProgressContainerComponent; + let fixture: ComponentFixture<SubmissionProgressContainerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SubmissionProgressContainerComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SubmissionProgressContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..15e6040ad140634b4d4f219c788ba90cbd17da71 --- /dev/null +++ b/subato-web/src/app/features/exercises/components/submission-progress-container/submission-progress-container.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ExerciseProgressResult } from '../../models/exercise-progress-result'; + +@Component({ + selector: 'ngx-submission-progress-container', + templateUrl: './submission-progress-container.component.html', + styleUrls: ['./submission-progress-container.component.scss'] +}) +export class SubmissionProgressContainerComponent implements OnInit { + @Input() exerciseProgressesResult: ExerciseProgressResult; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/subato-web/src/app/features/exercises/exercises.module.ts b/subato-web/src/app/features/exercises/exercises.module.ts index 6a4c558ed1c040b011ad4af5c49fe60a98bd22c9..9a7fe274bd47c14afa055fbf056b63c807eeb145 100644 --- a/subato-web/src/app/features/exercises/exercises.module.ts +++ b/subato-web/src/app/features/exercises/exercises.module.ts @@ -25,6 +25,8 @@ import { PointsPipe } from './pipes/points.pipe'; import { TaskInstanceSummaryComponent } from './components/task-instance-summary/task-instance-summary.component'; import { UserAttemptsPipe } from './pipes/user-attempts.pipe'; import { SolutionsModule } from '../solutions/solutions.module'; +import { SubmissionInfoBannerComponent } from './components/submission-info-banner/submission-info-banner.component'; +import { SubmissionProgressContainerComponent } from './components/submission-progress-container/submission-progress-container.component'; @NgModule({ declarations: [ @@ -51,6 +53,8 @@ import { SolutionsModule } from '../solutions/solutions.module'; SubmissionSelectionComponent, SubmissionsFinishedScreenComponent, TaskInstanceSummaryComponent, + SubmissionInfoBannerComponent, + SubmissionProgressContainerComponent, ], imports: [ CommonModule, @@ -61,7 +65,8 @@ import { SolutionsModule } from '../solutions/solutions.module'; ExercisesRoutingModule ], exports: [ - ExerciseProgressComponent + ExerciseProgressComponent, + SubmissionProgressContainerComponent ] }) export class ExercisesModule { } diff --git a/subato-web/src/app/features/exercises/models/exercise-progress-result.ts b/subato-web/src/app/features/exercises/models/exercise-progress-result.ts new file mode 100644 index 0000000000000000000000000000000000000000..7da8dcea44307277e562d301680258a730bf24f5 --- /dev/null +++ b/subato-web/src/app/features/exercises/models/exercise-progress-result.ts @@ -0,0 +1,12 @@ +import { ExerciseProgress } from "./exercise-progress"; +import { SubmissionsProgress } from "./submissions-progress"; + +export class ExerciseProgressResult { + exerciseProgresses: ExerciseProgress[]; + submissionsProgress: SubmissionsProgress; + + constructor(result: any) { + this.exerciseProgresses = result.exerciseProgresses.map(p => new ExerciseProgress(p)); + this.submissionsProgress = new SubmissionsProgress(result.submissionsProgress); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/exercises/pages/submission/submission.component.html b/subato-web/src/app/features/exercises/pages/submission/submission.component.html index 3f3dd4291e6bd475fa56f89ce4ce8b4e3fea3851..ee5397da64a4dbb6f88d840cef7fbdd429319ac4 100644 --- a/subato-web/src/app/features/exercises/pages/submission/submission.component.html +++ b/subato-web/src/app/features/exercises/pages/submission/submission.component.html @@ -149,7 +149,7 @@ <nb-card-body> <nb-tabset #solutionTabset class="no-side-padding d-flex flex-column"> <nb-tab tabId="solution" tabTitle="Lösung"> - <solution-viewer *ngIf="currentSolution" [solution]="currentSolution"></solution-viewer> + <solution-ide *ngIf="currentSolution" withBorder="true" [solution]="currentSolution"></solution-ide> <div class="p-3" *ngIf="!currentSolution">Keine Lösung ausgewählt</div> </nb-tab> <nb-tab tabTitle="Historie" style="max-height: 400px" diff --git a/subato-web/src/app/features/exercises/services/exercise.service.ts b/subato-web/src/app/features/exercises/services/exercise.service.ts index 718ca781b93ca52eb78d9c7be9baf0b42a7de4b3..415d6c2ae8e669c38c0ed3fe2a34a3dc344e06c4 100755 --- a/subato-web/src/app/features/exercises/services/exercise.service.ts +++ b/subato-web/src/app/features/exercises/services/exercise.service.ts @@ -16,6 +16,7 @@ import { ExerciseProgress } from "../models/exercise-progress"; import { TaskInstance } from "../models/task-instance"; import { FileRef } from "app/features/tasks/models/file-ref"; import { DownloadService } from "app/shared/services/download.service"; +import { ExerciseProgressResult } from "../models/exercise-progress-result"; @Injectable({ providedIn: 'root' @@ -90,18 +91,18 @@ export class ExerciseService extends BaseService { } - getProgressForUser(courseId: number, userId: number): Observable<ExerciseProgress[]> { + getProgressForUser(courseId: number, userId: number): Observable<ExerciseProgressResult> { let params = new HttpParams(); params = params.set('courseId', courseId); params = params.set('userId', userId); - return this.http.get<ExerciseProgress[]>(`${environment.backendUrl}/exercise/progress`, { + return this.http.get<ExerciseProgressResult>(`${environment.backendUrl}/exercise/progress`, { params: params }).pipe( catchError(e => { throw BackendError.parse(e); }), - map(exerciseProgresses => exerciseProgresses.map(exerciseProgress => new ExerciseProgress(exerciseProgress))) + map(result => new ExerciseProgressResult(result)) ) } diff --git a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html index 615aa167968e781a37faee55e1cf74e0db57dc46..bbe77ac50d56f7a5bfd55102bc98a7130f623203 100644 --- a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html +++ b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.html @@ -45,7 +45,8 @@ <div class="col-12"> <nb-tabset class="no-side-padding"> <nb-tab tabTitle="Ergebnisse"> - <ngx-exercise-progress *ngIf="exerciseProgresses" [showFilter]="true" [exerciseProgresses]="exerciseProgresses"> + <ngx-submission-progress-container *ngIf="exerciseProgressResult" [exerciseProgressesResult]="exerciseProgressResult"></ngx-submission-progress-container> + <ngx-exercise-progress *ngIf="exerciseProgressResult" [showFilter]="true" [exerciseProgresses]="exerciseProgressResult.exerciseProgresses"> </ngx-exercise-progress> </nb-tab> <nb-tab> diff --git a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts index cb8678b92ef82afa4adedeb352d14d0e1e544d0c..3a531c0daa22acea277dd50468f9612bd0238084 100644 --- a/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts +++ b/subato-web/src/app/features/progress/pages/course-unit-progress/course-unit-progress.component.ts @@ -17,6 +17,7 @@ import { ErrorContainerConfig } from 'app/shared/components/error-container/erro import { AppStateService } from 'app/core/services/app.state.service'; import { Breadcrumb } from 'app/shared/components/breadcrumbs/breadcrumbs.component'; import { SubatoPrincipal } from 'app/core/models/principal/subato-principal'; +import { ExerciseProgressResult } from 'app/features/exercises/models/exercise-progress-result'; @Component({ selector: 'ngx-course-unit-progress', @@ -30,7 +31,7 @@ export class CourseUnitProgressComponent extends BaseComponent implements OnInit loadingProgress: boolean = false; courseUnit: CourseUnit; participantAttendance: ParticipantAttendance; - exerciseProgresses: ExerciseProgress[] = []; + exerciseProgressResult: ExerciseProgressResult; loadingProgressError: BackendError; error: BackendError; @@ -118,9 +119,9 @@ export class CourseUnitProgressComponent extends BaseComponent implements OnInit .pipe( takeUntil(this.unsubscribe), ) - .subscribe(([participantAttendance, courseResult]) => { + .subscribe(([participantAttendance, exerciseProgressResult]) => { this.participantAttendance = participantAttendance; - this.exerciseProgresses = courseResult; + this.exerciseProgressResult = exerciseProgressResult; this.loadingProgress = false; }, (error) => { this.loadingProgress = false; diff --git a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html index b1a4549b5a9de9594ad9176db78ceca0f4e824c4..7ccf4b3809441d4885eded97ddadcf078077ac98 100644 --- a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html +++ b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.html @@ -20,9 +20,10 @@ <nb-card> <nb-card-body> <nb-tabset class="no-side-padding"> - <nb-tab tabTitle="Ergebnisse"> + <nb-tab *ngIf="exerciseProgressResult" tabTitle="Ergebnisse"> + <ngx-submission-progress-container [exerciseProgressesResult]="exerciseProgressResult"></ngx-submission-progress-container> <ngx-exercise-progress [showFilter]="canSeeFilter" - [exerciseProgresses]="exerciseProgresses"> + [exerciseProgresses]="exerciseProgressResult.exerciseProgresses"> </ngx-exercise-progress> </nb-tab> <nb-tab *ngFor="let participantAttendance of participantAttendances"> diff --git a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts index e8f016cdc0d33c777a8132d0e4fada1e51d924e4..f383413c96a0df81d2c7c7cc19a4eb66d367d8d4 100644 --- a/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts +++ b/subato-web/src/app/features/progress/pages/participant-progress/participant-progress.component.ts @@ -19,6 +19,7 @@ import { UserService } from 'app/core/services/user.service'; import { ErrorContainerConfig } from 'app/shared/components/error-container/error-container.component'; import { AppStateService } from 'app/core/services/app.state.service'; import { SubatoPrincipal } from 'app/core/models/principal/subato-principal'; +import { ExerciseProgressResult } from 'app/features/exercises/models/exercise-progress-result'; @Component({ selector: 'ngx-participant-progress', @@ -33,7 +34,7 @@ export class ParticipantProgressComponent extends BaseComponent implements OnIni course: Course; participantAttendances: ParticipantAttendance[]; events: CourseUnitEvent[]; - exerciseProgresses: ExerciseProgress[] = []; + exerciseProgressResult: ExerciseProgressResult; hideInvisible; error: BackendError; @@ -98,10 +99,10 @@ export class ParticipantProgressComponent extends BaseComponent implements OnIni this.userService.getById(userId) ) .pipe( - map(([attendances, exerciseProgresses, user]) => { + map(([attendances, exerciseProgressResult, user]) => { this.user = user; this.participantAttendances = attendances; - this.exerciseProgresses = exerciseProgresses; + this.exerciseProgressResult = exerciseProgressResult; }) ); } diff --git a/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html b/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html index 23710fd8414b10e635cda86593b6a3e376773b70..2c91b642ee60eed8ee438487ccaf384ad9c412fe 100644 --- a/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html +++ b/subato-web/src/app/features/solutions/components/acceptance-test/acceptance-test.component.html @@ -3,14 +3,14 @@ <div class="text-muted">Für diese Aufgabe sind Abnahmetests hinterlegt. </div> <div class="mt-3"> - <a *ngIf="!acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'primary'"> + <button *ngIf="!acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'primary'"> <nb-icon icon="play-circle-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Ausführen</span> - </a> - <a *ngIf="acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'basic'" class="ml-2"> + </button> + <button *ngIf="acceptanceResult" (click)="runAcceptanceTests()" nbButton [nbSpinner]="isAcceptanceTestRunning" [disabled]="isAcceptanceTestRunning" [status]="'basic'" class="ml-2"> <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Erneut ausführen</span> - </a> + </button> </div> </div> <div class="mt-4"> diff --git a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html index e8ee829bc763138e3755daf1ae8bbeb61c394167..a8c89b4527e76a6d19ab441fc4e508f05f2f10ae 100644 --- a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html +++ b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.html @@ -9,7 +9,7 @@ <a *ngIf="feedback.actions?.retry?.enabled" nbButton [routerLink]="['/', 'tasks', solution.task.id]" [status]="feedback.actions.retry.status" class="ml-2"> - <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Nochmal + <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nochmal versuchen</span> </a> </div> @@ -17,13 +17,13 @@ <a *ngIf="feedback.actions?.retry?.enabled" nbButton [routerLink]="['/', 'exercises', solution.exercise.id, 'tasks', solution.task.id]" [status]="feedback.actions.retry.status" class="ml-2"> - <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-lg-block d-xl-block">Nochmal + <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nochmal versuchen</span> </a> <a *ngIf="feedback.actions?.next?.enabled && nextTaskInstance" nbButton [routerLink]="['/', 'exercises', solution.exercise.id, 'tasks', nextTaskInstance.task.id]" [status]="feedback.actions.next.status" class="ml-2"> - <span class="d-none d-lg-block d-xl-block">Nächste Aufgabe</span> + <span class="d-none d-sm-block d-md-block d-lg-block d-xl-block">Nächste Aufgabe</span> <nb-icon icon="arrowhead-right-outline" pack="eva"></nb-icon> </a> </div> diff --git a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss index 7dd154e2cce3e5ed59d39d13fb35e19bee107596..2fc0fd2c6467475c201c9e81bbd450dbacb10d59 100644 --- a/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss +++ b/subato-web/src/app/features/solutions/components/solution-feedback-screen/solution-feedback-screen.component.scss @@ -5,7 +5,7 @@ align-items: center; .content { - width: 800px; + max-width: 800px; } .emoji { diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html new file mode 100755 index 0000000000000000000000000000000000000000..e192c078dab6ddf84be07f35cecfcbfeefca0e5c --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.html @@ -0,0 +1,29 @@ +<div class="browser-container h-100"> + <nb-form-field class="p-2" style="flex-grow: 1"> + <nb-icon nbPrefix icon="search-outline" pack="eva"></nb-icon> + <input type="text" (ngModelChange)="filterTree($event)" [ngModel]="query" nbInput fullWidth> + </nb-form-field> + <div class="table-wrapper scroll"> + <table [nbTreeGrid]="data" equalColumnsWidth> + + <tr nbTreeGridRow *nbTreeGridRowDef="let row; columns: allColumns" [class.selected]="row.data == selectedTree"> + </tr> + + <ng-container [nbTreeGridColumnDef]="customColumn"> + <td nbTreeGridCell *nbTreeGridCellDef="let row" (click)="onTreeClicked(row.data)"> + + <nb-tree-grid-row-toggle [expanded]="row.expanded" *ngIf="row.data.children.length !== 0"> + </nb-tree-grid-row-toggle> + + {{row.data.name}} + + </td> + </ng-container> + + <ng-container *ngFor="let column of defaultColumns" [nbTreeGridColumnDef]="column"> + <td nbTreeGridCell *nbTreeGridCellDef="let row">{{row.data[column]}}</td> + </ng-container> + + </table> + </div> +</div> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss new file mode 100755 index 0000000000000000000000000000000000000000..6a3bcd4eb90d817a998526647f0a0ed6adcf9829 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.scss @@ -0,0 +1,17 @@ +@use 'themes' as *; + +td { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + cursor: pointer; +} + +.browser-container { + background-color: nb-theme(background-basic-color-1) +} + +.table-wrapper { + overflow-y: auto; + height: 100%; +} \ No newline at end of file diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.spec.ts similarity index 100% rename from subato-web/src/app/shared/components/file-browser/file-browser.component.spec.ts rename to subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.spec.ts diff --git a/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts new file mode 100755 index 0000000000000000000000000000000000000000..3424a1e6e84caf72e3ecd56b3a12e79baa099363 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/file-browser/file-browser.component.ts @@ -0,0 +1,76 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BaseComponent } from 'app/shared/components/base/base.component'; +import { FileTree } from 'app/shared/models/file-tree'; +import { Subject } from 'rxjs'; +import { debounceTime, takeUntil } from 'rxjs/operators'; + +interface TreeNode<T> { + data: T; + children?: TreeNode<T>[]; + expanded?: boolean; +} + +@Component({ + selector: 'ngx-file-browser', + templateUrl: './file-browser.component.html', + styleUrls: ['./file-browser.component.scss'] +}) +export class FileBrowserComponent extends BaseComponent implements OnInit { + @Output() treeSelectedEvent = new EventEmitter<FileTree>(); + @Input() tree: FileTree; + filteredTree: FileTree; + + data: TreeNode<FileTree>[] = []; + + customColumn = 'filename'; + defaultColumns = []; + allColumns = [this.customColumn, ...this.defaultColumns]; + + selectedTree: FileTree | null = null; + + query: string; + private queryChanged: Subject<string> = new Subject<string>(); + + constructor() { + super(); + } + + private treeNodeFor(tree: FileTree, expanded: boolean = false): TreeNode<FileTree> { + return { + data: tree, + children: tree.children.map(child => this.treeNodeFor(child)), + expanded: expanded + }; + } + + setFilteredTree(val: FileTree, expand: boolean = false) { + this.filteredTree = val; + this.data = this.filteredTree?.children?.map(tree => this.treeNodeFor(tree, expand)) ?? []; + } + + ngOnInit(): void { + this.setFilteredTree(this.tree); + + this.queryChanged + .pipe( + takeUntil(this.unsubscribe), + debounceTime(300), + ) + .subscribe(query => { + this.setFilteredTree(this.tree.filterByName(query), true); + }); + } + + onTreeClicked(tree: FileTree) { + if (tree.children.length === 0) { + this.selectedTree = tree; + + this.treeSelectedEvent.emit(tree); + } + } + + filterTree(query) { + this.query = query; + this.queryChanged.next(query); + } +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5c574dd23488bec67be8d707364b5aad08f764f3 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.html @@ -0,0 +1,6 @@ +<nb-alert class="mb-0 nothing-here"> + <div class="content"> + <nb-icon icon="alert-circle-outline" class="text-danger mr-1"></nb-icon> + Datei konnte nicht geladen werden + </div> +</nb-alert> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..61d60c3eecdd12ba497c5edd15431fa04d1d1e21 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.scss @@ -0,0 +1,11 @@ +.nothing-here { + text-align: center; + align-items: center; + justify-content: center; + height: 100%; + + .content { + max-width: 800px; + } + +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5aca3956807230eae6562c8132c003d32c3e655 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IdeErrorComponent } from './ide-error.component'; + +describe('IdeErrorComponent', () => { + let component: IdeErrorComponent; + let fixture: ComponentFixture<IdeErrorComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ IdeErrorComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(IdeErrorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4422a494d5e5523d2d034233874b47dbf48e5184 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/ide-error/ide-error.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'ngx-ide-error', + templateUrl: './ide-error.component.html', + styleUrls: ['./ide-error.component.scss'] +}) +export class IdeErrorComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9207fa6070271c6c4fff41d3e4e58344a10b200 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SolutionIdeComponent } from './solution-ide/solution-ide.component'; +import { ViewerHostDirective } from './viewer/viewer-host.directive'; +import { PdfViewerComponent } from './viewer/pdf-viewer/pdf-viewer.component'; +import { SourceCodeViewerComponent } from './viewer/source-code-viewer/source-code-viewer.component'; +import { FileBrowserComponent } from './file-browser/file-browser.component'; +import { NothingViewerComponent } from './viewer/nothing-viewer/nothing-viewer.component'; +import { SharedModule } from 'app/shared/shared.module'; +import { AngularSplitModule } from 'angular-split'; +import { DefaultViewerComponent } from './viewer/default-viewer/default-viewer.component'; +import { IdeErrorComponent } from './ide-error/ide-error.component'; + +@NgModule({ + declarations: [ + SolutionIdeComponent, + ViewerHostDirective, + PdfViewerComponent, + SourceCodeViewerComponent, + FileBrowserComponent, + NothingViewerComponent, + DefaultViewerComponent, + IdeErrorComponent, + ], + imports: [ + CommonModule, + SharedModule, + AngularSplitModule + ], + exports: [ + SolutionIdeComponent + ] +}) +export class SolutionIdeModule { } diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html new file mode 100644 index 0000000000000000000000000000000000000000..1ed5f5243207ff2838176389ae058e312c6d8ca5 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.html @@ -0,0 +1,42 @@ +<div class="w-100" [class.with-border]="withBorder" maximize #maximize="maximize"> + <div class="top-nav w-100"> + <a *ngIf="fileBrowserEnabled" (click)="toggleMenu()" nbButton [status]="'basic'" class="mr-3"> + <nb-icon icon="menu-outline" pack="eva"></nb-icon> + </a> + <div class="ml-1"> + <span class="title" *ngIf="currentTree"> + {{ currentTree.name }} + </span> + </div> + <div class="actions ml-auto"> + <button (click)="downloadSolution()" nbButton [status]="'basic'"> + <nb-icon icon="download-outline" pack="eva"></nb-icon> + <span class="d-none d-md-block d-lg-block d-xl-block">Lösung herunterladen</span> + </button> + <button *ngIf="!(maximize.isMaximized$ | async)" (click)="maximize.maximize()" nbButton + [status]="'primary'"> + <nb-icon icon="expand-outline" pack="eva"></nb-icon> + <span class="d-none d-md-block d-lg-block d-xl-block">Maximieren</span> + </button> + <button *ngIf="(maximize.isMaximized$ | async)" (click)="maximize.minimize()" nbButton [status]="'primary'"> + <nb-icon icon="collapse-outline" pack="eva"></nb-icon> + <span class="d-none d-md-block d-lg-block d-xl-block">Minimieren</span> + </button> + </div> + </div> + <div class="split-container"> + <as-split [gutterSize]="compactMode ? 0 : 11" direction="horizontal"> + <as-split-area [size]="browserWidth"> + <div class="h-100"> + <ngx-file-browser [tree]="solution.tree" (treeSelectedEvent)="onTreeSelected($event)"> + </ngx-file-browser> + </div> + </as-split-area> + <as-split-area [size]="100-browserWidth" [class.overflow-hidden]="currentViewer == Viewer.pdf"> + <ngx-ide-error *ngIf="!loadingViewer && viewerError"></ngx-ide-error> + <ngx-loading-screen [loading]="loadingViewer"></ngx-loading-screen> + <ng-template viewerHost></ng-template> + </as-split-area> + </as-split> + </div> +</div> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..9815b9ae48e9a4b852f44c5b70810c234de8a055 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.scss @@ -0,0 +1,32 @@ +@use 'themes' as *; + +.fullscreen .split-container { + height: 100vh; +} + +.split-container { + height: 600px; +} + +.top-nav { + display: flex; + align-items: center; + padding: 15px 25px; + background-color: nb-theme(background-basic-color-1); + z-index: 10; + box-shadow: nb-theme(header-shadow); + position: relative; + + .actions { + gap: 15px; + display:flex + } +} + +.split-container { + background-color: nb-theme(background-basic-color-1); +} + +.with-border { + border: nb-theme(card-border-width) solid nb-theme(card-border-color); +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7af74a4ab6c7fe9da989ac742c9323e196fe6bcb --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SolutionIdeComponent } from './solution-ide.component'; + +describe('SolutionIdeComponent', () => { + let component: SolutionIdeComponent; + let fixture: ComponentFixture<SolutionIdeComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SolutionIdeComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SolutionIdeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc1bb4850a6a712ecaf4edafa99c9f7f9eeca295 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/solution-ide/solution-ide.component.ts @@ -0,0 +1,119 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { BaseComponent } from 'app/shared/components/base/base.component'; +import { FileTree } from 'app/shared/models/file-tree'; +import { take, takeUntil } from 'rxjs/operators'; +import { NotificationService } from 'app/shared/services/notification.service'; +import { BackendError } from 'app/shared/models/backend-error'; +import { Solution } from 'app/features/solutions/models/solution'; +import { SolutionService } from 'app/features/solutions/services/solution.service'; +import { ViewerHostDirective } from '../viewer/viewer-host.directive'; +import { ViewerFactory } from '../viewer/viewer.factory'; +import { Viewer } from '../viewer/viewer'; + +@Component({ + selector: 'solution-ide', + templateUrl: './solution-ide.component.html', + styleUrls: ['./solution-ide.component.scss'] +}) +export class SolutionIdeComponent extends BaseComponent implements OnInit { + @Input() withBorder: boolean = false; + + loading: boolean; + currentTree: FileTree; + _solution: Solution; + _compactMode: boolean = false; + fileBrowserEnabled: boolean = false; + + Viewer = Viewer; + currentViewer: Viewer = Viewer.none; + + defaultBrowserWidth: number = 15; + browserWidth: number = this.defaultBrowserWidth; + loadingViewer: boolean = false; + viewerError: any; + + @ViewChild(ViewerHostDirective, { static: true }) viewerHost!: ViewerHostDirective; + + constructor( + private solutionService: SolutionService, + private notificationService: NotificationService, + private viewerFactory: ViewerFactory + ) { + super(); + } + + ngOnInit(): void { + } + + @Input() set solution(val: Solution) { + this._solution = val; + if (val.tree.children.length != 0) { + let first = val.tree.children[0] + this.compactMode = val.tree.children.length == 1 && first.children.length == 0; + this.currentTree = this.compactMode ? first : null; + this.fileBrowserEnabled = !this.compactMode; + } + + this.createViewer(); + } + + get solution() { + return this._solution; + } + + get compactMode(): boolean { + return this._compactMode; + } + + set compactMode(val: boolean) { + this._compactMode = val; + this.browserWidth = this.compactMode ? 0 : this.defaultBrowserWidth; + } + + createViewer(): void { + const viewContainerRef = this.viewerHost.viewContainerRef; + viewContainerRef.clear(); + this.loadingViewer = true; + + this.viewerFactory.createViewer(this.solution, this.currentTree, viewContainerRef) + .pipe( + take(1) + ) + .subscribe(viewer => { + this.currentViewer = viewer; + this.loadingViewer = false; + }, error => { + this.viewerError = error; + this.loadingViewer = false; + }); + } + + onTreeSelected(tree: FileTree) { + if (tree.children.length != 0) { + return; + } + + this.currentTree = tree; + this.createViewer(); + } + + downloadSolution() { + this.solutionService.downloadSolution(this.solution.id) + .pipe(takeUntil(this.unsubscribe)) + .subscribe(() => { }, (error: any) => { + if (BackendError.isBackendError(error)) { + this.notificationService.showError( + error.format( + "Fehler beim Herunterladen der Lösung", + ) + ); + } else { + throw error; + } + }); + } + + toggleMenu() { + this.compactMode = !this.compactMode; + } +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6e05b942b470dc8f618352a04d5ea22906f9f011 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.html @@ -0,0 +1,15 @@ +<nb-alert class="mb-0 nothing-here"> + <div class="content"> + <div class="msg">Anzeige nicht möglich</div> + <div class="text-muted mt-1"> + Diese Datei können wir hier nicht anzeigen. + Du kannst diese Datei aber herunterladen. + </div> + <div class="mt-3"> + <button nbButton (click)="download()" [status]="'basic'"> + <nb-icon icon="download-outline" pack="eva"></nb-icon> + <span class="d-none d-md-block d-lg-block d-xl-block">{{ tree.name }}</span> + </button> + </div> + </div> +</nb-alert> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..d90a5c702e4e30a5c967792ae7a44bf8b18459a7 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.scss @@ -0,0 +1,14 @@ +.nothing-here { + text-align: center; + align-items: center; + justify-content: center; + height: 100%; + + .content { + max-width: 800px; + } + + .msg { + font-size: 20px; + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..883e7ac817ddfd9fcd8b13ab421bfb00b8d5ff2a --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DefaultViewerComponent } from './default-viewer.component'; + +describe('DefaultViewerComponent', () => { + let component: DefaultViewerComponent; + let fixture: ComponentFixture<DefaultViewerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DefaultViewerComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DefaultViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7d6ccee5b646e99886cde5f142c36f4fc2bb835 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { Solution } from 'app/features/solutions/models/solution'; +import { SolutionService } from 'app/features/solutions/services/solution.service'; +import { BaseComponent } from 'app/shared/components/base/base.component'; +import { BackendError } from 'app/shared/models/backend-error'; +import { FileTree } from 'app/shared/models/file-tree'; +import { NotificationService } from 'app/shared/services/notification.service'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'ngx-default-viewer', + templateUrl: './default-viewer.component.html', + styleUrls: ['./default-viewer.component.scss'] +}) +export class DefaultViewerComponent extends BaseComponent implements OnInit { + tree: FileTree; + solution: Solution; + + constructor( + private solutionService: SolutionService, + private notificationService: NotificationService + ) { + super(); + } + + ngOnInit(): void { + } + + download() { + this.solutionService.downloadFile(this.solution.id, this.tree) + .pipe(takeUntil(this.unsubscribe)) + .subscribe(() => {}, (error: any) => { + if (BackendError.isBackendError(error)) { + this.notificationService.showError( + error.format( + "Fehler beim Herunterladen der Lösung", + ) + ); + } else { + throw error; + } + }); + + } + +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..d700333082a50e941ec9b7536348c6738674294d --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/default-viewer/default-viewer.factory.ts @@ -0,0 +1,29 @@ +import { Injectable, ViewContainerRef } from "@angular/core"; +import { Solution } from "app/features/solutions/models/solution"; +import { SolutionService } from "app/features/solutions/services/solution.service"; +import { BaseComponent } from "app/shared/components/base/base.component"; +import { BackendError } from "app/shared/models/backend-error"; +import { FileTree } from "app/shared/models/file-tree"; +import { NotificationService } from "app/shared/services/notification.service"; +import { Observable, of } from "rxjs"; +import { takeUntil } from "rxjs/operators"; +import { DefaultViewerComponent } from "./default-viewer.component"; + +@Injectable({ + providedIn: 'root' +}) +export class DefaultViewerFactory extends BaseComponent { + constructor( + private solutionService: SolutionService, + private notificationService: NotificationService + ) { + super(); + } + + createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> { + const componentRef = containerRef.createComponent<DefaultViewerComponent>(DefaultViewerComponent); + componentRef.instance.tree = tree; + componentRef.instance.solution = solution; + return of(null); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2b8761ab671da56e72986d8aa26107734d6f1f9f --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.html @@ -0,0 +1,8 @@ +<nb-alert class="mb-0 nothing-here"> + <div class="content"> + <div class="msg">Nichts ausgewählt.</div> + <div class="text-muted mt-1"> + Die Lösung beinhaltet mehrere Dateien, wähle eine aus. + </div> + </div> +</nb-alert> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..d90a5c702e4e30a5c967792ae7a44bf8b18459a7 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.scss @@ -0,0 +1,14 @@ +.nothing-here { + text-align: center; + align-items: center; + justify-content: center; + height: 100%; + + .content { + max-width: 800px; + } + + .msg { + font-size: 20px; + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..abc7ab5de780968751542e52fba1ec08c91b1baf --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'ngx-nothing-viewer', + templateUrl: './nothing-viewer.component.html', + styleUrls: ['./nothing-viewer.component.scss'] +}) +export class NothingViewerComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bd3b2646d6da4f954562f9e6a5e79883ebc17e1 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/nothing-viewer/nothing-viewer.factory.ts @@ -0,0 +1,20 @@ +import { Injectable, ViewContainerRef } from "@angular/core"; +import { Solution } from "app/features/solutions/models/solution"; +import { BaseComponent } from "app/shared/components/base/base.component"; +import { FileTree } from "app/shared/models/file-tree"; +import { Observable, of } from "rxjs"; +import { NothingViewerComponent } from "./nothing-viewer.component"; + +@Injectable({ + providedIn: 'root' +}) +export class NothingViewerFactory extends BaseComponent { + constructor() { + super(); + } + + createViewer(containerRef: ViewContainerRef): Observable<void> { + containerRef.createComponent<NothingViewerComponent>(NothingViewerComponent); + return of(null); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html new file mode 100644 index 0000000000000000000000000000000000000000..30d8b3f67f8bd97ee5d5ec8e525cf8a4a304b85e --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.html @@ -0,0 +1 @@ +<embed *ngIf="pdfEmbedUrl" [src]="pdfEmbedUrl" type="application/pdf"> diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..137e8da861d68caa560752d96d9b452342aa3ca9 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.scss @@ -0,0 +1,4 @@ +embed { + height: 100%; + width: 100%; +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..892cc111b9645d326759f10483195045d0fda4df --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PdfViewerComponent } from './pdf-viewer.component'; + +describe('PdfViewerComponent', () => { + let component: PdfViewerComponent; + let fixture: ComponentFixture<PdfViewerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PdfViewerComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PdfViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab152ab493d52f509f6d840dd52630da180047f2 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'; + +@Component({ + selector: 'ngx-pdf-viewer', + templateUrl: './pdf-viewer.component.html', + styleUrls: ['./pdf-viewer.component.scss'] +}) +export class PdfViewerComponent implements OnInit { + @Input() url: string; + pdfEmbedUrl: SafeResourceUrl; + + constructor( + private sanitizer: DomSanitizer + ) { } + + ngOnInit(): void { + this.pdfEmbedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.url + '#view=FitH'); + } + +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..778996ec29b162db1ee33a7c588160a51dba3bd5 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/pdf-viewer/pdf-viewer.factory.ts @@ -0,0 +1,44 @@ +import { Injectable, ViewContainerRef } from "@angular/core"; +import { Solution } from "app/features/solutions/models/solution"; +import { SolutionService } from "app/features/solutions/services/solution.service"; +import { BaseComponent } from "app/shared/components/base/base.component"; +import { BackendError } from "app/shared/models/backend-error"; +import { FileTree } from "app/shared/models/file-tree"; +import { NotificationService } from "app/shared/services/notification.service"; +import { Observable } from "rxjs"; +import { catchError, map, takeUntil } from "rxjs/operators"; +import { PdfViewerComponent } from "./pdf-viewer.component"; + +@Injectable({ + providedIn: 'root' +}) +export class PdfViewerFactory extends BaseComponent { + constructor( + private solutionService: SolutionService, + private notificationService: NotificationService + ) { + super(); + } + + createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> { + return this.solutionService.getFileAsUrl(solution.id, tree) + .pipe( + takeUntil(this.unsubscribe), + catchError((error: any) => { + if (BackendError.isBackendError(error)) { + this.notificationService.showError( + error.format( + "Fehler beim Herunterladen der Lösung", + ) + ); + } + + throw error; + }), + map(([url, fileName]) => { + const componentRef = containerRef.createComponent<PdfViewerComponent>(PdfViewerComponent); + componentRef.instance.url = url; + }), + ); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html new file mode 100755 index 0000000000000000000000000000000000000000..41a9be882da89c9c47b6a31559cfd8e4ea72a22e --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.html @@ -0,0 +1,10 @@ +<div *ngIf="file" class="h-100"> + + <ngx-codemirror class="fillh" [(ngModel)]="file.content" [options]="{ + lineNumbers: true, + mode: file.tree.contentType | codemirrorMode, + theme: darkMode | codemirrorTheme, + readOnly: 'nocursor' + }"></ngx-codemirror> + +</div> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss new file mode 100755 index 0000000000000000000000000000000000000000..2435b40a30b3a890650d115deaa388758e7f0c49 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.scss @@ -0,0 +1,8 @@ +@use 'themes' as *; + +nb-card { + height: 100%; + border: 0px; + border-radius: 0 !important; + background-color: nb-theme(background-basic-color-1) +} \ No newline at end of file diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.spec.ts similarity index 100% rename from subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.spec.ts rename to subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.spec.ts diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts similarity index 75% rename from subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts rename to subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts index e6f01f94abcfcc17f2ef20bc3c59f2e4f14bdbf6..0c341cae82acd7e335669e14d5d650029991f6ba 100755 --- a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.ts +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.component.ts @@ -5,8 +5,8 @@ import { FileTree } from 'app/shared/models/file-tree'; import { AppStateService } from 'app/core/services/app.state.service'; export interface File { - reference: FileTree; - content?: string | Blob; + tree: FileTree; + content?: string; } @Component({ @@ -16,8 +16,6 @@ export interface File { }) export class SourceCodeViewerComponent extends BaseComponent implements OnInit { @Input() file: File; - @Output() downloadTreeEvent = new EventEmitter<FileTree>(); - @Output() toggleEvent = new EventEmitter<void>(); darkMode = false; constructor( @@ -31,12 +29,4 @@ export class SourceCodeViewerComponent extends BaseComponent implements OnInit { .pipe(takeUntil(this.unsubscribe)) .subscribe(darkMode => this.darkMode = darkMode); } - - open() { - this.downloadTreeEvent.emit(this.file.reference); - } - - toggle() { - this.toggleEvent.emit(); - } } diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..42a16a4173d6035240d1852c376cb000e61a3347 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/source-code-viewer/source-code-viewer.factory.ts @@ -0,0 +1,49 @@ +import { Injectable, ViewContainerRef } from "@angular/core"; +import { Solution } from "app/features/solutions/models/solution"; +import { SolutionService } from "app/features/solutions/services/solution.service"; +import { BaseComponent } from "app/shared/components/base/base.component"; +import { BackendError } from "app/shared/models/backend-error"; +import { FileTree } from "app/shared/models/file-tree"; +import { NotificationService } from "app/shared/services/notification.service"; +import { Observable } from "rxjs"; +import { catchError, map, takeUntil } from "rxjs/operators"; +import { SourceCodeViewerComponent } from "./source-code-viewer.component"; + +@Injectable({ + providedIn: 'root' +}) +export class SourceCodeViewerFactory extends BaseComponent { + constructor( + private solutionService: SolutionService, + private notificationService: NotificationService + ) { + super(); + } + + createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<void> { + return this.solutionService.getFile(solution.id, tree) + .pipe( + takeUntil(this.unsubscribe), + catchError((error: any) => { + if (BackendError.isBackendError(error)) { + this.notificationService.showError( + error.format( + "Fehler beim Herunterladen der Lösung", + ) + ); + } + + throw error; + }), + map(content => { + let file = { + tree: tree, + content: content + }; + + const componentRef = containerRef.createComponent<SourceCodeViewerComponent>(SourceCodeViewerComponent); + componentRef.instance.file = file; + }) + ); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..518c2a268fe1e3ad350b766c3bbc34575d528541 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer-host.directive.ts @@ -0,0 +1,8 @@ +import { Directive, ViewContainerRef } from '@angular/core'; + +@Directive({ + selector: '[viewerHost]', +}) +export class ViewerHostDirective { + constructor(public viewContainerRef: ViewContainerRef) { } +} diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..0fdfff49d60640091fdd5736ea293f51fc070286 --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.factory.ts @@ -0,0 +1,71 @@ +import { Injectable, ViewContainerRef } from "@angular/core"; +import { Solution } from "app/features/solutions/models/solution"; +import { BaseComponent } from "app/shared/components/base/base.component"; +import { FileTree } from "app/shared/models/file-tree"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { DefaultViewerFactory } from "./default-viewer/default-viewer.factory"; +import { NothingViewerFactory } from "./nothing-viewer/nothing-viewer.factory"; +import { PdfViewerFactory } from "./pdf-viewer/pdf-viewer.factory"; +import { SourceCodeViewerFactory } from "./source-code-viewer/source-code-viewer.factory"; +import { Viewer } from "./viewer"; + +@Injectable({ + providedIn: 'root' +}) +export class ViewerFactory extends BaseComponent { + constructor( + private sourceCodeViewerFactory: SourceCodeViewerFactory, + private pdfViewerFactory: PdfViewerFactory, + private nothingViewerFactory: NothingViewerFactory, + private defaultViewerFactory: DefaultViewerFactory, + ) { + super(); + } + + private getViewer(tree: FileTree): Viewer { + if (tree == null || tree.directory) { + return Viewer.none; + } + + if (tree.src) { + return Viewer.sourceCode; + } + + if (!tree.src && tree.isPdf) { + return Viewer.pdf; + } + + return Viewer.default; + } + + createViewer(solution: Solution, tree: FileTree, containerRef: ViewContainerRef): Observable<Viewer> { + + let viewer = this.getViewer(tree); + let create$: Observable<void>; + + if (viewer == Viewer.none) { + create$ = this.nothingViewerFactory.createViewer(containerRef); + } + + else if (viewer == Viewer.pdf) { + create$ = this.pdfViewerFactory.createViewer(solution, tree, containerRef); + } + + else if (viewer == Viewer.sourceCode) { + create$ = this.sourceCodeViewerFactory.createViewer(solution, tree, containerRef); + } + + else if (viewer == Viewer.default) { + create$ = this.defaultViewerFactory.createViewer(solution, tree, containerRef); + } + + else { + throw `no factory for viewer ${viewer}`; + } + + return create$.pipe( + map(() => viewer) + ); + } +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts new file mode 100644 index 0000000000000000000000000000000000000000..7cf4a1abae00a27097920f9af78a8c4e793da31b --- /dev/null +++ b/subato-web/src/app/features/solutions/components/solution-ide/viewer/viewer.ts @@ -0,0 +1,6 @@ +export enum Viewer { + none, + default, + sourceCode, + pdf +} \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html deleted file mode 100644 index c4ad412dbf48ecf520f8808261a28cbb294db7ef..0000000000000000000000000000000000000000 --- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.html +++ /dev/null @@ -1,11 +0,0 @@ -<div class="row"> - <div *ngIf="!compactMode" class="col-12 col-lg-3"> - <ngx-file-browser [tree]="solution.tree" (treeSelectedEvent)="onTreeSelected($event)"></ngx-file-browser> - </div> - <div class="col-12" [class.col-lg-9]="!compactMode" [class.col-lg-12]="compactMode"> - <span *ngIf="!currentTree">Nichts ausgewählt</span> - <ngx-source-code-viewer *ngIf="currentTree && viewer == FileViewer.sourceCode" [file]="currentFile" (toggleEvent)="compactMode = !compactMode" (downloadTreeEvent)="onDownloadTree($event)"></ngx-source-code-viewer> - <embed *ngIf="currentTree && viewer == FileViewer.pdf && pdfEmbedUrl" [src]="pdfEmbedUrl" style="height: 600px;width: 100%;" type="application/pdf"> - <span *ngIf="currentTree && viewer == FileViewer.none">Diese Datei kann nicht dargestellt werden.</span> - </div> -</div> \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts deleted file mode 100644 index 7fca4c23cd87556c0b1d75a06a948f7fee4a304f..0000000000000000000000000000000000000000 --- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SolutionViewerComponent } from './solution-viewer.component'; - -describe('SolutionViewerComponent', () => { - let component: SolutionViewerComponent; - let fixture: ComponentFixture<SolutionViewerComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ SolutionViewerComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(SolutionViewerComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts b/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts deleted file mode 100644 index 74648e8f53b42a69549f08d5c44ea90c24902571..0000000000000000000000000000000000000000 --- a/subato-web/src/app/features/solutions/components/solution-viewer/solution-viewer.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { BaseComponent } from 'app/shared/components/base/base.component'; -import { FileTree } from 'app/shared/models/file-tree'; -import { takeUntil } from 'rxjs/operators'; -import { SolutionService } from '../../services/solution.service'; -import { File } from 'app/shared/components/source-code-viewer/source-code-viewer.component'; -import { NotificationService } from 'app/shared/services/notification.service'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { BackendError } from 'app/shared/models/backend-error'; -import { Solution } from '../../models/solution'; - -enum FileViewer { - sourceCode, - pdf, - none -} - -@Component({ - selector: 'solution-viewer', - templateUrl: './solution-viewer.component.html', - styleUrls: ['./solution-viewer.component.scss'] -}) -export class SolutionViewerComponent extends BaseComponent implements OnInit { - loading: boolean; - currentTree: FileTree; - currentFile: File; - _solution: Solution; - compactMode: boolean = false; - - FileViewer = FileViewer; - pdfEmbedUrl: SafeResourceUrl; - - constructor( - private solutionService: SolutionService, - private notificationService: NotificationService, - private sanitizer: DomSanitizer - ) { - super(); - } - - ngOnInit(): void { - } - - @Input() set solution(val: Solution) { - this._solution = val; - if (this._solution.tree.children.length != 0) { - let first = this._solution.tree.children[0] - this.compactMode = val.tree.children.length == 1 && first.children.length == 0; - this.onTreeSelected(first); - } - } - - get solution() { - return this._solution; - } - - get viewer(): FileViewer { - if (this.currentTree == null) { - return null; - } - - if (this.currentTree.src) { - return FileViewer.sourceCode; - } - - if (!this.currentTree.src && this.currentTree.isPdf) { - return FileViewer.pdf; - } - - return FileViewer.none; - } - - onTreeSelected(tree: FileTree) { - if (tree.children.length != 0) { - return; - } - - this.currentTree = tree; - this.currentFile = { - reference: tree, - content: this.currentFile?.content - }; - - if(this.viewer == FileViewer.pdf) { - this.solutionService.getFileAsUrl(this.solution.id, tree) - .pipe(takeUntil(this.unsubscribe)) - .subscribe(([url, fileName]) => { - this.pdfEmbedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url + '#view=FitH'); - }, (error: any) => { - if (BackendError.isBackendError(error)) { - this.notificationService.showError( - error.format( - "Fehler beim Herunterladen der Lösung", - ) - ); - } else { - throw error; - } - }); - } - - if (this.viewer == FileViewer.sourceCode) { - this.solutionService.getFile(this.solution.id, tree) - .pipe(takeUntil(this.unsubscribe)) - .subscribe(content => { - this.currentFile = { - reference: tree, - content: content - }; - }, (error: any) => { - if (BackendError.isBackendError(error)) { - this.notificationService.showError( - error.format( - "Fehler beim Herunterladen der Lösung", - ) - ); - } else { - throw error; - } - }); - } - } - - onDownloadTree(tree: FileTree) { - this.solutionService.downloadFile(this.solution.id, tree) - .pipe(takeUntil(this.unsubscribe)) - .subscribe(() => {}, (error: any) => { - if (BackendError.isBackendError(error)) { - this.notificationService.showError( - error.format( - "Fehler beim Herunterladen der Lösung", - ) - ); - } else { - throw error; - } - }); - } -} diff --git a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html index 44b6288bb8f1653fc5fbf3fa1a95b38333c29ca9..e366caf158bee90fa63ebd9ac70f0c83a9d23342 100644 --- a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html +++ b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.html @@ -8,6 +8,7 @@ <div *ngIf="selectedMode == SubmissionMode.FILE" class="form-group"> <label for="file" class="label">Datei</label> <dropzone id="file" (fileChanged)="handleFile($event)"></dropzone> + <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small> </div> <div *ngIf="selectedMode == SubmissionMode.TEXT" class="form-group"> <label for="solutionContent" class="label">Lösung</label> diff --git a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts index bbd2d2ab9b56edbf0d0055e2c799bb6f19d97bdd..296e55d69bc4a097fcb49d121c803d5a646a8bbe 100644 --- a/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts +++ b/subato-web/src/app/features/solutions/components/submit-solution-form/submit-solution-form.component.ts @@ -21,6 +21,7 @@ import { Client } from '../../models/client'; import { Solution } from '../../models/solution'; import { SolutionService, SubmitFileSolution, SubmitInlineSolution } from '../../services/solution.service'; import { SubatoPrincipal } from 'app/core/models/principal/subato-principal'; +import { PublicConfig } from 'app/core/models/public-config'; @Component({ selector: 'ngx-submit-solution-form', @@ -42,6 +43,7 @@ export class SubmitSolutionFormComponent extends BaseComponent implements OnInit templateFile: FileRef; darkMode = false; principal: SubatoPrincipal; + config: PublicConfig; SubmissionMode = SubmissionMode; @@ -69,6 +71,7 @@ export class SubmitSolutionFormComponent extends BaseComponent implements OnInit private notificationService: NotificationService, private fb: UntypedFormBuilder) { super(); + this.config = appStateService.config; } ngAfterViewInit(): void { diff --git a/subato-web/src/app/features/solutions/models/solution.ts b/subato-web/src/app/features/solutions/models/solution.ts index f14ba31af5d365d96b34b67fc039d240c4cd1875..fc92ac7227aa97b5bff0d02e710f9dd12ca5b9aa 100755 --- a/subato-web/src/app/features/solutions/models/solution.ts +++ b/subato-web/src/app/features/solutions/models/solution.ts @@ -9,6 +9,7 @@ import { Iso8601Helper } from "app/shared/helper/iso8601.helper"; import { Task } from "app/features/tasks/models/task"; import { Exercise } from "app/features/exercises/models/exercise"; import { Submission } from "app/features/exercises/models/submission"; +import { Client } from "./client"; export class Solution { id: number; @@ -23,6 +24,7 @@ export class Solution { attemptsExceeded: boolean; comments: SolutionComment[]; commentsCount: number; + submittedWith: Client; constructor(solution: any) { this.id = solution.id; @@ -34,6 +36,7 @@ export class Solution { this.evalResult = solution.evalResult ? new EvalResult(solution.evalResult) : null; this.submission = solution.submission ? new Submission(solution.submission) : null; this.delayed = solution.delayed; + this.submittedWith = solution.submittedWith; this.attemptsExceeded = solution.attemptsExceeded; this.commentsCount = solution.commentsCount; this.comments = solution.comments ? solution.comments.map(c => new SolutionComment(c)) : []; diff --git a/subato-web/src/app/features/solutions/pages/solution/solution.component.html b/subato-web/src/app/features/solutions/pages/solution/solution.component.html index cbabfcb445d45747c5250dea4a0e8631a27c864e..8432e0072642c66563a7fa304c8abd76e27d8b87 100755 --- a/subato-web/src/app/features/solutions/pages/solution/solution.component.html +++ b/subato-web/src/app/features/solutions/pages/solution/solution.component.html @@ -14,8 +14,12 @@ <ng-container *ngIf="!solution.exercise">{{ solution.submittedBy | userName }}</ng-container> </ng-container> um {{ solution.submittedAt | date:'medium' }} + via + <span *ngIf="solution.submittedWith == Client.Web" class="badge badge-primary">Web</span> + <span *ngIf="solution.submittedWith == Client.EclipsePlugin" class="badge badge-primary">Eclipse-Plugin</span> </span> </div> + </nb-card-header> </nb-card> </div> @@ -38,6 +42,11 @@ Du hast nach Ende der Abgabefrist abgegeben oder die maximale Anzahl an Versuchen erreicht. Deine Lösung wird uns aber als nachgereicht angezeigt. </nb-alert> + + <div class="row"> + <div class="col-12"> + </div> + </div> <div class="row"> <div [class.col-xl-9]="solution.exercise" class="col-12"> @@ -55,8 +64,8 @@ <nb-accordion-item-header> Lösung </nb-accordion-item-header> - <nb-accordion-item-body> - <solution-viewer [solution]="solution"></solution-viewer> + <nb-accordion-item-body class="no-padding"> + <solution-ide [solution]="solution"></solution-ide> </nb-accordion-item-body> </nb-accordion-item> </nb-accordion> diff --git a/subato-web/src/app/features/solutions/pages/solution/solution.component.ts b/subato-web/src/app/features/solutions/pages/solution/solution.component.ts index 4bbafda305ef005d95dd83b60c3ac97a4a66ca65..708a82f3b81eb5b2efa4b28ccb3215501ead3fbb 100755 --- a/subato-web/src/app/features/solutions/pages/solution/solution.component.ts +++ b/subato-web/src/app/features/solutions/pages/solution/solution.component.ts @@ -16,6 +16,7 @@ import { SubatoPrincipal } from 'app/core/models/principal/subato-principal'; import { Solution } from '../../models/solution'; import { SolutionService } from '../../services/solution.service'; import { SolutionBreadcrumbBuilder } from './solution-breadcrumb-builder'; +import { Client } from '../../models/client'; @Component({ selector: 'ngx-solution', @@ -27,6 +28,8 @@ export class SolutionComponent extends BaseComponent implements OnInit { taskInstances: TaskInstance[]; principal: SubatoPrincipal; + Client = Client; + loading: boolean = true; justSubmitted = false; diff --git a/subato-web/src/app/features/solutions/services/solution.service.ts b/subato-web/src/app/features/solutions/services/solution.service.ts index bd937940eea912c9f7c77fdbe5eb1395573dc907..66ca42b0e6b6b5608460f6c846fe2526940c4360 100755 --- a/subato-web/src/app/features/solutions/services/solution.service.ts +++ b/subato-web/src/app/features/solutions/services/solution.service.ts @@ -152,4 +152,8 @@ export class SolutionService extends BaseService { downloadFile(solutionId: number, item: FileTree): Observable<void> { return this.downloadService.downloadFile(this.getFileUrl(solutionId, item.uuid), true); } + + downloadSolution(solutionId: number): Observable<void> { + return this.downloadService.downloadFile(`${environment.backendUrl}/solution/${solutionId}/download`, true); + } } \ No newline at end of file diff --git a/subato-web/src/app/features/solutions/solutions.module.ts b/subato-web/src/app/features/solutions/solutions.module.ts index fa044425e91fa0bd208a305aff19f23c8e4bc646..1e8f812e2ed8ef4f91336e3d9a29f5639d1ec94e 100644 --- a/subato-web/src/app/features/solutions/solutions.module.ts +++ b/subato-web/src/app/features/solutions/solutions.module.ts @@ -8,8 +8,9 @@ import { SubmitSolutionFormComponent } from './components/submit-solution-form/s import { SolutionsRoutingModule } from './solutions-routing.module'; import { SolutionComponent } from './pages/solution/solution.component'; import { EvalResultComponent } from './components/eval-result/eval-result.component'; -import { SolutionViewerComponent } from './components/solution-viewer/solution-viewer.component'; import { AcceptanceTestComponent } from './components/acceptance-test/acceptance-test.component'; +import { AngularSplitModule } from 'angular-split'; +import { SolutionIdeModule } from './components/solution-ide/solution-ide.module'; @NgModule({ @@ -17,26 +18,27 @@ import { AcceptanceTestComponent } from './components/acceptance-test/acceptance SolutionComponent, AcceptanceTestComponent, - SolutionViewerComponent, EvalResultComponent, SolutionHistoryComponent, SolutionCommentsComponent, SolutionFeedbackScreenComponent, - SubmitSolutionFormComponent + SubmitSolutionFormComponent, ], imports: [ CommonModule, SharedModule, - SolutionsRoutingModule + SolutionsRoutingModule, + AngularSplitModule, + SolutionIdeModule ], exports: [ SubmitSolutionFormComponent, - SolutionViewerComponent, SubmitSolutionFormComponent, SolutionHistoryComponent, SolutionCommentsComponent, AcceptanceTestComponent, - EvalResultComponent + EvalResultComponent, + SolutionIdeModule ] }) export class SolutionsModule { } diff --git a/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html b/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html index c1ef489de90d1190a075ff9eac559b389f064f83..7d1555e36738a2eb23aafb94caf69400867f5f69 100644 --- a/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html +++ b/subato-web/src/app/features/tasks/components/languages-list/languages-list.component.html @@ -33,11 +33,12 @@ <nb-icon icon="edit-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Bearbeiten</span> </a> - <a *ngIf="principal && principal.hasSuperAdminRole()" nbButton class="ml-2" + <button *ngIf="principal && principal.hasSuperAdminRole()" nbButton class="ml-2" + [disabled]="deletingLanguage == lang" [nbSpinner]="deletingLanguage == lang" (click)="deleteLanguage(lang)" [status]="'danger'"> <nb-icon icon="trash-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span> - </a> + </button> </div> </nb-list-item> </nb-list> diff --git a/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html b/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html index 609e83c52764710b2d041559a2abc21eb494324a..25d1c0d9de45a7bccd245187956bcb47960a3563 100644 --- a/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html +++ b/subato-web/src/app/features/tasks/components/task-feedback-info/task-feedback-info.component.html @@ -34,9 +34,9 @@ </div> </div> <div class="interaction"> - <a nbButton (click)="$event.stopPropagation(); markAsRead(fb)" [status]="'primary'"> + <button nbButton (click)="$event.stopPropagation(); markAsRead(fb)" [status]="'primary'"> <nb-icon icon="checkmark-outline" pack="eva"></nb-icon> Gelesen - </a> + </button> </div> </nb-list-item> </nb-card-body> diff --git a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html index 5511fbef554195267dd0bfa702169fd3d69b3937..270872f6baaaa098080d272d3c7a32095723f429 100755 --- a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html +++ b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.html @@ -8,7 +8,10 @@ <span>Die Aufgabe muss als zip-Datei im <a [href]="stefDocsUrl" target="_blank">Subato Task Exchange Format (STEF)</a> hochgeladen werden. </span> </nb-alert> - <dropzone [file]="file" (fileChanged)="handleFile($event)"></dropzone> + <div class="mb-3"> + <dropzone [file]="file" (fileChanged)="handleFile($event)"></dropzone> + <small>Die maximale Dateigröße beträgt {{ config.maxUploadSizeInMb }} MB.</small> + </div> <div style="display: flex; justify-content: space-between"> <button nbButton [nbSpinner]="importing" [status]="'success'" [disabled]="fileForm.invalid || importing" (click)="save()"> diff --git a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts index ad00d7020ea1917999df6d18d2894f9326a16255..7e058ce5c93c1b20852a196fb77328453b002599 100755 --- a/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts +++ b/subato-web/src/app/features/tasks/components/task-upload/task-upload.component.ts @@ -11,6 +11,8 @@ import {DropzoneComponent} from 'app/shared/components/dropzone/dropzone.compone import { BackendError } from 'app/shared/models/backend-error'; import { TaskPool } from '../../models/task-pool'; import { environment } from 'environments/environment'; +import { PublicConfig } from 'app/core/models/public-config'; +import { AppStateService } from 'app/core/services/app.state.service'; @Component({ selector: 'ngx-task-upload', @@ -26,15 +28,18 @@ export class TaskUploadComponent extends FormComponent implements OnInit { task: Task; file: File; + config: PublicConfig; stefDocsUrl = environment.stefDocsUrl; constructor( private taskPoolService: TaskPoolService, + private appStateService: AppStateService, private router: Router, private route: ActivatedRoute, private notificationService: NotificationService, private fb: UntypedFormBuilder) { super(); + this.config = appStateService.config } ngOnInit(): void { diff --git a/subato-web/src/app/features/tasks/models/task.ts b/subato-web/src/app/features/tasks/models/task.ts index 3805f0a7750a6f267a74238b89c74dae3c069deb..84e42caba50532678756fa51f697f71151158aab 100755 --- a/subato-web/src/app/features/tasks/models/task.ts +++ b/subato-web/src/app/features/tasks/models/task.ts @@ -22,6 +22,7 @@ export class Task { feedbackStats: TaskFeedbackStats; acceptanceTestAvailable: boolean; evaluator: Evaluator; + repositoryUrl: string; constructor(task: any) { this.id = task.id; @@ -39,5 +40,6 @@ export class Task { this.language = task.language ? new Language(task.language) : null; this.acceptanceTestAvailable = task.acceptanceTestAvailable; this.supportedByEclipsePlugin = task.supportedByEclipsePlugin; + this.repositoryUrl = task.repositoryUrl; } } \ No newline at end of file diff --git a/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html b/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html index 35be0bc4fab542eac95d47802b4ba7dc8cf3d2cf..36b793631d0c29f710b92ff32408de7b678cb8d2 100755 --- a/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html +++ b/subato-web/src/app/features/tasks/pages/task-pool/task-pool.component.html @@ -22,19 +22,19 @@ </div> </div> <div style="align-self: center; gap: 15px" class="d-flex"> - <a *ngIf="taskPool.linkedToRepository" nbButton [status]="'basic'" [nbSpinner]="syncing" (click)="sync()"> + <button *ngIf="taskPool.linkedToRepository" nbButton [status]="'basic'" [disabled]="syncing" [nbSpinner]="syncing" (click)="sync()"> <nb-icon icon="refresh-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Synchronisieren</span> - </a> + </button> <a nbButton [routerLink]="['edit']" [status]="'basic'"> <nb-icon icon="edit-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Bearbeiten</span> </a> - <a nbButton *ngIf="principal.hasSuperAdminRole()" [nbSpinner]="deletingPool" + <button nbButton *ngIf="principal.hasSuperAdminRole()" [disabled]="deletingPool" [nbSpinner]="deletingPool" (click)="$event.stopPropagation(); deletePool()" [status]="'danger'"> <nb-icon icon="trash-2-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Löschen</span> - </a> + </button> </div> </nb-card-header> </nb-card> @@ -50,7 +50,7 @@ <div class="row"> <div class="col-lg-8 col-12"> <nb-alert *ngIf="taskPool.linkedToRepository" accent="primary"> - <span>Dieser Aufgaben-Pool ist mit einem <a [href]="taskPool.repositoryUrl">Git-Repository</a> + <span>Dieser Aufgaben-Pool ist mit einem <a [href]="taskPool.repositoryUrl" target="_blank">Git-Repository</a> verknüpft.</span> <span>Für jede Aufgabe muss im Repository ein Ordner im <a [href]="stefDocsUrl" target="_blank">Subato Task Exchange Format (STEF)</a> angelegt werden. </span> diff --git a/subato-web/src/app/features/tasks/pages/task/task.component.html b/subato-web/src/app/features/tasks/pages/task/task.component.html index a64de79517461272f85693e2fe1afe04e00e59c2..8dbbef5534f6b83c49167b922811d83d0ee3bc5f 100644 --- a/subato-web/src/app/features/tasks/pages/task/task.component.html +++ b/subato-web/src/app/features/tasks/pages/task/task.component.html @@ -16,16 +16,16 @@ <div *ngIf="task.description" class="mt-1 text-muted" [innerHTML]="task.description"></div> </div> <div style="align-self: center"> - <a nbButton (click)="$event.stopPropagation(); downloadSpec()" + <button nbButton (click)="$event.stopPropagation(); downloadSpec()" [status]="'basic'"> <nb-icon icon="download-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Herunterladen</span> - </a> - <a nbButton [nbSpinner]="archivingTask" + </button> + <button *ngIf="!task.pool.linkedToRepository" nbButton [nbSpinner]="archivingTask" [disabled]="archivingTask" (click)="$event.stopPropagation(); archiveTask()" [status]="'danger'" class="ml-2"> <nb-icon icon="archive-outline" pack="eva"></nb-icon> <span class="d-none d-md-block d-lg-block d-xl-block">Archivieren</span> - </a> + </button> </div> </nb-card-header> </nb-card> @@ -38,11 +38,23 @@ </div> </div> + <div class="row"> + <div class="col-12"> + + </div> + </div> + <div class="row"> <div class="col-lg-8 col-12"> <div class="row"> <div class="col-12"> + <nb-alert *ngIf="task.pool.linkedToRepository && task.repositoryUrl" accent="primary"> + <span>Diese Aufgabe stammt aus einem <a [href]="task.pool.repositoryUrl" target="_blank">Git-Repository</a> + und kann <a [href]="task.repositoryUrl" target="_blank">hier</a> direkt eingesehen werden.</span> + </nb-alert> <ngx-task-summary [task]="task"></ngx-task-summary> + + </div> </div> diff --git a/subato-web/src/app/features/tasks/pages/task/task.component.ts b/subato-web/src/app/features/tasks/pages/task/task.component.ts index 584c872ed1d995a6630caf83d8e00698a3868085..076e864c3017b02cce7f9472cf4e889283d73669 100644 --- a/subato-web/src/app/features/tasks/pages/task/task.component.ts +++ b/subato-web/src/app/features/tasks/pages/task/task.component.ts @@ -36,7 +36,7 @@ export class TaskComponent extends BaseComponent implements OnInit { }; breadcrumbs: Breadcrumb[] = []; - + constructor( private authService: AuthService, private taskService: TaskService, diff --git a/subato-web/src/app/shared/components/dropzone/dropzone.component.html b/subato-web/src/app/shared/components/dropzone/dropzone.component.html index 89e8ef07c0aaffb5a2112e0d81e0ec8e8cf81fe5..d680f734e691c8fef266f7a07e40af89de5e9ff2 100644 --- a/subato-web/src/app/shared/components/dropzone/dropzone.component.html +++ b/subato-web/src/app/shared/components/dropzone/dropzone.component.html @@ -1,4 +1,4 @@ -<div class="custom-dropzone mb-3" ngx-dropzone (change)="fileSelected($event)"> +<div class="custom-dropzone mb-1" ngx-dropzone (change)="fileSelected($event)"> <p *ngIf="file == null" class="center-font">Datei hier ablegen (oder klicken)</p> <div *ngIf="file != null" class="custom-preview" [removable]="true"> <div class="custom-label">{{ file.name }} ({{ file.type }})</div> diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.html b/subato-web/src/app/shared/components/file-browser/file-browser.component.html deleted file mode 100755 index 8c91121b444826924c2ffe0e1e86885c078da2ec..0000000000000000000000000000000000000000 --- a/subato-web/src/app/shared/components/file-browser/file-browser.component.html +++ /dev/null @@ -1,26 +0,0 @@ -<nb-card class="h-100"> - <nb-card-body style="padding: 0px;"> - <table [nbTreeGrid]="data" equalColumnsWidth> - - <tr nbTreeGridRow *nbTreeGridRowDef="let row; columns: allColumns" style="cursor: pointer;"></tr> - - <ng-container [nbTreeGridColumnDef]="customColumn"> - <td nbTreeGridCell *nbTreeGridCellDef="let row" (click)="onTreeClicked(row.data)" style="cursor: pointer;"> - - <nb-tree-grid-row-toggle - [expanded]="row.expanded" - *ngIf="row.data.children.length !== 0"> - </nb-tree-grid-row-toggle> - - {{row.data.name}} - - </td> - </ng-container> - - <ng-container *ngFor="let column of defaultColumns" [nbTreeGridColumnDef]="column"> - <td nbTreeGridCell *nbTreeGridCellDef="let row">{{row.data[column]}}</td> - </ng-container> - - </table> - </nb-card-body> -</nb-card> \ No newline at end of file diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.scss b/subato-web/src/app/shared/components/file-browser/file-browser.component.scss deleted file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/subato-web/src/app/shared/components/file-browser/file-browser.component.ts b/subato-web/src/app/shared/components/file-browser/file-browser.component.ts deleted file mode 100755 index bdcaad5c1092df7e95bc5f1e5da89e59ce689575..0000000000000000000000000000000000000000 --- a/subato-web/src/app/shared/components/file-browser/file-browser.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FileTree } from 'app/shared/models/file-tree'; - -interface TreeNode<T> { - data: T; - children?: TreeNode<T>[]; - expanded?: boolean; -} - -@Component({ - selector: 'ngx-file-browser', - templateUrl: './file-browser.component.html', - styleUrls: ['./file-browser.component.scss'] -}) -export class FileBrowserComponent implements OnInit { - @Output() treeSelectedEvent = new EventEmitter<FileTree>(); - @Input() tree: FileTree; - - data: TreeNode<FileTree>[] = []; - - customColumn = 'filename'; - defaultColumns = []; - allColumns = [ this.customColumn, ...this.defaultColumns ]; - - constructor() { } - - private treeNodeFor(tree: FileTree): TreeNode<FileTree> { - return { - data: tree, - children: tree.children.map(child => this.treeNodeFor(child)), - expanded: false - }; - } - - ngOnInit(): void { - this.data = this.tree.children.map(tree => this.treeNodeFor(tree)); - } - - onTreeClicked(tree: FileTree) { - if(tree.children.length === 0) { - this.treeSelectedEvent.emit(tree); - } - } -} diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html b/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html deleted file mode 100755 index 372f150471617dbe2283421dab15d971d648638a..0000000000000000000000000000000000000000 --- a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.html +++ /dev/null @@ -1,23 +0,0 @@ -<div *ngIf="file"> - <nb-card [nbSpinner]="file.content == null" nbSpinnerStatus="basic"> - <nb-card-header class="d-flex" style="align-items: center;"> - <a nbButton (click)="toggle()" [status]="'basic'" class="mr-3"> - <nb-icon icon="menu-outline" pack="eva"></nb-icon> - </a> - <span style="flex-grow: 1">{{ file.reference.name }}</span> - <div> - <a nbButton (click)="open()" [status]="'basic'"> - <nb-icon icon="download-outline" pack="eva"></nb-icon> - </a> - </div> - </nb-card-header> - <nb-card-body class="p-0" style="max-height: 600px;"> - <ngx-codemirror class="fillh" [(ngModel)]="file.content" style="" [options]="{ - lineNumbers: true, - mode: file.reference.contentType | codemirrorMode, - theme: darkMode | codemirrorTheme, - readOnly: 'nocursor' - }"></ngx-codemirror> - </nb-card-body> - </nb-card> -</div> \ No newline at end of file diff --git a/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.scss b/subato-web/src/app/shared/components/source-code-viewer/source-code-viewer.component.scss deleted file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/subato-web/src/app/shared/directives/maximize.directive.ts b/subato-web/src/app/shared/directives/maximize.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..893402b92f01de2b41b601c463fba07958ad407d --- /dev/null +++ b/subato-web/src/app/shared/directives/maximize.directive.ts @@ -0,0 +1,50 @@ +import { Directive, ElementRef, HostListener, Inject, Renderer2 } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { tap } from "rxjs/operators"; +import {DOCUMENT, ViewportScroller} from '@angular/common'; + +// https://sreyaj.dev/fullscreen-toggle-angular-using-directives +// + enhanced by .noscroll + esc key + restore scroll pos + +@Directive({ + selector: "[maximize]", + exportAs: "maximize" // <-- Make not of this here +}) +export class MaximizeDirective { + private isMaximizedSubject = new BehaviorSubject(false); + isMaximized$ = this.isMaximizedSubject.pipe(); + scrollPosition: [number, number]; + + constructor( + private el: ElementRef, + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + private scroller: ViewportScroller) {} + + toggle() { + this.isMaximizedSubject?.getValue() ? this.minimize() : this.maximize(); + } + maximize() { + if (this.el) { + this.scrollPosition = this.scroller.getScrollPosition(); + this.isMaximizedSubject.next(true); + this.renderer.addClass(this.el.nativeElement, "fullscreen"); + this.renderer.addClass(this.document.body, 'noscroll'); + } + } + minimize() { + if (this.el) { + this.isMaximizedSubject.next(false); + this.renderer.removeClass(this.el.nativeElement, "fullscreen"); + this.renderer.removeClass(this.document.body, "noscroll"); + this.scroller.scrollToPosition(this.scrollPosition); + } + } + + @HostListener('document:keyup.escape', ['$event']) onEscKeyUpHandler(event: KeyboardEvent) { + let isMaximized = this.isMaximizedSubject?.getValue(); + if(isMaximized) { + this.minimize(); + } + } +} \ No newline at end of file diff --git a/subato-web/src/app/shared/models/file-tree.ts b/subato-web/src/app/shared/models/file-tree.ts index ac1b074a01e93a6e2c8f2fce1e7034d9282ca713..4ac0505ca166ca6d985d674f89b932bb12009c4f 100755 --- a/subato-web/src/app/shared/models/file-tree.ts +++ b/subato-web/src/app/shared/models/file-tree.ts @@ -4,6 +4,7 @@ export class FileTree { contentType: string; children: FileTree[]; src: boolean; + directory: boolean; constructor(item: any) { this.name = item.name; @@ -11,9 +12,29 @@ export class FileTree { this.contentType = item.contentType; this.children = item.children ? item.children.map(c => new FileTree(c)) : []; this.src = item.src; + this.directory = item.directory; } get isPdf() { - return this.name.endsWith(".pdf"); + return this.contentType == 'application/pdf'; + } + + filterByName(searchString: string): FileTree { + const filteredChildren = this.children + .map(child => child.filterByName(searchString)) + .filter(child => child !== null); + + if (this.name.toLowerCase().includes(searchString.toLowerCase()) || filteredChildren.length > 0) { + let tree = new FileTree({}); + tree.name = this.name; + tree.children = filteredChildren; + tree.contentType = this.contentType; + tree.src = this.src; + tree.uuid = this.uuid; + tree.directory = this.directory; + return tree; + } else { + return null; + } } } \ No newline at end of file diff --git a/subato-web/src/app/shared/shared.module.ts b/subato-web/src/app/shared/shared.module.ts index 0be377271ec0e47e24942801356556e22cd49500..168fd4a4a0d88c18dc2d1b4443f74ae3e3eb3aa8 100755 --- a/subato-web/src/app/shared/shared.module.ts +++ b/subato-web/src/app/shared/shared.module.ts @@ -7,8 +7,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NbMomentDateModule } from '@nebular/moment'; import { NbAlertModule, NbCardModule, NbIconModule, NbInputModule, NbBadgeModule, NbListModule, NbButtonModule, NbAccordionModule, NbDatepickerModule, NbTimepickerModule, NbToggleModule, NbSpinnerModule, NbCheckboxModule, NbSelectModule, NbTreeGridModule, NbFormFieldModule, NbActionsModule, NbContextMenuModule, NbLayoutModule, NbMenuModule, NbSearchModule, NbSidebarModule, NbUserModule, NbAutocompleteComponent, NbAutocompleteModule, NbTabsetModule, NbProgressBarModule, NbTagModule, NbPopoverModule, NbStepperModule } from '@nebular/theme'; import { BaseComponent } from './components/base/base.component'; -import { FileBrowserComponent } from './components/file-browser/file-browser.component'; -import { SourceCodeViewerComponent } from './components/source-code-viewer/source-code-viewer.component'; import { ValidationHintComponent } from './components/validation-hint/validation-hint.component'; import { ValidationSummaryComponent } from './components/validation-summary/validation-summary.component'; import { NbEvaIconsModule } from '@nebular/eva-icons'; @@ -43,16 +41,17 @@ import { NavigationListComponent } from './components/navigation-list/navigation import { NavigationListItemComponent } from './components/navigation-list-item/navigation-list-item.component'; import { TrustUrlPipe } from './pipes/trust-url.pipe'; import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal-menu.component'; +import { MaximizeDirective } from './directives/maximize.directive'; @NgModule({ declarations: [ LoadingScreenComponent, BaseComponent, - FileBrowserComponent, - SourceCodeViewerComponent, ValidationHintComponent, ValidationSummaryComponent, + MaximizeDirective, + PropPipe, FnPipe, CodemirrorThemePipe, @@ -110,6 +109,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal NbAutocompleteModule, NbTagModule, NbPopoverModule, + NbTreeGridModule, DragDropModule, CodemirrorModule, NgJoinPipeModule, @@ -117,7 +117,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal NgWherePipeModule, NgRoundPipeModule, NgMapPipeModule, - + PropPipe, FnPipe, CodemirrorThemePipe, @@ -126,11 +126,11 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal UserNamePipe, TrustUrlPipe, + MaximizeDirective, + RouteReuseLifeCycleDirective, LoadingScreenComponent, BaseComponent, - FileBrowserComponent, - SourceCodeViewerComponent, ValidationHintComponent, ValidationSummaryComponent, ErrorAlertComponent, @@ -194,7 +194,7 @@ import { HorizontalMenuComponent } from './components/horizontal-menu/horizontal NgPluckPipeModule, NgWherePipeModule, NgRoundPipeModule, - NgMapPipeModule + NgMapPipeModule, ] }) export class SharedModule { } diff --git a/subato-web/src/environments/shared.ts b/subato-web/src/environments/shared.ts index 3af8ae960a96ba5182f662e7c6d8df121b77f0a4..17f3208c7c5a716a327ad50d989a59b62f260e3b 100644 --- a/subato-web/src/environments/shared.ts +++ b/subato-web/src/environments/shared.ts @@ -2,7 +2,7 @@ let docsUrl = 'http://docs.subato-test2.local.cs.hs-rm.de'; let manualUrl = `${docsUrl}/docs`; export const shared = { - 'version': '2.0.0-beta2', + 'version': '2.0.0-beta3', docsUrl: docsUrl, manualUrl: manualUrl, stefDocsUrl: `${docsUrl}/stef`, diff --git a/subato/build.gradle b/subato/build.gradle index eb9bd43df4182d38f130f216f18fe8b083a73bdb..6f436d51e3ad1737777b208ab71c90a8b7ff206c 100755 --- a/subato/build.gradle +++ b/subato/build.gradle @@ -16,7 +16,7 @@ plugins { } group = 'de.hsrm.sls' -version = '2.0.0-beta2-SNAPSHOT' +version = '2.0.0-beta3-SNAPSHOT' repositories { mavenCentral() diff --git a/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java b/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java index f03ba415c851eb1c09bd0922df0f6c29fdac442d..aad33ca4f586b708b2d0d0254a72367ddee9c68a 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java +++ b/subato/src/main/java/de/hsrm/sls/subato/auth/KeycloakLoginHandler.java @@ -69,19 +69,19 @@ public class KeycloakLoginHandler { String username = alias; if(alias != null) { - logger.info(String.format("Logged in with alias user, skipping import/update...")); + logger.debug(String.format("Logged in with alias user, skipping import/update...")); } else { username = getUsername(auth); var mail = getMail(auth); var user = userRepository.findByLoginNameIgnoreCase(username).orElse(null); if(user == null && mail != null) { - logger.info(String.format("User with loginName %s not found, trying email %s", username, mail)); + logger.debug(String.format("User with loginName %s not found, trying email %s", username, mail)); user = userRepository.findByMailIgnoreCase(getMail(auth)).orElse(null); } if (user == null) { - logger.info("User not found, importing now..."); + logger.debug("User not found, importing now..."); user = new User(); } @@ -117,7 +117,7 @@ public class KeycloakLoginHandler { .collect(Collectors.toList()); user.getRoles().addAll(roles); user = userRepository.save(user); - logger.info("Imported/updated user %s".formatted(user.getLoginName())); + logger.debug("Imported/updated user %s".formatted(user.getLoginName())); return user; } diff --git a/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java b/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java index acbd50bd0a94034fbad6ccd526954ed091700d36..8176d2ee449db40603af3926b051a377a0da00dd 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java +++ b/subato/src/main/java/de/hsrm/sls/subato/auth/masking/TaskMasker.java @@ -24,6 +24,7 @@ public class TaskMasker implements Masker { obj.setEvaluator(null); obj.setPool(null); obj.setFeedbackStats(null); + obj.setRepositoryUrl(null); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java index 5bf85a5667ddee28342af8fd4659f4894bfbcd58..cf6e19bc5011ea0cc01860379e821a53b998e66c 100755 --- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java +++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/ExerciseController.java @@ -8,7 +8,8 @@ import de.hsrm.sls.subato.auth.policy.exercises.ExercisesAction; import de.hsrm.sls.subato.courses.course.CourseNotFoundException; import de.hsrm.sls.subato.courses.course.CourseService; import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressDto; -import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressMapper; +import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressResultDto; +import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressResultMapper; import de.hsrm.sls.subato.exercises.exercise.progress.ExerciseProgressService; import de.hsrm.sls.subato.exercises.taskinstance.*; import de.hsrm.sls.subato.shared.DangerousActionException; @@ -57,7 +58,7 @@ public class ExerciseController { private PrincipalService principalService; @Autowired - private ExerciseProgressMapper exerciseProgressMapper; + private ExerciseProgressResultMapper exerciseProgressResultMapper; @Autowired private ExerciseProgressService exerciseProgressService; @@ -160,12 +161,12 @@ public class ExerciseController { @GetMapping("/exercise/progress") @Operation(summary = "Liefert Informationen zum Fortschritt (Zusammenfassung) eines Benutzers über alle Übungen eines Kurses hineweg") - List<ExerciseProgressDto> getProgressForUser(@RequestParam Long courseId, @RequestParam Long userId) { + ExerciseProgressResultDto getProgressForUser(@RequestParam Long courseId, @RequestParam Long userId) { try { policy.checkAndRaise(CheckPolicyRequest.fromPrincipal(Pair.of(courseId, userId), CoursesAction.COURSE_PROGRESS_VIEW)); - var exerciseProgresses = exerciseProgressService.generateFor(courseId, userId); - return exerciseProgresses.stream().map(exerciseProgress -> exerciseProgressMapper.map(exerciseProgress)).toList(); + var result = exerciseProgressService.generateFor(courseId, userId); + return exerciseProgressResultMapper.map(result); } catch(CourseNotFoundException ex) { throw new ResponseStatusException(BAD_REQUEST, CourseNotFoundException.REASON); } diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java new file mode 100644 index 0000000000000000000000000000000000000000..da3da080a5e8bafac397e7bcfd7131eb8bf52462 --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResult.java @@ -0,0 +1,31 @@ +package de.hsrm.sls.subato.exercises.exercise.progress; + +import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgress; + +import java.util.List; + +public class ExerciseProgressResult { + private List<ExerciseProgress> exerciseProgresses; + private SubmissionsProgress submissionsProgress; + + public ExerciseProgressResult(List<ExerciseProgress> exerciseProgresses, SubmissionsProgress submissionsProgress) { + this.exerciseProgresses = exerciseProgresses; + this.submissionsProgress = submissionsProgress; + } + + public List<ExerciseProgress> getExerciseProgresses() { + return exerciseProgresses; + } + + public void setExerciseProgresses(List<ExerciseProgress> exerciseProgresses) { + this.exerciseProgresses = exerciseProgresses; + } + + public SubmissionsProgress getSubmissionsProgress() { + return submissionsProgress; + } + + public void setSubmissionsProgress(SubmissionsProgress submissionsProgress) { + this.submissionsProgress = submissionsProgress; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java new file mode 100644 index 0000000000000000000000000000000000000000..16aa8f824957374bd859d6d9f9954811bbb2148a --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultDto.java @@ -0,0 +1,26 @@ +package de.hsrm.sls.subato.exercises.exercise.progress; + +import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgressDto; + +import java.util.List; + +public class ExerciseProgressResultDto { + private List<ExerciseProgressDto> exerciseProgresses; + private SubmissionsProgressDto submissionsProgress; + + public List<ExerciseProgressDto> getExerciseProgresses() { + return exerciseProgresses; + } + + public void setExerciseProgresses(List<ExerciseProgressDto> exerciseProgresses) { + this.exerciseProgresses = exerciseProgresses; + } + + public SubmissionsProgressDto getSubmissionsProgress() { + return submissionsProgress; + } + + public void setSubmissionsProgress(SubmissionsProgressDto submissionsProgress) { + this.submissionsProgress = submissionsProgress; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java similarity index 52% rename from subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java rename to subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java index 040cab647f4070bf6517d437bfc15ccd99fba573..74a9e6f3b09c72f0be440283dfb1558d8eb5c135 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressMapper.java +++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressResultMapper.java @@ -3,6 +3,6 @@ package de.hsrm.sls.subato.exercises.exercise.progress; import org.mapstruct.Mapper; @Mapper(componentModel = "spring") -public interface ExerciseProgressMapper { - ExerciseProgressDto map(ExerciseProgress exerciseProgress); +public interface ExerciseProgressResultMapper { + ExerciseProgressResultDto map(ExerciseProgressResult result); } diff --git a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java index a35eadd9dc89049dc57540a587131c6bdfc12cb0..6baabc0b59b0cc37a3f38cf5e0d6e5cd31d20f88 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/exercises/exercise/progress/ExerciseProgressService.java @@ -8,6 +8,7 @@ import de.hsrm.sls.subato.courses.membership.StaffGroup; import de.hsrm.sls.subato.exercises.exercise.ExerciseFilter; import de.hsrm.sls.subato.exercises.exercise.ExerciseService; import de.hsrm.sls.subato.exercises.submission.SubmissionService; +import de.hsrm.sls.subato.exercises.submission.progress.SubmissionsProgress; import de.hsrm.sls.subato.exercises.taskinstance.TaskInstanceFilter; import de.hsrm.sls.subato.exercises.taskinstance.TaskInstanceService; import de.hsrm.sls.subato.user.User; @@ -40,7 +41,7 @@ public class ExerciseProgressService { @Autowired private CourseService courseService; - public List<ExerciseProgress> generateFor(long courseId, long userId) { + public ExerciseProgressResult generateFor(long courseId, long userId) { var course = courseService.findById(courseId); var user = userService.findById(userId); var currentUser = (SubatoPrincipal) principalService.getPrincipal(); @@ -49,7 +50,7 @@ public class ExerciseProgressService { currentUser.hasGroup(course, StaffGroup.TUTOR)); } - public List<ExerciseProgress> generateFor(Course course, User user, boolean includeInvisibleExercises) { + public ExerciseProgressResult generateFor(Course course, User user, boolean includeInvisibleExercises) { var exerciseProgresses = new ArrayList<ExerciseProgress>(); var exercises = exerciseService.findAll(new ExerciseFilter().courseId(course.getId())); @@ -87,6 +88,12 @@ public class ExerciseProgressService { exerciseProgresses.add(exerciseProgress); } exerciseProgresses.sort(Comparator.comparing((ExerciseProgress exerciseProgress) -> exerciseProgress.getExercise().getId()).reversed()); - return exerciseProgresses; + + var submissions = exerciseProgresses.stream() + .filter(p -> p.getSubmission() != null) + .map(p -> p.getSubmission()) + .toList(); + var submissionsProgress = SubmissionsProgress.fromSubmissions(submissions); + return new ExerciseProgressResult(exerciseProgresses, submissionsProgress); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java b/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java index 9dd521c53d5c866ee654fb3193cc2c830254ed1f..86cc6dc1b499d232b58977bdccab10cd4e6cabeb 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/ExceptionReasons.java @@ -6,4 +6,5 @@ package de.hsrm.sls.subato.shared; */ public class ExceptionReasons { public static final String CONSTRAINT_VALIDATION = "Eingabe ist ungültig"; + public static final String MAX_UPLOAD_SIZE_EXCEEDED = "Diese Datei überschreitet das Limit von (etwa) %s MB"; } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java index 541bb8e500efec30c66231577a2fae09c88f8fb6..91bee28564979ec26dd81fb63faadff70f36d349 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/CacheConfig.java @@ -38,7 +38,7 @@ public class CacheConfig extends CachingConfigurerSupport { public CaffeineCache currentUserCache(MeterRegistry registry) { var cache = new CaffeineCache("currentUser", Caffeine.newBuilder() - .expireAfterAccess(60, TimeUnit.MINUTES) + .expireAfterAccess(5, TimeUnit.MINUTES) .build()); Gauge.builder("subato.user.count",() -> { @@ -55,7 +55,7 @@ public class CacheConfig extends CachingConfigurerSupport { public CaffeineCache evaluatorRegistryCache() { return new CaffeineCache("evaluatorRegistry", Caffeine.newBuilder() - .expireAfterAccess(60, TimeUnit.MINUTES) + .expireAfterAccess(5, TimeUnit.MINUTES) .build()); } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..0569e7657777c3f01ab3f054a77ebc66899f21f2 --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/ConfigController.java @@ -0,0 +1,28 @@ +package de.hsrm.sls.subato.shared.config; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.MultipartConfigElement; + +@Component +@RestController +@Tag(name = "Querschnitt") +public class ConfigController { + @Autowired + MultipartConfigElement multipartConfigElement; + + @GetMapping("/config") + @Operation(summary = "Liefert ausgewählte Konfigurationswerte, damit diese von Clients berücksichtigt werden können") + public PublicConfig getConfig() { + var maxFileSizeInMb = multipartConfigElement.getMaxFileSize() / 1024 / 1024; + + PublicConfig response = new PublicConfig(); + response.setMaxUploadSizeInMb(maxFileSizeInMb); + return response; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..48fd43b9d9aa94bc2179b5c61b194d845a6e7383 --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/PublicConfig.java @@ -0,0 +1,13 @@ +package de.hsrm.sls.subato.shared.config; + +public class PublicConfig { + private long maxUploadSizeInMb; + + public long getMaxUploadSizeInMb() { + return maxUploadSizeInMb; + } + + public void setMaxUploadSizeInMb(long maxUploadSizeInMb) { + this.maxUploadSizeInMb = maxUploadSizeInMb; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java b/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java index ad6c6f8c7ef55776f32a1ca621006a6d630cf384..51e4e58dc22cf7772b0d718b325a1fe0f29b945e 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/config/SwaggerConfiguration.java @@ -33,7 +33,7 @@ import java.util.stream.Collectors; @Configuration public class SwaggerConfiguration implements WebMvcConfigurer { - private final String API_VERSION = "2.0.0-beta2"; + private final String API_VERSION = "2.0.0-beta3"; @Value("${keycloak.auth-server-url}") private String keycloakUrl; diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java b/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java index cbde71aaf519d1a725e71cab0ad8fb5bdb6c7305..d158075f411638ae52e9792c16fff8dd780cd128 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/error/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import de.hsrm.sls.subato.shared.ExceptionReasons; import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -14,8 +15,10 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.server.ResponseStatusException; +import javax.servlet.MultipartConfigElement; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolationException; import java.io.IOException; @@ -29,6 +32,18 @@ public class GlobalExceptionHandler { final String NOT_AUTHENTICATED_MESSAGE = "Nicht eingeloggt."; final String UNAUTHORIZED_MESSAGE = "Keine Rechte für diese Aktion."; + @Autowired + MultipartConfigElement multipartConfigElement; + + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity<ErrorResponse> maxUploadSizeExceededException(MaxUploadSizeExceededException ex) { + var statusCode = HttpStatus.BAD_REQUEST; + + var maxFileSizeInMb = multipartConfigElement.getMaxFileSize() / 1024 / 1024; + var errorResponse = new ErrorResponse(statusCode.value(), ExceptionReasons.MAX_UPLOAD_SIZE_EXCEEDED.formatted(maxFileSizeInMb), null); + return ResponseEntity.status(statusCode.value()).body(errorResponse); + } + // https://stackoverflow.com/questions/35451244/how-to-handle-clientabortexception-in-spring-mvc @ExceptionHandler(ClientAbortException.class) public void handleLockException(ClientAbortException exception, HttpServletRequest request) { diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java index c983476069f20bf15cfe85723ac8fda10a585aa5..e76ca190cecf5bae807038a0db6d500ad2c5bd31 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeCache.java @@ -2,20 +2,23 @@ package de.hsrm.sls.subato.shared.fs; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import de.hsrm.sls.subato.shared.mime.MimeMapping; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; @Service public class FileTreeCache { @Autowired FileTreeService fileTreeService; - private FileTree buildTree(Path path) { + private FileTree buildTree(Path path, List<MimeMapping> mimeMappingOverrides) { var file = path.toFile(); if(!file.exists()) { @@ -26,14 +29,14 @@ public class FileTreeCache { throw new RuntimeException("path is not a directory"); } - return fileTreeService.createTreeFrom(file, "", true, false); + return fileTreeService.createTreeFrom(file, "", true, false, mimeMappingOverrides); } - public synchronized void saveTree(FileTree tree, Path basePath) { + private synchronized void saveTree(FileTree tree, Path basePath) { try { ObjectMapper objectMapper = new ObjectMapper(); var serialized = objectMapper.writeValueAsString(tree); - var treeFile = basePath.resolve("tree.json").toFile(); + var treeFile = getTreeIndexPath(basePath).toFile(); Files.write(treeFile.toPath(), serialized.getBytes(StandardCharsets.UTF_8)); } catch (JsonProcessingException e) { throw new RuntimeException(e); @@ -42,10 +45,14 @@ public class FileTreeCache { } } - public synchronized FileTree getTree(Path basePath) { - var cacheFilePath = basePath.resolve("tree.json"); + public Path getTreeIndexPath(Path basePath) { + return basePath.resolve("tree.json"); + } + + public synchronized FileTree getTree(Path basePath, List<MimeMapping> mimeMappingOverrides) { + var cacheFilePath = getTreeIndexPath(basePath); if(!cacheFilePath.toFile().exists()) { - var tree = buildTree(basePath); + var tree = buildTree(basePath, mimeMappingOverrides); saveTree(tree, basePath); return tree; } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java index 353d552f83cedf4d2c8e255251477d5b2075215b..e4ae4de1e588f2820dce87a1b6c5fcc371533d02 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/fs/FileTreeService.java @@ -4,23 +4,25 @@ import com.google.common.base.Strings; import de.hsrm.sls.subato.shared.fs.FileTree; import de.hsrm.sls.subato.shared.helper.FileHelper; import de.hsrm.sls.subato.shared.mime.MimeDetector; +import de.hsrm.sls.subato.shared.mime.MimeMapping; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.File; import java.nio.file.Path; +import java.util.List; @Service public class FileTreeService { @Autowired MimeDetector mimeDetector; - public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot, String contentTypeOverride) { + public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot, List<MimeMapping> mimeMappingOverrides) { if(!file.exists()) { return null; } - var contentType = !Strings.isNullOrEmpty(contentTypeOverride) ? contentTypeOverride : mimeDetector.getContentType(file); + var contentType = mimeDetector.getContentType(file, mimeMappingOverrides); var isSourceFile = FileHelper.isSourceFile(contentType); var fileName = includeRoot ? file.getName() : ""; @@ -33,15 +35,11 @@ public class FileTreeService { var relativePath = Path.of(basePath).resolve(Path.of(fileName)); for (var f : file.listFiles()) { - item.getChildren().add(createTreeFrom(f, relativePath.toString(), recursive, true)); + item.getChildren().add(createTreeFrom(f, relativePath.toString(), recursive, true, mimeMappingOverrides)); } } } return item; } - - public FileTree createTreeFrom(File file, String basePath, boolean recursive, boolean includeRoot) { - return createTreeFrom(file, basePath, recursive, includeRoot, null); - } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java index d85d5eaabe69c3dd3c88ff6f1e97112a1127b237..666af5c8cd6c352db8577033297ef7c40d406ca0 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeDetector.java @@ -7,6 +7,10 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Responsible for mapping file names to mime types. @@ -29,13 +33,31 @@ public class MimeDetector { logger.info("Configured mime mappings: %s".formatted(mimeProperties.getMime())); } + private Map<String, String> mergeMappings(List<MimeMapping> globalMappings, List<MimeMapping> overrides) { + var mimeMappings = new ArrayList<MimeMapping>(); + mimeMappings.addAll(globalMappings); + mimeMappings.addAll(overrides); + + var d = new HashMap<String, String>(); + for(var mapping : mimeMappings) { + d.put(mapping.getMap(), mapping.getAs()); + } + + return d; + } + /** * @return actual content type or application/octet-stream if it can't be guessed */ - public String getContentType(String fileName) { - for(var mimeMapping : mimeProperties.getMime()) { - if(fileName.endsWith(mimeMapping.getMap())) { - return mimeMapping.getAs(); + public String getContentType(String fileName, List<MimeMapping> mimeMappingOverrides) { + var mergedMappings = mergeMappings(mimeProperties.getMime(), mimeMappingOverrides); + + for(var entry : mergedMappings.entrySet()) { + var map = entry.getKey(); + var as = entry.getValue(); + + if(fileName.endsWith(map)) { + return as; } } @@ -43,7 +65,11 @@ public class MimeDetector { return contentType; } - public String getContentType(File file) { - return getContentType(file.getName()); + public String getContentType(String fileName) { + return getContentType(fileName, new ArrayList<>()); + } + + public String getContentType(File file, List<MimeMapping> mimeMappingOverrides) { + return getContentType(file.getName(), mimeMappingOverrides); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java index 0dc4546ef2706c2aa512aeba1b2d19e8cf55a41f..471792c0e7cc630646676725620d681ea2b82fb2 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java +++ b/subato/src/main/java/de/hsrm/sls/subato/shared/mime/MimeMapping.java @@ -4,6 +4,14 @@ public class MimeMapping { private String map; private String as; + public MimeMapping(String map, String as) { + this.map = map; + this.as = as; + } + + public MimeMapping() { + } + public String getMap() { return map; } diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java index adb6f97c460ec2edd9a13459ca9f21c23aa776aa..4b53875dc38ea3725ae05bef2594eaffba902b42 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorRegistryService.java @@ -1,7 +1,9 @@ package de.hsrm.sls.subato.solutions.evaluation.evaluator; import de.hsrm.sls.subato.solutions.evaluation.EvaluatorConfig; +import de.hsrm.sls.subato.user.User; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; @@ -22,4 +24,7 @@ public class EvaluatorRegistryService { var response = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<List<EvaluatorEntry>>() {}); return response.getBody(); } + + @CacheEvict(value = "evaluatorRegistry") + public void evictCache() {} } diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java index 82dc4d03ea9c31b199defe1a26036c1039451b68..f78c23518b035fe646d4f5f86203dcc61d597917 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/evaluation/evaluator/EvaluatorService.java @@ -33,7 +33,7 @@ public class EvaluatorService { evaluator.setStyle(evaluatorVersion.getCapability().isStyle()); evaluator.setTest(evaluatorVersion.getCapability().isTest()); - return evaluator; + return evaluatorRepository.save(evaluator); } public Evaluator resolveReference(String reference) { diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java index 16fee0138fcabd80be81c86d560176604ed54914..7a02ce325e13001017a962363eaa2d98bf296e06 100755 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/Solution.java @@ -82,6 +82,7 @@ public class Solution { /** * how was this solution submitted? */ + @NotNull private Client submittedWith; @OneToMany(mappedBy = "solution", cascade = { CascadeType.ALL }) diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java index 4b110bd12bb260a113b9b398f6c18f8fba354a8a..77351bd54cf2b27c4d865604fd2cd8363359f36c 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionContentRepository.java @@ -4,6 +4,11 @@ import de.hsrm.sls.subato.shared.config.SubatoConfig; import de.hsrm.sls.subato.shared.fs.FileContent; import de.hsrm.sls.subato.shared.fs.FileTreeCache; import de.hsrm.sls.subato.shared.fs.FileTree; +import de.hsrm.sls.subato.shared.helper.ZipHelper; +import de.hsrm.sls.subato.shared.mime.MimeMapping; +import de.hsrm.sls.subato.tasks.stef.parser.SpecParser; +import de.hsrm.sls.subato.tasks.task.TaskContentRepository; +import net.lingala.zip4j.model.ExcludeFileFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.FileSystemUtils; @@ -11,6 +16,8 @@ import org.springframework.util.FileSystemUtils; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; @Service @@ -21,6 +28,9 @@ public class SolutionContentRepository { @Autowired private FileTreeCache fileTreeCache; + @Autowired + private TaskContentRepository taskContentRepository; + private Path getSolutionPath(Solution solution) { return Paths.get(subatoConfig.getDataDir()) .resolve("solution") @@ -35,7 +45,8 @@ public class SolutionContentRepository { } public FileTree getTree(Solution solution) { - return fileTreeCache.getTree(getSolutionPath(solution)); + var spec = taskContentRepository.getSpec(solution.getTask()); + return fileTreeCache.getTree(getSolutionPath(solution), spec.getMeta().getMimeMappings()); } /** @@ -47,13 +58,21 @@ public class SolutionContentRepository { */ public File getFile(Solution solution, UUID uuid) { var solutionPath = getSolutionPath(solution); - var solutionTree = fileTreeCache.getTree(solutionPath); + var spec = taskContentRepository.getSpec(solution.getTask()); + + var solutionTree = fileTreeCache.getTree(solutionPath, spec.getMeta().getMimeMappings()); var tree = solutionTree.find(i -> i.getUuid().equals(uuid)); var file = tree.getFile(solutionPath); return file; } + public File getSolutionAsZip(Solution solution) { + var root = getSolutionPath(solution); + ExcludeFileFilter excludeFilter = file -> file.toPath().equals(fileTreeCache.getTreeIndexPath(root)); + return ZipHelper.zip(root.toFile(), false, excludeFilter); + } + public void delete(Solution solution) { var solutionPath = getSolutionPath(solution); FileSystemUtils.deleteRecursively(solutionPath.toFile()); diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java index 3d9fdd57ede1f58fdeee8eb02aeac807d704aebb..16d9103431785d8b9b4a595477aedc03c69d2e5a 100755 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionController.java @@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -133,6 +134,19 @@ public class SolutionController { } } + @GetMapping("/solution/{id}/download") + ResponseEntity<Resource> downloadSolution(@PathVariable Long id) throws IOException { + try { + var solution = solutionService.findById(id); + policy.checkAndRaise(CheckPolicyRequest.fromPrincipal(solution, SolutionsAction.SOLUTION_VIEW)); + var zipFile = solutionService.getSolutionAsZip(solution); + var resource = new UrlResource(zipFile.toURI()); + return fileResponseBuilder.build(resource, "solution%s.zip".formatted(solution.getId()), true); + } catch(SolutionNotFoundException ex) { + throw new ResponseStatusException(BAD_REQUEST, "Die Lösung existiert nicht"); + } + } + @PostMapping(path ="/solution/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation(summary = solutionSubmitSummary, description = solutionSubmitDescription) SolutionDetailDto submit( @@ -172,6 +186,8 @@ public class SolutionController { throw new ResponseStatusException(BAD_REQUEST, InvalidModeException.REASON); } catch(EvaluationFailedException ex) { throw new ResponseStatusException(INTERNAL_SERVER_ERROR, EvaluationFailedException.REASON, ex); + } catch(InvalidSolutionException ex) { + throw new ResponseStatusException(BAD_REQUEST, "%s: %s".formatted(InvalidSolutionException.REASON, ex.getMessage())); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java index 2192cec9c5e7b822be71a145a393463cb2c330e3..8395eec1dbef0de7072953c10326222a1d3fa1a1 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/solution/SolutionService.java @@ -79,6 +79,10 @@ public class SolutionService { return solutionContentRepository.getFile(solution, uuid); } + File getSolutionAsZip(Solution solution) { + return solutionContentRepository.getSolutionAsZip(solution); + } + public FileTree getTree(Solution solution) { return solutionContentRepository.getTree(solution); } diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java index e6369237c52a65c5a7a836330ec42517e864b52d..29e2c83644d55b5359e250b4938ce10eda636310 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/ExerciseSubmitHandler.java @@ -22,6 +22,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; import java.time.LocalDateTime; /** @@ -71,6 +73,7 @@ public class ExerciseSubmitHandler implements SubmitHandler { solution.setTask(request.task()); solution.setSubmittedAt(submittedAt); solution.setSubmittedWith(request.client()); + solution.setExercise(request.exercise()); if(user != null && !Strings.isNullOrEmpty(request.comment())) { var comment = new SolutionComment(); @@ -81,7 +84,12 @@ public class ExerciseSubmitHandler implements SubmitHandler { solution.getComments().add(comment); } - solution.setExercise(request.exercise()); + var validator = Validation.buildDefaultValidatorFactory().getValidator(); + var violations = validator.validate(solution); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + solution = solutionRepository.save(solution); if(user != null) { diff --git a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java index 3ef3c6ad1c4f30710df5d6dcd8a5fe442c151926..c19d045c8eb9386dac200e139eeee14f69fc5125 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java +++ b/subato/src/main/java/de/hsrm/sls/subato/solutions/submit/TaskSubmitHandler.java @@ -15,6 +15,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; import java.time.LocalDateTime; /** @@ -55,6 +57,13 @@ public class TaskSubmitHandler implements SubmitHandler { solution.setTask(request.task()); solution.setSubmittedAt(submittedAt); solution.setSubmittedWith(request.client()); + + var validator = Validation.buildDefaultValidatorFactory().getValidator(); + var violations = validator.validate(solution); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + solution = solutionRepository.save(solution); solutionContentRepository.store(request.content(), solution); diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java index fae84f8769d9bc1d09ed565902769a125daed1d4..ae8a412f858a4d505bf17bfdec37fff6135382e7 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/imp/TaskImportService.java @@ -1,9 +1,13 @@ package de.hsrm.sls.subato.tasks.imp; +import com.google.common.base.Strings; import de.hsrm.sls.subato.shared.fs.FileContent; import de.hsrm.sls.subato.shared.fs.TmpDirProvider; +import de.hsrm.sls.subato.solutions.evaluation.evaluator.EvaluatorRegistryService; +import de.hsrm.sls.subato.solutions.evaluation.evaluator.EvaluatorRepository; import de.hsrm.sls.subato.tasks.imp.log.LogType; import de.hsrm.sls.subato.tasks.imp.log.TaskImportLogService; +import de.hsrm.sls.subato.tasks.lang.LanguageRepository; import de.hsrm.sls.subato.tasks.stef.file.FileType; import de.hsrm.sls.subato.tasks.stef.parser.InvalidSpecException; import de.hsrm.sls.subato.tasks.stef.StefSpec; @@ -23,6 +27,7 @@ import org.springframework.util.FileSystemUtils; import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.stream.Collectors; @@ -51,6 +56,15 @@ public class TaskImportService { @Autowired private TmpDirProvider tmpDirProvider; + @Autowired + private EvaluatorRepository evaluatorRepository; + + @Autowired + private LanguageRepository languageRepository; + + @Autowired + private EvaluatorRegistryService evaluatorRegistryService; + /** * checks whether there already is a task in the pool with the id given in the spec or not. * if not, a new task will be created and linked to the pool. @@ -72,8 +86,13 @@ public class TaskImportService { task.setDescription(spec.getMeta().getDescription()); task.setAttempts(spec.getMeta().getAttempts()); task.setContent(spec.getText()); - task.setEvaluator(spec.getMeta().getEvaluator()); - task.setLanguage(spec.getMeta().getLanguage()); + + var evaluator = spec.getMeta().getEvaluator() != null ? evaluatorRepository.findById(spec.getMeta().getEvaluator()).get() : null; + task.setEvaluator(evaluator); + + var language = spec.getMeta().getLanguage() != null ? languageRepository.findById(spec.getMeta().getLanguage()).get() : null; + task.setLanguage(language); + task.setDefaultSubmissionMode(spec.getMeta().getSubmissionModes().getDefaultMode()); task.setSupportsFileSubmissionMode(spec.getMeta().getSubmissionModes().getModes().contains(SubmissionMode.FILE)); task.setSupportsTextSubmissionMode(spec.getMeta().getSubmissionModes().getModes().contains(SubmissionMode.TEXT)); @@ -122,8 +141,13 @@ public class TaskImportService { // copy the tmp dir taskContentRepository.store(new ImportSpecRequest(spec, task), dir); + logger.debug("Imported task %s".formatted(task.getIdName())); return task; + } catch(InvalidSpecException ex) { + logger.debug("Format of task %s is invalid: %s".formatted(dir.getName(), ex.getMessage())); + throw ex; } catch(Exception ex) { + logger.error("Failed to import task %s".formatted(dir.getName()), ex); throw new TaskImportException(ex); } } @@ -136,6 +160,8 @@ public class TaskImportService { var tmpPath = tmpDirProvider.createTmpPath(); var tmpDir = tmpPath.toFile(); + evaluatorRegistryService.evictCache(); + try { var content = FileContent.fromMultipartFile(file); content.saveTo(tmpDir, true); @@ -164,8 +190,10 @@ public class TaskImportService { var importedTasks = new ArrayList<Task>(); var problems = 0L; var totalTaskDirectories = 0L; - - logger.info("Importing tasks in %s for pool %s".formatted(rootDir.getAbsolutePath(), pool.getName())); + + evaluatorRegistryService.evictCache(); + + logger.debug("Importing tasks in %s for pool %s...".formatted(rootDir.getAbsolutePath(), pool.getName())); for(var dir : rootDir.listFiles()) { if(!dir.isDirectory()) { continue; @@ -179,15 +207,16 @@ public class TaskImportService { try { var task = importTask(pool, dir); - importedTasks.add(task); - logger.info("Imported task %s".formatted(task.getIdName())); + var relativePath = rootDir.toPath().relativize(dir.toPath()); + task.setRepositoryPath(relativePath.toString()); + task = taskRepository.save(task); + + importedTasks.add(task); } catch(InvalidSpecException ex) { - logger.info("Format of task %s is invalid: %s".formatted(dir.getName(), ex.getMessage())); importLogService.log(pool, LogType.PROBLEM, "%s: %s. %s".formatted(dir.getName(), InvalidSpecException.REASON, ex.getMessage())); problems += 1; } catch(TaskImportException ex) { - logger.error("Failed to import task %s".formatted(dir.getName()), ex); importLogService.log(pool, LogType.PROBLEM, "%s: %s".formatted(dir.getName(), TaskImportException.REASON)); problems += 1; } @@ -197,17 +226,16 @@ public class TaskImportService { .filter(currentTask -> !importedTasks.stream().anyMatch(importedTask -> importedTask.getId() == currentTask.getId())) .collect(Collectors.toList()); - logger.info("Imported %s tasks for pool %s, %s tasks were missing in the root directory. Will be archived now...".formatted(importedTasks.size(), pool.getName(), missingTasks.size())); for(var missingTask : missingTasks) { missingTask.setArchived(true); missingTask = taskRepository.save(missingTask); - logger.info("Archived task %s".formatted(missingTask.getIdName())); + logger.debug("Archived task %s".formatted(missingTask.getIdName())); importLogService.log(pool, LogType.INFO, "Aufgabe %s archiviert".formatted(missingTask.getIdName())); } - importLogService.log(pool, LogType.INFO, "Import durchgeführt. %s Aufgaben importiert/aktualisiert, %s Aufgaben archiviert".formatted(importedTasks.size(), missingTasks.size())); - logger.info("Done importing tasks for pool %s".formatted(pool.getName())); + importLogService.log(pool, LogType.INFO, "Import durchgeführt. %s Aufgaben importiert/aktualisiert, %s Fehler, %s Aufgaben archiviert".formatted(importedTasks.size(), problems, missingTasks.size())); + logger.info("Imported %s tasks for pool %s, %s problems occured and %s tasks have been archived.".formatted(importedTasks.size(), pool.getName(), problems, missingTasks.size())); return new TaskImportResult(totalTaskDirectories, importedTasks.size(), missingTasks.size(), problems); } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java index 23cbf3570b5bb70579b11653d426ae6777b17fb1..2e2b556f6089a9a8265a98ff42b06b595896c1f5 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/Meta.java @@ -1,5 +1,6 @@ package de.hsrm.sls.subato.tasks.stef; +import de.hsrm.sls.subato.shared.mime.MimeMapping; import de.hsrm.sls.subato.solutions.evaluation.evaluator.Evaluator; import de.hsrm.sls.subato.tasks.lang.Language; import de.hsrm.sls.subato.tasks.stef.file.FileRef; @@ -15,10 +16,11 @@ public class Meta { String description; Integer attempts; Long legacyId; - Language language; - Evaluator evaluator; + String language; + Long evaluator; SubmissionModes submissionModes; List<FileRef> files; + List<MimeMapping> mimeMappings; public String getId() { return id; @@ -60,19 +62,19 @@ public class Meta { this.legacyId = legacyId; } - public Language getLanguage() { + public String getLanguage() { return language; } - public void setLanguage(Language language) { + public void setLanguage(String language) { this.language = language; } - public Evaluator getEvaluator() { + public Long getEvaluator() { return evaluator; } - public void setEvaluator(Evaluator evaluator) { + public void setEvaluator(Long evaluator) { this.evaluator = evaluator; } @@ -91,4 +93,12 @@ public class Meta { public void setFiles(List<FileRef> files) { this.files = files; } + + public List<MimeMapping> getMimeMappings() { + return mimeMappings; + } + + public void setMimeMappings(List<MimeMapping> mimeMappings) { + this.mimeMappings = mimeMappings; + } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java index 56d05328f6332fafef930ac066bc975c09d3cdf7..896edb2434c8ef048ba663eb62251a4108d0406c 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/EvaluatorExtractor.java @@ -25,6 +25,6 @@ public class EvaluatorExtractor { } } - meta.setEvaluator(evaluator); + meta.setEvaluator(evaluator != null ? evaluator.getEid() : null); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java index f44396744a939837a9048ada4cafb52a57b1ff16..beabcd94096ebe9b065a76bb3b767d2b5cb69401 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/FileRefsExtractor.java @@ -1,6 +1,7 @@ package de.hsrm.sls.subato.tasks.stef.parser.meta; import de.hsrm.sls.subato.shared.fs.FileTreeService; +import de.hsrm.sls.subato.shared.mime.MimeMapping; import de.hsrm.sls.subato.tasks.stef.Meta; import de.hsrm.sls.subato.tasks.stef.file.FileRef; import de.hsrm.sls.subato.tasks.stef.file.FileType; @@ -11,6 +12,7 @@ import org.springframework.stereotype.Service; import java.nio.file.Path; import java.util.ArrayList; +import java.util.List; @Service public class FileRefsExtractor { @@ -52,7 +54,7 @@ public class FileRefsExtractor { var actualFilePath = Path.of(fileRoot).resolve(targetFolder).resolve(filePath); var basePath = targetFolder.resolve(filePath).getParent(); - var f = fileTreeService.createTreeFrom(actualFilePath.toFile(), basePath != null ? basePath.toString() : "", false, true, fNode.getMimeType()); + var f = fileTreeService.createTreeFrom(actualFilePath.toFile(), basePath != null ? basePath.toString() : "", false, true, meta.getMimeMappings()); if (f == null) { throw new InvalidSpecException("Datei %s in meta.xml deklariert aber nicht im Ordner %s gefunden".formatted(filePath, targetFolder)); diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java index 979171c07ce6027b28485f3b58cddf3d24bc4689..79b618b237ebdb127b129cd4ea6a0f12a64f9aa5 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/LanguageExtractor.java @@ -22,6 +22,6 @@ public class LanguageExtractor { } } - meta.setLanguage(language); + meta.setLanguage(language != null ? language.getId() : null); } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java index 7b43b0d551fdef1010b252796869cbcbab07fd93..ad71722ba15d6760f259b4e6f776900c9dd063d2 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MetaParser.java @@ -40,6 +40,9 @@ public class MetaParser { @Autowired private FileRefsExtractor fileRefsExtractor; + @Autowired + private MimeMappingsExtractor mimeMappingsExtractor; + public Meta parse(String fileRoot) { var meta = new Meta(); @@ -52,6 +55,7 @@ public class MetaParser { attemptsExtractor.extract(doc, meta); legacyIdExtractor.extract(doc, meta); evaluatorExtractor.extract(doc, meta); + mimeMappingsExtractor.extract(doc, meta); fileRefsExtractor.extract(doc, meta, fileRoot); submissionModeExtractor.extract(doc, meta); diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..0768124931fbb92b03012516a7f5ce206bead960 --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/MimeMappingsExtractor.java @@ -0,0 +1,32 @@ +package de.hsrm.sls.subato.tasks.stef.parser.meta; + +import de.hsrm.sls.subato.shared.mime.MimeMapping; +import de.hsrm.sls.subato.tasks.stef.Meta; +import de.hsrm.sls.subato.tasks.stef.parser.InvalidSpecException; +import de.hsrm.sls.subato.tasks.stef.parser.meta.xml.*; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +public class MimeMappingsExtractor { + public void extract(TaskXml doc, Meta meta) { + var mimeMappings = new ArrayList<MimeMapping>(); + + if(doc.getMimeMappings() != null) { + for (var mapping : doc.getMimeMappings().getMappings()) { + if(mapping.getMap() == null) { + throw new InvalidSpecException("%s-Attribut in %s fehlt".formatted(MimeMappingXml.MAP, MimeMappingsXml.MIME_MAPPING)); + } + + if(mapping.getAs() == null) { + throw new InvalidSpecException("%s-Attribut in %s fehlt".formatted(MimeMappingXml.AS, MimeMappingsXml.MIME_MAPPING)); + } + + mimeMappings.add(new MimeMapping(mapping.getMap(), mapping.getAs())); + } + } + + meta.setMimeMappings(mimeMappings); + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java index 3f2b571ff8ff4e40561a4bbde5c40e6890523015..4e0dcd659a36892e41e91ca281f132b1e308f850 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/FileXml.java @@ -7,7 +7,6 @@ public class FileXml { public static final String TYPE = "type"; public static final String PUBLIC = "public"; public static final String PATH = "path"; - public static final String MIME_TYPE = "mimeType"; private String path; private String type; @@ -51,13 +50,4 @@ public class FileXml { public void setDescription(String description) { this.description = description; } - - public String getMimeType() { - return mimeType; - } - - @XmlAttribute(name = MIME_TYPE) - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java new file mode 100644 index 0000000000000000000000000000000000000000..62856f95e65c785245f82c713acd9e61f19edb2a --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingXml.java @@ -0,0 +1,30 @@ +package de.hsrm.sls.subato.tasks.stef.parser.meta.xml; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; + +public class MimeMappingXml { + public static final String MAP = "map"; + public static final String AS = "to"; + + private String map; + private String as; + + public String getMap() { + return map; + } + + @XmlAttribute(name = MAP) + public void setMap(String map) { + this.map = map; + } + + public String getAs() { + return as; + } + + @XmlAttribute(name = AS) + public void setAs(String as) { + this.as = as; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java new file mode 100644 index 0000000000000000000000000000000000000000..3e881dc4665ddd6a2b5a6f57d199e1b4879f8484 --- /dev/null +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/MimeMappingsXml.java @@ -0,0 +1,19 @@ +package de.hsrm.sls.subato.tasks.stef.parser.meta.xml; + +import javax.xml.bind.annotation.XmlElement; +import java.util.List; + +public class MimeMappingsXml { + public static final String MIME_MAPPING = "mimeMapping"; + + private List<MimeMappingXml> mappings; + + public List<MimeMappingXml> getMappings() { + return mappings; + } + + @XmlElement(name = MIME_MAPPING) + public void setMappings(List<MimeMappingXml> mappings) { + this.mappings = mappings; + } +} diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java index 0e9a09060afc73343440e3e9c1c164fd4b8c074c..beae75e9b65123a9b4d50ddda0d38e3521022708 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/stef/parser/meta/xml/TaskXml.java @@ -15,6 +15,7 @@ public class TaskXml { public static final String LANG = "lang"; public static final String SUBMISSION_MODES = "submissionModes"; public static final String FILES = "files"; + public static final String MIME_MAPPINGS = "mimeMappings"; private Long subatoId; private Integer attempts; @@ -24,6 +25,7 @@ public class TaskXml { private String description; private String evaluator; private FilesXml files; + private MimeMappingsXml mimeMappings; private SubmissionModesXml submissionModes; public String getId() { @@ -106,4 +108,13 @@ public class TaskXml { public void setSubmissionModes(SubmissionModesXml submissionModes) { this.submissionModes = submissionModes; } + + public MimeMappingsXml getMimeMappings() { + return mimeMappings; + } + + @XmlElement(name = MIME_MAPPINGS) + public void setMimeMappings(MimeMappingsXml mimeMappings) { + this.mimeMappings = mimeMappings; + } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java index 769f8b399d253e1221a11a4197ff9779c29efd9b..28d8384e1d21b0a708aaeb509fe693361d6d474b 100755 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/Task.java @@ -9,6 +9,7 @@ import de.hsrm.sls.subato.exercises.taskinstance.TaskInstance; import de.hsrm.sls.subato.tasks.pool.TaskPool; import javax.persistence.*; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -124,6 +125,12 @@ public class Task { @OneToMany(mappedBy = "task", cascade = CascadeType.REMOVE) private List<Solution> solutions; + /** + * relative url to the task in the repository + * only applicable if the linked task pool is linked to a git repository + */ + private String repositoryPath; + public Task() { } public Long getSourceId() { @@ -297,12 +304,30 @@ public class Task { public void setSupportsTextSubmissionMode(boolean supportsTextSubmissionMode) { this.supportsTextSubmissionMode = supportsTextSubmissionMode; } - + + public boolean isEvaluationPossible() { + return evaluationPossible; + } + + public String getRepositoryPath() { + return repositoryPath; + } + + public void setRepositoryPath(String repositoryPath) { + this.repositoryPath = repositoryPath; + } + public boolean isSupportedByEclipsePlugin() { var supportedLanguages = new String[] { Language.JAVA }; return language != null && Arrays.stream(supportedLanguages).anyMatch(langId -> language.getId().equals(langId)); } + public String getRepositoryUrl() { + return pool.isLinkedToRepository() && repositoryPath != null + ? Path.of(pool.getRepositoryUrl(), "-", "tree", "main", repositoryPath).toString() + : null; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java index 49c2d49052cc182d674cd3902c6978ea24580c2f..11d87dc077e752bd6885cf485011fc0d511915fd 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskContentRepository.java @@ -9,8 +9,10 @@ import de.hsrm.sls.subato.shared.helper.FileHelper; import de.hsrm.sls.subato.shared.helper.ZipHelper; import de.hsrm.sls.subato.tasks.imp.ImportSpecRequest; import de.hsrm.sls.subato.tasks.imp.TaskImportException; +import de.hsrm.sls.subato.tasks.stef.StefSpec; import de.hsrm.sls.subato.tasks.stef.file.FileRef; import de.hsrm.sls.subato.tasks.stef.file.FileType; +import de.hsrm.sls.subato.tasks.stef.parser.SpecParser; import net.lingala.zip4j.model.ExcludeFileFilter; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -36,28 +38,37 @@ public class TaskContentRepository { @Autowired private FileTreeService fileTreeService; + @Autowired + private SpecParser specParser; + public Path getTaskPath(Task task) { return Paths.get(subatoConfig.getDataDir()) .resolve("task") .resolve(String.valueOf(task.getId())); } - public List<FileRef> getFileRefs(Task task) { + public StefSpec getSpec(Task task) { var indexFile = getIndexFile(task); if(!indexFile.exists()) { - throw new RuntimeException(); + var spec = specParser.parseSpec(getTaskPath(task).toString()); + createIndex(spec, task); + return spec; } var objectMapper = new ObjectMapper(); try { var specContent = Files.readString(indexFile.toPath(), StandardCharsets.UTF_8); - var spec = objectMapper.readValue(specContent, new TypeReference<List<FileRef>>() {}); + var spec = objectMapper.readValue(specContent, StefSpec.class); return spec; } catch (IOException e) { throw new RuntimeException(e); } } + public List<FileRef> getFileRefs(Task task) { + return getSpec(task).getMeta().getFiles(); + } + public Optional<FileRef> getFileRefById(Task task, UUID uuid) { var files = getFileRefs(task); return files.stream().filter(f -> f.getFile().getUuid().equals(uuid)).findFirst(); @@ -111,7 +122,7 @@ public class TaskContentRepository { buildBundle(request); // create an index for performant access - createIndex(request); + createIndex(request.spec(), request.task()); } catch (Exception e) { try { FileUtils.copyDirectory(backupPath.toFile(), taskPath.toFile()); @@ -193,7 +204,7 @@ public class TaskContentRepository { var bundleFile = getBundleFile(request.task()); var zipFile = ZipHelper.zip(tempDir, bundleFile, false); - var item = fileTreeService.createTreeFrom(zipFile, getTaskPath(request.task()).relativize(bundleDir).toString(), false, true); + var item = fileTreeService.createTreeFrom(zipFile, getTaskPath(request.task()).relativize(bundleDir).toString(), false, true, new ArrayList<>()); var taskFile = new FileRef(); taskFile.setPath(bundleFile.getName()); @@ -212,14 +223,14 @@ public class TaskContentRepository { private File getIndexFile(Task task) { var taskPath = getTaskPath(task); - return taskPath.resolve(".stef.index.json").toFile(); + return taskPath.resolve(".stef.cache.json").toFile(); } - private void createIndex(ImportSpecRequest request) { + private void createIndex(StefSpec spec, Task task) { try { ObjectMapper objectMapper = new ObjectMapper(); - var serialized = objectMapper.writeValueAsString(request.spec().getMeta().getFiles()); - var indexFile = getIndexFile(request.task()); + var serialized = objectMapper.writeValueAsString(spec); + var indexFile = getIndexFile(task); Files.write(indexFile.toPath(), serialized.getBytes(StandardCharsets.UTF_8)); } catch (JsonProcessingException e) { throw new RuntimeException(e); diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java index cfba5810e1ecce5df88668d3809151c14ad05e72..f49ef828360e25151ee728b2ae7be7d2424c5d68 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskDetailDto.java @@ -27,6 +27,7 @@ public class TaskDetailDto { private boolean acceptanceTestAvailable; private boolean supportedByEclipsePlugin; + private String repositoryUrl; public long getId() { return id; @@ -147,4 +148,12 @@ public class TaskDetailDto { public void setSupportedByEclipsePlugin(boolean supportedByEclipsePlugin) { this.supportedByEclipsePlugin = supportedByEclipsePlugin; } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } } diff --git a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java index 2d1f3599a7dd13d12061cb681adc2356c5e9a45e..df53ff343759520761d3edf0e7ad5e88661deefb 100644 --- a/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java +++ b/subato/src/main/java/de/hsrm/sls/subato/tasks/task/TaskSpecifications.java @@ -13,7 +13,11 @@ public class TaskSpecifications { } public static Specification<Task> applyQuery(String q) { - return (ex, cq, cb) -> cb.or(ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.name)), ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.description))); + return (ex, cq, cb) -> cb.or( + ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.name)), + ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.idName)), + ignoreCaseContains(cb, Optional.of(q), ex.get(Task_.description)) + ); } public static Specification<Task> poolEquals(Long poolId) { diff --git a/subato/src/main/resources/application.properties b/subato/src/main/resources/application.properties index 36ade272307edb11cb75fc62d88fbcf9344241df..8b0146a132e58cf4a0eff0105f4b9503407d1074 100755 --- a/subato/src/main/resources/application.properties +++ b/subato/src/main/resources/application.properties @@ -16,7 +16,7 @@ subato.tmp_dir=/tmp/subato2-tmp subato.evaluator.url=${SUBATO2_EVALUATOR_URL:localhost} subato.evaluator.port=${SUBATO2_EVALUATOR_PORT:5081} subato.evaluator.registry_port=${SUBATO2_EVALUATOR_REGISTRY_PORT:7777} -subato.evaluator.timeout=${SUBATO2_EVALUATOR_TIMEOUT:60000} +subato.evaluator.timeout=${SUBATO2_EVALUATOR_TIMEOUT:90000} subato.task-pool-sync.git.user=${SUBATO2_TASK_POOL_SYNC_GIT_USER:Subato} subato.task-pool-sync.git.token=${SUBATO2_TASK_POOL_SYNC_GIT_TOKEN:} subato.task-pool-sync.enabled=${SUBATO2_TASK_POOL_SYNC_ENABLED:false} diff --git a/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml b/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml new file mode 100644 index 0000000000000000000000000000000000000000..119910b0caa5121c271fe093dafb2be930292200 --- /dev/null +++ b/subato/src/main/resources/db/changelog/2023/02/23-01-changelog.yaml @@ -0,0 +1,13 @@ +databaseChangeLog: + - changeSet: + id: 1677115320729-1 + author: fischer (generated) + objectQuotingStrategy: QUOTE_ONLY_RESERVED_WORDS + changes: + - addColumn: + columns: + - column: + name: repository_path + type: VARCHAR(255) + tableName: task + diff --git a/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml b/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1d8d4e78d924bd8472c38d59c33c1dee6194a3a2 --- /dev/null +++ b/subato/src/main/resources/db/changelog/2023/02/26-01-changelog.yaml @@ -0,0 +1,13 @@ +databaseChangeLog: + - changeSet: + id: 1677416347841-1 + author: fischer (generated) + objectQuotingStrategy: QUOTE_ONLY_RESERVED_WORDS + changes: + - addNotNullConstraint: + columnDataType: INT + columnName: submitted_with + tableName: solution + validate: true + defaultNullValue: 1 + diff --git a/subato/src/main/resources/db/changelog/db.changelog-master.yaml b/subato/src/main/resources/db/changelog/db.changelog-master.yaml index c1264041d7a4bb7792dafe7baed866703b60b02a..424ae0cbba0370d4487e1fd5b65a9ccf33f9e10b 100644 --- a/subato/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/subato/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1,3 +1,7 @@ databaseChangeLog: - include: - file: db/changelog/2023/02/08-01-changelog.yaml \ No newline at end of file + file: db/changelog/2023/02/08-01-changelog.yaml + - include: + file: db/changelog/2023/02/23-01-changelog.yaml + - include: + file: db/changelog/2023/02/26-01-changelog.yaml \ No newline at end of file diff --git a/subato/src/test/resources/application.properties b/subato/src/test/resources/application.properties index 7c5ea70774a34a8b067918051ea8544730fb57be..8109a8a7c6911538485f411576bb72992759b81d 100755 --- a/subato/src/test/resources/application.properties +++ b/subato/src/test/resources/application.properties @@ -18,12 +18,13 @@ spring.jackson.default-property-inclusion=non_null spring.jackson.mapper.default-view-inclusion=true server.servlet.session.persistent=false +subato.mime_file=${SUBATO2_MIME_FILE:config/mime.yml} subato.data_dir=subato2-data-test subato.tmp_dir=/tmp/subato2-tmp-test subato.evaluator.url=subato-test2.local.cs.hs-rm.de subato.evaluator.port=5081 subato.evaluator.registry_port=7777 -subato.evaluator.timeout=60000 +subato.evaluator.timeout=90000 subato.task-pool-sync.git.user=${SUBATO2_TASK_POOL_SYNC_GIT_USER:Subato} subato.task-pool-sync.git.token=${SUBATO2_TASK_POOL_SYNC_GIT_TOKEN:} subato.task-pool-sync.enabled=${SUBATO2_TASK_POOL_SYNC_ENABLED:false}