From 35e30db2476fae08d9f708757ec65a3806fa216c Mon Sep 17 00:00:00 2001 From: ylivuoto Date: Thu, 19 Sep 2024 09:26:45 +0300 Subject: [PATCH 01/15] [skip ci] --- timApp/document/macroinfo.py | 3 ++ timApp/static/scripts/tim/app.module.ts | 2 ++ timApp/static/scripts/tim/main.ts | 3 +- .../user-profile/user-profile.component.scss | 3 ++ .../user-profile/user-profile.component.ts | 35 +++++++++++++++++++ timApp/user/user.py | 4 +++ tim_common/html_sanitize.py | 1 + 7 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss create mode 100644 timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts diff --git a/timApp/document/macroinfo.py b/timApp/document/macroinfo.py index 12cf9612ea..04e0d7464d 100644 --- a/timApp/document/macroinfo.py +++ b/timApp/document/macroinfo.py @@ -153,6 +153,7 @@ def get_macros_preserving_user(self) -> dict[str, object]: "useremail": f"{self.macro_delimiter}useremail{self.macro_delimiter}", "loggedUsername": f"{self.macro_delimiter}loggedUsername{self.macro_delimiter}", "userfolder": f"{self.macro_delimiter}userfolder{self.macro_delimiter}", + "profilepicture": f"{self.macro_delimiter}profilepicture{self.macro_delimiter}", } ) return macros @@ -178,6 +179,7 @@ def get_user_specific_macros(user_ctx: UserContext) -> dict[str, str | None]: - useremail: The email address of the user. - loggedUsername: The username of the user that is logged in. - userfolder: The personal folder of the user. + - profilepicture: The profile picture of the user. :param user_ctx: User context to get the macros for. :return: Dictionary of user macros. @@ -192,6 +194,7 @@ def get_user_specific_macros(user_ctx: UserContext) -> dict[str, str | None]: "userfolder": escape( user.get_personal_folder().path ), # personal folder object is cached and usually reused + "profilepicture": escape(user.get_profile_picture()), } diff --git a/timApp/static/scripts/tim/app.module.ts b/timApp/static/scripts/tim/app.module.ts index 4728055a74..a640aa8ed7 100644 --- a/timApp/static/scripts/tim/app.module.ts +++ b/timApp/static/scripts/tim/app.module.ts @@ -42,6 +42,7 @@ import {SelfExpireComponent} from "tim/item/self-expire.component"; import {SearchButtonComponent} from "tim/search/search-button.component"; import {SessionVerify} from "tim/util/session-verify.interceptor"; import {RoleInfoComponent} from "tim/header/role-info.component"; +import {UserProfileComponent} from "tim/plugin/user-profile/user-profile.component"; @NgModule({ declarations: [ @@ -72,6 +73,7 @@ import {RoleInfoComponent} from "tim/header/role-info.component"; GamificationMapComponent, SelfExpireComponent, RoleInfoComponent, + UserProfileComponent, ], imports: [ BrowserModule, diff --git a/timApp/static/scripts/tim/main.ts b/timApp/static/scripts/tim/main.ts index cac5d14501..3915bb1ddd 100644 --- a/timApp/static/scripts/tim/main.ts +++ b/timApp/static/scripts/tim/main.ts @@ -78,6 +78,7 @@ import {genericglobals, isErrorGlobals} from "tim/util/globals"; import {ParCompiler} from "tim/editor/parCompiler"; import {PrintButtonComponent} from "tim/ui/print-button.component"; import {FormulaEditorLoaderComponent} from "../../../modules/cs/js/editor/math-editor/formula-editor-loader.component"; +import {UserProfileComponent} from "tim/plugin/user-profile/user-profile.component"; BackspaceDisabler.disable(); @@ -148,7 +149,7 @@ function createDowngradedAppModule() { doDowngrade(dg, "timSearchButton", SearchButtonComponent); doDowngrade(dg, "timRoleInfo", RoleInfoComponent); doDowngrade(dg, "csFormulaEditorLoader", FormulaEditorLoaderComponent); - + doDowngrade(dg, "timUserProfile", UserProfileComponent); return dg; } diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss new file mode 100644 index 0000000000..2b44d06f45 --- /dev/null +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss @@ -0,0 +1,3 @@ +div { + background-color: #0F4096; +} \ No newline at end of file diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts new file mode 100644 index 0000000000..712651d8e9 --- /dev/null +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts @@ -0,0 +1,35 @@ +import type {OnInit} from "@angular/core"; +import {Component} from "@angular/core"; +import {getURLParameter} from "tim/util/utils"; +import {inIframe} from "tim/plugin/util"; +import {$httpParamSerializer} from "tim/util/ngimport"; +import {showLoginDialog} from "tim/user/showLoginDialog"; +import {Users} from "tim/user/userService"; + +@Component({ + selector: "user-profile", + template: ` +
+ Hello + +
+ `, + styleUrls: ["./user-profile.component.scss"], +}) +export class UserProfileComponent implements OnInit { + ngOnInit() { + console.log("Kisu123"); + } + + clickThis() { + console.log("Hello123"); + } +} +/* +@NgModule({ + declarations: [UserProfileComponent], +}) +export class UserProfileModule implements DoBootstrap { + ngDoBootstrap(appRef: ApplicationRef) {} +} +*/ diff --git a/timApp/user/user.py b/timApp/user/user.py index b5b1b6bd4b..10bbbb3e1e 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -1608,6 +1608,10 @@ def get_model_answer_user() -> Optional["User"]: # TODO: think other languages also return User.get_by_name(current_app.config["MODEL_ANSWER_USER_NAME"]) + def get_profile_picture(self) -> Optional[str]: + path = self.get_personal_folder().path + return "/images/730648/Riikola_Olli-Pekka.png" + def get_membership_end(u: User, group_ids: set[int]) -> datetime | None: """ diff --git a/tim_common/html_sanitize.py b/tim_common/html_sanitize.py index 0a907dcf3c..44282cc0f9 100644 --- a/tim_common/html_sanitize.py +++ b/tim_common/html_sanitize.py @@ -114,6 +114,7 @@ "tim-message-send", "tim-notification-options", "tim-search-button", + "user-profile", ] TIM_SAFE_ATTRS_MAP = { From b67c164a941e55659bb26112d1141db9afabcfb8 Mon Sep 17 00:00:00 2001 From: ylivuoto Date: Fri, 20 Sep 2024 11:47:09 +0300 Subject: [PATCH 02/15] Basic idea ready for profile component --- .../user-profile/user-profile.component.scss | 37 ++++++++++++++- .../user-profile/user-profile.component.ts | 47 ++++++++++++++----- timApp/user-profile/routes.py | 16 +++++++ timApp/user/user.py | 4 +- tim_common/html_sanitize.py | 2 +- 5 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 timApp/user-profile/routes.py diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss index 2b44d06f45..b4d0b61ac2 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss @@ -1,3 +1,36 @@ -div { - background-color: #0F4096; +tim-user-profile { + width: 100%; + padding: 0; +} + +.container { + background-color: #ffefef; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: "left right"; + padding: 0; + max-width: 100%; +} + +.left-column { + grid-area: left; +} + +.right-column { + grid-area: right; +} + +.btn-profile { + height: 3em; +} + +.profile-heading { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.profile-heading h2 { + margin: unset; } \ No newline at end of file diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts index 712651d8e9..f8024f13f6 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts @@ -1,28 +1,49 @@ import type {OnInit} from "@angular/core"; import {Component} from "@angular/core"; -import {getURLParameter} from "tim/util/utils"; -import {inIframe} from "tim/plugin/util"; -import {$httpParamSerializer} from "tim/util/ngimport"; -import {showLoginDialog} from "tim/user/showLoginDialog"; -import {Users} from "tim/user/userService"; +import {HttpClient, HttpClientModule} from "@angular/common/http"; @Component({ - selector: "user-profile", + selector: "tim-user-profile", template: ` -
- Hello - +
+

