diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6b7c97a --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max_line_length = 150 diff --git a/.gcloudignore b/.gcloudignore index e6605a4..a798360 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -4,4 +4,4 @@ venv/ .gcloudignore .git .gitignore -*.tsv \ No newline at end of file +*.tsv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1495228 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: name-tests-test + - id: requirements-txt-fixer +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.5.0 + hooks: + - id: setup-cfg-fmt +- repo: https://github.com/asottile/reorder_python_imports + rev: v3.12.0 + hooks: + - id: reorder-python-imports + args: [--py37-plus, --add-import, 'from __future__ import annotations'] +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.4 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-use-pathlib +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.0 + hooks: + - id: mypy + additional_dependencies: + - types-toml + - types-requests diff --git a/QuizTheWord/admin/admin.py b/QuizTheWord/admin/admin.py index 9d6edf9..9b2c73f 100644 --- a/QuizTheWord/admin/admin.py +++ b/QuizTheWord/admin/admin.py @@ -1,6 +1,15 @@ +from __future__ import annotations + import json -from flask import render_template, Blueprint, request, jsonify, redirect, url_for -from flask_security import roles_required, permissions_accepted + +from flask import Blueprint +from flask import jsonify +from flask import redirect +from flask import render_template +from flask import request +from flask import url_for +from flask_security import permissions_accepted + from QuizTheWord import database @@ -116,12 +125,12 @@ def query_users(): def parse_question_query(query): if query: - query: dict = json.loads(query) - if "multiple_choice" in query.keys(): - query["multiple_choice"] = True if query["multiple_choice"] == "true" else False - if "hidden_answer" in query.keys(): - query["hidden_answer"] = True if query["hidden_answer"] == "true" else False - return query + parsed_query: dict = json.loads(query) + if "multiple_choice" in parsed_query.keys(): + parsed_query["multiple_choice"] = True if parsed_query["multiple_choice"] == "true" else False + if "hidden_answer" in parsed_query.keys(): + parsed_query["hidden_answer"] = True if parsed_query["hidden_answer"] == "true" else False + return parsed_query return None @@ -149,15 +158,15 @@ def parse_user_form_data(form): data = { "username": form.get("username"), "email": form.get("email"), - "roles": form.getlist("roles") + "roles": form.getlist("roles"), } return data def parse_user_query(query): if query: - query: dict = json.loads(query) + parsed_query: dict = json.loads(query) # if "admin" in query.keys(): # query["admin"] = True if query["admin"] == "true" else False - return query - return None + return parsed_query + return {} diff --git a/QuizTheWord/admin/static/bootstrap-table-filter-control.min.css b/QuizTheWord/admin/static/bootstrap-table-filter-control.min.css index 2803638..3ee6520 100644 --- a/QuizTheWord/admin/static/bootstrap-table-filter-control.min.css +++ b/QuizTheWord/admin/static/bootstrap-table-filter-control.min.css @@ -7,4 +7,4 @@ * @license MIT */ -@charset "UTF-8";.no-filter-control{height:34px}.filter-control{margin:0 2px 2px 2px}.ms-choice{border:0}.ms-parent>button:focus{outline:0} \ No newline at end of file +@charset "UTF-8";.no-filter-control{height:34px}.filter-control{margin:0 2px 2px 2px}.ms-choice{border:0}.ms-parent>button:focus{outline:0} diff --git a/QuizTheWord/admin/static/bootstrap-table.min.css b/QuizTheWord/admin/static/bootstrap-table.min.css index 0fa2968..3608fac 100644 --- a/QuizTheWord/admin/static/bootstrap-table.min.css +++ b/QuizTheWord/admin/static/bootstrap-table.min.css @@ -7,4 +7,4 @@ * @license MIT */ -.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} \ No newline at end of file +.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .columns,.bootstrap-table .fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table td,.bootstrap-table .fixed-table-container .table th{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px!important}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:700;display:inline-block;min-width:30%;width:auto!important;text-align:left!important}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value{width:100%!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=checkbox],.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type=radio]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:flex;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000;transition:visibility 0s,opacity .15s ease-in-out;opacity:0;visibility:hidden}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open{visibility:visible;opacity:1}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination,.bootstrap-table .fixed-table-pagination>.pagination-detail{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:'\2B05'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:'\27A1'}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}.bootstrap-table.bootstrap4 .pagination-lg .page-link,.bootstrap-table.bootstrap5 .pagination-lg .page-link{padding:.5rem 1rem}.bootstrap-table.bootstrap5 .float-left{float:left}.bootstrap-table.bootstrap5 .float-right{float:right}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} diff --git a/QuizTheWord/admin/templates/admin/base.html b/QuizTheWord/admin/templates/admin/base.html index 8a44c33..d78e41b 100644 --- a/QuizTheWord/admin/templates/admin/base.html +++ b/QuizTheWord/admin/templates/admin/base.html @@ -45,4 +45,4 @@ {% block scripts %}{% endblock %} - \ No newline at end of file + diff --git a/QuizTheWord/app.py b/QuizTheWord/app.py index b8c9ccf..c8cb934 100644 --- a/QuizTheWord/app.py +++ b/QuizTheWord/app.py @@ -1,11 +1,25 @@ +from __future__ import annotations + import os -import flask -from flask import request, render_template, jsonify, Flask, url_for, redirect, flash, session -from flask_security import Security, SQLAlchemySessionUserDatastore, login_user, logout_user, current_user +from authlib.integrations.flask_client import OAuth +from flask import flash +from flask import Flask +from flask import jsonify +from flask import redirect +from flask import render_template +from flask import request +from flask import session +from flask import url_for +from flask_security import current_user +from flask_security import login_user +from flask_security import logout_user +from flask_security import Security +from flask_security import SQLAlchemySessionUserDatastore + from QuizTheWord import database from QuizTheWord.admin import admin -from authlib.integrations.flask_client import OAuth + app = Flask(__name__) environment_configuration = os.environ.get('CONFIGURATION_SETUP', "QuizTheWord.config.Development") @@ -19,8 +33,8 @@ with app.app_context(): name="google", server_metadata_url=CONF_URL, client_kwargs={ - "scope": "openid email profile" - } + "scope": "openid email profile", + }, ) app.register_blueprint(admin.Admin) @@ -87,7 +101,7 @@ def check_answer(): def next_hidden_answer(): if "hidden_answer_ids" not in session: session["hidden_answer_ids"] = [] - difficulty = request.args.get("difficulty", 1) + difficulty = request.args.get("difficulty", 1, int) next_question = database.next_hidden_answer(session["hidden_answer_ids"], difficulty) if next_question: session["hidden_answer_ids"].append(next_question.question_id) @@ -102,7 +116,7 @@ def next_hidden_answer(): def next_multiple_choice(): if "multiple_choice_ids" not in session: session["multiple_choice_ids"] = [] - difficulty = request.args.get("difficulty", 1) + difficulty = request.args.get("difficulty", 1, int) next_question = database.next_multiple_choice(session["multiple_choice_ids"], difficulty) if next_question: session["multiple_choice_ids"].append(next_question.question_id) @@ -190,7 +204,7 @@ def error_404(e): @app.errorhandler(500) -def error_404(e): +def error_500(e): print(e) msg = "There was an error with the server." if app.config["DEBUG"]: diff --git a/QuizTheWord/config.py b/QuizTheWord/config.py index 77be34c..443b5bf 100644 --- a/QuizTheWord/config.py +++ b/QuizTheWord/config.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from os import environ + from dotenv import load_dotenv @@ -23,7 +26,7 @@ class Production(Config): DEBUG = False DB_SOCKET_DIR = environ.get("DB_SOCKET_DIR", "/cloudsql") CLOUD_SQL_CONNECTION_NAME = environ.get("CLOUD_SQL_CONNECTION_NAME") - DB_URL = f"postgresql+psycopg2://{Config.DB_USER}:{Config.DB_PASSWORD}@{Config.DB_HOST}:{Config.DB_PORT}/{Config.DB_NAME}?host={DB_SOCKET_DIR}/{CLOUD_SQL_CONNECTION_NAME}" + DB_URL = f"postgresql+psycopg://{Config.DB_USER}:{Config.DB_PASSWORD}@{Config.DB_HOST}:{Config.DB_PORT}/{Config.DB_NAME}?host={DB_SOCKET_DIR}/{CLOUD_SQL_CONNECTION_NAME}" # noqa: E501 class Development(Config): @@ -34,8 +37,8 @@ class Development(Config): DB_PASSWORD = "hello" DB_HOST = "localhost" DB_NAME = "postgres" - DB_PORT = 5432 - DB_URL = f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + DB_PORT = "5432" + DB_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" class Testing(Config): diff --git a/QuizTheWord/database.py b/QuizTheWord/database.py index 4852f6f..5379fd3 100644 --- a/QuizTheWord/database.py +++ b/QuizTheWord/database.py @@ -1,34 +1,57 @@ +from __future__ import annotations + import os -from flask import current_app, g -from flask_security import UserMixin, RoleMixin, SQLAlchemySessionUserDatastore, verify_password, hash_password -import sqlalchemy -from typing import Union, Optional, Literal, Type, List, Tuple import random -from sqlalchemy import Column, JSON, String, Integer, create_engine, ForeignKey, func, Boolean, UnicodeText, DateTime, Numeric, CheckConstraint -from sqlalchemy.sql.expression import and_ -from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime +from decimal import Decimal +from typing import Any +from typing import Literal +from typing import Sequence + +from flask import current_app +from flask import g +from flask_security import hash_password +from flask_security import RoleMixin +from flask_security import SQLAlchemySessionUserDatastore +from flask_security import UserMixin +from flask_security import verify_password +from sqlalchemy import and_ +from sqlalchemy import CheckConstraint +from sqlalchemy import create_engine +from sqlalchemy import ForeignKey +from sqlalchemy import func +from sqlalchemy import JSON +from sqlalchemy import select +from sqlalchemy import String from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.orm import sessionmaker, relationship, scoped_session +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship +from sqlalchemy.orm import scoped_session +from sqlalchemy.orm import sessionmaker from werkzeug.utils import import_string config = import_string(os.environ.get("CONFIGURATION_SETUP")) engine = create_engine(config.DB_URL, pool_size=20) -Base = declarative_base() -def get_scoped_session(): +class Base(DeclarativeBase): + pass + + +def get_scoped_session() -> scoped_session: session_factory = sessionmaker(bind=engine) return scoped_session(session_factory) -def get_session() -> sqlalchemy.orm.session.Session: +def get_session() -> scoped_session: if not hasattr(g, "session"): - Session = get_scoped_session() - Base.query = Session.query_property() # This is for compatibility with Flask-Security-Too which assumes usage of Flask-Sqlalchemy - # Session.query_property() - g.session = Session() - + session = get_scoped_session() + # This is for compatibility with Flask-Security-Too which assumes usage of Flask-Sqlalchemy + Base.query = session.query_property() + g.session = session() return g.session @@ -48,18 +71,18 @@ def get_user_datastore() -> SQLAlchemySessionUserDatastore: class User(Base, UserMixin): __tablename__ = "users" - user_id = Column(Integer, primary_key=True) - email = Column(String, unique=True, nullable=False) - username = Column(String, unique=True) - password = Column(String, nullable=True) - active = Column(Boolean, nullable=False) - last_login_at = Column(DateTime()) - current_login_at = Column(DateTime()) - last_login_ip = Column(String(100)) - current_login_ip = Column(String(100)) - login_count = Column(Integer) - fs_uniquifier = Column(String, unique=True, nullable=False) - google_id = Column(Numeric, nullable=True) + user_id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(unique=True) + username: Mapped[str] = mapped_column(unique=True) + password: Mapped[str | None] + active: Mapped[bool] + last_login_at: Mapped[datetime] + current_login_at: Mapped[datetime] + last_login_ip: Mapped[str] = mapped_column(String(100)) + current_login_ip: Mapped[str] = mapped_column(String(100)) + login_count: Mapped[int] + fs_uniquifier: Mapped[str] = mapped_column(unique=True) + google_id: Mapped[Decimal | None] __table_args__ = ( CheckConstraint("(password IS NOT NULL) OR (google_id IS NOT NULL)", "password_google_id_null_check"), @@ -87,26 +110,26 @@ class User(Base, UserMixin): class Role(Base, RoleMixin): __tablename__ = "roles" - role_id = Column(Integer, primary_key=True) - name = Column(String, unique=True) - description = Column(String) - permissions = Column(UnicodeText) + role_id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) + description: Mapped[str] + permissions: Mapped[str] class UsersRoles(Base): __tablename__ = "users_roles" - user_role_id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey("users.user_id")) - role_id = Column(Integer, ForeignKey("roles.role_id")) + user_role_id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.user_id")) + role_id: Mapped[int] = mapped_column(ForeignKey("roles.role_id")) class AllQuestions(Base): __tablename__ = "all_questions" - question_id = Column(Integer, primary_key=True, nullable=False) - question_text = Column(String) - answer = Column(String) - addresses = Column(String) + question_id: Mapped[int] = mapped_column(primary_key=True) + question_text: Mapped[str] + answer: Mapped[str] + addresses: Mapped[str] multiple_choice = relationship("MultipleChoice", uselist=False, back_populates="all_question_relationship") hidden_answer = relationship("HiddenAnswer", uselist=False, back_populates="all_question_relationship") @@ -137,9 +160,9 @@ class AllQuestions(Base): class HiddenAnswer(Base): __tablename__ = "category_hidden_answer" - question_id = Column(Integer, ForeignKey("all_questions.question_id"), primary_key=True) - hidden_answer_difficulty = Column(Integer) - hidden_answer_hint = Column(JSON) + question_id: Mapped[int] = mapped_column(ForeignKey("all_questions.question_id"), primary_key=True) + hidden_answer_difficulty: Mapped[int] + hidden_answer_hint: Mapped[dict | list | None] = mapped_column(JSON) all_question_relationship = relationship("AllQuestions", lazy="joined", back_populates="hidden_answer") question_text = association_proxy("all_question_relationship", "question_text") @@ -169,10 +192,10 @@ class HiddenAnswer(Base): class MultipleChoice(Base): __tablename__ = "category_multiple_choice" - question_id = Column(Integer, ForeignKey("all_questions.question_id"), primary_key=True) - multiple_choice_difficulty = Column(Integer) - multiple_choice_hint = Column(JSON) - wrong_answers = Column(JSON) + question_id: Mapped[int] = mapped_column(ForeignKey("all_questions.question_id"), primary_key=True) + multiple_choice_difficulty: Mapped[int] + multiple_choice_hint: Mapped[dict | list | None] = mapped_column(JSON) + wrong_answers: Mapped[list] = mapped_column(JSON) all_question_relationship = relationship("AllQuestions", lazy="joined", back_populates="multiple_choice") question_text = association_proxy("all_question_relationship", "question_text") @@ -188,7 +211,7 @@ class MultipleChoice(Base): self.answer_list = None def randomize_answer_list(self): - answer_list: List[str] = [*self.wrong_answers, self.answer] + answer_list: list[str] = [*self.wrong_answers, self.answer] random.shuffle(answer_list) self.answer_list = answer_list @@ -220,7 +243,8 @@ class MultipleChoice(Base): def add_multiple_choice_question(question, answer, addresses, difficulty, hint, wrong_answers): session = get_session() - question_id = session.query(AllQuestions).order_by(AllQuestions.question_id.desc()).first().question_id + 1 + select_stmt = select(AllQuestions).order_by(AllQuestions.question_id.desc()) + question_id = session.scalars(select_stmt).first().question_id + 1 print(question_id) base_question = AllQuestions(question_id, question, answer, addresses) multiple_choice_question = MultipleChoice(question_id, difficulty, hint, wrong_answers, base_question) @@ -231,7 +255,8 @@ def add_multiple_choice_question(question, answer, addresses, difficulty, hint, def add_question(data) -> AllQuestions: session = get_session() - question_id = session.query(AllQuestions).order_by(AllQuestions.question_id.desc()).first().question_id + 1 + select_stmt = select(AllQuestions).order_by(AllQuestions.question_id.desc()) + question_id = session.scalars(select_stmt).first().question_id + 1 question = AllQuestions(question_id) session.add(question) if data["create_multiple_choice"]: @@ -253,88 +278,104 @@ def add_question(data) -> AllQuestions: return question -def get_all_questions() -> List[AllQuestions]: +def get_all_questions() -> Sequence[AllQuestions]: session = get_session() - return session.query(AllQuestions).all() + return session.scalars(select(AllQuestions)).all() -def get_all_hidden_answer() -> List[HiddenAnswer]: +def get_all_hidden_answer() -> Sequence[HiddenAnswer]: session = get_session() - return session.query(HiddenAnswer).all() + return session.scalars(select(HiddenAnswer)).all() -def get_all_multiple_choice() -> List[MultipleChoice]: +def get_all_multiple_choice() -> Sequence[MultipleChoice]: session = get_session() - return session.query(MultipleChoice).all() + return session.scalars(select(MultipleChoice)).all() -def get_category_count(category: Union[Type[MultipleChoice], Type[HiddenAnswer], Type[AllQuestions]]) -> int: +def get_category_count(category: type[MultipleChoice] | type[HiddenAnswer] | type[AllQuestions]) -> int: session = get_session() - return session.query(category).count() + return session.scalar(select(func.count(category))) -def get_question(category: Union[Type[MultipleChoice], Type[HiddenAnswer], Type[AllQuestions]], question_id: int) -> \ - Optional[Union[MultipleChoice, HiddenAnswer, AllQuestions]]: +def get_question[T: (MultipleChoice, HiddenAnswer, AllQuestions)](category: type[T], question_id: int) -> T | None: session = get_session() - return session.query(category).filter(category.question_id == question_id).one_or_none() + select_stmt = select(category).where(category.question_id == question_id) + return session.scalars(select_stmt).one_or_none() -def get_random_question_of_difficulty(category: Union[Type[MultipleChoice], Type[HiddenAnswer]], - difficulty: Literal[1, 2, 3]): +def get_random_question_of_difficulty(category: type[MultipleChoice] | type[HiddenAnswer], difficulty: Literal[1, 2, 3]): session = get_session() - return session.query(category).filter(category.hidden_answer_difficulty == difficulty).order_by(func.random()).first() + select_stmt = select(category).where(category.hidden_answer_difficulty == difficulty).order_by(func.random()) + return session.scalars(select_stmt).first() -def get_random_hidden_answer(difficulty: Optional[Literal[1, 2, 3]] = None) -> HiddenAnswer: +def get_random_hidden_answer(difficulty: Literal[1, 2, 3] | None = None) -> HiddenAnswer: session = get_session() + select_stmt = select(HiddenAnswer).order_by(func.random()) if difficulty is not None: - return session.query(HiddenAnswer).filter(HiddenAnswer.hidden_answer_difficulty == difficulty).order_by(func.random()).first() - return session.query(HiddenAnswer).order_by(func.random()).first() + select_stmt = select_stmt.where(HiddenAnswer.hidden_answer_difficulty == difficulty) + return session.scalars(select_stmt).first() -def get_random_multiple_choice(difficulty: Optional[Literal[1, 2, 3]] = None) -> MultipleChoice: +def get_random_multiple_choice(difficulty: Literal[1, 2, 3] | None = None) -> MultipleChoice: session = get_session() + select_stmt = select(MultipleChoice).order_by(func.random()) if difficulty is not None: - return session.query(MultipleChoice).filter(MultipleChoice.multiple_choice_difficulty == difficulty).order_by( - func.random()).first() - return session.query(MultipleChoice).order_by(func.random()).first() + select_stmt = select_stmt.where(MultipleChoice.multiple_choice_difficulty == difficulty) + return session.scalars(select_stmt).first() -def check_answer(question_id: int, guess: str) -> Tuple[bool, str]: +def check_answer(question_id: int, guess: str) -> tuple[bool, str] | None: session = get_session() - question: AllQuestions = session.query(AllQuestions).filter(AllQuestions.question_id == question_id).one_or_none() + select_stmt = select(AllQuestions).where(AllQuestions.question_id == question_id) + question: AllQuestions = session.scalars(select_stmt).one_or_none() if question: answer = question.answer return answer == guess, answer + return None -def query_all_questions(offset, limit, query: dict = None, sort=None, order=None) -> Tuple[List[AllQuestions], int]: +def query_all_questions(offset, limit, query: dict[str, Any], sort=None, order=None) -> tuple[Sequence[AllQuestions], int]: session = get_session() - query_params = [] + select_stmt = select(AllQuestions) if query is not None: for key in query.keys(): if key == "multiple_choice" or key == "hidden_answer": if query[key]: - query_params.append(getattr(AllQuestions, key) != None) + select_stmt = select_stmt.where(getattr(AllQuestions, key).is_(None)) else: - query_params.append(getattr(AllQuestions, key) == None) + select_stmt = select_stmt.where(getattr(AllQuestions, key).is_(None)) else: - query_params.append(getattr(AllQuestions, key).ilike("%" + query[key] + "%")) - order_by = None + select_stmt = select_stmt.where(getattr(AllQuestions, key).ilike("%" + query[key] + "%")) if sort and order: - order_by = getattr(getattr(AllQuestions, sort), order)() - q = session.query(AllQuestions).filter(*query_params).order_by(order_by) - questions = q.offset(offset).limit(limit).all() - count = q.count() + select_stmt = select_stmt.order_by(getattr(getattr(AllQuestions, sort), order)()) + questions = session.scalars(select_stmt.offset(offset).limit(limit)).all() + count = session.scalar(func.count(select_stmt)) return questions, count -def get_unhealthy_questions() -> List[AllQuestions]: +def get_unhealthy_questions() -> list[AllQuestions]: session = get_session() questions = [] - missing_category_questions = session.query(AllQuestions).filter(and_(AllQuestions.hidden_answer == None, AllQuestions.multiple_choice == None)).all() - multiple_choice_difficulty = session.query(AllQuestions).filter(AllQuestions.multiple_choice.has(MultipleChoice.multiple_choice_difficulty == None)).all() - hidden_answer_difficulty = session.query(AllQuestions).filter(AllQuestions.hidden_answer.has(HiddenAnswer.hidden_answer_difficulty == None)).all() + missing_category_questions = ( + session.scalars( + select(AllQuestions) + .where(and_(AllQuestions.hidden_answer.is_(None), AllQuestions.multiple_choice.is_(None))), + ).all() + ) + multiple_choice_difficulty = ( + session.scalars( + select(AllQuestions) + .where(AllQuestions.multiple_choice.has(MultipleChoice.multiple_choice_difficulty.is_(None))), + ).all() + ) + hidden_answer_difficulty = ( + session.scalars( + select(AllQuestions) + .where(AllQuestions.hidden_answer.has(HiddenAnswer.hidden_answer_difficulty.is_(None))), + ).all() + ) questions.extend(missing_category_questions) questions.extend(multiple_choice_difficulty) questions.extend(hidden_answer_difficulty) @@ -343,7 +384,8 @@ def get_unhealthy_questions() -> List[AllQuestions]: def update_question(question_id, data): session = get_session() - question: AllQuestions = session.query(AllQuestions).get(question_id) + session.get(AllQuestions, question_id) + question = session.get(AllQuestions, question_id) if data["create_multiple_choice"]: multiple_choice = MultipleChoice(question_id) session.add(multiple_choice) @@ -371,7 +413,7 @@ def add_user(email, password, username=None, **kwargs): username = email user = user_datastore.create_user(email=email, password=password_hash, username=username, **kwargs) session.add(user) - role = session.query(Role).filter(Role.name == "basic").one_or_none() + role = session.scalar(select(Role).where(Role.name == "basic")) if role is not None: user_datastore.add_role_to_user(user, role) # user.add_role(role) @@ -387,24 +429,30 @@ def add_user(email, password, username=None, **kwargs): def get_user(user_id=None, google_id=None) -> User: session = get_session() if user_id is not None: - user = session.query(User).filter(User.user_id == user_id).one_or_none() + user = session.scalars( + select(User).where(User.user_id == user_id), + ).one_or_none() else: - user = session.query(User).filter(User.google_id == google_id).one_or_none() + user = session.scalars( + select(User).where(User.google_id == google_id), + ).one_or_none() return user def update_user(user_id, data): session = get_session() user_datastore = get_user_datastore() - user = session.query(User).filter(User.user_id == user_id).one_or_none() + select_stmt = select(User).where(User.user_id == user_id) + user = session.scalars(select_stmt).one_or_none() for role in user.roles: if role.name not in data["roles"]: # user.remove_role(role) user_datastore.remove_role_from_user(user, role) for role_name in data["roles"]: - role = session.query(Role).filter(Role.name == role_name).one_or_none() + select_stmt = select(Role).where(Role.name == role_name) + role = session.scalars(select_stmt).one_or_none() # if role is not None: - # user.add_role(role) + # user.add_role(role) user_datastore.add_role_to_user(user, role) for column in data.keys(): if column in User.__table__.columns: @@ -412,38 +460,46 @@ def update_user(user_id, data): session.commit() -def query_users(offset, limit, query: dict = None, sort=None, order=None) -> Tuple[List[User], int]: +def query_users(offset, limit, query: dict[str, Any], sort=None, order=None) -> tuple[Sequence[User], int]: session = get_session() - query_params = [] + select_stmt = select(User) if query is not None: for key in query.keys(): if hasattr(User, key): - query_params.append(getattr(User, key).ilike("%" + query[key] + "%")) - order_by = None + select_stmt = select_stmt.where(getattr(User, key).ilike("%" + query[key] + "%")) if sort and order: - order_by = getattr(getattr(User, sort), order)() - q = session.query(User).filter(*query_params).order_by(order_by) - users = q.offset(offset).limit(limit).all() - count = q.count() + select_stmt = select_stmt.order_by(getattr(getattr(User, sort), order)()) + users = session.scalars(select_stmt.offset(offset).limit(limit)).all() + count = session.scalar(func.count(select_stmt)) return users, count -def get_all_roles() -> List[Role]: +def get_all_roles() -> Sequence[Role]: session = get_session() - return session.query(Role).all() + return session.scalars(select(Role)).all() -def next_hidden_answer(previous_questions: List[int], difficulty) -> HiddenAnswer: +def next_hidden_answer(previous_questions: list[int], difficulty) -> HiddenAnswer: session = get_session() - return session.query(HiddenAnswer)\ - .filter(HiddenAnswer.hidden_answer_difficulty == difficulty, - HiddenAnswer.question_id.notin_(previous_questions))\ - .order_by(func.random()).first() + select_stmt = ( + select(HiddenAnswer) + .where( + HiddenAnswer.hidden_answer_difficulty == difficulty, + HiddenAnswer.question_id.notin_(previous_questions), + ) + .order_by(func.random()) + ) + return session.scalars(select_stmt).first() -def next_multiple_choice(previous_questions: List[int], difficulty) -> MultipleChoice: +def next_multiple_choice(previous_questions: list[int], difficulty) -> MultipleChoice: session = get_session() - return session.query(MultipleChoice)\ - .filter(MultipleChoice.multiple_choice_difficulty == difficulty, - MultipleChoice.question_id.notin_(previous_questions))\ - .order_by(func.random()).first() + select_stmt = ( + select(MultipleChoice) + .where( + MultipleChoice.multiple_choice_difficulty == difficulty, + MultipleChoice.question_id.notin_(previous_questions), + ) + .order_by(func.random()) + ) + return session.scalars(select_stmt).first() diff --git a/QuizTheWord/templates/base.html b/QuizTheWord/templates/base.html index 0018329..aa7d544 100644 --- a/QuizTheWord/templates/base.html +++ b/QuizTheWord/templates/base.html @@ -57,4 +57,4 @@ {% block scripts %}{% endblock %} - \ No newline at end of file + diff --git a/QuizTheWord/templates/error.html b/QuizTheWord/templates/error.html index 6ff4a8b..da5924c 100644 --- a/QuizTheWord/templates/error.html +++ b/QuizTheWord/templates/error.html @@ -5,4 +5,4 @@

{{ error_code }}

{{ error_msg }}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/QuizTheWord/templates/login.html b/QuizTheWord/templates/login.html index 21a3500..d7fda8f 100644 --- a/QuizTheWord/templates/login.html +++ b/QuizTheWord/templates/login.html @@ -37,4 +37,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app.yaml b/app.yaml index 9ed2ed7..1ab360c 100644 --- a/app.yaml +++ b/app.yaml @@ -1,4 +1,4 @@ runtime: python39 entrypoint: gunicorn -b :$PORT QuizTheWord.app:app env_variables: - CONFIGURATION_SETUP: "QuizTheWord.config.Production" \ No newline at end of file + CONFIGURATION_SETUP: "QuizTheWord.config.Production" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b1e9a5b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.autopep8] +max_line_length = 150 + +[tool.mypy] +check_untyped_defs = true diff --git a/requirements.txt b/requirements.txt index 5ed1bec..ffe82a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,13 @@ -click~=7.1.2 -Flask~=1.1.2 -itsdangerous~=1.1.0 -Jinja2~=2.11.3 -MarkupSafe~=1.1.1 -pg8000~=1.17.0 -psycopg2~=2.8.6 -python-dotenv~=0.15.0 -scramp~=1.2.0 -SQLAlchemy~=1.3.23 -Werkzeug~=1.0.1 -Flask-Security-Too~=4.0.0 -pytest~=6.2.2 -gunicorn~=20.1.0 +Authlib~=1.2.1 +bcrypt~=4.0.1 +Flask~=3.0.0 +Flask-Security-Too~=5.3.2 +gunicorn~=21.2.0 html5validate~=0.0.2 -Authlib~=0.15.4 -Flask-WTF~=0.14.3 -bcrypt~=3.2.0 -WTForms~=2.3.3 -html5validate~=0.0.2 -requests~=2.26.0 \ No newline at end of file +psycopg[binary]~=3.1.12 +pytest~=7.4.3 +python-dotenv~=1.0.0 +requests~=2.31.0 +setuptools~=68.2.2 +SQLAlchemy~=2.0.23 +Werkzeug~=3.0.1 diff --git a/scripts/file_parsing.py b/scripts/file_parsing.py index a0b17e2..a66109a 100644 --- a/scripts/file_parsing.py +++ b/scripts/file_parsing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pathlib @@ -10,7 +12,8 @@ def parse_multiple_choice_questions(question_file: pathlib.Path): correct_answer_index = int(question_data[0])-1 question = question_data[1] answer_list = question_data[2].split("//") - bible_verse = question_data[3] if question_data[3].lower() != "none" else None + bible_verse = question_data[3] if question_data[3].lower( + ) != "none" else None correct_answer = answer_list.pop(correct_answer_index) questions.append({ "question": question, diff --git a/tests/test_admin.py b/tests/admin_test.py similarity index 97% rename from tests/test_admin.py rename to tests/admin_test.py index 3c0cd54..e2b8853 100644 --- a/tests/test_admin.py +++ b/tests/admin_test.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import json -from html5validate import validate + from flask.testing import FlaskClient +from html5validate import validate def test_admin_home_with_basic_user(client: FlaskClient, login_basic_user): diff --git a/tests/conftest.py b/tests/conftest.py index d8a5111..1bdf85d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,19 @@ +from __future__ import annotations + import os -from flask_security import hash_password, SQLAlchemySessionUserDatastore -from QuizTheWord.database import User, Role, AllQuestions, MultipleChoice, HiddenAnswer, get_session, init_db, destroy_db + import pytest +from flask_security import hash_password +from flask_security import SQLAlchemySessionUserDatastore + +from QuizTheWord.database import AllQuestions +from QuizTheWord.database import destroy_db +from QuizTheWord.database import get_session +from QuizTheWord.database import HiddenAnswer +from QuizTheWord.database import init_db +from QuizTheWord.database import MultipleChoice +from QuizTheWord.database import Role +from QuizTheWord.database import User @pytest.fixture(scope="module")