Skip to content

Commit

Permalink
Merge pull request #3808 from TIM-JYU/mass-save-clean
Browse files Browse the repository at this point in the history
Improve multisave automatic saving, check readonly attributes, log answer requests
  • Loading branch information
dezhidki authored Feb 16, 2025
2 parents ddac4cd + 488e902 commit 6d07bb0
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 8 deletions.
28 changes: 28 additions & 0 deletions timApp/answer/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import Union, Any, Callable, TypedDict, Tuple, Sequence

import filelock
from flask import Response
from flask import current_app
from flask import request
Expand Down Expand Up @@ -146,6 +148,7 @@
ok_response,
to_dict,
text_response,
to_json_str,
)
from timApp.util.flask.typedblueprint import TypedBlueprint
from timApp.util.get_fields import (
Expand Down Expand Up @@ -636,6 +639,9 @@ def post_answer(
"""
curr_user = get_current_user_object()

if current_app.config["ANSWER_LOG_FILE"]:
log_answer_request(curr_user, task_id_ext, input, abData, options)

return json_response(
post_answer_impl(
task_id_ext,
Expand Down Expand Up @@ -667,6 +673,28 @@ def verify_ip_address(user: User) -> str | None:
return blocked_msg if not is_valid else None


def log_answer_request(
user: User,
task_id_ext: str,
input: InputAnswer,
abData: dict[str, Any],
options: dict[str, Any],
):
p = Path(current_app.config["FILES_PATH"]) / current_app.config["ANSWER_LOG_FILE"]
j = {
"time": datetime.now(),
"user": user.name,
"ip": request.remote_addr,
"task_id_ext": task_id_ext,
"input": input,
"abData": abData,
"options": options,
}
with filelock.FileLock(f"/tmp/answer_log"):
with p.open("a") as f:
f.write(to_json_str(j) + "\n")


@dataclass
class AnswerRouteResult:
result: dict[str, Any]
Expand Down
2 changes: 2 additions & 0 deletions timApp/defaultconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@
# The hosts where to back up the answers. Every entry should start with "https://".
BACKUP_ANSWER_HOSTS = None

ANSWER_LOG_FILE = None

SYNC_USER_GROUPS_SEND_SECRET = None
"""
Secret to use to when syncing user group info. If None, no user group memberships.
Expand Down
6 changes: 4 additions & 2 deletions timApp/modules/cs/js/csPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,9 @@ export class CsController extends CsBase implements ITimComponent {
return;
}

this.editor.setReadOnly(this.markup.editorreadonly === true);
this.editor.setReadOnly(
this.markup.editorreadonly === true || this.readonly
);
if (this.attrsall.submittedFiles || this.markup.files) {
const files = new OrderedSet<EditorFile>((f) => f.path);
const defaultMode =
Expand Down Expand Up @@ -1589,7 +1591,7 @@ export class CsController extends CsBase implements ITimComponent {
}
await this.runCode();
this.cdr.detectChanges();
return {saved: true, message: undefined};
return {saved: !this.edited, message: this.connectionErrorMessage};
}

formBehavior(): FormModeOption {
Expand Down
7 changes: 5 additions & 2 deletions timApp/modules/fields/js/dropdown-plugin.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const DropdownAll = t.intersection([
[(ngModel)]="selectedWord"
(ngModelChange)="updateSelection()"
[ngClass]="{warnFrame: isUnSaved(), alertFrame: saveFailed}"
[disabled]="attrsall['preview']"
[disabled]="attrsall['preview'] || readonly"
[ngStyle]="styles"
[tooltip]="connectionErrorMessage"
[isOpen]="connectionErrorMessage !== undefined"
Expand Down Expand Up @@ -250,7 +250,10 @@ export class DropdownPluginComponent
this.saveFailed = true;
}
this.initialWord = this.selectedWord;
return {saved: r.ok, message: this.error};
return {
saved: r.ok,
message: this.error ?? this.connectionErrorMessage,
};
}

updateSelection() {
Expand Down
54 changes: 53 additions & 1 deletion timApp/modules/fields/js/multisave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
Info,
withDefault,
} from "tim/plugin/attributes";
import {escapeRegExp, scrollToElement} from "tim/util/utils";
import {escapeRegExp, scrollToElement, timeout} from "tim/util/utils";
import type {TaskId} from "tim/plugin/taskid";
import {AngularPluginBase} from "tim/plugin/angular-plugin-base.directive";
import {TimUtilityModule} from "tim/ui/tim-utility.module";
Expand Down Expand Up @@ -49,6 +49,15 @@ const multisaveMarkup = t.intersection([
testOnly: t.boolean,
savedText: t.string,
unsavedText: t.string,
timer: t.number,
saveDelay: t.number,
showMessages: t.boolean,
messageOverrides: t.array(
t.type({
expect: t.string,
replace: t.string,
})
),
}),
GenericPluginMarkup,
t.type({
Expand Down Expand Up @@ -92,6 +101,9 @@ const multisaveAll = t.intersection([
</li>
</ul>
</div>
<div *ngIf="saveMessages" class="saveMessages">
<div *ngFor="let m of saveMessages">{{m}}</div>
</div>
<div *ngIf="allSaved()">
{{allSavedText}}
</div>
Expand Down Expand Up @@ -133,6 +145,8 @@ export class MultisaveComponent
unsavedTasksWithAliases: {component: ITimComponent; alias?: string}[] = [];
requiresTaskId = false;
listener = false;
timer?: number;
saveMessages: string[] = [];

constructor(
el: ElementRef<HTMLElement>,
Expand Down Expand Up @@ -295,6 +309,9 @@ export class MultisaveComponent

const promises = [];
for (const v of componentsToSave) {
if (this.markup.saveDelay) {
await timeout(this.markup.saveDelay);
}
const result = v.save();
promises.push(result);
}
Expand All @@ -303,6 +320,7 @@ export class MultisaveComponent
this.savedFields = 0;
let savedIndex = 0;
const fieldsToUpdate: string[] = [];
this.saveMessages = [];
for (const p of promises) {
const result = await p;
if (result.saved) {
Expand All @@ -312,8 +330,26 @@ export class MultisaveComponent
fieldsToUpdate.push(tid.docTask().toString());
}
}
if (this.markup.showMessages && result.message) {
let msg = result.message;
if (this.markup.messageOverrides) {
for (const o of this.markup.messageOverrides) {
const reg = new RegExp(`^${o.expect}$`);
if (reg.test(msg)) {
msg = o.replace;
break;
}
}
}
if (!this.saveMessages.includes(msg)) {
this.saveMessages.push(msg);
}
}
savedIndex++;
}
if (this.markup.timer && !this.allSaved()) {
this.setTimer();
}
if (this.markup.autoUpdateTables) {
this.vctrl.updateAllTables(fieldsToUpdate);
}
Expand Down Expand Up @@ -378,6 +414,22 @@ export class MultisaveComponent
this.hasUnsavedTargets = true;
this.refreshUnsavedList();
this.isSaved = false;
if (this.markup.timer) {
if (this.timer) {
window.clearTimeout(this.timer);
}
this.setTimer();
}
}

private setTimer() {
if (!this.markup.timer) {
return;
}
if (this.timer) {
window.clearTimeout(this.timer);
}
this.timer = window.setTimeout(() => this.save(), this.markup.timer);
}

public informAboutChanges(taskId: TaskId, state: ChangeType, tag?: string) {
Expand Down
2 changes: 2 additions & 0 deletions timApp/modules/fields/js/textfield-plugin.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ export class TextfieldPluginComponent
defaultErrorMessage;
this.redAlert = true;
this.saveFailed = true;
this.saveResponse.saved = false;
this.saveResponse.message = this.errormessage;
}
return this.saveResponse;
}
Expand Down
10 changes: 10 additions & 0 deletions timApp/modules/fields/multisave.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class MultisaveStateModel:
"""Model for the information that is stored in TIM database for each answer."""


@dataclass
class MessageOverrides:
expect: str
replace: str


@dataclass
class MultisaveMarkupModel(GenericMarkupModel):
aliases: dict[str, str] | Missing = missing
Expand All @@ -36,6 +42,10 @@ class MultisaveMarkupModel(GenericMarkupModel):
savedText: str | Missing = missing
tags: list[str] | Missing = missing
unsavedText: str | Missing = missing
timer: int | Missing = missing
saveDelay: int | Missing = missing
showMessages: bool | Missing = False
messageOverrides: list[MessageOverrides] | Missing = missing

# Sisu export-related fields; TODO: Should be a separate plugin.
destCourse: str | Missing = missing
Expand Down
10 changes: 7 additions & 3 deletions timApp/static/scripts/tim/plugin/qst/qst.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class QstComponent
private newAnswer?: AnswerTable;
private changes = false;
saveFailed = false;
private enabled = true;

constructor(
el: ElementRef<HTMLElement>,
Expand Down Expand Up @@ -173,11 +174,14 @@ export class QstComponent
if (!this.pluginMeta.isPreview()) {
this.vctrl.addTimComponent(this);
}
if (this.attrsall.markup.invalid || this.readonly) {
this.enabled = false;
}
this.preview = makePreview(this.attrsall.markup, {
answerTable: this.attrsall.state ?? [],
showCorrectChoices: this.attrsall.show_result,
showExplanations: this.attrsall.show_result,
enabled: !this.attrsall.markup.invalid,
enabled: this.enabled,
});
this.button = this.buttonText() ?? $localize`Save`;
}
Expand Down Expand Up @@ -337,7 +341,7 @@ export class QstComponent
if (data.web.markup && data.web.show_result) {
this.preview = makePreview(data.web.markup, {
answerTable: data.web.state,
enabled: true,
enabled: this.enabled ?? true,
});
this.preview.showExplanations = true;
this.preview.showCorrectChoices = true;
Expand All @@ -354,7 +358,7 @@ export class QstComponent
answerTable: this.savedAnswer,
showCorrectChoices: this.attrsall.show_result,
showExplanations: this.attrsall.show_result,
enabled: !this.attrsall.markup.invalid,
enabled: this.enabled,
});
this.checkChanges();
this.saveFailed = false;
Expand Down

0 comments on commit 6d07bb0

Please sign in to comment.