About

+ +
+
+
+ profile-picture +
+
+

+ Paragraph inside +

+
`, styleUrls: ["./user-profile.component.scss"], }) export class UserProfileComponent implements OnInit { - ngOnInit() { - console.log("Kisu123"); + pictureUrl = "http://localhost/images/68/Riikola_Olli-Pekka.png"; + profileUrl: string = ""; + + constructor(private http: HttpClient) {} + + async ngOnInit() { + const data = await this.getProfileData(); + console.log(data); + } + + async getProfileData() { + const data = this.http.get("/profile"); + return data; + } + modifyUserProfile() { + this.profileUrl = "URL not set."; + console.log("Go to profile document: " + this.profileUrl); } - clickThis() { - console.log("Hello123"); + get profilePictureUrl() { + return this.pictureUrl; } } /* diff --git a/timApp/user-profile/routes.py b/timApp/user-profile/routes.py new file mode 100644 index 0000000000..f04f23b253 --- /dev/null +++ b/timApp/user-profile/routes.py @@ -0,0 +1,16 @@ +from flask import Blueprint, current_app, Response +from sqlalchemy.orm import selectinload + +from timApp.auth.sessioninfo import get_current_user_object +from timApp.document.docentry import DocEntry, get_documents +from timApp.item.block import Block +from timApp.util.flask.responsehelper import json_response + +course_blueprint = Blueprint("profile", __name__, url_prefix="/profile") + + +@course_blueprint.get() +def get_data_from_profile_document() -> Response: + current_user = get_current_user_object() + profile_picture = current_user.get_profile_picture() + return json_response({"test": "Hello World!"}) diff --git a/timApp/user/user.py b/timApp/user/user.py index 10bbbb3e1e..70e3e2791e 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -1608,9 +1608,9 @@ def get_model_answer_user() -> Optional["User"]: # TODO: think other languages also return User.get_by_name(current_app.config["MODEL_ANSWER_USER_NAME"]) - def get_profile_picture(self) -> Optional[str]: + def get_profile(self) -> Optional[str]: path = self.get_personal_folder().path - return "/images/730648/Riikola_Olli-Pekka.png" + return "" def get_membership_end(u: User, group_ids: set[int]) -> datetime | None: diff --git a/tim_common/html_sanitize.py b/tim_common/html_sanitize.py index 44282cc0f9..007419c36b 100644 --- a/tim_common/html_sanitize.py +++ b/tim_common/html_sanitize.py @@ -114,7 +114,7 @@ "tim-message-send", "tim-notification-options", "tim-search-button", - "user-profile", + "tim-user-profile", ] TIM_SAFE_ATTRS_MAP = { From ce7b2357df9ddab06b7cb4713b32793a743e3e1b Mon Sep 17 00:00:00 2001 From: ylivuoto Date: Mon, 23 Sep 2024 12:00:00 +0300 Subject: [PATCH 03/15] Update, input add for user-profilecomponent [skip ci] --- timApp/document/macroinfo.py | 2 +- .../tim/plugin/user-profile/user-profile.component.ts | 10 +++++++--- timApp/user-profile/routes.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/timApp/document/macroinfo.py b/timApp/document/macroinfo.py index 04e0d7464d..34572421e2 100644 --- a/timApp/document/macroinfo.py +++ b/timApp/document/macroinfo.py @@ -194,7 +194,7 @@ def get_user_specific_macros(user_ctx: UserContext) -> dict[str, str | None]: "userfolder": escape( user.get_personal_folder().path ), # personal folder object is cached and usually reused - "profilepicture": escape(user.get_profile_picture()), + "profilepicture": escape(user.get_profile()), } diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts index f8024f13f6..429eb4e02f 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts @@ -1,6 +1,7 @@ -import type {OnInit} from "@angular/core"; +import {Input, OnInit} from "@angular/core"; import {Component} from "@angular/core"; import {HttpClient, HttpClientModule} from "@angular/common/http"; +import {redirectToItem} from "tim/item/IItem"; @Component({ selector: "tim-user-profile", @@ -15,7 +16,7 @@ import {HttpClient, HttpClientModule} from "@angular/common/http";

- Paragraph inside + Paragraph inside {{foo}}

@@ -23,14 +24,17 @@ import {HttpClient, HttpClientModule} from "@angular/common/http"; styleUrls: ["./user-profile.component.scss"], }) export class UserProfileComponent implements OnInit { - pictureUrl = "http://localhost/images/68/Riikola_Olli-Pekka.png"; + @Input() foo?: string | null; + pictureUrl: string = ""; profileUrl: string = ""; constructor(private http: HttpClient) {} async ngOnInit() { + // TODO: lue inputosta käyttäjä const data = await this.getProfileData(); console.log(data); + console.log(this.foo); } async getProfileData() { diff --git a/timApp/user-profile/routes.py b/timApp/user-profile/routes.py index f04f23b253..d054be2a1c 100644 --- a/timApp/user-profile/routes.py +++ b/timApp/user-profile/routes.py @@ -12,5 +12,5 @@ @course_blueprint.get() def get_data_from_profile_document() -> Response: current_user = get_current_user_object() - profile_picture = current_user.get_profile_picture() + profile_picture = current_user.get_profile() return json_response({"test": "Hello World!"}) From 5aebfc4a8f4ebd4b48f0519650e1fc38efb543bc Mon Sep 17 00:00:00 2001 From: ylivuoto Date: Thu, 26 Sep 2024 13:11:24 +0300 Subject: [PATCH 04/15] Profile picture feature add --- timApp/modules/cs/js/util/file-select.ts | 7 +- timApp/static/scripts/tim/app.module.ts | 2 + .../user-profile/user-profile.component.scss | 24 +++ .../user-profile/user-profile.component.ts | 160 ++++++++++++++++-- timApp/tim.py | 2 + timApp/upload/upload.py | 22 ++- timApp/user-profile/routes.py | 16 -- timApp/user/user.py | 2 +- timApp/user_profile/__init__.py | 0 timApp/user_profile/routes.py | 66 ++++++++ tim_common/html_sanitize.py | 3 + 11 files changed, 261 insertions(+), 43 deletions(-) delete mode 100644 timApp/user-profile/routes.py create mode 100644 timApp/user_profile/__init__.py create mode 100644 timApp/user_profile/routes.py diff --git a/timApp/modules/cs/js/util/file-select.ts b/timApp/modules/cs/js/util/file-select.ts index 032230516d..dfbca43f88 100644 --- a/timApp/modules/cs/js/util/file-select.ts +++ b/timApp/modules/cs/js/util/file-select.ts @@ -1,5 +1,5 @@ /* eslint no-underscore-dangle: ["error", { "allow": ["files_", "multipleElements_"] }] */ -import type {QueryList} from "@angular/core"; +import type {OnInit, QueryList} from "@angular/core"; import { ChangeDetectorRef, Component, @@ -433,7 +433,7 @@ export class FileSelectComponent { [maxSize]="maxSize"> `, }) -export class FileSelectManagerComponent { +export class FileSelectManagerComponent implements OnInit { // TODO: translations @Input() allowMultiple: boolean = true; @Input() dragAndDrop: boolean = true; @@ -461,6 +461,9 @@ export class FileSelectManagerComponent { }[] = []; constructor(public cdr: ChangeDetectorRef) {} + ngOnInit() { + console.log("File select init."); + } get multipleElements() { return this.multipleElements_; diff --git a/timApp/static/scripts/tim/app.module.ts b/timApp/static/scripts/tim/app.module.ts index a640aa8ed7..834715fa0d 100644 --- a/timApp/static/scripts/tim/app.module.ts +++ b/timApp/static/scripts/tim/app.module.ts @@ -43,6 +43,7 @@ import {SearchButtonComponent} from "tim/search/search-button.component"; import {SessionVerify} from "tim/util/session-verify.interceptor"; import {RoleInfoComponent} from "tim/header/role-info.component"; import {UserProfileComponent} from "tim/plugin/user-profile/user-profile.component"; +import {CsUtilityModule} from "../../../modules/cs/js/util/module"; @NgModule({ declarations: [ @@ -87,6 +88,7 @@ import {UserProfileComponent} from "tim/plugin/user-profile/user-profile.compone TypeaheadModule.forRoot(), TooltipModule.forRoot(), TabsModule.forRoot(), + CsUtilityModule, ], providers: [ SessionVerify, diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss index b4d0b61ac2..0ef62c0802 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss @@ -1,6 +1,17 @@ tim-user-profile { width: 100%; padding: 0; + +} + +.tim-user-profile-container { + box-shadow: 0 30px 200px #0000cc38, 0 0 20px #0F0C0020; + padding: 1em; + border-radius: 1em; + +} + +:host(tim-user-profile) { } .container { @@ -14,9 +25,11 @@ tim-user-profile { .left-column { grid-area: left; + padding: .5em; } .right-column { + padding: .5em; grid-area: right; } @@ -33,4 +46,15 @@ tim-user-profile { .profile-heading h2 { margin: unset; +} + +#tim-user-profile-picture { + aspect-ratio: 1; + border-radius: 50%; + width: 50%; + object-fit: cover; +} + +.profile-picture-box { + margin: 1em; } \ No newline at end of file diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts index 429eb4e02f..f718992c61 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts @@ -1,53 +1,181 @@ -import {Input, OnInit} from "@angular/core"; +import {Input, OnInit, ViewChild} from "@angular/core"; import {Component} from "@angular/core"; import {HttpClient, HttpClientModule} from "@angular/common/http"; -import {redirectToItem} from "tim/item/IItem"; +import {IItem, redirectToItem} from "tim/item/IItem"; +import type {IDocument} from "tim/item/IItem"; +import {to, to2} from "tim/util/utils"; +import {$http} from "tim/util/ngimport"; +import type { + IFile, + IFileSpecification, +} from "../../../../../modules/cs/js/util/file-select"; +import {FileSelectManagerComponent} from "../../../../../modules/cs/js/util/file-select"; +import {CsUtilityModule} from "../../../../../modules/cs/js/util/module"; +import {tr} from "date-fns/locale"; +import * as t from "io-ts"; +import {int} from "@zxing/library/es2015/customTypings"; + +interface ProfileData { + profile_picture_path: string; +} + +const UploadedFile = t.intersection([ + t.type({ + path: t.string, + type: t.string, + }), + t.partial({ + rotation: t.number, + }), +]); + +interface IUploadResponse { + file: string; + type: string; + block: number; +} + +interface IUploadedFile extends t.TypeOf {} @Component({ selector: "tim-user-profile", + styleUrls: ["./user-profile.component.scss"], template: ` + `, - styleUrls: ["./user-profile.component.scss"], }) export class UserProfileComponent implements OnInit { - @Input() foo?: string | null; + @Input() userId?: int = 0; + @Input() profileDocument?: int; + @Input() modifyEnabled: boolean = false; pictureUrl: string = ""; profileUrl: string = ""; + uploadUrl?: string; + dragAndDrop: boolean = true; + uploadedFiles: IUploadedFile[] = []; + stem: string = "Change a profile picture"; + fileSelect?: FileSelectManagerComponent; constructor(private http: HttpClient) {} async ngOnInit() { // TODO: lue inputosta käyttäjä - const data = await this.getProfileData(); - console.log(data); - console.log(this.foo); + await this.getProfileData(this.userId); + this.uploadUrl = `/profile/picture/${this.profileDocument}`; + console.log(this.userId); + } + + @ViewChild(FileSelectManagerComponent) + set fileSelectSetter(component: FileSelectManagerComponent | undefined) { + this.fileSelect = component; + if (!component) { + return; + } + + const files: IFileSpecification[] = []; + files.push({ + paths: ["profilepicture"], + upload: true, + }); + + component.allowMultiple = true; // this.markup.allowMultipleFiles; + component.multipleElements = true; // this.markup.multipleUploadElements; + component.files = files; } - async getProfileData() { - const data = this.http.get("/profile"); + async getProfileData(userId?: int) { + const data = this.http + .get(`/profile/${userId}`) + .subscribe((res: any) => { + console.log(res); + this.pictureUrl = res.profile_picture_path; + this.profileUrl = res.profile_path; + return true; + }); return data; } - modifyUserProfile() { - this.profileUrl = "URL not set."; + async modifyUserProfile() { + /*const doc = await to($http.get(this.profileUrl)); + + if (!doc.ok) { + console.log("No profile document."); + return doc.result.data.error; + } + + */ console.log("Go to profile document: " + this.profileUrl); + window.location.href = this.profileUrl; + } + + onFileLoad(file: IFile) { + console.log(file); + } + + onUpload(resp: any) { + console.log("On upload"); + console.log(resp); + if (!resp) { + return; + } + + const resps = resp as [IUploadResponse]; + for (const response of resps) { + this.uploadedFiles.push({path: response.file, type: response.type}); + } + } + + onPasteFocusout() { + console.log("Focus out"); + } + + onPaste(event: any) { + console.log("On paste"); + console.log(event); + } + + onImgLoad(e: Event, index: number): void { + console.log("On image load"); + console.log(e); } - get profilePictureUrl() { - return this.pictureUrl; + onUploadDone(success: boolean) { + console.log("Upload done"); } } /* diff --git a/timApp/tim.py b/timApp/tim.py index d5eaf87159..5e776eef68 100755 --- a/timApp/tim.py +++ b/timApp/tim.py @@ -88,6 +88,7 @@ from timApp.user.settings.settings import settings_page from timApp.user.settings.styles import styles from timApp.user.verification.routes import verify +from timApp.user_profile.routes import profile_blueprint from timApp.util.error_handlers import register_errorhandlers from timApp.util.flask.cache import cache from timApp.util.flask.requesthelper import ( @@ -166,6 +167,7 @@ quantum_circuit_plugin, symbolbutton_plugin, ide, + profile_blueprint, ] if app.config["BOOKMARKS_ENABLED"]: diff --git a/timApp/upload/upload.py b/timApp/upload/upload.py index 9b86ac72a3..b0880a5ade 100644 --- a/timApp/upload/upload.py +++ b/timApp/upload/upload.py @@ -431,16 +431,22 @@ def convert_pdf_or_compress_image(f: UploadedFile, u: User, d: DocInfo, task_id: @upload.post("/upload/") -def upload_file(): +def upload_file(doc_id: int = None, uploaded_file: UploadedFile = None): + print(request) if not logged_in(): raise AccessDenied("You have to be logged in to upload a file.") - file = request.files.get("file") - if file is None: + uploaded_file = ( + request.files.get("file") if uploaded_file is None else uploaded_file + ) + if uploaded_file is None: raise RouteException("Missing file") folder = request.form.get("folder") if folder is not None: - return upload_document(folder, file) - doc_id = request.form.get("doc_id") + return upload_document(folder, uploaded_file) + + # Doc id could be provided internally + if doc_id is None: + doc_id = request.form.get("doc_id") if not doc_id: raise RouteException("Missing doc_id") d = DocEntry.find_by_id(int(doc_id)) @@ -449,11 +455,11 @@ def upload_file(): attachment_params = json.loads(request.form.get("attachmentParams")) autostamp = attachment_params[len(attachment_params) - 1] # TODO: Notify the user that the file type cannot be stamped - if file.mimetype not in STAMPABLE_MIMETYPES and autostamp: + if uploaded_file.mimetype not in STAMPABLE_MIMETYPES and autostamp: raise StampDataInvalidError("Cannot stamp file") except: # Just go on with normal upload if necessary conditions are not met. - return upload_image_or_file(d, file) + return upload_image_or_file(d, uploaded_file) else: if autostamp: # Only go here if attachment params are valid enough and autostamping is valid and true @@ -474,7 +480,7 @@ def upload_file(): ) custom_stamp_model = attachment_params[len(attachment_params) - 2] return upload_and_stamp_attachment( - d, file, stamp_data, stamp_format, custom_stamp_model + d, uploaded_file, stamp_data, stamp_format, custom_stamp_model ) # If attachment isn't a pdf, gives an error too (since it's in 'showPdf' plugin) except PdfError as e: diff --git a/timApp/user-profile/routes.py b/timApp/user-profile/routes.py deleted file mode 100644 index d054be2a1c..0000000000 --- a/timApp/user-profile/routes.py +++ /dev/null @@ -1,16 +0,0 @@ -from flask import Blueprint, current_app, Response -from sqlalchemy.orm import selectinload - -from timApp.auth.sessioninfo import get_current_user_object -from timApp.document.docentry import DocEntry, get_documents -from timApp.item.block import Block -from timApp.util.flask.responsehelper import json_response - -course_blueprint = Blueprint("profile", __name__, url_prefix="/profile") - - -@course_blueprint.get() -def get_data_from_profile_document() -> Response: - current_user = get_current_user_object() - profile_picture = current_user.get_profile() - return json_response({"test": "Hello World!"}) diff --git a/timApp/user/user.py b/timApp/user/user.py index 70e3e2791e..e57c7af6c6 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -1610,7 +1610,7 @@ def get_model_answer_user() -> Optional["User"]: def get_profile(self) -> Optional[str]: path = self.get_personal_folder().path - return "" + return path def get_membership_end(u: User, group_ids: set[int]) -> datetime | None: diff --git a/timApp/user_profile/__init__.py b/timApp/user_profile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/user_profile/routes.py b/timApp/user_profile/routes.py new file mode 100644 index 0000000000..9c87ae898c --- /dev/null +++ b/timApp/user_profile/routes.py @@ -0,0 +1,66 @@ +from flask import Blueprint, Response +from timApp.auth.sessioninfo import get_current_user_object +from timApp.user.user import User +from timApp.document.docentry import DocEntry, get_documents +from timApp.util.flask.responsehelper import json_response +from dataclasses import dataclass +from flask import request +from timApp.upload.upload import upload_file +from timApp.upload.uploadedfile import ( + UploadedFile, +) + +profile_blueprint = Blueprint("profile", __name__, url_prefix="/profile") + + +@dataclass +class ProfileInfo: + username: str + profile_path: str + profile_picture_path: str + + def to_json(self) -> dict[str, str | None]: + return { + "username": self.username, + "profile_path": self.profile_path, + "profile_picture_path": self.profile_picture_path, + } + + +PROFILE_PICTURE_KEY = "profile_picture_path" + + +@profile_blueprint.get("/") +def get_data_from_profile_document(userid: int) -> Response: + """ + Provide user profile details according requested user. + :param userid: ID of the user + :return: JSON data, containing user profile details + """ + requested_user = User.get_by_id(userid) if userid else get_current_user_object() + username = requested_user.name + personal_folder = requested_user.get_personal_folder() + profile_path = f"/view/{personal_folder.path}/profile" + + # Find a profile picture url from document settings + profile_settings = personal_folder.get_document("profile").document.get_settings() + profile_picture_path = profile_settings.get(PROFILE_PICTURE_KEY) + + profile_data: ProfileInfo = ProfileInfo( + username, profile_path, profile_picture_path + ) + + return json_response(profile_data.to_json()) + + +@profile_blueprint.post("/picture//") +def upload_profile_picture(document_id: int) -> Response: + file_to_upload = request.files.get("file") + upload_response = upload_file(document_id, file_to_upload) + image_url_suffix = upload_response.json["image"] + document_entry = DocEntry.find_by_id(document_id) + document_entry.document.add_setting( + PROFILE_PICTURE_KEY, f"/images/{image_url_suffix}" + ) + + return upload_response diff --git a/tim_common/html_sanitize.py b/tim_common/html_sanitize.py index 007419c36b..03b97bf28d 100644 --- a/tim_common/html_sanitize.py +++ b/tim_common/html_sanitize.py @@ -264,6 +264,9 @@ "folder", "button-text", "wrapper", + "profile-id", + "profile-document", + "modify-enabled", ] ) From 7d8a9ec9f5cde90c70f1f74178fedf6b60b15cff Mon Sep 17 00:00:00 2001 From: ylivuoto Date: Fri, 4 Oct 2024 11:58:52 +0300 Subject: [PATCH 05/15] Profile feature update, textfield component add --- .../js/standalone-textfield.component.scss | 34 +++ .../js/standalone-textfield.component.ts | 199 +++++++++++++ timApp/static/scripts/tim/app.module.ts | 4 - .../user-profile/user-profile.component.scss | 12 +- .../user-profile/user-profile.component.ts | 269 +++++++++++++----- .../tim/ui/input-dialog.component.scss | 4 +- .../scripts/tim/ui/input-dialog.component.ts | 15 +- timApp/upload/upload.py | 22 +- timApp/user_profile/routes.py | 59 +++- tim_common/html_sanitize.py | 5 +- 10 files changed, 517 insertions(+), 106 deletions(-) create mode 100644 timApp/modules/fields/js/standalone-textfield.component.scss create mode 100644 timApp/modules/fields/js/standalone-textfield.component.ts diff --git a/timApp/modules/fields/js/standalone-textfield.component.scss b/timApp/modules/fields/js/standalone-textfield.component.scss new file mode 100644 index 0000000000..590c75d14e --- /dev/null +++ b/timApp/modules/fields/js/standalone-textfield.component.scss @@ -0,0 +1,34 @@ +@import "fields-box-common"; + +.form-control { + display: block; + width: 100%; +} + +textarea.form-control { + height: unset; + +} + +p { + padding-top: 0px; + padding-left: 0px; + padding-bottom: 0px; + margin-bottom: 0px; + margin-top: 0px; +} + +.timButton { + margin-bottom: 5px; +} + +.textfield input{ + padding: .3em; + margin: .5em 0 .5em 0; +} + +.textfield textarea { + padding: .3em; + overflow:hidden; + height: auto; +} \ No newline at end of file diff --git a/timApp/modules/fields/js/standalone-textfield.component.ts b/timApp/modules/fields/js/standalone-textfield.component.ts new file mode 100644 index 0000000000..d153a79c8d --- /dev/null +++ b/timApp/modules/fields/js/standalone-textfield.component.ts @@ -0,0 +1,199 @@ +/** + * Defines the client-side implementation of textfield/label plugin. + */ +import * as t from "io-ts"; +import { + ApplicationRef, + Component, + Output, + DoBootstrap, + Input, + NgModule, + NgZone, + OnInit, + EventEmitter, + SimpleChange, + SimpleChanges, + OnChanges, +} from "@angular/core"; +import { + GenericPluginMarkup, + Info, + nullable, + withDefault, +} from "../../../static/scripts/tim/plugin/attributes"; +import {HttpClient, HttpClientModule} from "@angular/common/http"; +import {FormsModule} from "@angular/forms"; +import {TooltipModule} from "ngx-bootstrap/tooltip"; +import {PluginJson} from "../../../static/scripts/tim/plugin/angular-plugin-base.directive"; +import {TimUtilityModule} from "../../../static/scripts/tim/ui/tim-utility.module"; +import {PurifyModule} from "../../../static/scripts/tim/util/purify.module"; +import {registerPlugin} from "../../../static/scripts/tim/plugin/pluginRegistry"; +import {CommonModule} from "@angular/common"; +import { + ChangeType, + FormModeOption, +} from "../../../static/scripts/tim/document/viewctrl"; +import {boolean, string} from "fp-ts"; +import {re} from "mathjs"; + +const TextfieldMarkup = t.intersection([ + t.partial({ + tag: nullable(t.string), + inputplaceholder: nullable(t.string), + inputstem: nullable(t.string), + initword: nullable(t.string), + validinput: nullable(t.string), + errormessage: nullable(t.string), + readOnlyStyle: nullable(t.string), + showname: nullable(t.number), + autosave: t.boolean, + nosave: t.boolean, + ignorestyles: t.boolean, + clearstyles: t.boolean, + textarea: t.boolean, + autogrow: t.boolean, + downloadButton: t.string, + downloadButtonFile: t.string, + }), + GenericPluginMarkup, + t.type({ + autoupdate: withDefault(t.number, 500), + autoUpdateTables: withDefault(t.boolean, true), + cols: withDefault(t.number, 6), + rows: withDefault(t.number, 1), + }), +]); +export const FieldContent = t.union([t.string, t.number, t.null]); +export const FieldBasicData = t.type({ + c: FieldContent, +}); +export const FieldDataWithStyles = t.intersection([ + FieldBasicData, + t.partial({ + styles: nullable(t.record(t.string, t.string)), + }), +]); +const TextfieldAll = t.intersection([ + t.partial({}), + t.type({ + info: Info, + markup: TextfieldMarkup, + preview: t.boolean, + state: nullable(FieldDataWithStyles), + }), +]); +export type TFieldContent = t.TypeOf; +export type InputType = "TEXTAREA" | "TEXT"; + +@Component({ + selector: "tim-standalone-textfield", + standalone: true, + styleUrls: ["standalone-textfield.component.scss"], + imports: [FormsModule, TooltipModule, CommonModule], + template: ` +
+ + + + +
+ `, +}) +export class StandaloneTextfieldComponent + implements OnInit, OnChanges, PluginJson +{ + @Input() inputType: InputType = "TEXT"; + @Input() initialValue: string = "Your description."; + @Input() placeholder: string = "Words are powerful."; + @Input() name: string = ""; + @Input() inputWarn: boolean | null = false; + @Output() valueChange = new EventEmitter(); + isRunning = false; + userword = ""; + errormessage?: string; + styles: Record = {}; + saveFailed = false; + + constructor(private http: HttpClient, private zone: NgZone) { + this.json = "{}"; + } + + ngOnInit() {} + + ngOnChanges(change: SimpleChanges) { + console.log(change); + } + + /** + * Returns (user) content in string form. + */ + getContent(): string { + return this.userword; + } + + /** + * Method to check grading input type for textfield. + * Used as e.g. grading checker for hyv | hyl | 1 | 2 | 3 | 4 | 5. + * @param re validinput defined by given attribute. + */ + validityCheck(re: string) { + if (this.userword === "") { + return new RegExp("").test(this.userword); + } + const regExpChecker = new RegExp(re); + return regExpChecker.test(this.userword); + } + + updateInput() { + this.valueChange.emit(this.initialValue); + console.log(this.inputWarn); + if (!this.inputWarn) { + this.inputWarn = true; + // this.hideSavedText = true; + } + } + + json: string; +} + +@NgModule({ + imports: [ + CommonModule, + HttpClientModule, + TimUtilityModule, + FormsModule, + PurifyModule, + TooltipModule.forRoot(), + StandaloneTextfieldComponent, + ], + exports: [StandaloneTextfieldComponent], +}) +export class StandaloneTextfieldModule implements DoBootstrap { + ngDoBootstrap(appRef: ApplicationRef) {} +} + +registerPlugin( + "standalone-textfield", + StandaloneTextfieldModule, + StandaloneTextfieldComponent +); diff --git a/timApp/static/scripts/tim/app.module.ts b/timApp/static/scripts/tim/app.module.ts index 834715fa0d..4728055a74 100644 --- a/timApp/static/scripts/tim/app.module.ts +++ b/timApp/static/scripts/tim/app.module.ts @@ -42,8 +42,6 @@ import {SelfExpireComponent} from "tim/item/self-expire.component"; import {SearchButtonComponent} from "tim/search/search-button.component"; import {SessionVerify} from "tim/util/session-verify.interceptor"; import {RoleInfoComponent} from "tim/header/role-info.component"; -import {UserProfileComponent} from "tim/plugin/user-profile/user-profile.component"; -import {CsUtilityModule} from "../../../modules/cs/js/util/module"; @NgModule({ declarations: [ @@ -74,7 +72,6 @@ import {CsUtilityModule} from "../../../modules/cs/js/util/module"; GamificationMapComponent, SelfExpireComponent, RoleInfoComponent, - UserProfileComponent, ], imports: [ BrowserModule, @@ -88,7 +85,6 @@ import {CsUtilityModule} from "../../../modules/cs/js/util/module"; TypeaheadModule.forRoot(), TooltipModule.forRoot(), TabsModule.forRoot(), - CsUtilityModule, ], providers: [ SessionVerify, diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss index 0ef62c0802..31ebf81b28 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.scss @@ -1,26 +1,27 @@ tim-user-profile { width: 100%; padding: 0; - + background-color: yellow; } .tim-user-profile-container { box-shadow: 0 30px 200px #0000cc38, 0 0 20px #0F0C0020; padding: 1em; border-radius: 1em; - + margin: 2em 0 2em 0; } :host(tim-user-profile) { + } .container { - background-color: #ffefef; display: grid; grid-template-columns: 1fr 1fr; grid-template-areas: "left right"; padding: 0; max-width: 100%; + } .left-column { @@ -53,8 +54,13 @@ tim-user-profile { border-radius: 50%; width: 50%; object-fit: cover; + margin: 1em auto 1em auto; } .profile-picture-box { margin: 1em; +} + +form { + width: 100%; } \ No newline at end of file diff --git a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts index f718992c61..9cde58bde2 100644 --- a/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts +++ b/timApp/static/scripts/tim/plugin/user-profile/user-profile.component.ts @@ -1,22 +1,47 @@ -import {Input, OnInit, ViewChild} from "@angular/core"; +import { + ApplicationRef, + DoBootstrap, + EventEmitter, + Input, + NgModule, + OnInit, + Output, + TrackByFunction, + ViewChild, +} from "@angular/core"; import {Component} from "@angular/core"; -import {HttpClient, HttpClientModule} from "@angular/common/http"; -import {IItem, redirectToItem} from "tim/item/IItem"; -import type {IDocument} from "tim/item/IItem"; -import {to, to2} from "tim/util/utils"; -import {$http} from "tim/util/ngimport"; +import {HttpClient} from "@angular/common/http"; +import {AngularError, toPromise} from "tim/util/utils"; import type { IFile, IFileSpecification, } from "../../../../../modules/cs/js/util/file-select"; import {FileSelectManagerComponent} from "../../../../../modules/cs/js/util/file-select"; -import {CsUtilityModule} from "../../../../../modules/cs/js/util/module"; -import {tr} from "date-fns/locale"; import * as t from "io-ts"; import {int} from "@zxing/library/es2015/customTypings"; +import {InputDialogKind} from "tim/ui/input-dialog.kind"; +import type {Result} from "tim/util/utils"; +import {showInputDialog} from "tim/ui/showInputDialog"; +import { + StandaloneTextfieldComponent, + StandaloneTextfieldModule, +} from "../../../../../modules/fields/js/standalone-textfield.component"; +import {CsUtilityModule} from "../../../../../modules/cs/js/util/module"; +import {FormsModule} from "@angular/forms"; +import {CommonModule} from "@angular/common"; +import {boolean} from "fp-ts"; interface ProfileData { - profile_picture_path: string; + username?: string; + realname?: string; + email?: string; + profile_description: string; + profile_links: string[]; + document_id?: int; +} + +interface ImageEvent extends Event { + image: string; } const UploadedFile = t.intersection([ @@ -40,65 +65,119 @@ interface IUploadedFile extends t.TypeOf {} @Component({ selector: "tim-user-profile", styleUrls: ["./user-profile.component.scss"], + imports: [ + StandaloneTextfieldComponent, + CsUtilityModule, + FormsModule, + CommonModule, + ], + standalone: true, template: `