diff --git a/Public/assets/js/flash-card/draft/create.js b/Public/assets/js/flash-card/draft/create.js index 8db2dfd..666d7eb 100644 --- a/Public/assets/js/flash-card/draft/create.js +++ b/Public/assets/js/flash-card/draft/create.js @@ -1,13 +1,13 @@ function createDraft() { - try { + jsonData().then(function (jsonData){ fetch("/api/notes", { method: "POST", headers: { "Accept": "application/json, text/plain, */*", "Content-Type" : "application/json" }, - body: jsonData() + body: jsonData }) .then(function (response) { if (response.ok) { @@ -21,12 +21,10 @@ function createDraft() { .then(function (json) { fetchNote(parseInt(json)); }) - .catch(function (error) { - presentErrorMessage(error.message); - }); - } catch(error) { + }) + .catch(function (error) { presentErrorMessage(error.message); - } + }); } function fetchNote(id) { @@ -80,7 +78,6 @@ function editJsonData() { } function resetCreateForm() { - $("#card-topic-id").val(""); $("#card-question").val(""); solution.value(""); } @@ -130,4 +127,27 @@ function subjectID() { path.indexOf(splitURI) + splitURI.length, path.lastIndexOf("/tasks/") )); +} + +function createSession() { + return fetch("/api/note-taking-sessions", { + method: "POST", + headers: { + "Accept": "application/json, text/plain, */*", + "Content-Type" : "application/json" + } + }) + .then(function (response) { + if (response.ok) { + return response.json(); + } else if (response.status == 400) { + throw new Error("Sjekk at all nødvendig info er fylt ut"); + } else { + throw new Error(response.statusText); + } + }) + .then(function (json) { + $("#note-session").val(json["id"]); + return json + }) } \ No newline at end of file diff --git a/Public/assets/js/flash-card/draft/json-data.js b/Public/assets/js/flash-card/draft/json-data.js index a6f40fd..926a3ab 100644 --- a/Public/assets/js/flash-card/draft/json-data.js +++ b/Public/assets/js/flash-card/draft/json-data.js @@ -1,20 +1,35 @@ function jsonData() { - let noteSession = $("#note-session").val(); - var subtopicId = parseInt($("#card-topic-id").val()); - var question = $("#card-question").val(); - var solutionValue = solution.value(); - if (isNaN(subtopicId) || subtopicId < 1) { - throw Error("Velg et tema"); - } - if (question.length < 1) { - throw Error("Du må skrive inn et spørsmål"); - } + return new Promise(function (resolve, reject) { + let noteSession = $("#note-session").val(); - return JSON.stringify({ - "noteSession" : noteSession, - "subtopicID" : subtopicId, - "question" : question, - "solution" : solutionValue - }); + var subtopicId = parseInt($("#card-topic-id").val()); + var question = $("#card-question").val(); + var solutionValue = solution.value(); + + if (isNaN(subtopicId) || subtopicId < 1) { + reject(Error("Velg et tema")); + } + if (question.length < 1) { + reject(Error("Du må skrive inn et spørsmål")); + } + + if (noteSession == "" || noteSession == null) { + createSession().then(function (json) { + resolve(JSON.stringify({ + "noteSession" : json["id"], + "subtopicID" : subtopicId, + "question" : question, + "solution" : solutionValue + })) + }) + } else { + resolve(JSON.stringify({ + "noteSession" : noteSession, + "subtopicID" : subtopicId, + "question" : question, + "solution" : solutionValue + })) + } + }) } \ No newline at end of file diff --git a/Public/assets/js/flash-card/submit-performance.js b/Public/assets/js/flash-card/submit-performance.js index 19f0a29..9a15ee3 100644 --- a/Public/assets/js/flash-card/submit-performance.js +++ b/Public/assets/js/flash-card/submit-performance.js @@ -3,7 +3,7 @@ var now = new Date(); var isSubmitting = false; var hasSubmittedSucessfully = false; -var knowledgeScore = 0; +var knowledgeScore = 2; var didSubmitt = false; var nextIndex=1; @@ -163,7 +163,7 @@ function estimatedScore(shouldSetScore) { "Accept": "application/json, text/plain, */*", "Content-Type" : "application/json" }, - body: answerJsonData("1") + body: answerJsonData("3") }) .then(function (response) { if (response.ok) { @@ -187,7 +187,7 @@ function estimatedScore(shouldSetScore) { } else if (roundedScore >= 2) { text += " kan noe 🙌" } else { - text += " burde lese litt mer 🤔" + text += " kanskje burde lese litt mer 🤔" } $("#estimate-spinner").addClass("d-none"); diff --git a/Public/assets/js/lecture-note-recap-session/create.js b/Public/assets/js/lecture-note-recap-session/create.js new file mode 100644 index 0000000..4047eb3 --- /dev/null +++ b/Public/assets/js/lecture-note-recap-session/create.js @@ -0,0 +1,34 @@ + +function recapSessionJsonData() { + let sessionID = $("#note-session").val(); + let numberOfNotes = Math.min($("#notes").children.length, 5); + + return JSON.stringify({ + "sessionID": sessionID, + "numberOfTasks": numberOfNotes + }) +} + +function startRecapSession() { + fetch("/api/lecture-note-recap", { + method: "POST", + headers: { + "Accept": "application/json, text/plain, */*", + "Content-Type" : "application/json" + }, + body: recapSessionJsonData() + }) + .then(function (response) { + if (response.ok) { + console.log(response); + return response.json(); + } else if (response.status == 400) { + throw new Error("Sjekk at all nødvendig info er fylt ut"); + } else { + throw new Error(response.statusText); + } + }) + .then(function (sessionID) { + window.location = "/lecture-note-recap/" + sessionID + "/tasks/0"; + }) +} \ No newline at end of file diff --git a/Public/assets/js/lecture-note-recap-session/submit.js b/Public/assets/js/lecture-note-recap-session/submit.js new file mode 100644 index 0000000..3329901 --- /dev/null +++ b/Public/assets/js/lecture-note-recap-session/submit.js @@ -0,0 +1,183 @@ +var startDate = new Date(); +var now = new Date(); + +var isSubmitting = false; +var hasSubmittedSucessfully = false; +var knowledgeScore = 2; +var didSubmitt = false; + +function revealSolution() { + didSubmitt = $("#solution").hasClass("d-none"); + presentControllsAndKnowledge(); + + if ($("#solution").hasClass("d-none")) { + now = new Date(); + $("#nextButton").removeClass("d-none"); + var goalValue = parseFloat($("#goal-value").text()); + var porgressBarValue = parseFloat($("#goal-progress-bar").attr("aria-valuenow")) + var currentCompleted = porgressBarValue / 100 * goalValue; + var progress = parseInt(Math.ceil((currentCompleted + 1) * 100 / goalValue)); + + if (!isNaN(progress)) { + $("#goal-progress-label").text(progress + "% "); + $("#goal-progress-bar").attr("aria-valuenow", progress); + $("#goal-progress-bar").attr("style", "width: " + progress + "%;"); + if (progress >= 100) { + $("#goal-progress-bar").addClass("bg-success"); + } + } + submitPerformance(knowledgeScore) + } +} + +function updateScoreButton() { + if (knowledgeScore < 2) { + $("#" + knowledgeScore).attr("class", "btn btn-danger"); + } else if (knowledgeScore < 4) { + $("#" + knowledgeScore).attr("class", "btn btn-warning"); + } else { + $("#" + knowledgeScore).attr("class", "btn btn-success"); + } +} + +function registerScore(score) { + $("#" + knowledgeScore).attr("class", "btn btn-light"); + knowledgeScore = score; + updateScoreButton() + submitPerformance(knowledgeScore) +} + +function submitAndEndSession() { + endSession() +} + +function submitPerformance(score) { + + if (isSubmitting) { + return + } + isSubmitting = true; + + var url = "/api/lecture-note-recap/" + sessionID() + "/tasks/" + taskIndex() + "/submit"; + + fetch(url, { + method: "POST", + headers: { + "Accept": "application/json, text/plain, */*", + "Content-Type" : "application/json" + }, + body: answerJsonData(score) + }) + .then(function (response) { + isSubmitting = false; + if (response.ok) { + return + } else { + throw new Error(response.statusText); + } + }) + .catch(function (error) { + isSubmitting = false; + $("#submitButton").attr("disabled", false); + $("#error-massage").text(error.message); + $("#error-div").fadeIn(); + $("#error-div").removeClass("d-none"); + }); +} + +function presentControlls() { + estimatedScore(didSubmitt); + $("#flash-card-answer").prop('readonly', true) + $("#submitButton").prop('disabled', true) + $("#nextButton").removeClass("d-none"); + $(".reveal").each(function () { + $(this).fadeIn(); + $(this).removeClass("d-none"); + }); + fetchSolutions(); + $("#knowledge-card").removeClass("d-none"); + updateScoreButton() +} + +function presentControllsAndKnowledge() { + presentControlls() + hasSubmitted = false; + hasSubmittedSucessfully = false; +} + + +Number.prototype.toMinuteString = function() { + var minutes = Math.floor((this % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((this % (1000 * 60)) / 1000); + if (minutes < 10) { minutes = "0" + minutes; } + if (seconds < 10) { seconds = "0" + seconds; } + return minutes + ":" + seconds; +} + +function endSession() { + $("#end-session-form").submit(); +} + +function answerJsonData(score) { + var timeUsed = (now.getTime() - startDate.getTime()) / 1000; + let knowledge = parseFloat(score); + let submitedAnswer = $("#flash-card-answer").val(); + + if (knowledge == null) { + return + } + return JSON.stringify({ + "timeUsed" : timeUsed, + "knowledge": knowledge / 4, + "answer": submitedAnswer + }); +} + +function estimatedScore(shouldSetScore) { + let url = "/api/lecture-note-recap/" + sessionID() + "/tasks/" + taskIndex() + "/estimate"; + + $("#estimated-score-card").fadeIn(); + $("#estimated-score-card").removeClass("d-none"); + $("#estimate-spinner").html('
Loading...
') + + fetch(url, { + method: "POST", + headers: { + "Accept": "application/json, text/plain, */*", + "Content-Type" : "application/json" + }, + body: answerJsonData("2") + }) + .then(function (response) { + if (response.ok) { + return response.json(); + } else { + throw new Error(response.statusText); + } + }) + .then(function (json) { + let score = json["score"]; + let roundedScore = Math.round(score * 4); + + var text = "Vi estimerer at du"; + if (shouldSetScore == true) { + registerScore(roundedScore); + } + if (roundedScore >= 4) { + text += " kan denne oppgaven veldig godt 💯" + } else if (roundedScore >= 3) { + text += " kan denne oppgaven godt 🔥" + } else if (roundedScore >= 2) { + text += " kan noe 🙌" + } else { + text += " kanskje burde lese litt mer 🤔" + } + + $("#estimate-spinner").addClass("d-none"); + $("#answer-estimate").text(text); + $("#answer-estimate").removeClass("d-none"); + }) + .catch(function (error) { + console.log(error) + }) +} \ No newline at end of file diff --git a/Sources/App/Lecture Note/LectureNoteRecapSessionWebController.swift b/Sources/App/Lecture Note/LectureNoteRecapSessionWebController.swift new file mode 100644 index 0000000..b73cecb --- /dev/null +++ b/Sources/App/Lecture Note/LectureNoteRecapSessionWebController.swift @@ -0,0 +1,74 @@ +// +// LectureNoteWebController.swift +// App +// +// Created by Mats Mollestad on 05/10/2020. +// + +import Vapor +import KognitaModels + +struct LectureNoteRecapSessionWebController: RouteCollection { + + func boot(routes: RoutesBuilder) throws { + let recapInstance = routes.grouped("lecture-note-recap", LectureNote.RecapSession.parameter) + recapInstance.get("tasks", Int.parameter, use: taskForIndex(on:)) + recapInstance.get("tasks", Int.parameter, "solutions", use: getSolutions) + recapInstance.get("results", use: getSessionResult(_:)) + } + + func taskForIndex(on req: Request) throws -> EventLoopFuture { + + return try req.controllers.lectureNoteRecapSessionController.taskForIndex(on: req) + .flatMap { executeTask in + + LectureNote.RecapSession.Templates.ExecuteTask() + .render( + with: LectureNote.RecapSession.Templates.ExecuteTask.Context(executeTask: executeTask), + for: req + ) + } + } + + func getSolutions(on req: Request) throws -> EventLoopFuture { + + let user = try req.auth.require(User.self) + + return try req.controllers.lectureNoteRecapSessionController + .solutionForIndex(on: req) + .flatMapThrowing { solutions in + try req.htmlkit + .render( + TaskSolution.Templates.List.self, + with: .init( + user: user, + solutions: solutions + ) + ) + } + } + + /// Get the statistics of a session + /// + /// - Parameter req: The HTTP request + /// - Returns: A rendered view + /// - Throws: If unauth or any other error + func getSessionResult(_ req: Request) throws -> EventLoopFuture { + + let user = try req.auth.require(User.self) + + return try req.controllers.lectureNoteRecapSessionController + .results(on: req) + .flatMapThrowing { results in + + try req.htmlkit + .render( + PracticeSession.Templates.Result.self, + with: .init( + user: user, + result: results + ) + ) + } + } +} diff --git a/Sources/App/Practice Session/PracticeSessionWebController.swift b/Sources/App/Practice Session/PracticeSessionWebController.swift index 672cf07..f279057 100644 --- a/Sources/App/Practice Session/PracticeSessionWebController.swift +++ b/Sources/App/Practice Session/PracticeSessionWebController.swift @@ -125,7 +125,7 @@ final class PracticeSessionWebController: RouteCollection { try req.controllers.practiceSessionController .end(session: req) .map { session in - req.redirect(to: "/practice-sessions/\(session.id ?? 0)/result") + req.redirect(to: "/practice-sessions/\(session.id)/result") } } } diff --git a/Sources/App/User/UserWebController.swift b/Sources/App/User/UserWebController.swift index 61c5882..166d405 100644 --- a/Sources/App/User/UserWebController.swift +++ b/Sources/App/User/UserWebController.swift @@ -34,7 +34,7 @@ final class UserWebController: RouteCollection { func signupForm(_ req: Request) throws -> EventLoopFuture { User.Templates.Signup() - .render(with: .init(), for: req) + .render(with: .init(showCookieMessage: req.cookies.isAccepted == false), for: req) } func loginForm(_ req: Request) throws -> EventLoopFuture { @@ -44,7 +44,7 @@ final class UserWebController: RouteCollection { } return try req.htmlkit - .render(LoginPage.self, with: .init()) + .render(LoginPage.self, with: .init(showCookieMessage: req.cookies.isAccepted == false)) .encodeResponse(for: req) } @@ -64,6 +64,7 @@ final class UserWebController: RouteCollection { let response = try? req.htmlkit.render( User.Templates.Signup.self, with: .init( + showCookieMessage: req.cookies.isAccepted == false, errorMessage: error.localizedDescription, submittedForm: createUser ) @@ -84,11 +85,12 @@ final class UserWebController: RouteCollection { .failableFlatMap { user in guard let user = user else { return try req.htmlkit - .render(LoginPage.self, with: .init(errorMessage: "Feil brukernavn eller passord")) + .render(LoginPage.self, with: .init(showCookieMessage: req.cookies.isAccepted == false, errorMessage: "Feil brukernavn eller passord")) .encodeResponse(for: req) } req.auth.login(user) - return req.eventLoop.future().transform(to: req.redirect(to: "/subjects")) + return req.repositories.userRepository.logLogin(for: user, with: req.remoteAddress?.ipAddress) + .map { req.redirect(to: "/subjects") } } } @@ -102,7 +104,7 @@ final class UserWebController: RouteCollection { return try req.htmlkit .render( User.Templates.ResetPassword.Start.self, - with: .init() + with: .init(showCookieMessage: req.cookies.isAccepted == false) ) } @@ -111,7 +113,7 @@ final class UserWebController: RouteCollection { let successPage = try req.htmlkit .render( User.Templates.ResetPassword.Start.self, - with: .init(state: .success) + with: .init(state: .success, showCookieMessage: req.cookies.isAccepted == false) ) return try req.controllers.userController .startResetPassword(on: req) @@ -126,7 +128,7 @@ final class UserWebController: RouteCollection { return try req.htmlkit .render( User.Templates.ResetPassword.Reset.self, - with: .init(token: tokenContent.token) + with: .init(token: tokenContent.token, showCookieMessage: req.cookies.isAccepted == false) ) } else { @@ -135,6 +137,7 @@ final class UserWebController: RouteCollection { User.Templates.ResetPassword.Reset.self, with: .init( token: "", + showCookieMessage: req.cookies.isAccepted == false, alertMessage: ( message: "Ups! Denne forespørselen er enten gått ut på dato eller eksisterer ikke", colorClass: "danger" diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index f6bdac4..489ff9d 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -40,15 +40,19 @@ private func setupUserWeb(for app: Application) throws { if req.auth.get(User.self) != nil { return req.eventLoop.future(req.redirect(to: "/subjects")) } - return try req.htmlkit.render(view: Pages.Landing.self) + let context = Pages.Landing.Context(showCookieMessage: req.cookies.isAccepted == false) + + return try req.htmlkit.render(view: Pages.Landing.self, with: context) .encodeResponse(for: req) } - app.get("privacy-policy") { req in - try req.htmlkit.render(view: Pages.PrivacyPolicy.self) + app.get("privacy-policy") { req -> View in + let context = Pages.PrivacyPolicy.Context(showCookieMessage: req.cookies.isAccepted == false) + return try req.htmlkit.render(view: Pages.PrivacyPolicy.self, with: context) } - app.get("terms-of-service") { req in - try req.htmlkit.render(view: Pages.TermsOfService.self) + app.get("terms-of-service") { req -> View in + let context = Pages.TermsOfService.Context(showCookieMessage: req.cookies.isAccepted == false) + return try req.htmlkit.render(view: Pages.TermsOfService.self, with: context) } try sessionMiddle.register(collection: UserWebController()) @@ -62,4 +66,11 @@ private func setupUserWeb(for app: Application) throws { try redirectMiddle.register(collection: SubjectTestWebController()) try redirectMiddle.register(collection: TestSessionWebController()) try redirectMiddle.register(collection: TaskDiscussionWebController()) + try redirectMiddle.register(collection: LectureNoteRecapSessionWebController()) +} + +extension HTTPCookies { + var isAccepted: Bool { + self.all["cookies-accepted"] != nil + } }