diff --git a/CodeChallenge/api/questions.py b/CodeChallenge/api/questions.py index 4b8d733..9ea2480 100644 --- a/CodeChallenge/api/questions.py +++ b/CodeChallenge/api/questions.py @@ -22,7 +22,7 @@ def json_error(reason, status=400): def end_code_challenge(): if core.challenge_ended(): r = jsonify(status="error", - message="code challenge has ended") + reason="code challenge has ended") r.status_code = 403 abort(r) @@ -84,7 +84,7 @@ def next_question(): def answer_limit_attempts(): - return current_app.config.get("ANSWER_ATTEMPT_LIMIT", "3 per 30 minutes") + return current_app.config.get("ANSWER_ATTEMPT_LIMIT", "1 per 1 minutes") @bp.route("/answer", methods=["POST"]) @@ -163,10 +163,10 @@ def reset_all(): db.session.commit() - return jsonify(status="success", message="all answers and rank reset") + return jsonify(status="success", reason="all answers and rank reset") return jsonify(status="error", - message="resetting not allowed at this time"), 403 + reason="resetting not allowed at this time"), 403 @bp.route("/final", methods=["POST"]) diff --git a/CodeChallenge/cli/questions.py b/CodeChallenge/cli/questions.py index fd3e3f7..e49ead2 100644 --- a/CodeChallenge/cli/questions.py +++ b/CodeChallenge/cli/questions.py @@ -18,17 +18,21 @@ @click.argument("answer") @click.argument("rank") @click.argument("asset") -def q_add(title, answer, rank, asset): +@click.argument("hint1") +@click.argument("hint2") +def q_add(title, answer, rank, asset, hint1, hint2): """Add a new question to the database TITLE is the text for the title of the question ANSWER is the answer stored only in the database RANK is the day rank the queestion should be revealed on ASSET is a path to a file to upload for a question + HINT1 is a hint string + HINT2 is a hint string """ asset = os.path.abspath(asset) - qid = add_question(title, answer, rank, asset) + qid = add_question(title, answer, rank, asset, hint1, hint2) click.echo(f"added question id {qid}") @@ -66,7 +70,9 @@ def q_del(qid): @click.argument("answer") @click.argument("rank") @click.argument("asset") -def q_replace(title, answer, rank, asset): +@click.argument("hint1") +@click.argument("hint2") +def q_replace(title, answer, rank, asset, hint1, hint2): """Replace an existing rank's question. This basically deletes the previous rank then adds the new rank diff --git a/CodeChallenge/manage.py b/CodeChallenge/manage.py index c6523d4..23f3154 100644 --- a/CodeChallenge/manage.py +++ b/CodeChallenge/manage.py @@ -3,7 +3,7 @@ from .models import Question, db -def add_question(title, answer, rank, asset) -> Question: +def add_question(title, answer, rank, asset, hint1=None, hint2=None) -> Question: q = Question.query.filter_by(rank=rank).first() @@ -19,6 +19,8 @@ def add_question(title, answer, rank, asset) -> Question: q.asset_ext = os.path.splitext(asset)[1] q.rank = rank + q.hint1 = hint1 + q.hint2 = hint2 db.session.add(q) db.session.commit() diff --git a/Pipfile.lock b/Pipfile.lock index 12f5ead..8f14295 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -310,6 +310,14 @@ ], "version": "==19.3.0" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.3" + }, "entrypoints": { "hashes": [ "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", diff --git a/package.json b/package.json index 43f4e42..e24211d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "moment": "^2.24.0", "serialize-javascript": "^2.1.2", "vue": "^2.6.10", + "vue-codemirror": "^4.0.6", "vue-moment": "^4.1.0", "vue-router": "^3.1.3", "vue-the-mask": "^0.11.1", @@ -33,6 +34,7 @@ "prettier": "^1.18.2", "sass": "^1.19.0", "sass-loader": "^8.0.0", + "vue-cli-plugin-codemirror": "^0.0.6", "vue-cli-plugin-vuetify": "^2.0.2", "vue-template-compiler": "^2.6.10", "vuetify-loader": "^1.3.0" diff --git a/public/index.html b/public/index.html index 7f068dc..bb32131 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,23 @@ - + + + + + + + + + + + + + + + + + CodeWizardsHQ CODE CHALLENGE diff --git a/src/api/auth.js b/src/api/auth.js index 3042cb5..897d4ce 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -43,6 +43,10 @@ async function createAccount(data) { await login(data.username, data.password, false); } +async function requestPasswordReset(email) { + await request(routes.userapi_forgot, { data: { email } }); +} + async function fetchState() { const userData = await request(routes.userapi_hello, {}, state.auth); await setState({ @@ -75,11 +79,19 @@ function currentUser() { } async function forgotPassword(email) { - return await request(routes.userapi_forgot_password, { data: { email } }, false); + return await request( + routes.userapi_forgot_password, + { data: { email } }, + false + ); } async function resetPassword(token, password) { - return await request(routes.userapi_reset_password, { data: { token, password } }, false) + return await request( + routes.userapi_reset_password, + { data: { token, password } }, + false + ); } export default { @@ -89,6 +101,7 @@ export default { fetchState, createAccount, currentUser, + requestPasswordReset, onAuthStateChange, offAuthStateChange, forgotPassword, diff --git a/src/api/quiz.js b/src/api/quiz.js index e854d8d..a36d99c 100644 --- a/src/api/quiz.js +++ b/src/api/quiz.js @@ -6,7 +6,7 @@ async function getQuestion() { } async function getRank() { - return (await request(routes.questionsapi_get_rank)).rank; + return request(routes.questionsapi_get_rank); } async function resetRank() { @@ -22,9 +22,21 @@ async function submit(answer) { return result.correct; } +async function submitFinal(answer, language, checkOnly) { + const result = await request(routes.questionsapi_answer_final_question, { + data: { + checkOnly, + text: answer, + language + } + }); + return result; +} + export default { getQuestion, submit, + submitFinal, getRank, resetRank }; diff --git a/src/api/request.js b/src/api/request.js index 145ed0d..6cbdaf1 100644 --- a/src/api/request.js +++ b/src/api/request.js @@ -1,7 +1,10 @@ import axios from "axios"; -import routes from "./routes"; +// import routes from "./routes"; -export default async function request(route, options = {}, tryRefresh = true) { +export default async function request(route, options = {}, _tryRefresh = true) { + if (_tryRefresh) { + // satisfy linter + } try { // attempt initial request and return great response const response = await axios({ @@ -9,24 +12,31 @@ export default async function request(route, options = {}, tryRefresh = true) { url: route.path, ...options }); - return response.data; - } catch (err) { - if (err.response.status == 401 && tryRefresh) { - // our tokens have possibly expired, send refresh - // TODO: THIS IS UNFINISHED - await axios({ - method: "POST", - url: routes.userapi_refresh.path - }); - // try and return original request marked with no refresh - return request(route, options, false); - } + return { + ...response.data, + headers: response.headers + }; + } catch (err) { + // console.log(err.response); + // if (err.response.status == 401 && tryRefresh) { + // console.log("Trying refresh"); + // // our tokens have possibly expired, send refresh + // // TODO: THIS IS UNFINISHED + // await axios({ + // method: "POST", + // url: routes.userapi_refresh.path + // }); + // // try and return original request marked with no refresh + // return request(route, options, false); + // } // return original error + // console.log() return Promise.reject({ status: err.response.status, headers: err.response.headers, + data: err.response.data, message: !!err.response.data && !!err.response.data.reason ? err.response.data.reason diff --git a/src/api/routes.js b/src/api/routes.js index a79ae8f..10f2a1f 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -11,13 +11,15 @@ export default { userapi_login: route("/api/v1/users/token/auth", "POST"), userapi_logout: route("/api/v1/users/token/remove", "POST"), userapi_hello: route("/api/v1/users/hello", "GET"), + userapi_forgot: route("/api/v1/users/forgot", "POST"), userapi_refresh: route("/api/v1/users/token/refresh", "POST"), userapi_forgot_password: route("/api/v1/users/forgot", "POST"), userapi_reset_password: route("/api/v1/users/reset-password", "POST"), questionsapi_rank_reset: route("/api/v1/questions/reset", "DELETE"), questionsapi_answer_next_question: route("/api/v1/questions/answer", "POST"), + questionsapi_answer_final_question: route("/api/v1/questions/final", "POST"), questionsapi_get_rank: route("/api/v1/questions/rank", "GET"), - questions_api_next_question: route("/api/v1/questions/next", "GET"), + questions_api_next_question: route("/api/v1/questions/next", "GET") }; // export default { diff --git a/src/components/QuizAnswer.vue b/src/components/QuizAnswer.vue deleted file mode 100644 index e227ee8..0000000 --- a/src/components/QuizAnswer.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - diff --git a/src/components/TermsOfServiceContent.vue b/src/components/TermsOfServiceContent.vue new file mode 100644 index 0000000..cd40102 --- /dev/null +++ b/src/components/TermsOfServiceContent.vue @@ -0,0 +1,420 @@ + + + diff --git a/src/main.js b/src/main.js index 1e6bee8..f8b1747 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,7 @@ import "./plugins/moment"; import store from "./store"; import "@/styles/styles.scss"; import { auth } from "@/api"; - +import "./plugins/codemirror"; Vue.config.productionTip = false; (async function() { @@ -14,7 +14,17 @@ Vue.config.productionTip = false; store.dispatch("User/refresh"); }); - await auth.autoLogin(); + try { + await auth.autoLogin(); + } catch (err) { + // console.error("Was unable to authenticate user"); + } + + try { + await store.dispatch("Quiz/refresh"); + } catch (err) { + // console.error("Was unable to refresh question status", err.reason); + } new Vue({ router, diff --git a/src/plugins/codemirror.js b/src/plugins/codemirror.js new file mode 100644 index 0000000..3cb675c --- /dev/null +++ b/src/plugins/codemirror.js @@ -0,0 +1,5 @@ +import Vue from "vue"; +import VueCodeMirror from "vue-codemirror"; +import "codemirror/lib/codemirror.css"; + +Vue.use(VueCodeMirror, {}); diff --git a/src/plugins/router.js b/src/plugins/router.js index 2360214..b881787 100644 --- a/src/plugins/router.js +++ b/src/plugins/router.js @@ -1,6 +1,6 @@ import Vue from "vue"; import VueRouter from "vue-router"; -import {auth} from "@/api"; +import { auth } from "@/api"; import store from "@/store"; Vue.use(VueRouter); @@ -9,7 +9,9 @@ const routes = [ { path: "/home", name: "home", - component: () => import("@/views/Home") + redirect: { + name: "quiz" + } }, { path: "/login", @@ -59,40 +61,52 @@ const routes = [ { path: "/quiz", name: "quiz", - component: () => import("@/views/Quiz"), - meta: { - secured: true + component: async () => { + await store.dispatch("Quiz/refresh"); + + // CHALLENGE IS OVER + if (store.state.Quiz.quizHasEnded) { + return import("@/views/Quiz/QuizFinished"); + } + + // CHALLENGE HAS NOT STARTED + if (!store.state.Quiz.quizHasStarted) { + return import("@/views/Quiz/QuizCountdown"); + } + + // USER HAS FINISHED QUIZ + if (store.state.Quiz.maxRank === store.state.User.rank - 1) { + return import("@/views/Quiz/QuizFinished"); + } + + // MUST WAIT FOR NEXT QUESTION + if (store.state.Quiz.awaitNextQuestion) { + return import("@/views/Quiz/QuizCountdown"); + } + + // SHOW THE LAST QUESTION + if (store.state.Quiz.isLastQuestion) { + return import("@/views/Quiz/QuizFinalQuestion"); + } + + // NORMAL QUIZ MODE + return import("@/views/Quiz/Quiz"); }, - beforeEnter: (to, from, next) => { - if (!store.state.Quiz.hasSeenIntro && store.state.User.rank == 0) { + beforeEnter(from, to, next) { + // USER MUST SEE INTRO VIDEO + if (!store.state.Quiz.hasSeenIntro && store.state.User.rank == 1) { next({ name: "quiz-intro" }); - } else { - next(); } - } - }, - { - path: "/quiz-scores", - name: "quiz-scores", - component: () => import("@/views/QuizScores"), + next(); + }, meta: { secured: true - }, - beforeEnter: (to, from, next) => { - if (!store.state.Quiz.hasScores) { - next({ name: "quiz" }); - } else { - next(); - } } }, { - path: "/quiz-intro", + path: "/quiz/intro", name: "quiz-intro", - component: () => import("@/views/QuizIntro"), - meta: { - secured: true - } + component: () => import("@/views/Quiz/QuizIntro") }, { path: "*", @@ -114,7 +128,7 @@ router.beforeEach((to, from, next) => { const requireAnon = to.matched.some(record => record.meta.anon); if (!isAuthenticated && requireAuth) { - next({ name: "home" }); + next({ name: "register" }); return; } diff --git a/src/store/index.js b/src/store/index.js index ffa6cda..27f5b6d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -17,4 +17,4 @@ export default new Vuex.Store({ } }); -export { User, Snackbar }; +export { User, Quiz, Snackbar }; diff --git a/src/store/quiz.js b/src/store/quiz.js index 1109a2c..90a9d4d 100644 --- a/src/store/quiz.js +++ b/src/store/quiz.js @@ -1,11 +1,53 @@ import { mapState } from "vuex"; +import { quiz } from "@/api"; +import moment from "moment"; const moduleName = "Quiz"; +function parseDateResponse(dateResponse) { + const timeSplit = dateResponse.split(","); + let daysString = "0 days"; + let timeString = "0:0:0"; + + if (timeSplit.length == 1) { + // returning only timeString + timeString = timeSplit[0]; + } else if (timeSplit.length == 2) { + // returning days and timeString + daysString = timeSplit[0]; + timeString = timeSplit[1]; + } else { + throw new Error("Unexpected error with time response"); + } + const days = parseInt(daysString); + const time = timeString.split(":"); + const hours = time[0]; + const minutes = time[1]; + const seconds = time[2]; + return moment() + .add(days, "days") + .add(hours, "hours") + .add(minutes, "minutes") + .add(seconds, "seconds"); +} + function getDefaultState() { return { hasSeenIntro: false, - hasScores: false + nextUnlockMoment: moment(), + quizStartedMoment: moment(), + question: "", + asset: "", + rank: 0, + maxRank: 0, + isLastQuestion: false, + hints: ["", ""], + wrongCount: localStorage.getItem("wrongCount") + ? parseInt(localStorage.getItem("wrongCount")) + : 0, + quizHasStarted: false, + quizHasEnded: false, + awaitNextQuestion: false }; } @@ -17,17 +59,114 @@ const actions = { async markAsSeen({ commit }) { commit("hasSeenIntro", true); }, - async setScores({ commit }) { - commit("scores", true); + async addWrongCount({ state, commit }) { + commit("wrongCount", state.wrongCount + 1); + }, + async clearWrongCount({ commit }) { + commit("wrongCount", 0); + }, + async refresh({ state, commit }) { + // get current rank and see if quiz has started + try { + const rank = await quiz.getRank(); + commit("maxRank", rank.maxRank); + commit( + "quizStartedMoment", + moment(rank.startsOn + "+0000", "MM/DD/YYYY HH:mm Z") + ); + if (rank.rank < 0) { + commit("quizHasStarted", false); + commit("awaitNextQuestion", false); + commit("question", ""); + commit("asset", ""); + commit("rank", 0); + commit("hints", ["", ""]); + commit("nextUnlockMoment", parseDateResponse(rank.timeUntilNextRank)); + return; + } + commit("quizHasStarted", true); + commit("rank", rank.rank); + } catch (err) { + if (err.status === 403) { + commit("quizHasEnded", true); + return; + } + throw new Error(err); + } + + // get current question and see if question is even unlocked + try { + const response = await quiz.getQuestion(); + commit("awaitNextQuestion", false); + commit("question", response.question); + commit("asset", response.asset); + commit("rank", response.rank); + commit("hints", response.hints); + commit("nextUnlockMoment", moment()); + commit("isLastQuestion", response.rank === state.maxRank); + } catch (err) { + if (err.status === 404) { + commit("awaitNextQuestion", true); + commit("question", ""); + commit("asset", ""); + commit("rank", 0); + commit("hints", ["", ""]); + commit( + "nextUnlockMoment", + parseDateResponse(err.data.timeUntilNextRank) + ); + } else if (err.status === 401) { + commit("question", ""); + commit("asset", ""); + commit("rank", 0); + commit("hints", ["", ""]); + } else { + return Promise.reject(err); + } + } } }; const mutations = { + quizHasStarted(state, value) { + state.quizHasStarted = value; + }, hasSeenIntro(state, value) { state.hasSeenIntro = value; }, - scores() { - state.hasScores = true; + awaitNextQuestion(state, value) { + state.awaitNextQuestion = value; + }, + nextUnlockMoment(state, value) { + state.nextUnlockMoment = value; + }, + question(state, value) { + state.question = value; + }, + rank(state, value) { + state.rank = value; + }, + asset(state, value) { + state.asset = value; + }, + quizStartedMoment(state, value) { + state.quizStartedMoment = value; + }, + wrongCount(state, value) { + state.wrongCount = value; + localStorage.setItem("wrongCount", state.wrongCount); + }, + hints(state, value) { + state.hints = value; + }, + maxRank(state, value) { + state.maxRank = value; + }, + isLastQuestion(state, value) { + state.isLastQuestion = value; + }, + quizHasEnded(state, value) { + state.quizHasEnded = value; } }; diff --git a/src/store/user.js b/src/store/user.js index 473eecb..2e75d37 100644 --- a/src/store/user.js +++ b/src/store/user.js @@ -23,7 +23,7 @@ const state = { const actions = { async refresh({ commit }) { const user = api.auth.currentUser(); - + user.rank = user.rank + 1; if (user.auth) { commit("set", user); } else { diff --git a/src/styles/quiz-answer.scss b/src/styles/quiz-answer.scss index 37c2d8d..6c6669d 100644 --- a/src/styles/quiz-answer.scss +++ b/src/styles/quiz-answer.scss @@ -8,6 +8,11 @@ padding: 12px; z-index: 2; + .attempts-remaining{ + text-align: center; + height: 100px; + } + .v-btn { width: 100%; margin-top: 12px; diff --git a/src/views/ForgotPassword.vue b/src/views/ForgotPassword.vue index 2bbeda1..9c76307 100644 --- a/src/views/ForgotPassword.vue +++ b/src/views/ForgotPassword.vue @@ -19,99 +19,93 @@ - + Send Reset Password Request - + >Send Reset Password Request + - + Multiple Accounts - That email address is associated with multiple accounts. - Password reset emails have been sent for each account. - Double check the email body for the username so that you reset the intended account's password! + That email address is associated with multiple accounts. Password + reset emails have been sent for each account. Double check the email + body for the username so that you reset the intended account's + password! - + OK - diff --git a/src/views/Home.vue b/src/views/Home.vue deleted file mode 100644 index 26e76f7..0000000 --- a/src/views/Home.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/src/views/Login.vue b/src/views/Login.vue index 630c2e1..4833d26 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -43,9 +43,9 @@ diff --git a/src/views/Quiz/CodeEditor.vue b/src/views/Quiz/CodeEditor.vue new file mode 100644 index 0000000..e5c383e --- /dev/null +++ b/src/views/Quiz/CodeEditor.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/views/Quiz/Quiz.vue b/src/views/Quiz/Quiz.vue new file mode 100644 index 0000000..b94a716 --- /dev/null +++ b/src/views/Quiz/Quiz.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/views/Quiz/QuizAnswer.vue b/src/views/Quiz/QuizAnswer.vue new file mode 100644 index 0000000..661c2c7 --- /dev/null +++ b/src/views/Quiz/QuizAnswer.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/views/Quiz/QuizCountdown.vue b/src/views/Quiz/QuizCountdown.vue new file mode 100644 index 0000000..df24cb8 --- /dev/null +++ b/src/views/Quiz/QuizCountdown.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/views/Quiz/QuizFinalQuestion.vue b/src/views/Quiz/QuizFinalQuestion.vue new file mode 100644 index 0000000..6efdfd4 --- /dev/null +++ b/src/views/Quiz/QuizFinalQuestion.vue @@ -0,0 +1,202 @@ + + + diff --git a/src/views/Quiz/QuizFinished.vue b/src/views/Quiz/QuizFinished.vue new file mode 100644 index 0000000..32e7ccb --- /dev/null +++ b/src/views/Quiz/QuizFinished.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/views/QuizIntro.vue b/src/views/Quiz/QuizIntro.vue similarity index 100% rename from src/views/QuizIntro.vue rename to src/views/Quiz/QuizIntro.vue diff --git a/src/views/QuizScores.vue b/src/views/QuizScores.vue deleted file mode 100644 index 3fd5535..0000000 --- a/src/views/QuizScores.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/src/views/Register/Step3.vue b/src/views/Register/Step3.vue index 6f85aac..0ead2f6 100644 --- a/src/views/Register/Step3.vue +++ b/src/views/Register/Step3.vue @@ -3,72 +3,18 @@ - Terms Of Service: What Is It? A Terms of Service, or TOS, is a set of - rules that a user must agree to before they can engage in services or - use a product. It can also serve as a disclaimer under certain - conditions, such as for website use. A properly executed Terms of - Service is legally binding for both parties. A Terms of Service - agreement usually has many different sections, such as definitions, - user rights and responsibilities, and disclaimers. Terms of Service - can change often, and they will need to be re-accepted as changes are - made to the agreement. It is important to any company or business to - include as much information as possible in their Terms of Service to - avoid problems in the future. Getting legal counsel for the Terms of - Service can ensure no major points have been missed and that the Terms - of Service is legally binding to users. Click here to get started now! - Recent Reviews “I am very pleased with FormSwift products and have - already recommended them to a number of my friends. The ease of - creating documents has saved me countless hours.” -Carrie L. "I love - FormSwift. There have been so many new documents added since first - signing up. They walk you through every step. Great job and thanks for - everything you guys do for making this happen." -John M. "FormSwift - was very easy to use, even for someone who is not very tech savvy like - me. Will use again." -Phil T. What Are Terms of Service? what are - terms of service Terms of service perform two essential functions. The - first is to educate your customers on the rules of using your products - and services. The second is to protect your company from lawsuits . - The TOS is a simple enough document to draw up, whether you’re using a - free terms of service generator or working from a sample. Terms of - service should be one of the first documents issued to your clients, - and you should be sure to get it read and signed by each client before - proceeding with their business. Part One - Language A TOS starts off - by familiarizing the reader with the terms that will be used - throughout the document. Generally these terms are fairly, well, - general ones, such as “Terms,” “Services,” “The Company,” etc. Part - Two – Rules of the Road. The TOS then goes on to outline the rights - and responsibilities of the user. The key to this section is to keep - it short and sweet while including all relevant detail. The best - approach is to tell your client what they can’t do rather than what - they can do. Omission is more illustrative than inclusion. Plus, it’s - a good idea to keep the TOS as short as possible so people actually - them. Part Three – What Is and Is Not Your Company’s Fault Limit your - liabilities. Limit them as much as you can, and make their limits - clear. For example: TO THE MAXIMUM EXTENT PERMITTED BY LAW, - ARTHURMACARTHUR, INC. SHALL NOT BE LIABLE FOR AN YDIRECT, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, OR SPECIAL DAMAGES OR LOSSES, WHETHER - TANGIBLE OR INTANGIBLE, RESULTING FROM AUTHORIZED OR UNAUTHORIZED USE - OF OR ACCESS TO OUR PRODUCTS You also might want to include a - disclaimer or return policy, particularly if you’re in the retail - business. This protects you from customers blaming you for damaged - goods: SUPERDUPER, INC. IS NOT RESPONSIBLE FOR THE CONDITIONS OF ITS - MERCHANDISE. THE COMPANY SELLS ALL ITS MERCHANDISE ON AN “AS IS” BASIS - AND ASSUMES NO LIABILITY OR RESPONSIBILITY FOR ANY DAMAGES INCURRED IN - THE TRANSFER OR USE OF OUR PRODUCTS BY YOU OR ANY THIRD PARTY. Part - Four – Verification Put a statement at the end like, “I have read and - agreed to abide by PenDragonPencil’s Terms of Service.” If you’re - issuing a paper TOS, follow this with a place for the user to print - his or her name and sign and date the form provide instructions as to - how the document should be returned to the company. If your TOS is - online, you can just have the user insert his or her initials and hit - the “I Agree” button. + - - - - + + + + + + + diff --git a/tests/conftest.py b/tests/conftest.py index f701485..8d1b5e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ def client(app): app.config["MAIL_SUPPRESS_SEND"] = True app.config["CODE_CHALLENGE_START"] = time.time() app.config["SANDBOX_API_URL"] = os.getenv("SANDBOX_API_URL") + app.config["ANSWER_ATTEMPT_LIMIT"] = "3/minute" with app.test_client() as client: with app.app_context(): diff --git a/tests/test_question.py b/tests/test_question.py index af27755..3aa2b20 100644 --- a/tests/test_question.py +++ b/tests/test_question.py @@ -23,6 +23,7 @@ def client_challenge_today(): app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" app.config["CODE_CHALLENGE_START"] = time.time() app.config["ALLOW_RESET"] = True + app.config["ANSWER_ATTEMPT_LIMIT"] = "3/minute" with app.test_client() as client: with app.app_context(): @@ -50,6 +51,7 @@ def client_challenge_future(): app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" app.config["CODE_CHALLENGE_START"] = CC_2D_FUTURE app.config["SANDBOX_API_URL"] = os.getenv("SANDBOX_API_URL") + app.config["ANSWER_ATTEMPT_LIMIT"] = "3/minute" with app.test_client() as client: with app.app_context(): @@ -62,6 +64,7 @@ def client_challenge_past(): app.config["TESTING"] = True app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" app.config["CODE_CHALLENGE_START"] = CC_2D_PRIOR + app.config["ANSWER_ATTEMPT_LIMIT"] = "3/minute" with app.test_client() as client: with app.app_context(): @@ -74,6 +77,7 @@ def client_challenge_lastq(): app.config["TESTING"] = True app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" app.config["CODE_CHALLENGE_START"] = CC_4D_PRIOR + app.config["ANSWER_ATTEMPT_LIMIT"] = "3/minute" with app.test_client() as client: with app.app_context(): diff --git a/yarn.lock b/yarn.lock index 904cd4b..7202fef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2199,6 +2199,11 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codemirror@^5.41.0: + version "5.51.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.51.0.tgz#7746caaf5223e68f5c55ea11e2f3cc82a9a3929e" + integrity sha512-vyuYYRv3eXL0SCuZA4spRFlKNzQAewHcipRQCOKgRy7VNAvZxTKzbItdbCl4S5AgPZ5g3WkHp+ibWQwv9TLG7Q== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2883,6 +2888,11 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== +diff-match-patch@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" + integrity sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -8428,6 +8438,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vue-cli-plugin-codemirror@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/vue-cli-plugin-codemirror/-/vue-cli-plugin-codemirror-0.0.6.tgz#ccc884bfe4b96eb453379d604fe10ba902b9b4fa" + integrity sha512-F6eRARgT2XoJlcOkRRn5TS3W/i/vyQ0oXNvQiOhd7DXclfgCgHv4V9PpoIYYASbAjdJlqpsUMc4k1Dau6QOxgg== + vue-cli-plugin-vuetify@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.2.tgz#160e573d59f2594b89531e95b0fdd74ca76ecd9d" @@ -8436,6 +8451,14 @@ vue-cli-plugin-vuetify@^2.0.2: semver "^6.0.0" shelljs "^0.8.3" +vue-codemirror@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/vue-codemirror/-/vue-codemirror-4.0.6.tgz#b786bb80d8d762a93aab8e46f79a81006f0437c4" + integrity sha512-ilU7Uf0mqBNSSV3KT7FNEeRIxH4s1fmpG4TfHlzvXn0QiQAbkXS9lLfwuZpaBVEnpP5CSE62iGJjoliTuA8poQ== + dependencies: + codemirror "^5.41.0" + diff-match-patch "^1.0.0" + vue-eslint-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1"