Skip to content
Snippets Groups Projects
Commit b4947d0a authored by Victor Löfgren's avatar Victor Löfgren
Browse files

Resolve "Add automatic documentation generation"

parent feaca508
No related branches found
No related tags found
1 merge request!113Resolve "Add automatic documentation generation"
Pipeline #42993 passed
Showing
with 252 additions and 49 deletions
__pycache__
*.db
*/env
*.coverage *.coverage
*/coverage */coverage
htmlcov
.pytest_cache
/.idea /.idea
.vs/ .vs/
/server/app/static/
...@@ -41,5 +41,6 @@ ...@@ -41,5 +41,6 @@
"search.exclude": { "search.exclude": {
"**/env": true "**/env": true
}, },
"python.pythonPath": "server\\env\\Scripts\\python.exe" "python.pythonPath": "server\\env\\Scripts\\python.exe",
"restructuredtext.confPath": "${workspaceFolder}\\server\\sphinx\\source"
} }
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
{ {
"label": "Open client coverage", "label": "Open client coverage",
"type": "shell", "type": "shell",
"group": "build",
"command": "start ./output/coverage/jest/index.html", "command": "start ./output/coverage/jest/index.html",
"problemMatcher": [], "problemMatcher": [],
"options": { "options": {
...@@ -54,7 +53,7 @@ ...@@ -54,7 +53,7 @@
}, },
}, },
{ {
"label": "Populate server", "label": "Populate database",
"type": "shell", "type": "shell",
"group": "build", "group": "build",
"command": "env/Scripts/python populate.py", "command": "env/Scripts/python populate.py",
...@@ -66,13 +65,30 @@ ...@@ -66,13 +65,30 @@
{ {
"label": "Open server coverage", "label": "Open server coverage",
"type": "shell", "type": "shell",
"group": "build",
"command": "start ./htmlcov/index.html", "command": "start ./htmlcov/index.html",
"problemMatcher": [], "problemMatcher": [],
"options": { "options": {
"cwd": "${workspaceFolder}/server" "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", "label": "Start client and server",
"group": "build", "group": "build",
......
__pycache__
env
htmlcov
.pytest_cache
app/*.db
app/static/
# Documentation files
docs/build
...@@ -7,6 +7,11 @@ from app.core.dto import MediaDTO ...@@ -7,6 +7,11 @@ from app.core.dto import MediaDTO
def create_app(config_name="configmodule.DevelopmentConfig"): 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 = Flask(__name__, static_url_path="/static", static_folder="static")
app.config.from_object(config_name) app.config.from_object(config_name)
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
......
"""
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 app.database import Base, ExtendedQuery
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from flask_jwt_extended.jwt_manager import JWTManager from flask_jwt_extended.jwt_manager import JWTManager
......
"""
Contains all functions purely related to creating and verifying a code.
"""
import random import random
import re import re
import string import string
...@@ -7,9 +11,14 @@ ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ...@@ -7,9 +11,14 @@ ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
CODE_RE = re.compile(f"^[{ALLOWED_CHARS}]{{{CODE_LENGTH}}}$") 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)) return "".join(random.choices(ALLOWED_CHARS, k=CODE_LENGTH))
def verify_code(c): def verify_code(code: str) -> bool:
return CODE_RE.search(c.upper()) is not None """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
"""
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.rich_schemas as rich_schemas
import app.core.schemas as schemas import app.core.schemas as schemas
from flask_restx import Namespace from flask_restx import Namespace
......
"""
Contains functions related to file handling, mainly saving and deleting images.
"""
from PIL import Image, ImageChops from PIL import Image, ImageChops
from flask import current_app from flask import current_app, has_app_context
import os import os
import datetime import datetime
from flask_uploads import IMAGES, UploadSet from flask_uploads import IMAGES, UploadSet
PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] if has_app_context():
THUMBNAIL_SIZE = current_app.config["THUMBNAIL_SIZE"] PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"]
image_set = UploadSet("photos", IMAGES) THUMBNAIL_SIZE = current_app.config["THUMBNAIL_SIZE"]
image_set = UploadSet("photos", IMAGES)
def compare_images(input_image, output_image): # def compare_images(input_image, output_image):
# compare image dimensions (assumption 1) # # compare image dimensions (assumption 1)
if input_image.size != output_image.size: # if input_image.size != output_image.size:
return False # return False
rows, cols = input_image.size # rows, cols = input_image.size
# compare image pixels (assumption 2 and 3) # # compare image pixels (assumption 2 and 3)
for row in range(rows): # for row in range(rows):
for col in range(cols): # for col in range(cols):
input_pixel = input_image.getpixel((row, col)) # input_pixel = input_image.getpixel((row, col))
output_pixel = output_image.getpixel((row, col)) # output_pixel = output_image.getpixel((row, col))
if input_pixel != output_pixel: # if input_pixel != output_pixel:
return False # return False
return True # return True
def _delete_image(filename): def _delete_image(filename):
...@@ -33,6 +38,10 @@ def _delete_image(filename): ...@@ -33,6 +38,10 @@ def _delete_image(filename):
def save_image_with_thumbnail(image_file): 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_filename = image_set.save(image_file)
saved_path = os.path.join(PHOTO_PATH, saved_filename) saved_path = os.path.join(PHOTO_PATH, saved_filename)
with Image.open(saved_path) as im: with Image.open(saved_path) as im:
...@@ -45,6 +54,9 @@ def save_image_with_thumbnail(image_file): ...@@ -45,6 +54,9 @@ def save_image_with_thumbnail(image_file):
def delete_image_and_thumbnail(filename): def delete_image_and_thumbnail(filename):
"""
Delete the given image together with its thumbnail.
"""
_delete_image(filename) _delete_image(filename)
_delete_image(f"thumbnail_{filename}") _delete_image(f"thumbnail_{filename}")
......
"""
This module defines all the http status codes thats used in the api.
"""
OK = 200 OK = 200
NO_CONTENT = 204 NO_CONTENT = 204
BAD_REQUEST = 400 BAD_REQUEST = 400
......
"""
This module contains the parsers used to parse the data gotten in api requests.
"""
from flask_restx import inputs, reqparse from flask_restx import inputs, reqparse
......
"""
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.core.schemas as schemas
import app.database.models as models import app.database.models as models
from app.core import ma from app.core import ma
......
"""
This module contains schemas used to convert database objects into
dictionaries.
"""
from marshmallow.decorators import pre_load from marshmallow.decorators import pre_load
from marshmallow.decorators import pre_dump, post_dump from marshmallow.decorators import pre_dump, post_dump
import app.database.models as models import app.database.models as models
......
"""
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 import app.database.controller as dbc
from app.core import db from app.core import db
from app.database.models import Competition, Slide, Team, ViewType, Code from app.database.models import Competition, Slide, Team, ViewType, Code
...@@ -20,12 +27,16 @@ presentations = {} ...@@ -20,12 +27,16 @@ presentations = {}
@sio.on("connect") @sio.on("connect")
def connect(): def connect() -> None:
logger.info(f"Client '{request.sid}' connected") logger.info(f"Client '{request.sid}' connected")
@sio.on("disconnect") @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(): for competition_id, presentation in presentations.items():
if request.sid in presentation["clients"]: if request.sid in presentation["clients"]:
del presentation["clients"][request.sid] del presentation["clients"][request.sid]
...@@ -40,7 +51,11 @@ def disconnect(): ...@@ -40,7 +51,11 @@ def disconnect():
@sio.on("start_presentation") @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"] competition_id = data["competition_id"]
if competition_id in presentations: if competition_id in presentations:
...@@ -62,7 +77,15 @@ def start_presentation(data): ...@@ -62,7 +77,15 @@ def start_presentation(data):
@sio.on("end_presentation") @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"] competition_id = data["competition_id"]
if competition_id not in presentations: if competition_id not in presentations:
...@@ -91,8 +114,13 @@ def end_presentation(data): ...@@ -91,8 +114,13 @@ def end_presentation(data):
@sio.on("join_presentation") @sio.on("join_presentation")
def join_presentation(data): def join_presentation(data: Dict) -> None:
team_view_id = 1 """
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"] code = data["code"]
item_code = db.session.query(Code).filter(Code.code == code).first() item_code = db.session.query(Code).filter(Code.code == code).first()
...@@ -126,7 +154,15 @@ def join_presentation(data): ...@@ -126,7 +154,15 @@ def join_presentation(data):
@sio.on("set_slide") @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"] competition_id = data["competition_id"]
slide_order = data["slide_order"] slide_order = data["slide_order"]
...@@ -165,7 +201,14 @@ def set_slide(data): ...@@ -165,7 +201,14 @@ def set_slide(data):
@sio.on("set_timer") @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"] competition_id = data["competition_id"]
timer = data["timer"] timer = data["timer"]
......
"""
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 import json
from flask_restx import abort from flask_restx import abort
...@@ -16,7 +21,15 @@ class Base(Model): ...@@ -16,7 +21,15 @@ class Base(Model):
class ExtendedQuery(BaseQuery): 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): 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() item = self.first()
if required and not item: if required and not item:
...@@ -27,6 +40,10 @@ class ExtendedQuery(BaseQuery): ...@@ -27,6 +40,10 @@ class ExtendedQuery(BaseQuery):
return item return item
def pagination(self, page=0, page_size=15, order_column=None, order=1): 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 query = self
if order_column: if order_column:
if order == 1: if order == 1:
...@@ -40,17 +57,17 @@ class ExtendedQuery(BaseQuery): ...@@ -40,17 +57,17 @@ class ExtendedQuery(BaseQuery):
return items, total return items, total
class Dictionary(TypeDecorator): # class Dictionary(TypeDecorator):
impl = Text # impl = Text
def process_bind_param(self, value, dialect): # def process_bind_param(self, value, dialect):
if value is not None: # if value is not None:
value = json.dumps(value) # value = json.dumps(value)
return value # return value
def process_result_value(self, value, dialect): # def process_result_value(self, value, dialect):
if value is not None: # if value is not None:
value = json.loads(value) # value = json.loads(value)
return value # return value
# 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.core import db
from app.database.controller import add, copy, delete, edit, get, search, utils from app.database.controller import add, copy, delete, edit, get, search, utils
"""
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 app.core import bcrypt, db
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
......
"""
This module defines the different component types.
"""
ID_TEXT_COMPONENT = 1 ID_TEXT_COMPONENT = 1
ID_IMAGE_COMPONENT = 2 ID_IMAGE_COMPONENT = 2
ID_QUESTION_COMPONENT = 3 ID_QUESTION_COMPONENT = 3
\ No newline at end of file
# 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)
@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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment