From b4947d0aa1bd38487ee64c19749cc99548335060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Wed, 28 Apr 2021 19:12:44 +0000 Subject: [PATCH] Resolve "Add automatic documentation generation" --- .gitignore | 6 -- .vscode/settings.json | 3 +- .vscode/tasks.json | 22 ++++- server/.gitignore | 9 ++ server/app/__init__.py | 5 ++ server/app/core/__init__.py | 5 ++ server/app/core/codes.py | 15 +++- server/app/core/dto.py | 5 ++ server/app/core/files.py | 46 ++++++---- server/app/core/http_codes.py | 4 + server/app/core/parsers.py | 4 + server/app/core/rich_schemas.py | 5 ++ server/app/core/schemas.py | 5 ++ server/app/core/sockets.py | 59 +++++++++++-- server/app/database/__init__.py | 37 +++++--- server/app/database/controller/__init__.py | 6 +- server/app/database/models.py | 6 ++ server/app/database/types.py | 4 + server/docs/Makefile | 20 +++++ server/docs/make.bat | 35 ++++++++ server/docs/source/conf.py | 66 ++++++++++++++ server/docs/source/development.md | 59 +++++++++++++ server/docs/source/index.rst | 33 +++++++ server/docs/source/installation.md | 45 ++++++++++ .../source/modules/app.apis.alternatives.rst | 7 ++ .../docs/source/modules/app.apis.answers.rst | 7 ++ server/docs/source/modules/app.apis.auth.rst | 7 ++ server/docs/source/modules/app.apis.codes.rst | 7 ++ .../source/modules/app.apis.competitions.rst | 7 ++ .../source/modules/app.apis.components.rst | 7 ++ server/docs/source/modules/app.apis.media.rst | 7 ++ server/docs/source/modules/app.apis.misc.rst | 7 ++ .../source/modules/app.apis.questions.rst | 7 ++ server/docs/source/modules/app.apis.rst | 26 ++++++ .../docs/source/modules/app.apis.slides.rst | 7 ++ server/docs/source/modules/app.apis.teams.rst | 7 ++ server/docs/source/modules/app.apis.users.rst | 7 ++ server/docs/source/modules/app.core.codes.rst | 7 ++ server/docs/source/modules/app.core.dto.rst | 5 ++ server/docs/source/modules/app.core.files.rst | 7 ++ .../source/modules/app.core.http_codes.rst | 5 ++ .../docs/source/modules/app.core.parsers.rst | 7 ++ .../source/modules/app.core.rich_schemas.rst | 4 + server/docs/source/modules/app.core.rst | 22 +++++ .../docs/source/modules/app.core.schemas.rst | 4 + .../docs/source/modules/app.core.sockets.rst | 7 ++ .../modules/app.database.controller.add.rst | 7 ++ .../modules/app.database.controller.copy.rst | 7 ++ .../app.database.controller.delete.rst | 7 ++ .../modules/app.database.controller.edit.rst | 7 ++ .../modules/app.database.controller.get.rst | 7 ++ .../modules/app.database.controller.rst | 21 +++++ .../app.database.controller.search.rst | 7 ++ .../modules/app.database.controller.utils.rst | 7 ++ .../source/modules/app.database.models.rst | 7 ++ server/docs/source/modules/app.database.rst | 24 ++++++ .../source/modules/app.database.types.rst | 7 ++ server/docs/source/modules/app.rst | 17 ++++ server/docs/source/overview.md | 81 ++++++++++++++++++ server/requirements.txt | Bin 2448 -> 3238 bytes 60 files changed, 839 insertions(+), 49 deletions(-) create mode 100644 server/.gitignore create mode 100644 server/docs/Makefile create mode 100644 server/docs/make.bat create mode 100644 server/docs/source/conf.py create mode 100644 server/docs/source/development.md create mode 100644 server/docs/source/index.rst create mode 100644 server/docs/source/installation.md create mode 100644 server/docs/source/modules/app.apis.alternatives.rst create mode 100644 server/docs/source/modules/app.apis.answers.rst create mode 100644 server/docs/source/modules/app.apis.auth.rst create mode 100644 server/docs/source/modules/app.apis.codes.rst create mode 100644 server/docs/source/modules/app.apis.competitions.rst create mode 100644 server/docs/source/modules/app.apis.components.rst create mode 100644 server/docs/source/modules/app.apis.media.rst create mode 100644 server/docs/source/modules/app.apis.misc.rst create mode 100644 server/docs/source/modules/app.apis.questions.rst create mode 100644 server/docs/source/modules/app.apis.rst create mode 100644 server/docs/source/modules/app.apis.slides.rst create mode 100644 server/docs/source/modules/app.apis.teams.rst create mode 100644 server/docs/source/modules/app.apis.users.rst create mode 100644 server/docs/source/modules/app.core.codes.rst create mode 100644 server/docs/source/modules/app.core.dto.rst create mode 100644 server/docs/source/modules/app.core.files.rst create mode 100644 server/docs/source/modules/app.core.http_codes.rst create mode 100644 server/docs/source/modules/app.core.parsers.rst create mode 100644 server/docs/source/modules/app.core.rich_schemas.rst create mode 100644 server/docs/source/modules/app.core.rst create mode 100644 server/docs/source/modules/app.core.schemas.rst create mode 100644 server/docs/source/modules/app.core.sockets.rst create mode 100644 server/docs/source/modules/app.database.controller.add.rst create mode 100644 server/docs/source/modules/app.database.controller.copy.rst create mode 100644 server/docs/source/modules/app.database.controller.delete.rst create mode 100644 server/docs/source/modules/app.database.controller.edit.rst create mode 100644 server/docs/source/modules/app.database.controller.get.rst create mode 100644 server/docs/source/modules/app.database.controller.rst create mode 100644 server/docs/source/modules/app.database.controller.search.rst create mode 100644 server/docs/source/modules/app.database.controller.utils.rst create mode 100644 server/docs/source/modules/app.database.models.rst create mode 100644 server/docs/source/modules/app.database.rst create mode 100644 server/docs/source/modules/app.database.types.rst create mode 100644 server/docs/source/modules/app.rst create mode 100644 server/docs/source/overview.md diff --git a/.gitignore b/.gitignore index 3c5d2e1e..f20a5fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ -__pycache__ -*.db -*/env *.coverage */coverage -htmlcov -.pytest_cache /.idea .vs/ -/server/app/static/ diff --git a/.vscode/settings.json b/.vscode/settings.json index b02ef900..ffbd77a7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,5 +41,6 @@ "search.exclude": { "**/env": true }, - "python.pythonPath": "server\\env\\Scripts\\python.exe" + "python.pythonPath": "server\\env\\Scripts\\python.exe", + "restructuredtext.confPath": "${workspaceFolder}\\server\\sphinx\\source" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7029dbe3..8b7286d1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -23,7 +23,6 @@ { "label": "Open client coverage", "type": "shell", - "group": "build", "command": "start ./output/coverage/jest/index.html", "problemMatcher": [], "options": { @@ -54,7 +53,7 @@ }, }, { - "label": "Populate server", + "label": "Populate database", "type": "shell", "group": "build", "command": "env/Scripts/python populate.py", @@ -66,13 +65,30 @@ { "label": "Open server coverage", "type": "shell", - "group": "build", "command": "start ./htmlcov/index.html", "problemMatcher": [], "options": { "cwd": "${workspaceFolder}/server" }, }, + { + "label": "Generate server documentation", + "type": "shell", + "command": "../env/Scripts/activate; ./make html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server/docs" + }, + }, + { + "label": "Open server documentation", + "type": "shell", + "command": "start index.html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server/docs/build/html" + }, + }, { "label": "Start client and server", "group": "build", diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..6ff704b8 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,9 @@ +__pycache__ +env +htmlcov +.pytest_cache +app/*.db +app/static/ + +# Documentation files +docs/build diff --git a/server/app/__init__.py b/server/app/__init__.py index 2add6544..4a28018c 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -7,6 +7,11 @@ from app.core.dto import MediaDTO def create_app(config_name="configmodule.DevelopmentConfig"): + """ + Creates Flask app, returns it and a SocketIO instance. Call run on the + SocketIO instance and pass in the Flask app to start the server. + """ + app = Flask(__name__, static_url_path="/static", static_folder="static") app.config.from_object(config_name) app.url_map.strict_slashes = False diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py index acfca554..94d1daf2 100644 --- a/server/app/core/__init__.py +++ b/server/app/core/__init__.py @@ -1,3 +1,8 @@ +""" +The core submodule contains everything important to the server that doesn't +fit neatly in either apis or database. +""" + from app.database import Base, ExtendedQuery from flask_bcrypt import Bcrypt from flask_jwt_extended.jwt_manager import JWTManager diff --git a/server/app/core/codes.py b/server/app/core/codes.py index 47150767..ad6d844c 100644 --- a/server/app/core/codes.py +++ b/server/app/core/codes.py @@ -1,3 +1,7 @@ +""" +Contains all functions purely related to creating and verifying a code. +""" + import random import re import string @@ -7,9 +11,14 @@ ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") -def generate_code_string(): +def generate_code_string() -> str: + """Generates a 6 character long random sequence containg uppercase letters + and numbers. + """ return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH)) -def verify_code(c): - return CODE_RE.search(c.upper()) is not None +def verify_code(code: str) -> bool: + """Returns True if code only contains letters and/or numbers + and is exactly 6 characters long.""" + return CODE_RE.search(code.upper()) is not None diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 90e49d00..6bbbdf62 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -1,3 +1,8 @@ +""" +The DTO module (short for Data Transfer Object) connects the namespace of an +API and its related schemas. +""" + import app.core.rich_schemas as rich_schemas import app.core.schemas as schemas from flask_restx import Namespace diff --git a/server/app/core/files.py b/server/app/core/files.py index 01a03166..5d22c8e0 100644 --- a/server/app/core/files.py +++ b/server/app/core/files.py @@ -1,30 +1,35 @@ +""" +Contains functions related to file handling, mainly saving and deleting images. +""" + from PIL import Image, ImageChops -from flask import current_app +from flask import current_app, has_app_context import os import datetime from flask_uploads import IMAGES, UploadSet -PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] -THUMBNAIL_SIZE = current_app.config["THUMBNAIL_SIZE"] -image_set = UploadSet("photos", IMAGES) +if has_app_context(): + PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] + THUMBNAIL_SIZE = current_app.config["THUMBNAIL_SIZE"] + image_set = UploadSet("photos", IMAGES) -def compare_images(input_image, output_image): - # compare image dimensions (assumption 1) - if input_image.size != output_image.size: - return False +# def compare_images(input_image, output_image): +# # compare image dimensions (assumption 1) +# if input_image.size != output_image.size: +# return False - rows, cols = input_image.size +# rows, cols = input_image.size - # compare image pixels (assumption 2 and 3) - for row in range(rows): - for col in range(cols): - input_pixel = input_image.getpixel((row, col)) - output_pixel = output_image.getpixel((row, col)) - if input_pixel != output_pixel: - return False +# # compare image pixels (assumption 2 and 3) +# for row in range(rows): +# for col in range(cols): +# input_pixel = input_image.getpixel((row, col)) +# output_pixel = output_image.getpixel((row, col)) +# if input_pixel != output_pixel: +# return False - return True +# return True def _delete_image(filename): @@ -33,6 +38,10 @@ def _delete_image(filename): def save_image_with_thumbnail(image_file): + """ + Saves the given image and also creates a small thumbnail for it. + """ + saved_filename = image_set.save(image_file) saved_path = os.path.join(PHOTO_PATH, saved_filename) with Image.open(saved_path) as im: @@ -45,6 +54,9 @@ def save_image_with_thumbnail(image_file): def delete_image_and_thumbnail(filename): + """ + Delete the given image together with its thumbnail. + """ _delete_image(filename) _delete_image(f"thumbnail_{filename}") diff --git a/server/app/core/http_codes.py b/server/app/core/http_codes.py index 95a99175..f6f19ed1 100644 --- a/server/app/core/http_codes.py +++ b/server/app/core/http_codes.py @@ -1,3 +1,7 @@ +""" +This module defines all the http status codes thats used in the api. +""" + OK = 200 NO_CONTENT = 204 BAD_REQUEST = 400 diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index 160d67a0..4c152520 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -1,3 +1,7 @@ +""" +This module contains the parsers used to parse the data gotten in api requests. +""" + from flask_restx import inputs, reqparse diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index ae1c6ab6..7d883584 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -1,3 +1,8 @@ +""" +This module contains rich schemas used to convert database objects into +dictionaries. This is the rich variant which means that objects will +pull in other whole objects instead of just the id. +""" import app.core.schemas as schemas import app.database.models as models from app.core import ma diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 4008b886..73ac21cb 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -1,3 +1,8 @@ +""" +This module contains schemas used to convert database objects into +dictionaries. +""" + from marshmallow.decorators import pre_load from marshmallow.decorators import pre_dump, post_dump import app.database.models as models diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index b62f82c9..78c88241 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -1,3 +1,10 @@ +""" +Contains all functionality related sockets. That is starting and ending a presentation, +joining and leaving a presentation and syncing slides and timer bewteen all clients +connected to the same presentation. +""" + +from typing import Dict import app.database.controller as dbc from app.core import db from app.database.models import Competition, Slide, Team, ViewType, Code @@ -20,12 +27,16 @@ presentations = {} @sio.on("connect") -def connect(): +def connect() -> None: logger.info(f"Client '{request.sid}' connected") @sio.on("disconnect") -def disconnect(): +def disconnect() -> None: + """ + Remove client from the presentation it was in. Delete presentation if no + clients are connected to it. + """ for competition_id, presentation in presentations.items(): if request.sid in presentation["clients"]: del presentation["clients"][request.sid] @@ -40,7 +51,11 @@ def disconnect(): @sio.on("start_presentation") -def start_presentation(data): +def start_presentation(data: Dict) -> None: + """ + Starts a presentation if that competition is currently not active. + """ + competition_id = data["competition_id"] if competition_id in presentations: @@ -62,7 +77,15 @@ def start_presentation(data): @sio.on("end_presentation") -def end_presentation(data): +def end_presentation(data: Dict) -> None: + """ + End a presentation by sending end_presentation to all connected clients. + + The only clients allowed to do this is the one that started the presentation. + + Log error message if no presentation exists with the send id or if this + client is not in that presentation. + """ competition_id = data["competition_id"] if competition_id not in presentations: @@ -91,8 +114,13 @@ def end_presentation(data): @sio.on("join_presentation") -def join_presentation(data): - team_view_id = 1 +def join_presentation(data: Dict) -> None: + """ + Join a currently active presentation. + + Log error message if given code doesn't exist, if not presentation associated + with that code exists or if client is already in the presentation. + """ code = data["code"] item_code = db.session.query(Code).filter(Code.code == code).first() @@ -126,7 +154,15 @@ def join_presentation(data): @sio.on("set_slide") -def set_slide(data): +def set_slide(data: Dict) -> None: + """ + Sync slides between all clients in the same presentation by sending + set_slide to them. + + Log error if the given competition_id is not active, if client is not in + that presentation or the client is not the one who started the presentation. + """ + competition_id = data["competition_id"] slide_order = data["slide_order"] @@ -165,7 +201,14 @@ def set_slide(data): @sio.on("set_timer") -def set_timer(data): +def set_timer(data: Dict) -> None: + """ + Sync slides between all clients in the same presentation by sending + set_timer to them. + + Log error if the given competition_id is not active, if client is not in + that presentation or the client is not the one who started the presentation. + """ competition_id = data["competition_id"] timer = data["timer"] diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index e0c78ad3..9b002830 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -1,3 +1,8 @@ +""" +The database submodule contaisn all functionality that has to do with the +database. It can add, get, delete, edit, search and copy items. +""" + import json from flask_restx import abort @@ -16,7 +21,15 @@ class Base(Model): class ExtendedQuery(BaseQuery): + """ + Extensions to a regular query which makes using the database more convenient. + """ + def first_extended(self, required=True, error_message=None, error_code=404): + """ + Extensions of the first() functions otherwise used on queries. Abort + if no item was found and it was required. + """ item = self.first() if required and not item: @@ -27,6 +40,10 @@ class ExtendedQuery(BaseQuery): return item def pagination(self, page=0, page_size=15, order_column=None, order=1): + """ + When looking for lists of items this is used to only return a few of + them to allow for pagination. + """ query = self if order_column: if order == 1: @@ -40,17 +57,17 @@ class ExtendedQuery(BaseQuery): return items, total -class Dictionary(TypeDecorator): +# class Dictionary(TypeDecorator): - impl = Text +# impl = Text - def process_bind_param(self, value, dialect): - if value is not None: - value = json.dumps(value) +# def process_bind_param(self, value, dialect): +# if value is not None: +# value = json.dumps(value) - return value +# return value - def process_result_value(self, value, dialect): - if value is not None: - value = json.loads(value) - return value +# def process_result_value(self, value, dialect): +# if value is not None: +# value = json.loads(value) +# return value diff --git a/server/app/database/controller/__init__.py b/server/app/database/controller/__init__.py index a46a65f1..b8d6fca3 100644 --- a/server/app/database/controller/__init__.py +++ b/server/app/database/controller/__init__.py @@ -1,3 +1,7 @@ -# import add, get +""" +The controller subpackage provides a simple interface to the database. It +exposes methods to simply add, copy, delete, edit, get and search for items. +""" + from app.core import db from app.database.controller import add, copy, delete, edit, get, search, utils diff --git a/server/app/database/models.py b/server/app/database/models.py index f9f18438..d4e177b0 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,3 +1,9 @@ +""" +This file contains every model in the database. In regular SQL terms, it +defines every table, the fields in those tables and their relationship to +each other. +""" + from app.core import bcrypt, db from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property diff --git a/server/app/database/types.py b/server/app/database/types.py index f53835ec..46db168c 100644 --- a/server/app/database/types.py +++ b/server/app/database/types.py @@ -1,3 +1,7 @@ +""" +This module defines the different component types. +""" + ID_TEXT_COMPONENT = 1 ID_IMAGE_COMPONENT = 2 ID_QUESTION_COMPONENT = 3 \ No newline at end of file diff --git a/server/docs/Makefile b/server/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/server/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/server/docs/make.bat b/server/docs/make.bat new file mode 100644 index 00000000..9534b018 --- /dev/null +++ b/server/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/server/docs/source/conf.py b/server/docs/source/conf.py new file mode 100644 index 00000000..e42217c3 --- /dev/null +++ b/server/docs/source/conf.py @@ -0,0 +1,66 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys + +basepath = os.path.dirname(__file__) +filepath = os.path.abspath(os.path.join(basepath, "../..")) +sys.path.insert(0, filepath) + + +# -- Project information ----------------------------------------------------- + +project = "Teknikattan scoring system" +copyright = "2021, Albin Henriksson, Sebastian Karlsson, Victor Löfgren, Björn Modée, Josef Olsson, Max Rüdinger, Carl Schönfelder, Emil Wahlqvist" +author = "Albin Henriksson, Sebastian Karlsson, Victor Löfgren, Björn Modée, Josef Olsson, Max Rüdinger, Carl Schönfelder, Emil Wahlqvist" +version = "1.0" + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc", "myst_parser"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +autodoc_member_order = "bysource" + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ["_static"] + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = False + + +logo_path = os.path.abspath(os.path.join(basepath, "../../app/static/images/t8.jpg")) +html_logo = logo_path + +# favicon_path = os.path.abspath(os.path.join(basepath, "../../../client/public/favicon.ico")) +# html_favicon = favicon_path diff --git a/server/docs/source/development.md b/server/docs/source/development.md new file mode 100644 index 00000000..a6b07cc6 --- /dev/null +++ b/server/docs/source/development.md @@ -0,0 +1,59 @@ +# Development + +In this section we give all the instructions necessary to continue the development of this project. +We also give some recommentations for how to go about it and some ides for how to improve it. + +## Working with Python + +In this section we briefly describe how to work with Python. + +### Virtual environments + +Python virtual environments are used to isolate packages for each project from each other. +In the [Installation](installation.md) you installed `virtualenv` and created and activated a virtual environment. + +### Pip + +Python uses `pip` to manage it's packages. +Here we briefly describe to use it. +All of the following instructions assume you have created and activated a virtual environment. + +To install a package, run `pip install <package>`. + +To uninstall a package, run `pip uninstall <package>`. + +To save a package as a dependency to the project, run `pip freeze > requirements.txt`. + +To install all project dependencies, run `pip install -r requirements.txt`. + +## Visual Studio Code + +The development of this project was mainly done using Visual Studio Code (VSCode). +It is not that surprising, then, that we recommend you use it. + +### Tasks + +A task in VSCode is a simple action that can be run by pressing `ctrl+shift+p` and selecting `Tasks: Run Task`. +A few such tasks has been setup in this project and tasks related to the server will be described below. + +The `Start server` task will start the server. + +The `Populate database` task will populate the database with a few competitions, teams, users and such. + +The `Test server` task will run the server tests located in the `tests/` folder. + +The `Open server coverage` can only be run after running the server tests and will open the coverage report generated by those tests in a webbrowser. + +The `Generate server documentation` will generate the server documentation, i.e. this document, in the `docs/build/html/` folder. + +The `Open server documentation` can only be run after generating the documentation and will open it in a webbrowser. + +## Further development + +Because the project was time limited a lot is left to be done. +A few ideas for things to be improved are given here. + +### Replacing reqparse + +As mention in the [Parsing request](overview.md#Parsing-request), the reqparse module from RestX is deprecated and should be replaced with for example marsmallow. +Parsing is a rather small and simple matter which makes it quite fine not to use the most optimal tool, but it should nevertheless be replaced. diff --git a/server/docs/source/index.rst b/server/docs/source/index.rst new file mode 100644 index 00000000..df8af7fb --- /dev/null +++ b/server/docs/source/index.rst @@ -0,0 +1,33 @@ +Welcome to Teknikattan scoring system's documentation! +====================================================== + +This is the documentation for the backend of teknikattans scorings system. +Below you will find an overview of how the backend works. +Then you will find instructions for how to install it and get it up and running. +You will then find instructions for how to continue the development of this project. +Last you will find documentation on all of the modules. + +Documentation +============= + +.. toctree:: + :maxdepth: 2 + + overview + installation + development + +Modules +======= + +.. toctree:: + :maxdepth: 1 + + modules/app + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/server/docs/source/installation.md b/server/docs/source/installation.md new file mode 100644 index 00000000..700ec0c4 --- /dev/null +++ b/server/docs/source/installation.md @@ -0,0 +1,45 @@ +# Installation + +It is recommended to use [Visual Studio Code](https://code.visualstudio.com/) to install and use the server, but it is not necessary. +In order to install the server, you will need to do the following: + +Install [Python](https://www.python.org/downloads/). + +Clone [teknikattan-scoring-system](https://gitlab.liu.se/tddd96-grupp11/teknikattan-scoring-system). + +Open a terminal and navigate to the root of the cloned project. + +Install virtualenv and create a virtual environment: + +``` +pip install virtualenv +cd server +py -m venv env +``` + +Activate the virtual environment (which is done slightly differently on Windows and Linux/Mac): + +On Windows: + +``` +Set-ExecutionPolicy Unrestricted -Scope Process +./env/Scripts/activate +``` + +On Linux/Mac: + +``` +source env/bin/activate +``` + +Install all project depencies: + +``` +pip install -r requirements.txt +``` + +You should now be ready to start the server. +Try it by running `python main.py` and navigate to `localhost:5000`. +If everything worked as it should you should see a list of all available API calls. + +Continue to [Development](development.md). diff --git a/server/docs/source/modules/app.apis.alternatives.rst b/server/docs/source/modules/app.apis.alternatives.rst new file mode 100644 index 00000000..dc712f7c --- /dev/null +++ b/server/docs/source/modules/app.apis.alternatives.rst @@ -0,0 +1,7 @@ +app.apis.alternatives module +============================ + +.. automodule:: app.apis.alternatives + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.answers.rst b/server/docs/source/modules/app.apis.answers.rst new file mode 100644 index 00000000..f59bdc1f --- /dev/null +++ b/server/docs/source/modules/app.apis.answers.rst @@ -0,0 +1,7 @@ +app.apis.answers module +======================= + +.. automodule:: app.apis.answers + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.auth.rst b/server/docs/source/modules/app.apis.auth.rst new file mode 100644 index 00000000..7da9d2ea --- /dev/null +++ b/server/docs/source/modules/app.apis.auth.rst @@ -0,0 +1,7 @@ +app.apis.auth module +==================== + +.. automodule:: app.apis.auth + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.codes.rst b/server/docs/source/modules/app.apis.codes.rst new file mode 100644 index 00000000..3ee856a5 --- /dev/null +++ b/server/docs/source/modules/app.apis.codes.rst @@ -0,0 +1,7 @@ +app.apis.codes module +===================== + +.. automodule:: app.apis.codes + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.competitions.rst b/server/docs/source/modules/app.apis.competitions.rst new file mode 100644 index 00000000..305d952e --- /dev/null +++ b/server/docs/source/modules/app.apis.competitions.rst @@ -0,0 +1,7 @@ +app.apis.competitions module +============================ + +.. automodule:: app.apis.competitions + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.components.rst b/server/docs/source/modules/app.apis.components.rst new file mode 100644 index 00000000..1db7934e --- /dev/null +++ b/server/docs/source/modules/app.apis.components.rst @@ -0,0 +1,7 @@ +app.apis.components module +========================== + +.. automodule:: app.apis.components + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.media.rst b/server/docs/source/modules/app.apis.media.rst new file mode 100644 index 00000000..f82097d1 --- /dev/null +++ b/server/docs/source/modules/app.apis.media.rst @@ -0,0 +1,7 @@ +app.apis.media module +===================== + +.. automodule:: app.apis.media + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.misc.rst b/server/docs/source/modules/app.apis.misc.rst new file mode 100644 index 00000000..43dc154a --- /dev/null +++ b/server/docs/source/modules/app.apis.misc.rst @@ -0,0 +1,7 @@ +app.apis.misc module +==================== + +.. automodule:: app.apis.misc + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.questions.rst b/server/docs/source/modules/app.apis.questions.rst new file mode 100644 index 00000000..ffab7b63 --- /dev/null +++ b/server/docs/source/modules/app.apis.questions.rst @@ -0,0 +1,7 @@ +app.apis.questions module +========================= + +.. automodule:: app.apis.questions + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.rst b/server/docs/source/modules/app.apis.rst new file mode 100644 index 00000000..1f372a16 --- /dev/null +++ b/server/docs/source/modules/app.apis.rst @@ -0,0 +1,26 @@ +api +=== + +.. automodule:: app.apis + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + app.apis.alternatives + app.apis.answers + app.apis.auth + app.apis.codes + app.apis.competitions + app.apis.components + app.apis.media + app.apis.misc + app.apis.questions + app.apis.slides + app.apis.teams + app.apis.users diff --git a/server/docs/source/modules/app.apis.slides.rst b/server/docs/source/modules/app.apis.slides.rst new file mode 100644 index 00000000..7525025c --- /dev/null +++ b/server/docs/source/modules/app.apis.slides.rst @@ -0,0 +1,7 @@ +app.apis.slides module +====================== + +.. automodule:: app.apis.slides + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.teams.rst b/server/docs/source/modules/app.apis.teams.rst new file mode 100644 index 00000000..0abf49f3 --- /dev/null +++ b/server/docs/source/modules/app.apis.teams.rst @@ -0,0 +1,7 @@ +app.apis.teams module +===================== + +.. automodule:: app.apis.teams + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.apis.users.rst b/server/docs/source/modules/app.apis.users.rst new file mode 100644 index 00000000..a40fc2f4 --- /dev/null +++ b/server/docs/source/modules/app.apis.users.rst @@ -0,0 +1,7 @@ +app.apis.users module +===================== + +.. automodule:: app.apis.users + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.core.codes.rst b/server/docs/source/modules/app.core.codes.rst new file mode 100644 index 00000000..0e6f5a6c --- /dev/null +++ b/server/docs/source/modules/app.core.codes.rst @@ -0,0 +1,7 @@ +codes +===== + +.. automodule:: app.core.codes + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.core.dto.rst b/server/docs/source/modules/app.core.dto.rst new file mode 100644 index 00000000..bc77b7be --- /dev/null +++ b/server/docs/source/modules/app.core.dto.rst @@ -0,0 +1,5 @@ +dto +=== + +.. automodule:: app.core.dto + :members: diff --git a/server/docs/source/modules/app.core.files.rst b/server/docs/source/modules/app.core.files.rst new file mode 100644 index 00000000..38808edc --- /dev/null +++ b/server/docs/source/modules/app.core.files.rst @@ -0,0 +1,7 @@ +files +===== + +.. automodule:: app.core.files + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.core.http_codes.rst b/server/docs/source/modules/app.core.http_codes.rst new file mode 100644 index 00000000..ee3c1d25 --- /dev/null +++ b/server/docs/source/modules/app.core.http_codes.rst @@ -0,0 +1,5 @@ +http_codes +========== + +.. automodule:: app.core.http_codes + diff --git a/server/docs/source/modules/app.core.parsers.rst b/server/docs/source/modules/app.core.parsers.rst new file mode 100644 index 00000000..9875cb6b --- /dev/null +++ b/server/docs/source/modules/app.core.parsers.rst @@ -0,0 +1,7 @@ +parsers +======= + +.. automodule:: app.core.parsers + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.core.rich_schemas.rst b/server/docs/source/modules/app.core.rich_schemas.rst new file mode 100644 index 00000000..38c7283b --- /dev/null +++ b/server/docs/source/modules/app.core.rich_schemas.rst @@ -0,0 +1,4 @@ +rich schemas +============ + +.. automodule:: app.core.rich_schemas diff --git a/server/docs/source/modules/app.core.rst b/server/docs/source/modules/app.core.rst new file mode 100644 index 00000000..4452b463 --- /dev/null +++ b/server/docs/source/modules/app.core.rst @@ -0,0 +1,22 @@ +core +==== + +.. automodule:: app.core + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + app.core.codes + app.core.dto + app.core.files + app.core.http_codes + app.core.parsers + app.core.rich_schemas + app.core.schemas + app.core.sockets diff --git a/server/docs/source/modules/app.core.schemas.rst b/server/docs/source/modules/app.core.schemas.rst new file mode 100644 index 00000000..b2b864c6 --- /dev/null +++ b/server/docs/source/modules/app.core.schemas.rst @@ -0,0 +1,4 @@ +schemas +======= + +.. automodule:: app.core.schemas diff --git a/server/docs/source/modules/app.core.sockets.rst b/server/docs/source/modules/app.core.sockets.rst new file mode 100644 index 00000000..c331255b --- /dev/null +++ b/server/docs/source/modules/app.core.sockets.rst @@ -0,0 +1,7 @@ +sockets +======= + +.. automodule:: app.core.sockets + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.add.rst b/server/docs/source/modules/app.database.controller.add.rst new file mode 100644 index 00000000..72163b22 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.add.rst @@ -0,0 +1,7 @@ +app.database.controller.add module +================================== + +.. automodule:: app.database.controller.add + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.copy.rst b/server/docs/source/modules/app.database.controller.copy.rst new file mode 100644 index 00000000..6a8cbde8 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.copy.rst @@ -0,0 +1,7 @@ +app.database.controller.copy module +=================================== + +.. automodule:: app.database.controller.copy + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.delete.rst b/server/docs/source/modules/app.database.controller.delete.rst new file mode 100644 index 00000000..cd3c7572 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.delete.rst @@ -0,0 +1,7 @@ +app.database.controller.delete module +===================================== + +.. automodule:: app.database.controller.delete + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.edit.rst b/server/docs/source/modules/app.database.controller.edit.rst new file mode 100644 index 00000000..b81d5a8a --- /dev/null +++ b/server/docs/source/modules/app.database.controller.edit.rst @@ -0,0 +1,7 @@ +app.database.controller.edit module +=================================== + +.. automodule:: app.database.controller.edit + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.get.rst b/server/docs/source/modules/app.database.controller.get.rst new file mode 100644 index 00000000..d02168e9 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.get.rst @@ -0,0 +1,7 @@ +app.database.controller.get module +================================== + +.. automodule:: app.database.controller.get + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.rst b/server/docs/source/modules/app.database.controller.rst new file mode 100644 index 00000000..c1dcf937 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.rst @@ -0,0 +1,21 @@ +app.database.controller package +=============================== + +.. automodule:: app.database.controller + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + app.database.controller.add + app.database.controller.copy + app.database.controller.delete + app.database.controller.edit + app.database.controller.get + app.database.controller.search + app.database.controller.utils diff --git a/server/docs/source/modules/app.database.controller.search.rst b/server/docs/source/modules/app.database.controller.search.rst new file mode 100644 index 00000000..6c1052c3 --- /dev/null +++ b/server/docs/source/modules/app.database.controller.search.rst @@ -0,0 +1,7 @@ +app.database.controller.search module +===================================== + +.. automodule:: app.database.controller.search + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.controller.utils.rst b/server/docs/source/modules/app.database.controller.utils.rst new file mode 100644 index 00000000..997cfeac --- /dev/null +++ b/server/docs/source/modules/app.database.controller.utils.rst @@ -0,0 +1,7 @@ +app.database.controller.utils module +==================================== + +.. automodule:: app.database.controller.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.models.rst b/server/docs/source/modules/app.database.models.rst new file mode 100644 index 00000000..633beb8f --- /dev/null +++ b/server/docs/source/modules/app.database.models.rst @@ -0,0 +1,7 @@ +app.database.models module +========================== + +.. automodule:: app.database.models + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.database.rst b/server/docs/source/modules/app.database.rst new file mode 100644 index 00000000..c6f25650 --- /dev/null +++ b/server/docs/source/modules/app.database.rst @@ -0,0 +1,24 @@ +app.database package +==================== + +.. automodule:: app.database + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + app.database.controller + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + app.database.models + app.database.types diff --git a/server/docs/source/modules/app.database.types.rst b/server/docs/source/modules/app.database.types.rst new file mode 100644 index 00000000..e33a5857 --- /dev/null +++ b/server/docs/source/modules/app.database.types.rst @@ -0,0 +1,7 @@ +app.database.types module +========================= + +.. automodule:: app.database.types + :members: + :undoc-members: + :show-inheritance: diff --git a/server/docs/source/modules/app.rst b/server/docs/source/modules/app.rst new file mode 100644 index 00000000..e471b989 --- /dev/null +++ b/server/docs/source/modules/app.rst @@ -0,0 +1,17 @@ +app +=== + +.. automodule:: app + :members: create_app + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 1 + + app.apis + app.core + app.database diff --git a/server/docs/source/overview.md b/server/docs/source/overview.md new file mode 100644 index 00000000..67645bf6 --- /dev/null +++ b/server/docs/source/overview.md @@ -0,0 +1,81 @@ +# Server + +The backend is mainly responsible for storing every user and all competitions. +It also needs to make sure the every API call and socket event is authorized. +The server is written in Python together with the micro-framework Flask. + +## Overview + +The server has two main responsibilites. +The first is to handle API calls from the client to store, update and delete information, such as competitions or users. +It also needs to make sure that only authorized people can access these. +The other is to sync slides, timer and answers between clients in an active competition. +Both of these will be described in more detail below. + +## Receiving API calls + +An API call is a way the client can communicates with the server. +When a request is received the server begins by authorizing it (making sure the person sending the request is allowed to access the route). +After that it makes sure that it got all information in the request it needed. +The server will then do the thing the client requested. +And finally it will need to generate repsonse, usually in the form of an object from the database. +All of these steps are described in more detail below. + +### Routes + +Each route which is possible to call is specified in the files in the `app/apis/` folder. +All available routes can also be seen by navigating to `localhost:5000` after starting the server. + +### Authorization + +When the server receives an API call the first thing it does is to authorize it. +The authorization is done using JSON Web Tokens (JWT) by comparing the contents of them with what is expected. +Whenever a client logs into an account or joins a competition, it is given a JWT generated by the server, and the client will need to use this token in every subsequent request sent to the server to authenticate itself. + +What authorization to be done on the server is specified by the `@protect_route()` decorator. +This decorator specifies who is allowed to access this route, which can either be users with specific roles, or people who have joined competitions with specific views. +If the route is not decorated everyone is allowed to access it, the only routes currently like that is logging in as a user and joining a competition, by necessity. + +### Parsing request + +After the request is authorized the server will need to parse contents of the request. +The parsing is done with [reqparse](https://flask-restx.readthedocs.io/en/latest/parsing.html) from RestX (this module is deprecated and should be replaced). +Each API call expects different parameters in different places and this is specificied in each of the files in `app/apis/` folder, together with the route. + +### Handling request + +After the request has been authorized and parsed the server needs to act on the request. +What the server does of course depends on the route and given arguments, but it usually gets, edits or deletes something from the database. +The server uses an SQL database and interfaces to it via SQLAlchemy. +Everything related to the database is located in the `app/database/` folder. + +### Responding + +When the server is done handling the request it usually responds with an item from the database. +Converting a database object to json is done with [Marsmallow](https://marshmallow.readthedocs.io/en/stable/). +How to do this conversion is specified in two files in in the folder `app/core/`. +The file `schemas.py` just converts a record in the database field by field. +The file `rich_schemas.py` on the other hand converts an `id` in one table to an entire object in the another table, thus the name rich. +In this way, for example, an entire competition with it's teams, codes, slides and the slides' questions and components can be returned in a single API call. + +## Active competitions + +Slides, timers and answers needs to be synced during an active presentation. +This is done using SocketIO together with flask_socketio. +Events sent is also authorized via json web tokens. +Whenever client joins a competition they will connect via sockets. +Only a single instance of a competition can be active at a time. +All of the functionality related to an active competition and sockets can be found in the file `app/core/sockets.py`. + +### Starting and joing presentations + +Whenever a client types in a code in the client, the code will be checked via the `api/auth/login/code` API call. +If there is such a code and it was an operator code, the client will receive a JWT it will need to use to authenticate itself for there on out. +It will also emit the `start_presentation` event to start the presentation. +If there is such a code and the associated competition is active, the client will also receive a JWT, regardless if it was an operator code or not. +In this case the client will instead emit the `join_presentation` event. + +### Syncing between clients + +The operator will emit `set_slide` and `set_timer` events that syncs their slides and timers between all clients connected to the same presentation. +The operator can also emit `end_presentation` to end the current presentation, which will disconnect all connected clients. diff --git a/server/requirements.txt b/server/requirements.txt index bda47a88036c81470bc7a6b2112b2ecedd904ed7..fe7b13ed57bb2ed619f1b2237f00498c71c67ce0 100644 GIT binary patch delta 796 zcmb7CJ4?e*6h5h~FK{XjrL$lWrRh@#1ziNOAUcUt`XH?~wf3gImd<WUW=EY|r1%pY z`#T&Q{0Hjy-LxStEfQ`5`5xyx=iK}Fb7J}D+mfbOQpQo@N=-Vli6<><Qp7POBMUMw z50RJnj^@?CCnE!N<MnCRNLJ-sD$<3z0ZavYQwseLV<nPwAhcnvK)A)1auae`cyrOu z6!DIn35~9#C9B}Mc(>&yFe{MneMR3=)euTf6QynZYlTWPT)}&c<(C|IurV@OMg3{f zlq{G<ZxDM=*x~?df$O(Gk98cge>Yt+77t)_EZb6&eH|(fDCZ4koSuS+!8yaGo&e|Q zXghzJv|__)J<B6Rsq4Vk+KCagn?_X#t-zOoI$)R4f(48X*P!Pf+SK=k)|@9-HO`<4 z*=1P#4{aSm925<e*6JSbN_JxDQv~CpT^Nim0F~xM<%QM+{RWYTHTT90%-?oHFNSxI Zx5P#Ozmri6t|rGA-pN#=e>l5k`~t|kku(4R delta 49 zcmV-10M7rW8IThK|NfCEtg-Mu0kcj5CIXX~1mcsZ1+bHd1}>A}2Eenz2SfprBnf1* HPzzQ85`+__ -- GitLab