From 9ad0f4256b89413c2ed57c3b8ef6b525d3435332 Mon Sep 17 00:00:00 2001 From: Dari Date: Tue, 31 May 2022 00:54:49 +0300 Subject: [PATCH 01/42] alpha architecture --- .gitignore | 3 +++ requirements.txt | 0 web-app/adapters/__init__.py | 0 web-app/config.py | 0 web-app/django_entrypoints/__init__.py | 0 web-app/django_entrypoints/api/__init__.py | 0 web-app/rust_server/.gitkeep | 0 web-app/service/__init__.py | 0 web-app/static/.gitkeep | 0 web-app/static/scripts/.gitkeep | 0 web-app/static/templates/.gitkeep | 0 11 files changed, 3 insertions(+) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 web-app/adapters/__init__.py create mode 100644 web-app/config.py create mode 100644 web-app/django_entrypoints/__init__.py create mode 100644 web-app/django_entrypoints/api/__init__.py create mode 100644 web-app/rust_server/.gitkeep create mode 100644 web-app/service/__init__.py create mode 100644 web-app/static/.gitkeep create mode 100644 web-app/static/scripts/.gitkeep create mode 100644 web-app/static/templates/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a90f10a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/__pycache__ +**/.idea +venv/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/web-app/adapters/__init__.py b/web-app/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/config.py b/web-app/config.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/django_entrypoints/__init__.py b/web-app/django_entrypoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/django_entrypoints/api/__init__.py b/web-app/django_entrypoints/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/rust_server/.gitkeep b/web-app/rust_server/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web-app/service/__init__.py b/web-app/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/static/.gitkeep b/web-app/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web-app/static/scripts/.gitkeep b/web-app/static/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web-app/static/templates/.gitkeep b/web-app/static/templates/.gitkeep new file mode 100644 index 0000000..e69de29 From 884bd1862abe04a635d06df70b350418028db169 Mon Sep 17 00:00:00 2001 From: Dari Date: Tue, 31 May 2022 02:01:59 +0300 Subject: [PATCH 02/42] some django staff --- manage.py | 22 ++++++++ web-app/asgi.py | 16 ++++++ web-app/settings.py | 123 ++++++++++++++++++++++++++++++++++++++++++++ web-app/urls.py | 21 ++++++++ web-app/wsgi.py | 16 ++++++ 5 files changed, 198 insertions(+) create mode 100755 manage.py create mode 100644 web-app/asgi.py create mode 100644 web-app/settings.py create mode 100644 web-app/urls.py create mode 100644 web-app/wsgi.py diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..06aadd6 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/web-app/asgi.py b/web-app/asgi.py new file mode 100644 index 0000000..29021bb --- /dev/null +++ b/web-app/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for web-app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') + +application = get_asgi_application() diff --git a/web-app/settings.py b/web-app/settings.py new file mode 100644 index 0000000..33c796b --- /dev/null +++ b/web-app/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for web-app project. + +Generated by 'django-admin startproject' using Django 4.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-(cy0k*i03w^xt)#ecgso3si44hpk7 &hw7k35n==or)c#)#%' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'web-app.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'web-app.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/web-app/urls.py b/web-app/urls.py new file mode 100644 index 0000000..6321bb5 --- /dev/null +++ b/web-app/urls.py @@ -0,0 +1,21 @@ +"""web-app URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/web-app/wsgi.py b/web-app/wsgi.py new file mode 100644 index 0000000..9950ac3 --- /dev/null +++ b/web-app/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for web-app project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') + +application = get_wsgi_application() From da41d34a8043128fdadde884f0ee4791d8c48f2e Mon Sep 17 00:00:00 2001 From: Nertsal Date: Fri, 3 Jun 2022 14:02:05 +0300 Subject: [PATCH 03/42] Setup rust project --- rust-server/.gitignore | 2 ++ rust-server/Cargo.toml | 8 ++++++++ rust-server/README.md | 11 +++++++++++ rust-server/src/main.rs | 3 +++ 4 files changed, 24 insertions(+) create mode 100644 rust-server/.gitignore create mode 100644 rust-server/Cargo.toml create mode 100644 rust-server/README.md create mode 100644 rust-server/src/main.rs diff --git a/rust-server/.gitignore b/rust-server/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/rust-server/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml new file mode 100644 index 0000000..30bb810 --- /dev/null +++ b/rust-server/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust-server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/rust-server/README.md b/rust-server/README.md new file mode 100644 index 0000000..05a506c --- /dev/null +++ b/rust-server/README.md @@ -0,0 +1,11 @@ +# Building + +Install rust using rustup [here](https://www.rust-lang.org/tools/install) + +Build the project using + +```rust +cargo build --release +``` + +The executable file will be generated at `target/release/` diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/rust-server/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From d68d196e803dd137263ecb9f48ae6bcca6dcd0bf Mon Sep 17 00:00:00 2001 From: ADari-Ka Date: Fri, 3 Jun 2022 20:25:53 +0300 Subject: [PATCH 04/42] docker (for rust and db) --- docker-compose.yaml | 26 ++++++++++++++++++++++++++ rust-server/Dockerfile | 5 +++++ web-app/rust_server/.gitkeep | 0 3 files changed, 31 insertions(+) create mode 100644 docker-compose.yaml create mode 100644 rust-server/Dockerfile delete mode 100644 web-app/rust_server/.gitkeep diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..465a3c0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,26 @@ +version: "3.8" + +services: + rust_service: + build: rust-server/ + ports: + - 8080:8080 + depends_on: + - postgres_db + + postgres_db: + image: postgres + container_name: database + restart: always + environment: + POSTGRES_DB: test + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + volumes: + - postgres_db:/var/lib/postgresql/data + + +volumes: + postgres_db: \ No newline at end of file diff --git a/rust-server/Dockerfile b/rust-server/Dockerfile new file mode 100644 index 0000000..8f823b1 --- /dev/null +++ b/rust-server/Dockerfile @@ -0,0 +1,5 @@ +FROM rust:1.61 + +COPY ./ ./ +COPY Cargo.toml Cargo.toml +RUN cargo build --release \ No newline at end of file diff --git a/web-app/rust_server/.gitkeep b/web-app/rust_server/.gitkeep deleted file mode 100644 index e69de29..0000000 From bc4c02d263a6adb58577f0e7b1b74982b859dd98 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Mon, 6 Jun 2022 21:09:54 +0300 Subject: [PATCH 05/42] Create database connection trait --- rust-server/Cargo.toml | 2 ++ rust-server/src/database/mod.rs | 15 ++++++++ rust-server/src/database/psql.rs | 59 ++++++++++++++++++++++++++++++++ rust-server/src/game.rs | 23 +++++++++++++ rust-server/src/main.rs | 11 ++++-- 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 rust-server/src/database/mod.rs create mode 100644 rust-server/src/database/psql.rs create mode 100644 rust-server/src/game.rs diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index 30bb810..bc78742 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +postgres = "0.19.3" +rand = "0.8.5" diff --git a/rust-server/src/database/mod.rs b/rust-server/src/database/mod.rs new file mode 100644 index 0000000..89141a5 --- /dev/null +++ b/rust-server/src/database/mod.rs @@ -0,0 +1,15 @@ +pub mod psql; + +use crate::game::{self, GameCode, GameState}; + +/// Trait representing a handler for a games database +pub trait DBAccessor { + /// The type of errors that may occur + type Error: std::error::Error; + /// Add a new game to the database + fn create_game(&mut self) -> Result; + /// Get information about an existing game + fn get_game(&mut self, game_code: &GameCode) -> Result; + fn start_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error>; + fn finish_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error>; +} diff --git a/rust-server/src/database/psql.rs b/rust-server/src/database/psql.rs new file mode 100644 index 0000000..1f08175 --- /dev/null +++ b/rust-server/src/database/psql.rs @@ -0,0 +1,59 @@ +use super::*; + +pub use postgres::Error as PsqlError; +use postgres::{Client, NoTls}; + +pub type PsqlResult = Result; + +/// Handles access to the postgres games database +pub struct PsqlAccess { + client: Client, +} + +impl PsqlAccess { + /// Sets up access to the database and initializes all the neccessary tables + pub fn new() -> PsqlResult { + let client = Client::connect("host=localhost user=test password=test", NoTls)?; + let db = Self { client }.init()?; + Ok(db) + } + + fn init(mut self) -> PsqlResult { + self.client.batch_execute( + " + CREATE TABLE IF NOT EXISTS games ( + id SERIAL PRIMARY KEY, + code VARCHAR NOT NULL + state INT + ) + ", + )?; + Ok(self) + } +} + +impl DBAccessor for PsqlAccess { + type Error = PsqlError; + + fn create_game(&mut self) -> Result { + let code = game::game_code_generator(); + let state = GameState::Lobby; + self.client.execute( + "INSERT INTO games (code, state) VALUES ($1, $2)", + &[&code, &(state as i32)], + )?; + Ok(code) + } + + fn get_game(&mut self, game_code: &GameCode) -> Result { + todo!() + } + + fn start_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error> { + todo!() + } + + fn finish_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error> { + todo!() + } +} diff --git a/rust-server/src/game.rs b/rust-server/src/game.rs new file mode 100644 index 0000000..c3876d8 --- /dev/null +++ b/rust-server/src/game.rs @@ -0,0 +1,23 @@ +/// Represents different states of a game +#[derive(Debug, Clone, Copy)] +pub enum GameState { + /// Initial state of the game + Lobby, + InProgress, + Finished, +} + +pub type GameCode = String; + +pub fn game_code_generator() -> GameCode { + use rand::Rng; + const LEN: usize = 4; + + let mut rng = rand::thread_rng(); + let mut code = String::new(); + for _ in 0..LEN { + let symbol = rng.gen_range('A'..='Z'); + code.push(symbol); + } + code +} diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index e7a11a9..1331467 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -1,3 +1,10 @@ -fn main() { - println!("Hello, world!"); +mod database; +mod game; + +use database::{psql::*, DBAccessor}; + +fn main() -> PsqlResult<()> { + let db = PsqlAccess::new()?; + + Ok(()) } From e45427088913ab09ec36cb48b70834f39752ae8b Mon Sep 17 00:00:00 2001 From: Nertsal Date: Thu, 9 Jun 2022 14:07:14 +0300 Subject: [PATCH 06/42] [Rust] Implement basic database access trait Closes #10 --- rust-server/Cargo.toml | 1 + rust-server/src/database/mod.rs | 2 -- rust-server/src/database/psql.rs | 42 +++++++++++++++++++++++++++----- rust-server/src/game.rs | 3 ++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index bc78742..b68556a 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] postgres = "0.19.3" rand = "0.8.5" +num_enum = "0.5.7" diff --git a/rust-server/src/database/mod.rs b/rust-server/src/database/mod.rs index 89141a5..76c9bbb 100644 --- a/rust-server/src/database/mod.rs +++ b/rust-server/src/database/mod.rs @@ -10,6 +10,4 @@ pub trait DBAccessor { fn create_game(&mut self) -> Result; /// Get information about an existing game fn get_game(&mut self, game_code: &GameCode) -> Result; - fn start_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error>; - fn finish_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error>; } diff --git a/rust-server/src/database/psql.rs b/rust-server/src/database/psql.rs index 1f08175..201b528 100644 --- a/rust-server/src/database/psql.rs +++ b/rust-server/src/database/psql.rs @@ -18,6 +18,7 @@ impl PsqlAccess { Ok(db) } + /// Initialize all the neccessary database stuff fn init(mut self) -> PsqlResult { self.client.batch_execute( " @@ -32,8 +33,15 @@ impl PsqlAccess { } } +#[derive(Debug)] +pub enum Error { + Psql(PsqlError), + GameNotFound(GameCode), + DBCorrupt, +} + impl DBAccessor for PsqlAccess { - type Error = PsqlError; + type Error = Error; fn create_game(&mut self) -> Result { let code = game::game_code_generator(); @@ -46,14 +54,36 @@ impl DBAccessor for PsqlAccess { } fn get_game(&mut self, game_code: &GameCode) -> Result { - todo!() + let res = self + .client + .query("SELECT state FROM games WHERE code == $1", &[game_code])?; + match &res[..] { + [] => Err(Error::GameNotFound(game_code.to_owned())), + [row] => { + let state = row.get::(0); + u8::try_from(state) + .map_err(|_| Error::DBCorrupt) + .and_then(|state| GameState::try_from(state).map_err(|_| Error::DBCorrupt)) + } + _ => panic!("Expected at most one game to match the code"), // TODO: move that check to game creation + } } +} - fn start_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error> { - todo!() +impl From for Error { + fn from(error: PsqlError) -> Self { + Self::Psql(error) } +} - fn finish_game(&mut self, game_code: &GameCode) -> Result<(), Self::Error> { - todo!() +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Psql(error) => error.fmt(f), + Self::GameNotFound(code) => write!(f, "Failed to find a game with the code: {code}"), + Self::DBCorrupt => write!(f, "Database appears to be corrupt. Oops..."), + } } } + +impl std::error::Error for Error {} diff --git a/rust-server/src/game.rs b/rust-server/src/game.rs index c3876d8..2bf5099 100644 --- a/rust-server/src/game.rs +++ b/rust-server/src/game.rs @@ -1,5 +1,6 @@ /// Represents different states of a game -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, num_enum::TryFromPrimitive)] +#[repr(u8)] pub enum GameState { /// Initial state of the game Lobby, From f17b15ba1208c6110d2a42d315f56bd1a556ca84 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Thu, 9 Jun 2022 14:42:15 +0300 Subject: [PATCH 07/42] [Rust] Setup rocket server --- rust-server/Cargo.toml | 1 + rust-server/src/main.rs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index b68556a..4278f04 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" postgres = "0.19.3" rand = "0.8.5" num_enum = "0.5.7" +rocket = "0.5.0-rc.2" diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index 1331467..7bcb67e 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -1,10 +1,19 @@ mod database; mod game; -use database::{psql::*, DBAccessor}; +// use database::{psql::*, DBAccessor}; -fn main() -> PsqlResult<()> { - let db = PsqlAccess::new()?; +#[rocket::launch] +fn launch() -> _ { + let config = rocket::Config { + log_level: rocket::log::LogLevel::Debug, + address: std::net::Ipv4Addr::new(127, 0, 0, 1).into(), + port: 8000, + ..Default::default() + }; + rocket(config) +} - Ok(()) +fn rocket(config: rocket::Config) -> rocket::Rocket { + rocket::custom(config).mount("/", rocket::routes![]) } From 9f025f75fc8eec8b8abd8a3f36b92839d7ed7bb3 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Thu, 9 Jun 2022 14:51:26 +0300 Subject: [PATCH 08/42] [Rust] Server module and index --- rust-server/Cargo.toml | 2 +- rust-server/src/main.rs | 7 ++----- rust-server/src/server/mod.rs | 10 ++++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 rust-server/src/server/mod.rs diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index 4278f04..554d504 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -9,4 +9,4 @@ edition = "2021" postgres = "0.19.3" rand = "0.8.5" num_enum = "0.5.7" -rocket = "0.5.0-rc.2" +rocket = { version = "=0.5.0-rc.2", features = ["json"] } diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index 7bcb67e..78394b3 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -1,5 +1,6 @@ mod database; mod game; +mod server; // use database::{psql::*, DBAccessor}; @@ -11,9 +12,5 @@ fn launch() -> _ { port: 8000, ..Default::default() }; - rocket(config) -} - -fn rocket(config: rocket::Config) -> rocket::Rocket { - rocket::custom(config).mount("/", rocket::routes![]) + server::rocket(config) } diff --git a/rust-server/src/server/mod.rs b/rust-server/src/server/mod.rs new file mode 100644 index 0000000..d05a194 --- /dev/null +++ b/rust-server/src/server/mod.rs @@ -0,0 +1,10 @@ +use rocket::get; + +pub fn rocket(config: rocket::Config) -> rocket::Rocket { + rocket::custom(config).mount("/", rocket::routes![index]) +} + +#[get("/")] +fn index() -> &'static str { + "This server handles games\n" +} From 2a99c7b9283e158e04d0b2b10a7f834c4dccd722 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Thu, 9 Jun 2022 23:53:29 +0300 Subject: [PATCH 09/42] [Rust] Rework sql access & finish server --- rust-server/Cargo.toml | 7 ++- rust-server/src/database/mod.rs | 44 +++++++++++++--- rust-server/src/database/psql.rs | 89 -------------------------------- rust-server/src/database/sql.rs | 65 +++++++++++++++++++++++ rust-server/src/main.rs | 6 +-- rust-server/src/server/mod.rs | 46 +++++++++++++++-- 6 files changed, 153 insertions(+), 104 deletions(-) delete mode 100644 rust-server/src/database/psql.rs create mode 100644 rust-server/src/database/sql.rs diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index 554d504..4ceca9f 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -6,7 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -postgres = "0.19.3" rand = "0.8.5" num_enum = "0.5.7" rocket = { version = "=0.5.0-rc.2", features = ["json"] } +async-trait = "0.1.56" + +[dependencies.sqlx] +version = "0.5.13" +default-features = false +features = ["runtime-tokio-native-tls", "macros", "offline", "any", "postgres", "mysql", "sqlite", "mssql"] diff --git a/rust-server/src/database/mod.rs b/rust-server/src/database/mod.rs index 76c9bbb..6d3e99d 100644 --- a/rust-server/src/database/mod.rs +++ b/rust-server/src/database/mod.rs @@ -1,13 +1,43 @@ -pub mod psql; +use async_trait::async_trait; +pub use sqlx::any::AnyPool as DBPool; -use crate::game::{self, GameCode, GameState}; +use crate::game::GameCode; + +pub mod sql; + +/// The type of error that may occur when interacting +/// with the database +#[derive(Debug)] +pub enum DBError { + /// A game could not be created because another + /// with confliting properties already exists + GameNotFound(GameCode), + /// The database is corrupted and could not be + /// processed properly + DBCorrupt(String), + /// Some other error occured + Other(Box), +} + +pub type DBResult = Result; /// Trait representing a handler for a games database -pub trait DBAccessor { - /// The type of errors that may occur - type Error: std::error::Error; +#[async_trait] +pub trait DBAccessor: Send + Sync { /// Add a new game to the database - fn create_game(&mut self) -> Result; + async fn create_game(&self) -> DBResult; /// Get information about an existing game - fn get_game(&mut self, game_code: &GameCode) -> Result; + async fn get_game(&self, game_code: &GameCode) -> DBResult<()>; } + +impl std::fmt::Display for DBError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GameNotFound(code) => write!(f, "Failed to find a game with the code: {code}"), + Self::DBCorrupt(msg) => write!(f, "Database is corrupted: {msg}"), + Self::Other(error) => error.fmt(f), + } + } +} + +impl std::error::Error for DBError {} diff --git a/rust-server/src/database/psql.rs b/rust-server/src/database/psql.rs deleted file mode 100644 index 201b528..0000000 --- a/rust-server/src/database/psql.rs +++ /dev/null @@ -1,89 +0,0 @@ -use super::*; - -pub use postgres::Error as PsqlError; -use postgres::{Client, NoTls}; - -pub type PsqlResult = Result; - -/// Handles access to the postgres games database -pub struct PsqlAccess { - client: Client, -} - -impl PsqlAccess { - /// Sets up access to the database and initializes all the neccessary tables - pub fn new() -> PsqlResult { - let client = Client::connect("host=localhost user=test password=test", NoTls)?; - let db = Self { client }.init()?; - Ok(db) - } - - /// Initialize all the neccessary database stuff - fn init(mut self) -> PsqlResult { - self.client.batch_execute( - " - CREATE TABLE IF NOT EXISTS games ( - id SERIAL PRIMARY KEY, - code VARCHAR NOT NULL - state INT - ) - ", - )?; - Ok(self) - } -} - -#[derive(Debug)] -pub enum Error { - Psql(PsqlError), - GameNotFound(GameCode), - DBCorrupt, -} - -impl DBAccessor for PsqlAccess { - type Error = Error; - - fn create_game(&mut self) -> Result { - let code = game::game_code_generator(); - let state = GameState::Lobby; - self.client.execute( - "INSERT INTO games (code, state) VALUES ($1, $2)", - &[&code, &(state as i32)], - )?; - Ok(code) - } - - fn get_game(&mut self, game_code: &GameCode) -> Result { - let res = self - .client - .query("SELECT state FROM games WHERE code == $1", &[game_code])?; - match &res[..] { - [] => Err(Error::GameNotFound(game_code.to_owned())), - [row] => { - let state = row.get::(0); - u8::try_from(state) - .map_err(|_| Error::DBCorrupt) - .and_then(|state| GameState::try_from(state).map_err(|_| Error::DBCorrupt)) - } - _ => panic!("Expected at most one game to match the code"), // TODO: move that check to game creation - } - } -} - -impl From for Error { - fn from(error: PsqlError) -> Self { - Self::Psql(error) - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Psql(error) => error.fmt(f), - Self::GameNotFound(code) => write!(f, "Failed to find a game with the code: {code}"), - Self::DBCorrupt => write!(f, "Database appears to be corrupt. Oops..."), - } - } -} - -impl std::error::Error for Error {} diff --git a/rust-server/src/database/sql.rs b/rust-server/src/database/sql.rs new file mode 100644 index 0000000..4488002 --- /dev/null +++ b/rust-server/src/database/sql.rs @@ -0,0 +1,65 @@ +use super::*; + +use sqlx::AnyPool; + +/// Handles access to the postgres games database +pub struct SqlAccess { + pool: AnyPool, +} + +impl SqlAccess { + /// Sets up access to the database and initializes all the neccessary tables + pub async fn new(db_uri: &str) -> DBResult { + let pool = DBPool::connect(db_uri) + .await + .expect("Failed to connect to the database"); + let sql = Self { pool }.init().await?; + Ok(sql) + } + + /// Initialize all the neccessary database stuff + async fn init(self) -> DBResult { + sqlx::query( + " + CREATE TABLE IF NOT EXISTS games ( + id SERIAL PRIMARY KEY, + code VARCHAR NOT NULL + ) + ", + ) + .execute(&self.pool) + .await?; + Ok(self) + } +} + +#[async_trait] +impl DBAccessor for SqlAccess { + async fn create_game(&self) -> DBResult { + let code = crate::game::game_code_generator(); + let query = format!( + "INSERT INTO games (code) VALUES (\'{}\') RETURNING id", + code + ); + let _ = sqlx::query(&query).fetch_one(&self.pool).await?; + Ok(code) + } + + async fn get_game(&self, game_code: &GameCode) -> DBResult<()> { + let query = format!("SELECT FROM games WHERE code=\'{}\'", game_code); + let res = sqlx::query(&query).fetch_all(&self.pool).await?; + match &res[..] { + [] => Err(DBError::GameNotFound(game_code.to_owned())), + [_row] => Ok(()), + _ => Err(DBError::DBCorrupt(format!( + "More than one game with equal codes exist" + ))), + } + } +} + +impl From for DBError { + fn from(error: sqlx::Error) -> Self { + Self::Other(Box::new(error)) + } +} diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index 78394b3..0364725 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -2,15 +2,13 @@ mod database; mod game; mod server; -// use database::{psql::*, DBAccessor}; - #[rocket::launch] -fn launch() -> _ { +async fn launch() -> _ { let config = rocket::Config { log_level: rocket::log::LogLevel::Debug, address: std::net::Ipv4Addr::new(127, 0, 0, 1).into(), port: 8000, ..Default::default() }; - server::rocket(config) + server::rocket(config, "postgresql://test:test@localhost:5432").await } diff --git a/rust-server/src/server/mod.rs b/rust-server/src/server/mod.rs index d05a194..cf41fa1 100644 --- a/rust-server/src/server/mod.rs +++ b/rust-server/src/server/mod.rs @@ -1,10 +1,50 @@ -use rocket::get; +use rocket::{get, post, State}; -pub fn rocket(config: rocket::Config) -> rocket::Rocket { - rocket::custom(config).mount("/", rocket::routes![index]) +use crate::database::{DBAccessor, DBError, DBResult}; +use crate::game::GameCode; + +type Database = Box; + +pub async fn rocket(config: rocket::Config, db_uri: &str) -> rocket::Rocket { + let database = crate::database::sql::SqlAccess::new(db_uri) + .await + .expect("Failed to access the database"); + rocket::custom(config) + .manage(Box::new(database) as Database) + .mount("/", rocket::routes![index, create_game, get_game]) } #[get("/")] fn index() -> &'static str { "This server handles games\n" } + +#[post("/games")] +async fn create_game(database: &State) -> DBResult { + let game_code = database.inner().create_game().await?; + Ok(format!("Created a new game with the code: {game_code}")) +} + +#[get("/games/")] +async fn get_game(code: GameCode, database: &State) -> DBResult { + let () = database.inner().get_game(&code).await?; + Ok(format!("Found the game with the code: {code}")) +} + +impl DBError { + fn status(&self) -> rocket::http::Status { + use rocket::http::Status; + match self { + DBError::GameNotFound(_) => Status::NotFound, + DBError::DBCorrupt(_) => Status::InternalServerError, + DBError::Other(_) => Status::InternalServerError, + } + } +} + +impl<'r> rocket::response::Responder<'r, 'static> for DBError { + fn respond_to(self, _request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { + println!("Encountered an error: {self}"); + Err(self.status()) + } +} From fb16fa9553d33449c06d4abdbac27c493d2c3936 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Fri, 10 Jun 2022 10:07:57 +0300 Subject: [PATCH 10/42] [Rust] Setup end-to-end testing Closes #11 --- rust-server/src/main.rs | 3 +++ rust-server/src/tests/end_to_end.rs | 29 +++++++++++++++++++++++++++++ rust-server/src/tests/mod.rs | 3 +++ 3 files changed, 35 insertions(+) create mode 100644 rust-server/src/tests/end_to_end.rs create mode 100644 rust-server/src/tests/mod.rs diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index 0364725..31a6cdd 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -2,6 +2,9 @@ mod database; mod game; mod server; +#[cfg(test)] +mod tests; + #[rocket::launch] async fn launch() -> _ { let config = rocket::Config { diff --git a/rust-server/src/tests/end_to_end.rs b/rust-server/src/tests/end_to_end.rs new file mode 100644 index 0000000..fb10855 --- /dev/null +++ b/rust-server/src/tests/end_to_end.rs @@ -0,0 +1,29 @@ +use rocket::{ + http::Status, + local::asynchronous::{Client, LocalRequest, LocalResponse}, +}; + +use super::*; + +#[tokio::test] +async fn test() { + let config = rocket::Config { + log_level: rocket::log::LogLevel::Debug, + address: std::net::Ipv4Addr::new(127, 0, 0, 1).into(), + port: 8000, + ..Default::default() + }; + let rocket = crate::server::rocket(config, "postgresql://test:test@localhost:5432").await; + let client = Client::tracked(rocket).await.unwrap(); + + let response = request(client.get("/")).await; + + assert_eq!(response.status(), Status::Ok); +} + +async fn request<'c>(request: LocalRequest<'c>) -> LocalResponse<'c> { + println!("---\nProcessing: {request:?}"); + let response = request.dispatch().await; + println!("Got response:\n{response:?}\n---"); + response +} diff --git a/rust-server/src/tests/mod.rs b/rust-server/src/tests/mod.rs new file mode 100644 index 0000000..31e4e73 --- /dev/null +++ b/rust-server/src/tests/mod.rs @@ -0,0 +1,3 @@ +use rocket::tokio; + +mod end_to_end; From df660eaf95e8272ea623dc7fee0d37e5e20e6484 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:23:05 +0300 Subject: [PATCH 11/42] Webapp file system structure creation Model sketch orm draft --- manage.py | 22 ---- web-app/adapters/orm.py | 32 +++++ .../__init__.py => adapters/repository.py} | 0 web-app/asgi.py | 16 --- web-app/domain/__init__.py | 1 + web-app/domain/model.py | 58 +++++++++ .../api => entrypoints}/__init__.py | 0 .../__init__.py => entrypoints/flask_app.py} | 0 .../.gitkeep => service_layer/__init__.py} | 0 .../.gitkeep => service_layer/services.py} | 0 web-app/settings.py | 123 ------------------ web-app/static/templates/.gitkeep | 0 web-app/urls.py | 21 --- web-app/wsgi.py | 16 --- 14 files changed, 91 insertions(+), 198 deletions(-) delete mode 100755 manage.py create mode 100644 web-app/adapters/orm.py rename web-app/{django_entrypoints/__init__.py => adapters/repository.py} (100%) delete mode 100644 web-app/asgi.py create mode 100644 web-app/domain/__init__.py create mode 100644 web-app/domain/model.py rename web-app/{django_entrypoints/api => entrypoints}/__init__.py (100%) rename web-app/{service/__init__.py => entrypoints/flask_app.py} (100%) rename web-app/{static/.gitkeep => service_layer/__init__.py} (100%) rename web-app/{static/scripts/.gitkeep => service_layer/services.py} (100%) delete mode 100644 web-app/settings.py delete mode 100644 web-app/static/templates/.gitkeep delete mode 100644 web-app/urls.py delete mode 100644 web-app/wsgi.py diff --git a/manage.py b/manage.py deleted file mode 100755 index 06aadd6..0000000 --- a/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py new file mode 100644 index 0000000..358c627 --- /dev/null +++ b/web-app/adapters/orm.py @@ -0,0 +1,32 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, Text, Enum +from sqlalchemy.orm import declarative_base + +from domain.model import QuestionTypes + +Base = declarative_base() + + +class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True, autoincrement=True) + nickname = Column(Text, unique=True) + email = Column(Text, unique=True) + password = Column(Text) + + +class Quiz(Base): + __tablename__ = 'quiz' + + id = Column(Integer, primary_key=True, autoincrement=True) + quiz_name = Column(Text) + + +class Question(Base): + __tablename__ = 'question' + + id = Column(Integer, primary_key=True, autoincrement=True) + quiz_id = Column(Integer, ForeignKey('quiz.id')) + type = Column(Enum(QuestionTypes)) + description = Column(Text) + possible_answers = () diff --git a/web-app/django_entrypoints/__init__.py b/web-app/adapters/repository.py similarity index 100% rename from web-app/django_entrypoints/__init__.py rename to web-app/adapters/repository.py diff --git a/web-app/asgi.py b/web-app/asgi.py deleted file mode 100644 index 29021bb..0000000 --- a/web-app/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for web-app project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') - -application = get_asgi_application() diff --git a/web-app/domain/__init__.py b/web-app/domain/__init__.py new file mode 100644 index 0000000..36ec720 --- /dev/null +++ b/web-app/domain/__init__.py @@ -0,0 +1 @@ +from . import model \ No newline at end of file diff --git a/web-app/domain/model.py b/web-app/domain/model.py new file mode 100644 index 0000000..08bbf31 --- /dev/null +++ b/web-app/domain/model.py @@ -0,0 +1,58 @@ +import enum +from dataclasses import dataclass +from datetime import date +from typing import Optional, List, Set, NewType + +# Types declaration + +# For user +NickName = NewType('NickName', str) +Email = NewType('Email', str) +Password = NewType('Password', str) + +# For quiz +QuizId = NewType('QuizId', int) +QuizName = NewType('QuizName', str) + +# For question +QuestionAnswer = NewType('QuestionAnswers', str) +QuestionDescription = NewType('QuestionDescription', str) + + +# Possible types of question in a quiz +class QuestionTypes(enum.Enum): + poll = "polls" + quiz = "quiz" + + +# Question class, to store information about question: +# question type, description, possible answers, and correct answers +class Question: + def __init__(self, question_type: QuestionTypes, text: QuestionDescription, + answers: List[QuestionAnswer], correct_answer: QuestionAnswer): + self.question_type = question_type + self.text = text + self.answers = answers + self.correct_answer = correct_answer + + # [maybe deleted] + # return next question + def next(self): + pass + + +# Quiz class, +class Quiz: + def __init__(self, quiz_id: QuizId, quiz_name: QuizName, questions: [Question]): + self.quiz_id = quiz_id + self.quiz_name = quiz_name + self.questions = questions + + +# User class, to manage stored in db information through nickname/email +class User: + def __init__(self, nickname: NickName, email: Email, password: Password, quizzes: [Quiz]): + self.nickname = nickname + self.email = email + self.password = password + self.quizzes = quizzes diff --git a/web-app/django_entrypoints/api/__init__.py b/web-app/entrypoints/__init__.py similarity index 100% rename from web-app/django_entrypoints/api/__init__.py rename to web-app/entrypoints/__init__.py diff --git a/web-app/service/__init__.py b/web-app/entrypoints/flask_app.py similarity index 100% rename from web-app/service/__init__.py rename to web-app/entrypoints/flask_app.py diff --git a/web-app/static/.gitkeep b/web-app/service_layer/__init__.py similarity index 100% rename from web-app/static/.gitkeep rename to web-app/service_layer/__init__.py diff --git a/web-app/static/scripts/.gitkeep b/web-app/service_layer/services.py similarity index 100% rename from web-app/static/scripts/.gitkeep rename to web-app/service_layer/services.py diff --git a/web-app/settings.py b/web-app/settings.py deleted file mode 100644 index 33c796b..0000000 --- a/web-app/settings.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Django settings for web-app project. - -Generated by 'django-admin startproject' using Django 4.0.4. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.0/ref/settings/ -""" - -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-(cy0k*i03w^xt)#ecgso3si44hpk7 &hw7k35n==or)c#)#%' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'web-app.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'web-app.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/4.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/web-app/static/templates/.gitkeep b/web-app/static/templates/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/web-app/urls.py b/web-app/urls.py deleted file mode 100644 index 6321bb5..0000000 --- a/web-app/urls.py +++ /dev/null @@ -1,21 +0,0 @@ -"""web-app URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path - -urlpatterns = [ - path('admin/', admin.site.urls), -] diff --git a/web-app/wsgi.py b/web-app/wsgi.py deleted file mode 100644 index 9950ac3..0000000 --- a/web-app/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for web-app project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web-app.settings') - -application = get_wsgi_application() From 64fc0b177ada21c3dde3cde26a52ab601782e222 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:55:40 +0300 Subject: [PATCH 12/42] Mapper sketch: Added user, quiz, question, answer tables --- web-app/adapters/orm.py | 87 +++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 358c627..7c614e6 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -1,32 +1,79 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Text, Enum +from sqlalchemy import Column, MetaData, Integer, String, ForeignKey, Text, Enum, Table, Boolean from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import mapper -from domain.model import QuestionTypes -Base = declarative_base() +# from domain.model import QuestionTypes +# Заглушка +# To distinguish between question types +class QuestionTypes: + pass -class User(Base): - __tablename__ = 'user' - id = Column(Integer, primary_key=True, autoincrement=True) - nickname = Column(Text, unique=True) - email = Column(Text, unique=True) - password = Column(Text) +# Base = declarative_base() +metadata = MetaData() -class Quiz(Base): - __tablename__ = 'quiz' +# relations between tables: +# - To find quizzes: search quizzes table by user_id +# - To find questions: search question table by quiz_id +# - To find answers: search answers table by question_id - id = Column(Integer, primary_key=True, autoincrement=True) - quiz_name = Column(Text) +# TODO: proper comments for tables +# Table for users +user_table = Table("users", metadata, + Column('id', Integer, primary_key=True, autoincrement=True), + Column('nickname', Text, unique=True), + Column('email', Text, unique=True), + Column('password', Text)) -class Question(Base): - __tablename__ = 'question' +# Table for quizzes +quiz_table = Table("quizzes", metadata, + Column('user_id', Integer, ForeignKey('user.id')), + Column('name', Text)) - id = Column(Integer, primary_key=True, autoincrement=True) - quiz_id = Column(Integer, ForeignKey('quiz.id')) - type = Column(Enum(QuestionTypes)) - description = Column(Text) - possible_answers = () +# Table for questions +question_table = Table("questions", metadata, + Column('id', Integer, primary_key=True, autoincrement=True), + Column('quiz_id', Integer, ForeignKey('quiz.id')), + Column('type', Enum(QuestionTypes)), + Column('description', Text)) + +# Table for answers +answer_table = Table("Answer", metadata, + Column('id', Integer, primary_key=True, autoincrement=True), + Column('question_id', ForeignKey('question.id')), + Column('correct_answer'), Boolean) + +# Draft +# class User(Base): +# __tablename__ = 'user' +# +# id = Column(Integer, primary_key=True, autoincrement=True) +# nickname = Column(Text, unique=True) +# email = Column(Text, unique=True) +# password = Column(Text) +# +# +# class Quiz(Base): +# __tablename__ = 'quiz' +# +# id = Column(Integer, primary_key=True, autoincrement=True) +# user_id = Column(Integer, ForeignKey('user.id')) +# quiz_name = Column(Text) +# +# +# class Question(Base): +# __tablename__ = 'question' +# +# id = Column(Integer, primary_key=True, autoincrement=True) +# quiz_id = Column(Integer, ForeignKey('quiz.id')) +# type = Column(Enum(QuestionTypes)) +# description = Column(Text) +# possible_answers = () +# +# +# class Answers(Base): +# pass From 9fb3bc7e315bbe9910f8e23f5b1b82d3b216be6f Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 10 Jun 2022 14:45:26 +0300 Subject: [PATCH 13/42] [Rust] Game creation and joining --- rust-server/Cargo.toml | 1 - rust-server/src/database/mod.rs | 17 ++--- rust-server/src/database/sql.rs | 34 +--------- rust-server/src/game.rs | 24 ------- rust-server/src/main.rs | 1 - rust-server/src/server/games.rs | 85 ++++++++++++++++++++++++ rust-server/src/server/mod.rs | 112 ++++++++++++++++++++++++++++---- 7 files changed, 190 insertions(+), 84 deletions(-) delete mode 100644 rust-server/src/game.rs create mode 100644 rust-server/src/server/games.rs diff --git a/rust-server/Cargo.toml b/rust-server/Cargo.toml index 4ceca9f..20a6acc 100644 --- a/rust-server/Cargo.toml +++ b/rust-server/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] rand = "0.8.5" -num_enum = "0.5.7" rocket = { version = "=0.5.0-rc.2", features = ["json"] } async-trait = "0.1.56" diff --git a/rust-server/src/database/mod.rs b/rust-server/src/database/mod.rs index 6d3e99d..95f4efb 100644 --- a/rust-server/src/database/mod.rs +++ b/rust-server/src/database/mod.rs @@ -1,17 +1,17 @@ use async_trait::async_trait; pub use sqlx::any::AnyPool as DBPool; -use crate::game::GameCode; - pub mod sql; +pub type QuizId = u64; + /// The type of error that may occur when interacting /// with the database #[derive(Debug)] pub enum DBError { /// A game could not be created because another /// with confliting properties already exists - GameNotFound(GameCode), + QuizNotFound(QuizId), /// The database is corrupted and could not be /// processed properly DBCorrupt(String), @@ -21,19 +21,14 @@ pub enum DBError { pub type DBResult = Result; -/// Trait representing a handler for a games database +/// Trait representing a handler for the database #[async_trait] -pub trait DBAccessor: Send + Sync { - /// Add a new game to the database - async fn create_game(&self) -> DBResult; - /// Get information about an existing game - async fn get_game(&self, game_code: &GameCode) -> DBResult<()>; -} +pub trait DBAccessor: Send + Sync {} impl std::fmt::Display for DBError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::GameNotFound(code) => write!(f, "Failed to find a game with the code: {code}"), + Self::QuizNotFound(id) => write!(f, "Failed to find a quiz with id: {id}"), Self::DBCorrupt(msg) => write!(f, "Database is corrupted: {msg}"), Self::Other(error) => error.fmt(f), } diff --git a/rust-server/src/database/sql.rs b/rust-server/src/database/sql.rs index 4488002..c2aaf32 100644 --- a/rust-server/src/database/sql.rs +++ b/rust-server/src/database/sql.rs @@ -19,44 +19,12 @@ impl SqlAccess { /// Initialize all the neccessary database stuff async fn init(self) -> DBResult { - sqlx::query( - " - CREATE TABLE IF NOT EXISTS games ( - id SERIAL PRIMARY KEY, - code VARCHAR NOT NULL - ) - ", - ) - .execute(&self.pool) - .await?; Ok(self) } } #[async_trait] -impl DBAccessor for SqlAccess { - async fn create_game(&self) -> DBResult { - let code = crate::game::game_code_generator(); - let query = format!( - "INSERT INTO games (code) VALUES (\'{}\') RETURNING id", - code - ); - let _ = sqlx::query(&query).fetch_one(&self.pool).await?; - Ok(code) - } - - async fn get_game(&self, game_code: &GameCode) -> DBResult<()> { - let query = format!("SELECT FROM games WHERE code=\'{}\'", game_code); - let res = sqlx::query(&query).fetch_all(&self.pool).await?; - match &res[..] { - [] => Err(DBError::GameNotFound(game_code.to_owned())), - [_row] => Ok(()), - _ => Err(DBError::DBCorrupt(format!( - "More than one game with equal codes exist" - ))), - } - } -} +impl DBAccessor for SqlAccess {} impl From for DBError { fn from(error: sqlx::Error) -> Self { diff --git a/rust-server/src/game.rs b/rust-server/src/game.rs deleted file mode 100644 index 2bf5099..0000000 --- a/rust-server/src/game.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Represents different states of a game -#[derive(Debug, Clone, Copy, num_enum::TryFromPrimitive)] -#[repr(u8)] -pub enum GameState { - /// Initial state of the game - Lobby, - InProgress, - Finished, -} - -pub type GameCode = String; - -pub fn game_code_generator() -> GameCode { - use rand::Rng; - const LEN: usize = 4; - - let mut rng = rand::thread_rng(); - let mut code = String::new(); - for _ in 0..LEN { - let symbol = rng.gen_range('A'..='Z'); - code.push(symbol); - } - code -} diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index 31a6cdd..c26b453 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -1,5 +1,4 @@ mod database; -mod game; mod server; #[cfg(test)] diff --git a/rust-server/src/server/games.rs b/rust-server/src/server/games.rs new file mode 100644 index 0000000..d612daa --- /dev/null +++ b/rust-server/src/server/games.rs @@ -0,0 +1,85 @@ +use rocket::serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Games { + map: HashMap, +} + +/// Represents different states of a game +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub enum GameState { + /// Initial state of the game + Lobby, + InProgress, + Finished, +} + +pub type GameCode = String; +pub type PlayerName = String; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Player {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Game { + players: HashMap, +} + +impl Games { + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } + + pub fn create_game(&mut self) -> GameCode { + let code = loop { + let code = game_code_generator(); + if !self.map.contains_key(&code) { + break code; + } + }; + let game = Game::new(); + let res = self.map.insert(code.clone(), game); + assert!(res.is_none()); + code + } + + pub fn get_game(&self, code: &GameCode) -> Option<&Game> { + self.map.get(code) + } + + pub fn get_game_mut(&mut self, code: &GameCode) -> Option<&mut Game> { + self.map.get_mut(code) + } +} + +impl Game { + pub fn new() -> Self { + Self { + players: HashMap::new(), + } + } + + pub fn player_join(&mut self, player_name: PlayerName) { + self.players.entry(player_name).or_insert_with(|| Player {}); + } +} + +pub fn game_code_generator() -> GameCode { + use rand::Rng; + const LEN: usize = 4; + + let mut rng = rand::thread_rng(); + let mut code = String::new(); + for _ in 0..LEN { + let symbol = rng.gen_range('A'..='Z'); + code.push(symbol); + } + code +} diff --git a/rust-server/src/server/mod.rs b/rust-server/src/server/mod.rs index cf41fa1..e5fa83f 100644 --- a/rust-server/src/server/mod.rs +++ b/rust-server/src/server/mod.rs @@ -1,9 +1,23 @@ +use std::sync::Mutex; + use rocket::{get, post, State}; -use crate::database::{DBAccessor, DBError, DBResult}; -use crate::game::GameCode; +use crate::database::{DBAccessor, DBError}; + +mod games; + +use games::*; type Database = Box; +type SResult = Result; +type GamesState = Mutex; + +#[derive(Debug)] +enum Error { + Database(DBError), + GameNotFound(GameCode), + BadRequest, +} pub async fn rocket(config: rocket::Config, db_uri: &str) -> rocket::Rocket { let database = crate::database::sql::SqlAccess::new(db_uri) @@ -11,7 +25,8 @@ pub async fn rocket(config: rocket::Config, db_uri: &str) -> rocket::Rocket &'static str { "This server handles games\n" } -#[post("/games")] -async fn create_game(database: &State) -> DBResult { - let game_code = database.inner().create_game().await?; - Ok(format!("Created a new game with the code: {game_code}")) +/// Create a new game from the quiz id +/// or join an existing game with the specified code +#[post("/games/", data = "")] +async fn create_or_join_game( + id: GameCode, + name: Option, + games: &State, + database: &State, +) -> Result { + match id.parse::() { + Ok(quiz_id) => create_game(quiz_id, games, database).await, + Err(_) => match name { + Some(name) => join_game(id, name, games).await.map(|()| format!("")), + None => Err(Error::BadRequest), + }, + } } #[get("/games/")] -async fn get_game(code: GameCode, database: &State) -> DBResult { - let () = database.inner().get_game(&code).await?; - Ok(format!("Found the game with the code: {code}")) +async fn get_game(code: GameCode, games: &State) -> SResult { + match games.lock().unwrap().get_game(&code) { + Some(_game) => Ok(code), + None => Err(Error::GameNotFound(code)), + } +} + +async fn create_game( + _quiz_id: u64, + games: &State, + _database: &State, +) -> SResult { + // TODO: query quiz information from the database + let code = games.lock().unwrap().create_game(); + Ok(code) +} + +async fn join_game( + game_code: GameCode, + player_name: PlayerName, + games: &State, +) -> SResult<()> { + match games.lock().unwrap().get_game_mut(&game_code) { + None => Err(Error::GameNotFound(game_code)), + Some(game) => { + game.player_join(player_name); + Ok(()) + } + } +} + +impl Error { + fn status(&self) -> rocket::http::Status { + use rocket::http::Status; + match self { + Self::Database(error) => error.status(), + Self::GameNotFound(_) => Status::NotFound, + Self::BadRequest => Status::BadRequest, + } + } } impl DBError { fn status(&self) -> rocket::http::Status { use rocket::http::Status; match self { - DBError::GameNotFound(_) => Status::NotFound, - DBError::DBCorrupt(_) => Status::InternalServerError, - DBError::Other(_) => Status::InternalServerError, + Self::QuizNotFound(_) => Status::NotFound, + Self::DBCorrupt(_) => Status::InternalServerError, + Self::Other(_) => Status::InternalServerError, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Database(error) => error.fmt(f), + Self::GameNotFound(msg) => { + write!(f, "A game with the code \'{msg}\' could not be found") + } + Self::BadRequest => write!(f, "Bad request"), } } } -impl<'r> rocket::response::Responder<'r, 'static> for DBError { +impl std::error::Error for Error {} + +impl From for Error { + fn from(error: DBError) -> Self { + Self::Database(error) + } +} + +impl<'r> rocket::response::Responder<'r, 'static> for Error { fn respond_to(self, _request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { println!("Encountered an error: {self}"); Err(self.status()) From e0d817e430857370dec62ee5bbd1e84602daaaec Mon Sep 17 00:00:00 2001 From: mpvnlv Date: Fri, 10 Jun 2022 15:14:57 +0300 Subject: [PATCH 14/42] Add templates --- web-app/static/images/Google_icon.png | Bin 0 -> 2004 bytes web-app/static/images/inno_icon.png | Bin 0 -> 3664 bytes web-app/static/scripts/jquery-1.10.1.min.js | 0 web-app/static/scripts/jquery-3.6.0.min.js | 2 + web-app/static/scripts/script.js | 0 web-app/static/scripts/wh.js | 0 web-app/static/styles/style.css | 534 ++++++++++++++++++ .../static/templates/authorization_page.html | 28 + web-app/static/templates/main_page.html | 53 ++ web-app/static/templates/page_of_answer.html | 24 + web-app/static/templates/page_of_create.html | 34 ++ .../static/templates/registration_page.html | 42 ++ web-app/static/templates/waiting_hall.html | 47 ++ 13 files changed, 764 insertions(+) create mode 100644 web-app/static/images/Google_icon.png create mode 100644 web-app/static/images/inno_icon.png create mode 100644 web-app/static/scripts/jquery-1.10.1.min.js create mode 100644 web-app/static/scripts/jquery-3.6.0.min.js create mode 100644 web-app/static/scripts/script.js create mode 100644 web-app/static/scripts/wh.js create mode 100644 web-app/static/styles/style.css create mode 100644 web-app/static/templates/authorization_page.html create mode 100644 web-app/static/templates/main_page.html create mode 100644 web-app/static/templates/page_of_answer.html create mode 100644 web-app/static/templates/page_of_create.html create mode 100644 web-app/static/templates/registration_page.html create mode 100644 web-app/static/templates/waiting_hall.html diff --git a/web-app/static/images/Google_icon.png b/web-app/static/images/Google_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41578024f96d863f0e9acee82dfe5ecd6e0ff160 GIT binary patch literal 2004 zcmV;_2P^oAP) zwP_>RLMaxI9l}!f+1_^Fz3<+$_{Y6(W*C-AQsYfdet+Ea-uLrc&$;k_e#B?^Tl>ET zkbndDPcQ53Ts*P!?xuRbO;{_6Q8Mt}`Ku?N+;`|I5Ca-8pA~=va=%~Fd(YCY$=_;i zs(;we`7?zh)8AT)RT>i~Hjd)c`EvNy?#oyH@ZJ5#jsVq91Gyc50K84pr%&53Ys!<& z4Rv30eAf}K3$BBZBHdyQRvD~P*hFIzg{dYwA4ack{p7;N&1Wv11rlzNqFVt7p#Cqt zpI`IEj+QN+=XD6rLAVaWl^|sX0M=MA7OOQ@X>6j0L3FJ9Kg#F7|JCECwgKVIAe>u> zH@-P%)?@cIH9jvbP1sZflX}Z2A)6>ntgwl~#yYcUG*G5t63)66jg41c%IA+}k+>~@ z0P0_w(zAAcL&I|dJPcTyiE9Iyr!@$Zs0<<#Pc&pUh0x9fvN1~vH#nXscoG#B@UX1m&14(s|v`cOPkU;B# zO$&bIdUF1ZV-*}LMuc_Enp8ktpYze8BX3KWoE` zd3Eh|Yj>^6v0zt$Nd-mD$ry#fN)SSzmn_CCe*inR8-zea=aBio9s%I?)>H#TlDS>VmJZaUwyqxYvyGfx z{dIObJ?x9xIGnU`!JkH5&qK6!&!!>w4*vKGx%Py*jyPQtVsh;XUaOX!?b1B^vU;}$ zP>X;AwB*|BAFPGr$f9OWyxq^t(-F;{PtI|WKi|YVE%)-lfe_zwaGezFTEXz|OPc$8 z7O}E>GcCb)5W*tHG`Y1fWukl#XaNd7H!OR?FDK6trb`!QHgfQ z8lbk%#{FT*YKw4F%F)R8dDYt`4U_U;B9Af>l_n(^sRjVLizkywCc@T?Ao35CsHLNDzfoG8;q{f-vAK zy=}wDtXo*&i-SI(mf$W&B0jR>s!nHUbU$rV5y<_TO*VTV8ecAwDp4~(kl&Azn zf}lt_C{n2u34y~HRN6?;@e*F5`b>2E+j(;NmcK(X&%eFuk*<@dJD9ls>i6yR8pVz@Ib zmOVr7vuEgiEEbzUwXCU+V_vxT{IDOk;X$A{wgxOv0WKbU z@#xlgFe&TEVq%L`q$i=Nc{Gw1fxu+-@>th-nq4*GwRX^nM_l!ZL!Y5RxBTNR{ab;H znU;m?k7EPqxESlCs#WXEwuMp%$=K(*8EY{ybUOh*>^hCG0&AQ!--(CH#DlW0pMD|# z{PsQJ+rT-}>ltGKAiXFgr8C8%MaXg8&2t4pQX5c~;YeW9Vl|27iHWD_bP|km(Mg)` zB=V_-K0(RX&%BcV<1>HGzXlx7@^5YhU`X2|Dx4hZi;795aZ2+XA*7$iC%c5|R9wfF zFL`;^H4dO-4^{O@B9FL|BMN*5`kmmZtpi)1`|Hr3fFrp$3c z#%pV6cN+YORv~OcS{I2$Rt-ODx{RMl;?SpB@reSTO4&94JP;rJ-mfqH_Pv9_PT(+b zE&Jc=GkOz%+T#Olz*JyXV^8zE&ilKUHBWDzoolUYl@jqtLy7fGMXUj>g}pYY^7~JP z$6wqzwCkf&;Q`1(ld4A^F*?X7V<+b9uqDhgIC|a^A%aRqlvg6i@h1x&dG=~EvXwaZ;e)LECrv-|( zC{UmW&>%^JT0v5zHDW1omDo}2B$eV+wqr}SL{X+FQQ{#km&kR%@zSJ+V0XaU}?MR>Qp2`Rb+J9r(o_naUFw+#GU1H3x_ zqF*m3za^L(gCcVM60)*r7a>7|BpAHfutfp7A<|23m^^JXfj}WDtq_!!RY8fq5oqf_ zCx}h|P_rxlj~ng^VB5T{qLTkKjKXpFK%M=LThU})VB?*u{5JfkyOl!#nQTFn)8)~4LMv*yEfF+1!KIJt&?He@G&v?zr zikhhfc3ps{uU`C}?E@Vz&Ybx-q$giN$X%h(H)21zmf7O4_ z;0ZyH7HR=3*E0BjjkU~Eur^ zR9}RE!i-cEVkAO>_A!#_Nq3M_(A)GV-OSTP&w5c|$mfeXDtehwp~B_yOZi;xQ4T)E z^TPvdS=Q^M+XpN3kQR=kr1U@vhT*3-!p*%vpcUabD-lD}0cUfuC3-ol3((3I%BF+P4jV`uU^CnGV5K4w46A1bnp? zr`6@*iY1hw1kiWjx6rfi2;^uR!TIS`7dA{Ua+ZSA7p@|!rLmeRAi0)BY9)>3^(->! z0t#i4nB;}VoGvo#HJf?OBonGREioNr`c{Byn-KFEgqqr*J2Bw(K`T^Kx4jmy&e%-@ zPNhKul~0TdwwNt945qgt(0 z8i!^;X=RzuVufafiu#r6ZxwpmFFT*?D<3A{kY(A$Dh!s8m6C)FsztaM*3MESgX~N& zGL_@|hvO8h;<$(S9 zh;Q#iEE>cEJ9nTx#Bk;MogjC>IV#0s)#lwai9u%1YKHsxw6yeiQ^vK@_Qj1a1tB(mE5ZKyPU9GIaaeJ1{aZh~d5%hPvp< zZWSR$`W??Xx|%m~Y~EhQN+yN%Yzpb}8j3ne@b(OITZE*^c=W(yh;-~F0kSeHuK%bJ zYjyah0XsJfqO3VeSXHU0^b$(RW%wfn#KICryV?aUH`Ch5aj{xGS$C1d+|qkUEM7xuc>x=xC8UK7 z*5DjLY9w#6v`Zw173-t=&us6`8eVghCPCe`ljv>+y@~U+Zmm#s`DJ30*fYIdSWFmHQqP^flaG zxP|1#GL_aU(#*%Kk)_%ylW3@>xgAtkXPu^wQKsNM9mjZen3^3YmP0APba{%vDK&hqGJ!?2wSbnr%@tI}HKL`K&V%=xMJjMAa2Uv?)fr zx=<{V`Mo5EO&)atcIKq$SXxQ1!`m9cbFX|0)0qVve(Yg{na};wb_}=mVNV+uzcY`y z4ggh4MG@C1GI9t#&68=6)|hFe9S*~YRK=dRd~DhtZLnGKZIDkH} z9Xq#u6!&iX2v#c7NGwcac5aGW$9bez7a2Art3Vhbg0G|%^mh+b^LKG^5xwn04l};= zYXjWe(VTek#nV45m2;mR3if07$OsaX=izA&BU8vj@CukNq%bv}#7mdYBNl1FSpN<@ zxMMeVbao={8bM2Z0K4LcFrB)BsT=PQ_$-QS0%#Qrt^O|bMh8fG7V;Z8^zQ5YOF_AT&!@q4WG88D|7cbz(#0~83J;dtUiTAF~z^izncwGOy zS?@ac)z!p%f8v?h`G+UQFZ|%p6Tj)6!V13lmA}BdS^|2A_FQB2P$?=^b|ty1a@wAkdJ4m1W1FYw ztpe--fA#cBUux~^cs90c2w#2k6u$h^lSun2P(vzvQw8YUDS`W5hjGA?BvLBMh5|Qr zvX-T8X81?gx*6;4$H9Ack=NnJaM$S z<$FuBNqR=XZ}0mEdOR)gltdKR$I{u5V^C89%Y36yZQ`)Q$u1ykavYMS)fG8=&dcivD-7>&?9^C(8Kke6c_~tHLBuwu{^oav8^4wEzI5Nt4MJt zwdcSd(sTXCmH~E_vi+XHug^?QLo06JXx~8`-Fq0Wf(d~wSr_Z4({C=M*#+Z}=T%DaR0I3VSK~x&LENx`Ybf9$IYquwwvMvm+n*W@0&q0XFJC zapXx1d*hhDF%7GrC-(iu{U6^7uv;3^?=R@-r$64CPOf3<-EqX(C;!5+&tRlE4kJ~B zz~*zn&wdVDuNA(I5hY5<0AH=plf}g}di|~V{1czX{^l{HmsjxC**C3wckbE0RbaQQ z4*$;u#k;l-OuaLCJ<`^Jhhqnjdqjiv%C~W8;X2HK$X70WbHHTM+0iib4CAzpK2H>1 z_}DW{g-2lI3m88?!Jb#=v;DhvILx(W$ClLLU$vUEKRo;B?d99AKK$fkd>IkI`L)aV z(y4#J8?%>yh^wl*%`(f>s)qf2qd5M^(>U6D1X5YU&%XZ?cxBJ;Kk?Mj=e7*#FCKK) z06WkB&i7A0Q_%|FJp9;0@c4sxEpY+gz4#-XpBu-bk!HU+fcruN_{5=Oc%{QoSroy}(L z7hZTlKY8-x-_-r^5BPw)9@4*R9R$6H{je`55{W;f;Fvgm{5apFH;#q{8Ww0+pkaZA i1sWD;Sm6Je1^ydnYLQs?RTxD80000+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 + + + + + + + + Document + + +
+
+

Autorithation

+ + +

OR

+
+ + +
+
+ +
+ + + + \ No newline at end of file diff --git a/web-app/static/templates/main_page.html b/web-app/static/templates/main_page.html new file mode 100644 index 0000000..e4cae74 --- /dev/null +++ b/web-app/static/templates/main_page.html @@ -0,0 +1,53 @@ + + + + + + + + + Document + + + + + + +
+ +
+ + + + + +
+ + + +
+ + +
+
+
+ Join Quiz! + +
+ + + +

OR

+ +
+
+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/web-app/static/templates/page_of_answer.html b/web-app/static/templates/page_of_answer.html new file mode 100644 index 0000000..a83b5e4 --- /dev/null +++ b/web-app/static/templates/page_of_answer.html @@ -0,0 +1,24 @@ + + + +page_of_answer + + + + +
+TimeScale +
+
Task
+
+ + + + +
+ + + + \ No newline at end of file diff --git a/web-app/static/templates/page_of_create.html b/web-app/static/templates/page_of_create.html new file mode 100644 index 0000000..28225e8 --- /dev/null +++ b/web-app/static/templates/page_of_create.html @@ -0,0 +1,34 @@ + + + +page_of_answer + + + + + +
+

Header

+
+

Name_of_quiz

+ +
+
Block
+
Block
+
+ +
+ + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/web-app/static/templates/registration_page.html b/web-app/static/templates/registration_page.html new file mode 100644 index 0000000..ba48598 --- /dev/null +++ b/web-app/static/templates/registration_page.html @@ -0,0 +1,42 @@ + + + + + + + + + Document + + +
+

HEADER

+
+
+
+

Registration

+ +
+ + +
+ + + +

or

+
+ + +
+ +
+
+ + +
+

FOOTER

+
+ + + + \ No newline at end of file diff --git a/web-app/static/templates/waiting_hall.html b/web-app/static/templates/waiting_hall.html new file mode 100644 index 0000000..a80ccab --- /dev/null +++ b/web-app/static/templates/waiting_hall.html @@ -0,0 +1,47 @@ + + + + + + Bootstrap demo + + + + + + +
+
+

Intaby

+ +
+ + + +
+
+
+
+ Name of quiz +
+
+ Stars in _ +
+
+
+
+
+ Count of people +
+
+ +
+ +
+ +
+ + + \ No newline at end of file From 2bc5284cae589176ee4a1c47305782e5c7ea38e9 Mon Sep 17 00:00:00 2001 From: mpvnlv Date: Fri, 10 Jun 2022 15:22:07 +0300 Subject: [PATCH 15/42] Change styles of main page --- web-app/static/images/logo.png | Bin 0 -> 45724 bytes web-app/static/styles/style.css | 630 +++++++++++++----------- web-app/static/templates/main_page.html | 68 +-- 3 files changed, 391 insertions(+), 307 deletions(-) create mode 100644 web-app/static/images/logo.png diff --git a/web-app/static/images/logo.png b/web-app/static/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6f45a702c713f23ddc5589b15d9c5ff947300d5c GIT binary patch literal 45724 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 literal 0 HcmV?d00001 diff --git a/web-app/static/styles/style.css b/web-app/static/styles/style.css index 8ac33c8..659b6e6 100644 --- a/web-app/static/styles/style.css +++ b/web-app/static/styles/style.css @@ -1,58 +1,57 @@ -*{ +* { padding: 0%; margin: 0%; box-sizing: border-box; - } -body{ - height:100%; + +body { + height: 100%; } -.header{ + +.header { width: 80%; background-color: gray; margin-left: 10%; margin-top: 2%; text-align: center; } -.Name_of_quiz{ + +.Name_of_quiz { width: 40%; background-color: gray; margin-left: 10%; margin-top: 2%; text-align: left; margin-bottom: 1%; - - - - } -.Block1{ + +.Block1 { display: inline-block; width: 15%; background-color: gray; margin-left: 10%; text-align: left; height: 2em; - } -.Block{ + +.Block { display: inline-block; width: 15%; background-color: gray; margin-left: 9.75%; text-align: left; height: 2em; - } -.Answer_text{ + +.Answer_text { box-sizing: border-box; width: 35%; margin-left: 10%; margin-top: 3%; - height: 3em; } -.Editor{ + +.Editor { position: absolute; width: 80%; height: 40%; @@ -61,26 +60,30 @@ body{ margin-top: 1%; text-align: left; } -.Question_text{ + +.Question_text { margin-top: 3%; height: 3em; width: 50%; margin-left: 10%; } -.time_mode{ + +.time_mode { margin-top: 3%; height: 3em; width: 20%; margin-left: 10%; } -.Add_answer{ + +.Add_answer { width: 35%; margin-left: 10%; margin-top: 3%; height: 3em; } -.save{ + +.save { height: 5%; width: 5%; @@ -88,7 +91,10 @@ body{ margin-top: 60%; } -.Answer1, .Answer2, .Answer3, .Answer4{ +.Answer1, +.Answer2, +.Answer3, +.Answer4 { width: 40%; display: inline-block; margin-left: 5%; @@ -96,7 +102,8 @@ body{ height: 7em; background-color: aqua; } -.Task{ + +.Task { width: 85%; text-align: center; background-color: aqua; @@ -105,7 +112,8 @@ body{ margin-left: 5%; } -.TimeScale{ + +.TimeScale { background-color: aqua; text-align: center; width: 85%; @@ -116,7 +124,8 @@ body{ margin-left: 5%; } -.Nav{ + +.Nav { background-color: aqua; text-align: center; width: 85%; @@ -124,124 +133,158 @@ body{ margin: 3%; margin-bottom: 2%; border-radius: 10px; - margin-left: 5%; + margin-left: 5%; } -header{ + +header { width: 100%; height: 2.5em; margin-top: 0%; background-color: #142036; - + } -h1{ + +h1 { margin: 0; } -footer{ + +footer { width: 100%; height: 5em; - left:0; bottom: 0; + left: 0; + bottom: 0; background-color: aqua; position: fixed; } -.Registration{ + +.Registration { width: 30%; height: fit-content; position: absolute; - margin-left: 35%; - margin-top:9% ; + margin-left: 35%; + margin-top: 9%; background-color: aqua; } -.email{ + +.email { height: 2em; margin: 1%; width: 80%; } -.namesurname{ + +.namesurname { width: 100%; margin-left: 10%; margin: 1%; display: inline-block; } -.name, .surname{ + +.name, +.surname { width: 40%; - + height: 2em; - + } -.password{ + +.password { width: 80%; height: 2em; margin: 1%; - + } -.reg{ + +.reg { width: 40%; height: 2em; margin: 0; } - .UI{ + +.UI { width: 48px; height: 48px; padding: 0px; } -.img1{ + +.img1 { width: 40%; display: inline; } -.google{ + +.google { padding: 0px; margin-bottom: 1.5%; } -.buttons{ + +.buttons { display: flexbox; - + right: 0; width: 50%; float: right; -position: absolute; + position: absolute; } -.main_page{ + +.main_page { + height: max-content; -height: max-content; +} +.logo{ + height: 7%; + width: 7%; } -.Sign_up{ - +.right{ + width: 50%; + height: 100%; + display: flex; + justify-content: flex-end; +} + +.left{ + width: 50%; + height: 100%; + display: flex; + justify-content: flex-start; +} + +.Sign_up { background-color: #FFFFFF; } -.Sign_up a{ + +.Sign_up a { color: #0D4278; text-decoration: none; font-size: medium; } -.Log_in a{ + +.Log_in a { color: #FFFFFF; text-decoration: none; - font-size: medium; + font-size: medium; +} +.Log_in { -} -.Log_in{ background-color: #0D4278; } -.name2{ - width:100%; - display: flex; - justify-content: flex-end; - align-items: center; - - +.name2 { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center } -.Intaby{ + +.Intaby { float: left; margin: 1%; @@ -251,222 +294,243 @@ height: max-content; } -.join_quiz{ - +.join_quiz { width: 100%; min-height: 85vh; -position: relative; -display: flex; -align-items: center; -justify-content: center; } - - @media (min-width:759px) { - fieldset - { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +@media (min-width:759px) { + fieldset { border-width: 20px 10px 20px 10px; border-style: solid; width: 100%; padding: 20px; - - - } - .join_quiz1{ + + .join_quiz1 { width: 50%; padding: 30px; - + /* outline: 2px solid white; Чёрная рамка */ - + /* border: 10px solid #0D4278; */ - + } - .join_quiz_2{ - - padding: 5%; - width: 90%; - background-color:#3D71A0; + + .join_quiz_2 { + padding: 3%; + background-color: #3D71A0; margin: 5%; - } - .Log_in, .Sign_up{ - float: right; - width: 10%; + display: flex; + align-items: center; + flex-direction: column; + flex-wrap: wrap; + } + + .Log_in, + .Sign_up { + width: 20%; margin: 1%; height: 3em; - border-radius: 20px; - } - .code{ - width: 80%; - height: 2em; + + .code { + width: 94%; + height: 3em; background-color: #DAE3ED; font-size: larger; - margin-left: 10%; - margin-bottom: 1em; - - + margin-bottom: 5%; + margin-top: 2%; } - .mp_name{ - background-color: #DAE3ED; - width: 80%; - height: 2em; - margin-left: 10%; - margin-top: 5%; - font-size: xx-large; - margin-bottom: 1em; + .mp_name { + background-color: #DAE3ED; + width: 80%; + height: 3em; + margin-left: 10%; + margin-top: 2%; + font-size: xx-large; + margin-bottom: 2%; } - - } - @media (max-width:760px) { - fieldset - { + +} + +@media (max-width:760px) { + fieldset { border-width: 10px 5px 10px 5px; border-style: solid; width: 100%; padding: 20px; - - - - } - - .join_quiz1{ + + .join_quiz1 { width: 90%; /* outline: 2px solid #000; /* Чёрная рамка */ - /* border: 3px solid #fff; */ - + /* border: 3px solid #fff; */ + } - .join_quiz_2{ - height: max-content; - padding: 5%; - margin-top: 5%; - padding-top: 5%; - background-color:#3D71A0; - - - } - .Log_in, .Sign_up{ - float: right; - width: 20%; + + .join_quiz_2 { + padding: 3%; + background-color: #3D71A0; + margin: 5%; + display: flex; + align-items: center; + flex-direction: column; + flex-wrap: wrap; + + } + + .Log_in, + .Sign_up { + width: 60%; margin: 1%; height: 2em; border-radius: 20px; - + } - .code{ - width: 80%; + + .logo{ + width: 5%; + height: 5%; + } + + .code { + width: 95%; height: 2em; - margin-left: 10%; margin-bottom: 1em; background-color: #DAE3ED; - - - } - .mp_name{ + + .mp_name { background-color: #DAE3ED; + width: 80%; + height: 3em; + margin-left: 10%; + margin-bottom: 1em; + } + +} - width: 80%; - height: 3em; - margin-left: 10%; - margin-bottom: 1em; +.connect { + width: 35%; + height: 3em; + margin-bottom: 2.5%; + background-color: #142036; + border: 3px solid white; + color: #FFFFFF; + font-size: medium; + + +} + +.mp_join { + color: #FFFFFF; + text-align: center; +} + +.or { + margin-top: 4vh; + text-align: center; + +} + + +.Or { + text-align: center; + color: white; + font-size: 180%; +} + + +.qr { + width: 35%; + height: 3em; + margin-top: 2.5%; + margin-bottom: 2%; + background-color: #142036; + border: 3px solid white; + color: #FFFFFF; + font-size: medium; +} + +.login, +.password2 { + display: block; + width: 40%; + height: 3em; + margin: 0; +} + +.autorithation { + width: 50%; + margin-top: 35vh; +} + +.autorithation { + margin-bottom: 4vh; + font-family: "Trocchi", serif; +} + +@media (min-width:768px) { + .wh_name { + margin-right: 8.3%; } - - } - - .connect{ - width: 60%; - height: 2em; - margin-left: 20%; - background-color: #142036; - color: #FFFFFF; - font-size:medium; - - } - .mp_join{ - color: #FFFFFF; - text-align: center; - } - .or{ - margin-top: 4vh; - text-align: center; - } - .Or{ - text-align: center; - } - .qr{ - - width: 60%; - height: 2em; - margin-left: 20%; - background-color: #142036; - color: #FFFFFF; - font-size: medium; - } - .login, .password2{ - display: block; - width: 40%; - height: 3em; - margin: 0; - } - - .autorithation{ - width: 50%; - margin-top:35vh ; - } - .autorithation{ - margin-bottom: 4vh; - font-family: "Trocchi", serif; - } - @media (min-width:768px){ - .wh_name{margin-right: 8.3%;} - - } - /* @media(max-width:576px){ + +} + +/* @media(max-width:576px){ mar } */ - - .wh_name{ - background-color: aqua; - margin-top: 5%; - display: flex; + +.wh_name { + background-color: aqua; + margin-top: 5%; + display: flex; height: 10vh; align-items: center; - font-size:xx-large; + font-size: xx-large; - } - .wh_start{ +} + +.wh_start { height: 10vh; - background-color: aquamarine; - margin-top: 5%; - display: flex; - /* justify-content: center; */ - align-items: center; - font-size:xx-large; - } - @media (max-width:576px) { - .wh_count{ + background-color: aquamarine; + margin-top: 5%; + display: flex; + /* justify-content: center; */ + align-items: center; + font-size: xx-large; +} + +@media (max-width:576px) { + .wh_count { display: flex; right: 0; bottom: 0; position: absolute; height: 5vh; - float: right; - background-color: black; - display: flex; - /* justify-content: center; */ - align-items: center; - padding: 3px; - font-size:large; - color: aliceblue; - margin: 3%; - - } - } - @media (min-width:575px) { - .wh_count{ + float: right; + background-color: black; + display: flex; + /* justify-content: center; */ + align-items: center; + padding: 3px; + font-size: large; + color: aliceblue; + margin: 3%; + + } +} + +@media (min-width:575px) { + .wh_count { right: 0; bottom: 0; position: absolute; @@ -474,61 +538,77 @@ justify-content: center; } display: flex; margin-top: 25%; height: 7vh; - float: right; - background-color: black; - display: flex; - /* justify-content: center; */ - align-items: center; - padding-left: 10px; - font-size:xx-large; - color: aliceblue; - - } - - } - .wh_hall{ + float: right; + background-color: black; + display: flex; + /* justify-content: center; */ + align-items: center; + padding-left: 10px; + font-size: xx-large; + color: aliceblue; + + } + +} + +.wh_hall { margin-top: 5%; - height: 50vh; - position: relative; - background-color: blue; - } + height: 50vh; + position: relative; + background-color: blue; +} - .mh_top{ +.mh_top { display: flex; justify-content: center; align-items: center; - } - @keyframes move { - from {/*Начальная позиция. В самом начале вверху и слева равно 0*/ - /* top: 0; */ - left: 15%; +} + +@keyframes move { + from { + /*Начальная позиция. В самом начале вверху и слева равно 0*/ + /* top: 0; */ + left: 15%; } - - 25%, - 75% {left: 25%;}/*при 25% и 75% длительности анимации максимум равен 80%; */ - 50% {left: 70%;}/*at 250% of the animaton duration top equals 50%;*/ - to { /*finish position В самом конце вверху и слева равны 0. Мы используем эти свойства. bcz мы хотим поместить элемент в начальную позицию*/ - /* top: 0; */ - left: 15%; - } + + 25%, + 75% { + left: 25%; + } + + /*при 25% и 75% длительности анимации максимум равен 80%; */ + 50% { + left: 70%; } - .circle { - margin-top: 15%; - width: 40px; - height: 40px; - border-radius: 50%; - background-color: rgb(7, 206, 173); - position: absolute; - /* top: 0px; */ - left: 0px; - animation-name: move;/*название анимации */ - animation-duration: 4s;/*и ее продолжительность в секундах*/ - animation-iteration-count: infinite; + + /*at 250% of the animaton duration top equals 50%;*/ + to { + /*finish position В самом конце вверху и слева равны 0. Мы используем эти свойства. bcz мы хотим поместить элемент в начальную позицию*/ + /* top: 0; */ + left: 15%; } +} -legend -{ - padding-left: 40px; /* Свои значения */ - padding-right: 40px; /* Свои значения */ +.circle { + margin-top: 15%; + width: 40px; + height: 40px; + border-radius: 50%; + background-color: rgb(7, 206, 173); + position: absolute; + /* top: 0px; */ + left: 0px; + animation-name: move; + /*название анимации */ + animation-duration: 4s; + /*и ее продолжительность в секундах*/ + animation-iteration-count: infinite; +} + +legend { + padding-left: 40px; + /* Свои значения */ + padding-right: 40px; + /* Свои значения */ font-size: xx-large; } \ No newline at end of file diff --git a/web-app/static/templates/main_page.html b/web-app/static/templates/main_page.html index e4cae74..e1e40b1 100644 --- a/web-app/static/templates/main_page.html +++ b/web-app/static/templates/main_page.html @@ -1,5 +1,6 @@ + @@ -8,46 +9,49 @@ Document + +
+
+
+ +
+
+ + + +
-
- -
- - - - - -
- - + +
- - -
-
-
- Join Quiz! - -
- - - -

OR

- + + +
+
+
+ Join Quiz! + + +
+ + +

OR

+
-
- - -
- - +
+ +
- - + + +
+ + + \ No newline at end of file From c77a1ba1172b2236678f9fd22db1f45a58c3331f Mon Sep 17 00:00:00 2001 From: mpvnlv Date: Fri, 10 Jun 2022 15:35:44 +0300 Subject: [PATCH 16/42] Page of user --- web-app/static/templates/page_of_user.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 web-app/static/templates/page_of_user.html diff --git a/web-app/static/templates/page_of_user.html b/web-app/static/templates/page_of_user.html new file mode 100644 index 0000000..e69de29 From e828f2921f3fab428c9c49bf079a884adfde9fe1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 10 Jun 2022 15:43:27 +0300 Subject: [PATCH 17/42] [Rust] Setup game answer --- rust-server/src/server/games.rs | 4 ++++ rust-server/src/server/mod.rs | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/rust-server/src/server/games.rs b/rust-server/src/server/games.rs index d612daa..d303117 100644 --- a/rust-server/src/server/games.rs +++ b/rust-server/src/server/games.rs @@ -30,6 +30,10 @@ pub struct Game { players: HashMap, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct GameAnswer {} + impl Games { pub fn new() -> Self { Self { diff --git a/rust-server/src/server/mod.rs b/rust-server/src/server/mod.rs index e5fa83f..7d8452b 100644 --- a/rust-server/src/server/mod.rs +++ b/rust-server/src/server/mod.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use rocket::{get, post, State}; +use rocket::{get, post, put, serde::json::Json, State}; use crate::database::{DBAccessor, DBError}; @@ -60,6 +60,12 @@ async fn get_game(code: GameCode, games: &State) -> SResult", data = "")] +async fn game_answer(code: GameCode, answer: Json) -> SResult<()> { + // TODO: register answer + Ok(()) +} + async fn create_game( _quiz_id: u64, games: &State, From 8d9a9f4896867d700eae137b1ed6578349db3444 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Fri, 10 Jun 2022 20:30:00 +0300 Subject: [PATCH 18/42] Repository sketch added Model amendings: Added Answer class for answers table Amended class Question, due to the changing above Orm: mapping added __init__'s added imports tests: just created file to write tests for existing code --- web-app/adapters/__init__.py | 1 + web-app/adapters/orm.py | 29 +++++++++++++++--------- web-app/adapters/repository.py | 40 ++++++++++++++++++++++++++++++++++ web-app/domain/model.py | 9 ++++++-- web-app/tests/test_orm.py | 0 5 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 web-app/tests/test_orm.py diff --git a/web-app/adapters/__init__.py b/web-app/adapters/__init__.py index e69de29..eec1779 100644 --- a/web-app/adapters/__init__.py +++ b/web-app/adapters/__init__.py @@ -0,0 +1 @@ +from . import orm, repository \ No newline at end of file diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 7c614e6..f35e740 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -1,19 +1,18 @@ from sqlalchemy import Column, MetaData, Integer, String, ForeignKey, Text, Enum, Table, Boolean -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import registry from sqlalchemy.orm import mapper - -# from domain.model import QuestionTypes +import model # Заглушка # To distinguish between question types -class QuestionTypes: - pass +# class QuestionTypes: +# pass # Base = declarative_base() - -metadata = MetaData() +mapper_registry = registry() +metadata = mapper_registry.metadata # relations between tables: # - To find quizzes: search quizzes table by user_id @@ -38,15 +37,25 @@ class QuestionTypes: question_table = Table("questions", metadata, Column('id', Integer, primary_key=True, autoincrement=True), Column('quiz_id', Integer, ForeignKey('quiz.id')), - Column('type', Enum(QuestionTypes)), + Column('type', Enum(model.QuestionTypes)), Column('description', Text)) # Table for answers -answer_table = Table("Answer", metadata, +answer_table = Table("answers", metadata, Column('id', Integer, primary_key=True, autoincrement=True), Column('question_id', ForeignKey('question.id')), - Column('correct_answer'), Boolean) + Column('text', Text), + Column('correct_answer', Boolean)) + + +def start_mappers(): + user_mapper = mapper_registry.map_imperatively(model.User, user_table) + question_mapper = mapper_registry.map_imperatively(model.Question, question_table) + answer_mapper = mapper_registry.map_imperatively(model.Answer, answer_table) + +if __name__ == '__main__': + start_mappers() # Draft # class User(Base): # __tablename__ = 'user' diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index e69de29..d00b2de 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -0,0 +1,40 @@ +# from abc import ABC, abstractmethod + + +import model + +# drafts +# class AbstractRepository(ABC): +# @abstractmethod +# def add(self, thing): +# raise NotImplementedError +# +# @abstractmethod +# def get(self, ref): +# raise NotImplementedError + + +# repository pattern to structure code +# SqlAlchemy repository to access db +# TODO: comments +class SqlAlchemyRepository: + def __init__(self, session): + self.session = session + + def add_user(self, user: model.Quiz): + self.session.add(user) + + def add_quiz(self, quiz: model.Quiz): + self.session.add(quiz) + + def get_quiz(self, quiz_id): + return self.session.query(model.Quiz).filter_by(id=quiz_id).one() + + def list_quizzes(self, user_id): + return self.session.query(model.User).filter_by(user_id=user_id).all() + + def get_question(self, question_id): + return self.session.query(model.Question).filter_by(id=question_id).one() + + def list_questions(self, quiz_id): + return self.session.query(model.Question).filter_by(quiz_id=quiz_id).all() diff --git a/web-app/domain/model.py b/web-app/domain/model.py index 08bbf31..c2352d2 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -19,6 +19,12 @@ QuestionDescription = NewType('QuestionDescription', str) +class Answer: + def __init__(self, text: QuestionAnswer, is_correct: bool): + self.text = text + self.is_correct = is_correct + + # Possible types of question in a quiz class QuestionTypes(enum.Enum): poll = "polls" @@ -29,11 +35,10 @@ class QuestionTypes(enum.Enum): # question type, description, possible answers, and correct answers class Question: def __init__(self, question_type: QuestionTypes, text: QuestionDescription, - answers: List[QuestionAnswer], correct_answer: QuestionAnswer): + answers: List[Answer]): self.question_type = question_type self.text = text self.answers = answers - self.correct_answer = correct_answer # [maybe deleted] # return next question diff --git a/web-app/tests/test_orm.py b/web-app/tests/test_orm.py new file mode 100644 index 0000000..e69de29 From 8e8d4b1d80dc8558e8ecb522293af99693b33c3a Mon Sep 17 00:00:00 2001 From: rusin Date: Sat, 11 Jun 2022 12:43:46 +0300 Subject: [PATCH 19/42] add styles to registration page (need fixes) --- web-app/static/images/Google_icon.png | Bin 2004 -> 5215 bytes web-app/static/images/inno_icon.png | Bin 3664 -> 4842 bytes web-app/static/images/logo.png | Bin 45724 -> 44588 bytes web-app/static/styles/style.css | 175 ++++++++++++++---- web-app/static/templates/main_page.html | 5 +- .../static/templates/registration_page.html | 71 ++++--- 6 files changed, 182 insertions(+), 69 deletions(-) diff --git a/web-app/static/images/Google_icon.png b/web-app/static/images/Google_icon.png index 41578024f96d863f0e9acee82dfe5ecd6e0ff160..7a057665cc73d4e70d4d94903008f8cd8941807c 100644 GIT binary patch literal 5215 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 Z{Vi_-_QP>k$!Guo002ovPDHLkV1nEO@CN_@ literal 2004 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 QegFUf07*qoM6N<$g5Sw4`Tzg` literal 3664 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 literal 45724 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 diff --git a/web-app/static/styles/style.css b/web-app/static/styles/style.css index 659b6e6..bdb3c0e 100644 --- a/web-app/static/styles/style.css +++ b/web-app/static/styles/style.css @@ -160,16 +160,41 @@ footer { } .Registration { - width: 30%; - height: fit-content; - position: absolute; - margin-left: 35%; - margin-top: 9%; - background-color: aqua; + width: 40%; + height: max-content; + padding: 2%; + padding-top: 0%; + /* margin-left: 35%; + margin-top: 9%; */ + +} + +.Registration_wrapper { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + min-height: 90vh; + position: relative; +} + +.Registration_inner { + padding-bottom: 1%; + padding-top: 5%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + background-color: #3D71A0; +} + +.reg_test { + color: white; + text-align: center; } .email { - height: 2em; + height: 3em; margin: 1%; width: 80%; } @@ -179,47 +204,80 @@ footer { margin-left: 10%; margin: 1%; display: inline-block; - + display: flex; + justify-content: center; } .name, .surname { - width: 40%; - - height: 2em; - + width: 39%; + height: 3em; +} +.name { + margin-right: 2%; } .password { width: 80%; - height: 2em; + height: 3em; margin: 1%; +} +.reg_button { + display: flex; + justify-content: center; + margin-top: 5%; + margin-bottom: 4%; } .reg { - width: 40%; - height: 2em; - + height: 3em; + padding-left: 100%; + padding-right: 100%; margin: 0; + background-color: #142036; + border: #FFFFFF 2px solid; } -.UI { - width: 48px; - height: 48px; - padding: 0px; +.reg_text { + color: #FFFFFF; } + + .img1 { + margin-top: 3%; + margin-bottom: 2%; + width: 30%; + display: flex; + justify-content: space-evenly; + align-items: center; +} + +.circled_UI { + background-color: white; width: 40%; - display: inline; + border-radius: 50%; +} + +.circled_Google { + background-color: white; + width: 40%; + border-radius: 60px; } .google { + width: 48px; + height: 48px; + padding: 0px; +} + +.UI { + width: 48px; + height: 48px; padding: 0px; - margin-bottom: 1.5%; } .buttons { @@ -234,33 +292,37 @@ footer { .main_page { height: max-content; - + } -.logo{ - height: 7%; - width: 7%; +.logo { + width: 10%; + margin-left: 2%; + margin-top: 1%; + margin-bottom: 1%; } -.right{ +.right { width: 50%; height: 100%; display: flex; justify-content: flex-end; } -.left{ +.left { width: 50%; height: 100%; display: flex; justify-content: flex-start; } -.Sign_up { +.Sign_up, +.Back { background-color: #FFFFFF; } -.Sign_up a { +.Sign_up a, +.Back a { color: #0D4278; text-decoration: none; font-size: medium; @@ -269,7 +331,7 @@ footer { .Log_in a { color: #FFFFFF; text-decoration: none; - font-size: medium; + font-size: medium; } .Log_in { @@ -333,7 +395,8 @@ footer { } .Log_in, - .Sign_up { + .Sign_up, + .Back { width: 20%; margin: 1%; height: 3em; @@ -361,7 +424,7 @@ footer { } -@media (max-width:760px) { +@media (max-width:740px) { fieldset { border-width: 10px 5px 10px 5px; border-style: solid; @@ -388,17 +451,20 @@ footer { } .Log_in, - .Sign_up { + .Sign_up, + .Back { width: 60%; - margin: 1%; + margin: 1% 3% 1% 1%; height: 2em; border-radius: 20px; } - .logo{ - width: 5%; - height: 5%; + .logo { + width: 20%; + margin-left: 2%; + margin-top: 1%; + margin-bottom: 1%; } .code { @@ -416,6 +482,31 @@ footer { margin-bottom: 1em; } + + /* Для registrtation_page */ + + .Registration { + width: 80%; + height: max-content; + padding: 2%; + padding-top: 0%; + } + + .logo { + width: 50%; + margin-left: 5%; + margin-top: 3%; + margin-bottom: 3%; + } + + .img1 { + margin-top: 3%; + margin-bottom: 2%; + width: 40%; + display: flex; + justify-content: space-between; + align-items: center; + } } .connect { @@ -438,7 +529,13 @@ footer { .or { margin-top: 4vh; text-align: center; - + +} + +.or_reg { + text-align: center; + color: white; + font-size: large; } diff --git a/web-app/static/templates/main_page.html b/web-app/static/templates/main_page.html index e1e40b1..5c8c199 100644 --- a/web-app/static/templates/main_page.html +++ b/web-app/static/templates/main_page.html @@ -3,7 +3,7 @@ - + @@ -17,7 +17,7 @@

- +
@@ -36,7 +36,6 @@
Join Quiz! -
diff --git a/web-app/static/templates/registration_page.html b/web-app/static/templates/registration_page.html index ba48598..9d5a08e 100644 --- a/web-app/static/templates/registration_page.html +++ b/web-app/static/templates/registration_page.html @@ -1,42 +1,59 @@ + - + Document - -
-

HEADER

-
-
-
-

Registration

- -
- - + + +
+ +
+
+ +
+
+ +
- - - -

or

-
- - -
- -
-
+ +
+
+ +
+ Registration -
-

FOOTER

-
+
- + +
+ + +
+ + +
+

OR

+
+ + +
+
+
+
+
+ + + + + \ No newline at end of file From c3a45f48a757829d5ed6582e6c6b049e3f47242d Mon Sep 17 00:00:00 2001 From: rusin Date: Sat, 11 Jun 2022 13:36:19 +0300 Subject: [PATCH 20/42] fix adaptive layout in reg_page --- web-app/static/images/logo.png | Bin 44588 -> 42146 bytes web-app/static/styles/style.css | 16 ++++++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/web-app/static/images/logo.png b/web-app/static/images/logo.png index 914f3f6a3645b8edd3d97062328b6d5408cbbb44..223f13f1e40977fda6329d7c185e3b92cfd73b62 100644 GIT binary patch literal 42146 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

RjY#y@OD&L5;Wc{AIfNkgkDu9pi%T-2C> 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 literal 44588 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 diff --git a/web-app/static/styles/style.css b/web-app/static/styles/style.css index bdb3c0e..fb15c38 100644 --- a/web-app/static/styles/style.css +++ b/web-app/static/styles/style.css @@ -169,16 +169,9 @@ footer { } -.Registration_wrapper { - width: 100%; - display: flex; - justify-content: center; - align-items: center; - min-height: 90vh; - position: relative; -} .Registration_inner { + width: 100%; padding-bottom: 1%; padding-top: 5%; display: flex; @@ -197,6 +190,7 @@ footer { height: 3em; margin: 1%; width: 80%; + background-color: #DAE3ED; } .namesurname { @@ -212,6 +206,7 @@ footer { .surname { width: 39%; height: 3em; + background-color: #DAE3ED; } .name { @@ -222,6 +217,7 @@ footer { width: 80%; height: 3em; margin: 1%; + background-color: #DAE3ED; } .reg_button { @@ -356,7 +352,7 @@ footer { } -.join_quiz { +.join_quiz, .Registration_wrapper { width: 100%; min-height: 85vh; position: relative; @@ -502,7 +498,7 @@ footer { .img1 { margin-top: 3%; margin-bottom: 2%; - width: 40%; + width: 50%; display: flex; justify-content: space-between; align-items: center; From 2ad52e4e4324bbb255f7898c95c6a8af16d3059f Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:42:27 +0300 Subject: [PATCH 21/42] Main: Resources added (User, Quiz, Question, Answer) flask_app: start basic functions repository: function to get repository from whole project model: without major changes --- web-app/api/resource/AnswerResource.py | 0 web-app/api/resource/QuestionResource.py | 0 web-app/api/resource/QuizResource.py | 0 web-app/api/resource/UserResource.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 web-app/api/resource/AnswerResource.py create mode 100644 web-app/api/resource/QuestionResource.py create mode 100644 web-app/api/resource/QuizResource.py create mode 100644 web-app/api/resource/UserResource.py diff --git a/web-app/api/resource/AnswerResource.py b/web-app/api/resource/AnswerResource.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/api/resource/QuestionResource.py b/web-app/api/resource/QuestionResource.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/api/resource/QuizResource.py b/web-app/api/resource/QuizResource.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/api/resource/UserResource.py b/web-app/api/resource/UserResource.py new file mode 100644 index 0000000..e69de29 From 9792fa490937343d79a41b1d1449075d7d001d75 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:45:17 +0300 Subject: [PATCH 22/42] (Previous commit repeatition) Main: Resources added (User, Quiz, Question, Answer) flask_app: start basic functions repository: function to get repository from whole project model: without major changes --- web-app/adapters/repository.py | 79 +++++++++++++++++++++++- web-app/api/resource/AnswerResource.py | 11 ++++ web-app/api/resource/QuestionResource.py | 16 +++++ web-app/api/resource/QuizResource.py | 34 ++++++++++ web-app/api/resource/UserResource.py | 20 ++++++ web-app/domain/model.py | 4 +- web-app/entrypoints/flask_app.py | 23 +++++++ 7 files changed, 183 insertions(+), 4 deletions(-) diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index d00b2de..282d610 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -1,8 +1,10 @@ # from abc import ABC, abstractmethod - import model +__repo = None + + # drafts # class AbstractRepository(ABC): # @abstractmethod @@ -17,6 +19,7 @@ # repository pattern to structure code # SqlAlchemy repository to access db # TODO: comments +# TODO: command for put request class SqlAlchemyRepository: def __init__(self, session): self.session = session @@ -24,6 +27,10 @@ def __init__(self, session): def add_user(self, user: model.Quiz): self.session.add(user) + def get_user(self, user_id): + return self.session.query(model.User).filter_by(id=user_id).one() + + # Working with quizzes def add_quiz(self, quiz: model.Quiz): self.session.add(quiz) @@ -33,8 +40,78 @@ def get_quiz(self, quiz_id): def list_quizzes(self, user_id): return self.session.query(model.User).filter_by(user_id=user_id).all() + # Working with questions + def add_question(self, question: model.Question): + self.session.add(question) + def get_question(self, question_id): return self.session.query(model.Question).filter_by(id=question_id).one() def list_questions(self, quiz_id): return self.session.query(model.Question).filter_by(quiz_id=quiz_id).all() + + # Working with answers + def add_answer(self, answer: model.Answer): + self.session.add(answer) + + def get_answer(self, answer_id): + return self.session.query(model.Answer).filter_by(id=answer_id) + + def list_answers(self, question_id): + return self.session.query(model.Answer).filter_by(question_id=question_id) + + +# Debug +class FakeRepository: + def add_user(self, user: model.Quiz): + print(f"Adding user {user}") + + def get_user(self, user_id): + print(f"Getting user {user_id}") + + # Working with quizzes + def add_quiz(self, quiz: model.Quiz): + print(f"Adding quiz {quiz}") + + def get_quiz(self, quiz_id): + print(f"Getting quiz {quiz_id}") + + def list_quizzes(self, user_id): + print(f"Getting list of quizzes of user {user_id}") + + # Working with questions + def add_question(self, question: model.Question): + print(f"Adding question {question}") + + def get_question(self, question_id): + print(f"Getting question {question_id}") + + def list_questions(self, quiz_id): + print(f"Getting list of question of quiz {quiz_id}") + + # Working with answers + def add_answer(self, answer: model.Answer): + print(f"Adding answer {answer}") + + def get_answer(self, answer_id): + print(f"Getting answer {answer_id}") + + def list_answers(self, question_id): + print(f"Getting list of answers of question {question_id}") + + +# Debug +__repo = FakeRepository() + + +# To access repo from different modules +def initialize_repo(session): + global __repo + + if not __repo or __repo.__class__ == FakeRepository: + __repo = SqlAlchemyRepository(session) + + +def get_repo(): + global __repo + return __repo diff --git a/web-app/api/resource/AnswerResource.py b/web-app/api/resource/AnswerResource.py index e69de29..c46472e 100644 --- a/web-app/api/resource/AnswerResource.py +++ b/web-app/api/resource/AnswerResource.py @@ -0,0 +1,11 @@ +from flask import jsonify +from flask_restful import reqparse, abort, Resource +from adapters.repository import SqlAlchemyRepository, get_repo +from domain.model import Answer + + +class AnswerResource(Resource): + def get(self, answer_id): + repo = get_repo() + answer = repo.get_answer(answer_id) + return answer diff --git a/web-app/api/resource/QuestionResource.py b/web-app/api/resource/QuestionResource.py index e69de29..ba1956d 100644 --- a/web-app/api/resource/QuestionResource.py +++ b/web-app/api/resource/QuestionResource.py @@ -0,0 +1,16 @@ +from flask import jsonify +from flask_restful import reqparse, abort, Resource +from adapters.repository import SqlAlchemyRepository, get_repo +from domain.model import Question, QuestionTypes + +parser = reqparse.RequestParser() +parser.add_argument('id', required=True, type=int) +parser.add_argument('text', required=True) +parser.add_argument('question_type', required=True, type=QuestionTypes) + + +class QuestionResource(Resource): + def get(self, question_id): + repo = get_repo() + question = repo.get_question(question_id) + return question diff --git a/web-app/api/resource/QuizResource.py b/web-app/api/resource/QuizResource.py index e69de29..8c8345d 100644 --- a/web-app/api/resource/QuizResource.py +++ b/web-app/api/resource/QuizResource.py @@ -0,0 +1,34 @@ +from flask import jsonify +from flask_restful import reqparse, abort, Resource +from adapters.repository import SqlAlchemyRepository, get_repo +from domain.model import Quiz + +parser = reqparse.RequestParser() +parser.add_argument('id', required=True, type=int) +parser.add_argument('name', required=True) +parser.add_argument('questions', required=True) + + +class QuizResource(Resource): + def get(self, quiz_id): + repo = get_repo() + quiz = repo.get_quiz(quiz_id) + return quiz + + +class QuizListResource(Resource): + def get(self): + repo = get_repo() + quizzes = repo.list_quizzes() + return quizzes + + def post(self): + args = parser.parse_args() + repo = get_repo() + + repo.add_quiz(Quiz(quiz_id=args['id'], quiz_name=args['name'])) + + # TODO: parse questions and answers to add them to SQL + # + def post(self): + pass diff --git a/web-app/api/resource/UserResource.py b/web-app/api/resource/UserResource.py index e69de29..7dfc697 100644 --- a/web-app/api/resource/UserResource.py +++ b/web-app/api/resource/UserResource.py @@ -0,0 +1,20 @@ +from flask import jsonify +from flask_restful import reqparse, abort, Resource +from adapters.repository import SqlAlchemyRepository, get_repo + +parser = reqparse.RequestParser() +parser.add_argument() + + +def abort_if_user_not_found(user_id): + repo = get_repo() + user = repo.get_user(user_id) + if not user: + abort(404, message=f"User {user_id} not found") + + +class UserResource(Resource): + def get(self, user_id): + abort_if_user_not_found(user_id) + user = get_repo().get_user(user_id) + return user diff --git a/web-app/domain/model.py b/web-app/domain/model.py index c2352d2..d378735 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -1,7 +1,5 @@ import enum -from dataclasses import dataclass -from datetime import date -from typing import Optional, List, Set, NewType +from typing import List, NewType # Types declaration diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index e69de29..044a32d 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -0,0 +1,23 @@ +from flask import Flask, request +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +import config +from domain import model +from adapters import orm, repository +from service_layer import services + +orm.start_mappers() +# get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) +app = Flask(__name__) + +# @app.route("/api/quiz", methods=["POST", "GET", "PUT"]) +# def quiz_management(): +# session = get_session() +# repo = repository.SqlAlchemyRepository(session) +# +# if request.method == "GET": +# return repo.list_quizzes() +# +# if request.method == "POST": +# repo.add_quiz From b71ea18cdc62bce33872548da4ee44d36475db81 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 11 Jun 2022 16:28:02 +0300 Subject: [PATCH 23/42] Testing API: Amendings in model method - all classes takes the same arguments as their corresponding tables have repository - fake repository added to test API api_initialization - changed urls for API, to get all necessary information for processing request QuizResource - fixed bugs test, test_main - files used for testing --- web-app/adapters/orm.py | 2 +- web-app/adapters/repository.py | 2 +- web-app/domain/model.py | 17 ++++++------- web-app/entrypoints/api/__init__.py | 0 web-app/entrypoints/api/api_initialization.py | 11 +++++++++ .../api/resource/AnswerResource.py | 0 .../api/resource/QuestionResource.py | 0 .../api/resource/QuizResource.py | 19 ++++++++------- .../api/resource/UserResource.py | 4 ++-- web-app/entrypoints/api/resource/__init__.py | 0 web-app/test.py | 24 +++++++++++++++++++ web-app/test_main.py | 12 ++++++++++ 12 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 web-app/entrypoints/api/__init__.py create mode 100644 web-app/entrypoints/api/api_initialization.py rename web-app/{ => entrypoints}/api/resource/AnswerResource.py (100%) rename web-app/{ => entrypoints}/api/resource/QuestionResource.py (100%) rename web-app/{ => entrypoints}/api/resource/QuizResource.py (67%) rename web-app/{ => entrypoints}/api/resource/UserResource.py (88%) create mode 100644 web-app/entrypoints/api/resource/__init__.py create mode 100644 web-app/test.py create mode 100644 web-app/test_main.py diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index f35e740..e7c1826 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import registry from sqlalchemy.orm import mapper -import model +from domain import model # Заглушка # To distinguish between question types diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 282d610..48a3b21 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -1,6 +1,6 @@ # from abc import ABC, abstractmethod -import model +from domain import model __repo = None diff --git a/web-app/domain/model.py b/web-app/domain/model.py index d378735..01048b9 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -18,7 +18,8 @@ class Answer: - def __init__(self, text: QuestionAnswer, is_correct: bool): + def __init__(self, answer_id: int, text: QuestionAnswer, is_correct: bool): + self.answer_id = answer_id self.text = text self.is_correct = is_correct @@ -32,11 +33,11 @@ class QuestionTypes(enum.Enum): # Question class, to store information about question: # question type, description, possible answers, and correct answers class Question: - def __init__(self, question_type: QuestionTypes, text: QuestionDescription, - answers: List[Answer]): + def __init__(self, question_id: int, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int): + self.question_id = question_id self.question_type = question_type self.text = text - self.answers = answers + self.quiz_id = quiz_id # [maybe deleted] # return next question @@ -46,16 +47,16 @@ def next(self): # Quiz class, class Quiz: - def __init__(self, quiz_id: QuizId, quiz_name: QuizName, questions: [Question]): + def __init__(self, quiz_id: QuizId, quiz_name: QuizName, user_id: int = 0): self.quiz_id = quiz_id self.quiz_name = quiz_name - self.questions = questions + self.user_id = user_id # User class, to manage stored in db information through nickname/email class User: - def __init__(self, nickname: NickName, email: Email, password: Password, quizzes: [Quiz]): + def __init__(self, user_id: int, nickname: NickName, email: Email, password: Password): self.nickname = nickname self.email = email self.password = password - self.quizzes = quizzes + self.user_id = user_id diff --git a/web-app/entrypoints/api/__init__.py b/web-app/entrypoints/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/entrypoints/api/api_initialization.py b/web-app/entrypoints/api/api_initialization.py new file mode 100644 index 0000000..094ebef --- /dev/null +++ b/web-app/entrypoints/api/api_initialization.py @@ -0,0 +1,11 @@ +from entrypoints.api.resource import AnswerResource, QuizResource, QuestionResource, UserResource +from flask_restful import Api + + +def api_initialization(app) -> Api: + api = Api(app) + api.add_resource(UserResource.UserResource, '/api/user/') + api.add_resource(QuizResource.QuizResource, '/api/user//quiz/') + api.add_resource(QuizResource.QuizListResource, '/api/user//quiz') + + return api diff --git a/web-app/api/resource/AnswerResource.py b/web-app/entrypoints/api/resource/AnswerResource.py similarity index 100% rename from web-app/api/resource/AnswerResource.py rename to web-app/entrypoints/api/resource/AnswerResource.py diff --git a/web-app/api/resource/QuestionResource.py b/web-app/entrypoints/api/resource/QuestionResource.py similarity index 100% rename from web-app/api/resource/QuestionResource.py rename to web-app/entrypoints/api/resource/QuestionResource.py diff --git a/web-app/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py similarity index 67% rename from web-app/api/resource/QuizResource.py rename to web-app/entrypoints/api/resource/QuizResource.py index 8c8345d..b263150 100644 --- a/web-app/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -6,7 +6,7 @@ parser = reqparse.RequestParser() parser.add_argument('id', required=True, type=int) parser.add_argument('name', required=True) -parser.add_argument('questions', required=True) +parser.add_argument('questions', required=True, type=dict) class QuizResource(Resource): @@ -17,18 +17,21 @@ def get(self, quiz_id): class QuizListResource(Resource): - def get(self): + def get(self, user_id): repo = get_repo() - quizzes = repo.list_quizzes() + quizzes = repo.list_quizzes(user_id) return quizzes - def post(self): + def post(self, user_id): args = parser.parse_args() repo = get_repo() - repo.add_quiz(Quiz(quiz_id=args['id'], quiz_name=args['name'])) + repo.add_quiz(Quiz(quiz_id=args['id'], quiz_name=args['name'], user_id=user_id)) - # TODO: parse questions and answers to add them to SQL - # - def post(self): + # TODO: parse questions and answers to add them to SQL + # + + return jsonify({"success": "OK"}) + + def put(self): pass diff --git a/web-app/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py similarity index 88% rename from web-app/api/resource/UserResource.py rename to web-app/entrypoints/api/resource/UserResource.py index 7dfc697..f2d9690 100644 --- a/web-app/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -2,8 +2,8 @@ from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo -parser = reqparse.RequestParser() -parser.add_argument() +# parser = reqparse.RequestParser() +# parser.add_argument() def abort_if_user_not_found(user_id): diff --git a/web-app/entrypoints/api/resource/__init__.py b/web-app/entrypoints/api/resource/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/test.py b/web-app/test.py new file mode 100644 index 0000000..4f287f3 --- /dev/null +++ b/web-app/test.py @@ -0,0 +1,24 @@ +import json + +from requests import get, post + + +def test_quiz_post_request(): + quiz_post_request_body = { + "id": 500, + "questions": "hello", + "name": "Quiz name" + } + + url = "http://127.0.0.1:8080/api/user/1/quiz" + return post(url, data=quiz_post_request_body).text + + +def test_quiz_get_request(): + url = "http://127.0.0.1:8080/api/user/1/quiz" + return get(url).text + + +# get('http://127.0.0.1:8080/api/quiz') +print(test_quiz_post_request()) +print(test_quiz_get_request()) diff --git a/web-app/test_main.py b/web-app/test_main.py new file mode 100644 index 0000000..8feabb6 --- /dev/null +++ b/web-app/test_main.py @@ -0,0 +1,12 @@ +from flask import Flask + +from adapters import orm +from domain import model +from entrypoints.api import api_initialization + +orm.start_mappers() +app = Flask(__name__) +api = api_initialization.api_initialization(app) + +if __name__ == '__main__': + app.run(port=8080, host='127.0.0.1') From 999defa89ecc10246328c2c82a0f1f01e0fbc917 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 11 Jun 2022 17:19:20 +0300 Subject: [PATCH 24/42] Resources, repository: Declaration of all methods for API !!! There are NOT implemented methods !!! (Added TODOs in comments) Model: deletion fields that are not in DB UserResource: Added one more resource (UserRegistractionResource) to make possible registration requests Api_initialization: added one more api request for UserRegistrationResource Test: added more tests for API --- web-app/adapters/repository.py | 35 +++++++++++++++++-- web-app/domain/model.py | 12 +++---- web-app/entrypoints/api/api_initialization.py | 2 ++ .../api/resource/QuestionResource.py | 13 ++++--- .../entrypoints/api/resource/QuizResource.py | 25 +++++++++---- .../entrypoints/api/resource/UserResource.py | 14 ++++++-- web-app/test.py | 24 ++++++++++--- 7 files changed, 99 insertions(+), 26 deletions(-) diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 48a3b21..53bbf60 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -26,6 +26,7 @@ def __init__(self, session): def add_user(self, user: model.Quiz): self.session.add(user) + self.session.commit() def get_user(self, user_id): return self.session.query(model.User).filter_by(id=user_id).one() @@ -33,16 +34,27 @@ def get_user(self, user_id): # Working with quizzes def add_quiz(self, quiz: model.Quiz): self.session.add(quiz) + self.session.commit() def get_quiz(self, quiz_id): return self.session.query(model.Quiz).filter_by(id=quiz_id).one() + def put_quiz(self, quiz: model.Quiz): + # self.session.query(model.Quiz).f + raise NotImplemented + + def delete_quiz(self, quiz_id): + self.session.query(model.Quiz).filter_by(id=quiz_id).delete(synchronize_session=False) + # TODO: deletion of questions and answers + self.session.commit() + def list_quizzes(self, user_id): return self.session.query(model.User).filter_by(user_id=user_id).all() # Working with questions def add_question(self, question: model.Question): self.session.add(question) + self.session.commit() def get_question(self, question_id): return self.session.query(model.Question).filter_by(id=question_id).one() @@ -53,6 +65,7 @@ def list_questions(self, quiz_id): # Working with answers def add_answer(self, answer: model.Answer): self.session.add(answer) + self.session.commit() def get_answer(self, answer_id): return self.session.query(model.Answer).filter_by(id=answer_id) @@ -61,7 +74,7 @@ def list_answers(self, question_id): return self.session.query(model.Answer).filter_by(question_id=question_id) -# Debug +# Testing Repository class FakeRepository: def add_user(self, user: model.Quiz): print(f"Adding user {user}") @@ -79,6 +92,12 @@ def get_quiz(self, quiz_id): def list_quizzes(self, user_id): print(f"Getting list of quizzes of user {user_id}") + def put_quiz(self, quiz: model.Quiz): + print(f"Changing quiz using {quiz}") + + def delete_quiz(self, quiz_id): + print(f"deletion of quiz {quiz_id}") + # Working with questions def add_question(self, question: model.Question): print(f"Adding question {question}") @@ -89,6 +108,12 @@ def get_question(self, question_id): def list_questions(self, quiz_id): print(f"Getting list of question of quiz {quiz_id}") + def put_question(self, question: model.Question): + print(f"Changing quiz using {question}") + + def delete_question(self, question_id): + print(f"deletion of question {question_id}") + # Working with answers def add_answer(self, answer: model.Answer): print(f"Adding answer {answer}") @@ -99,8 +124,14 @@ def get_answer(self, answer_id): def list_answers(self, question_id): print(f"Getting list of answers of question {question_id}") + def put_answer(self, answer: model.Quiz): + print(f"Changing answer using {answer}") + + def delete_answer(self, answer_id): + print(f"deletion of answer {answer_id}") + -# Debug +# Testing __repo = FakeRepository() diff --git a/web-app/domain/model.py b/web-app/domain/model.py index 01048b9..7418dfb 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -18,8 +18,7 @@ class Answer: - def __init__(self, answer_id: int, text: QuestionAnswer, is_correct: bool): - self.answer_id = answer_id + def __init__(self, text: QuestionAnswer, is_correct: bool): self.text = text self.is_correct = is_correct @@ -33,8 +32,7 @@ class QuestionTypes(enum.Enum): # Question class, to store information about question: # question type, description, possible answers, and correct answers class Question: - def __init__(self, question_id: int, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int): - self.question_id = question_id + def __init__(self, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int): self.question_type = question_type self.text = text self.quiz_id = quiz_id @@ -47,16 +45,14 @@ def next(self): # Quiz class, class Quiz: - def __init__(self, quiz_id: QuizId, quiz_name: QuizName, user_id: int = 0): - self.quiz_id = quiz_id + def __init__(self, quiz_name: QuizName, user_id: int = 0): self.quiz_name = quiz_name self.user_id = user_id # User class, to manage stored in db information through nickname/email class User: - def __init__(self, user_id: int, nickname: NickName, email: Email, password: Password): + def __init__(self, nickname: NickName, email: Email, password: Password): self.nickname = nickname self.email = email self.password = password - self.user_id = user_id diff --git a/web-app/entrypoints/api/api_initialization.py b/web-app/entrypoints/api/api_initialization.py index 094ebef..1d23479 100644 --- a/web-app/entrypoints/api/api_initialization.py +++ b/web-app/entrypoints/api/api_initialization.py @@ -5,6 +5,8 @@ def api_initialization(app) -> Api: api = Api(app) api.add_resource(UserResource.UserResource, '/api/user/') + api.add_resource(UserResource.UserRegistrationResource, '/api/user') + api.add_resource(QuizResource.QuizResource, '/api/user//quiz/') api.add_resource(QuizResource.QuizListResource, '/api/user//quiz') diff --git a/web-app/entrypoints/api/resource/QuestionResource.py b/web-app/entrypoints/api/resource/QuestionResource.py index ba1956d..74ee5fe 100644 --- a/web-app/entrypoints/api/resource/QuestionResource.py +++ b/web-app/entrypoints/api/resource/QuestionResource.py @@ -4,13 +4,18 @@ from domain.model import Question, QuestionTypes parser = reqparse.RequestParser() -parser.add_argument('id', required=True, type=int) -parser.add_argument('text', required=True) -parser.add_argument('question_type', required=True, type=QuestionTypes) +parser.add_argument('answer', required=True) class QuestionResource(Resource): - def get(self, question_id): + def get(self, quiz_id, question_id): repo = get_repo() question = repo.get_question(question_id) return question + + def post(self, quiz_id, question_id): + args = parser.parse_args() + repo = get_repo() + + chosen_answer = args['answer'] + # TODO: Find all answers to questions, and find the chosen one diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index b263150..7f63be5 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -4,9 +4,8 @@ from domain.model import Quiz parser = reqparse.RequestParser() -parser.add_argument('id', required=True, type=int) parser.add_argument('name', required=True) -parser.add_argument('questions', required=True, type=dict) +parser.add_argument('questions', required=True) class QuizResource(Resource): @@ -15,6 +14,23 @@ def get(self, quiz_id): quiz = repo.get_quiz(quiz_id) return quiz + def put(self, quiz_id): + args = parser.parse_args() + repo = get_repo() + repo.put_quiz(Quiz(quiz_id=quiz_id, name=args['name'], user_id=args['user_id'])) + + # TODO: Putting questions + answers + + return jsonify({"success": "OK"}) + + def delete(self, quiz_id): + repo = get_repo() + quiz = repo.delete_quiz(quiz_id) + + # TODO: Deleting questions + answers + + return jsonify({"success": "OK"}) + class QuizListResource(Resource): def get(self, user_id): @@ -26,12 +42,9 @@ def post(self, user_id): args = parser.parse_args() repo = get_repo() - repo.add_quiz(Quiz(quiz_id=args['id'], quiz_name=args['name'], user_id=user_id)) + repo.add_quiz(Quiz(quiz_name=args['name'], user_id=user_id)) # TODO: parse questions and answers to add them to SQL # return jsonify({"success": "OK"}) - - def put(self): - pass diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index f2d9690..2e65f1e 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -1,9 +1,12 @@ from flask import jsonify from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo +from domain.model import User -# parser = reqparse.RequestParser() -# parser.add_argument() +parser = reqparse.RequestParser() +parser.add_argument('nickname', required=True) +parser.add_argument('email', required=True) +parser.add_argument('password', required=True) def abort_if_user_not_found(user_id): @@ -18,3 +21,10 @@ def get(self, user_id): abort_if_user_not_found(user_id) user = get_repo().get_user(user_id) return user + + +class UserRegistrationResource(Resource): + def post(self): + args = parser.parse_args() + repo = get_repo() + repo.add_user(User(nickname=args['nickname'], email=args['email'], password=args['password'])) diff --git a/web-app/test.py b/web-app/test.py index 4f287f3..8eb91b7 100644 --- a/web-app/test.py +++ b/web-app/test.py @@ -6,7 +6,7 @@ def test_quiz_post_request(): quiz_post_request_body = { "id": 500, - "questions": "hello", + "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, "name": "Quiz name" } @@ -19,6 +19,22 @@ def test_quiz_get_request(): return get(url).text -# get('http://127.0.0.1:8080/api/quiz') -print(test_quiz_post_request()) -print(test_quiz_get_request()) +def test_user_get_request(): + url = "http://127.0.0.1:8080/api/user/1" + return get(url).text + + +def test_user_post_request(): + url = "http://127.0.0.1:8080/api/user" + + user_post_request_body = { + 'nickname': 'Il', + 'email': 'il@k.r', + 'password': 'strong_pass' + } + post(url, data=user_post_request_body) + + +# print(test_quiz_post_request()) +# print(test_quiz_get_request()) +print(test_user_post_request()) From cc77a4e79c0d7a71c0963d24f90ece18696063d1 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:07:54 +0300 Subject: [PATCH 25/42] flask_app is ready to start app --- web-app/main.py | 0 web-app/test_main.py | 12 ------------ web-app/{test.py => tests/test_api.py} | 0 3 files changed, 12 deletions(-) create mode 100644 web-app/main.py delete mode 100644 web-app/test_main.py rename web-app/{test.py => tests/test_api.py} (100%) diff --git a/web-app/main.py b/web-app/main.py new file mode 100644 index 0000000..e69de29 diff --git a/web-app/test_main.py b/web-app/test_main.py deleted file mode 100644 index 8feabb6..0000000 --- a/web-app/test_main.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask import Flask - -from adapters import orm -from domain import model -from entrypoints.api import api_initialization - -orm.start_mappers() -app = Flask(__name__) -api = api_initialization.api_initialization(app) - -if __name__ == '__main__': - app.run(port=8080, host='127.0.0.1') diff --git a/web-app/test.py b/web-app/tests/test_api.py similarity index 100% rename from web-app/test.py rename to web-app/tests/test_api.py From e3cabef8b055db5253ba207c7bb4eb95cbd2d539 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:08:49 +0300 Subject: [PATCH 26/42] Repetition of the previous commit: flask_app is ready to start app --- web-app/adapters/repository.py | 2 +- web-app/entrypoints/flask_app.py | 20 ++++++++++---------- web-app/main.py | 3 +++ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 53bbf60..5846708 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -19,7 +19,7 @@ # repository pattern to structure code # SqlAlchemy repository to access db # TODO: comments -# TODO: command for put request +# TODO: command for put, delete request class SqlAlchemyRepository: def __init__(self, session): self.session = session diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 044a32d..e734504 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -6,18 +6,18 @@ from domain import model from adapters import orm, repository from service_layer import services +from entrypoints.api import api_initialization orm.start_mappers() # get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) +# initialize_repo(get_session()) app = Flask(__name__) +api = api_initialization.api_initialization(app) -# @app.route("/api/quiz", methods=["POST", "GET", "PUT"]) -# def quiz_management(): -# session = get_session() -# repo = repository.SqlAlchemyRepository(session) -# -# if request.method == "GET": -# return repo.list_quizzes() -# -# if request.method == "POST": -# repo.add_quiz + +def main(): + app.run(port=8080, host='127.0.0.1') + + +if __name__ == '__main__': + main() diff --git a/web-app/main.py b/web-app/main.py index e69de29..695cac7 100644 --- a/web-app/main.py +++ b/web-app/main.py @@ -0,0 +1,3 @@ +from entrypoints import flask_app + +flask_app.main() From a2aa097e5f8c9367c263e1c2c5237e3de78f76ee Mon Sep 17 00:00:00 2001 From: ADari-Ka Date: Tue, 14 Jun 2022 13:48:05 +0300 Subject: [PATCH 27/42] python in docker --- requirements.txt => web-app/requirements.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename requirements.txt => web-app/requirements.txt (100%) diff --git a/requirements.txt b/web-app/requirements.txt similarity index 100% rename from requirements.txt rename to web-app/requirements.txt From ce040b291dc20c13dbdcddc745ef59fd0204cf83 Mon Sep 17 00:00:00 2001 From: Nertsal Date: Tue, 14 Jun 2022 15:21:01 +0300 Subject: [PATCH 28/42] Fix rust server db uri and docker --- rust-server/.dockerignore | 1 + rust-server/Dockerfile | 3 ++- rust-server/src/database/sql.rs | 4 +--- rust-server/src/main.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 rust-server/.dockerignore diff --git a/rust-server/.dockerignore b/rust-server/.dockerignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/rust-server/.dockerignore @@ -0,0 +1 @@ +target diff --git a/rust-server/Dockerfile b/rust-server/Dockerfile index 8f823b1..f778c46 100644 --- a/rust-server/Dockerfile +++ b/rust-server/Dockerfile @@ -2,4 +2,5 @@ FROM rust:1.61 COPY ./ ./ COPY Cargo.toml Cargo.toml -RUN cargo build --release \ No newline at end of file +RUN cargo build --release +CMD ["./target/release/rust-server"] diff --git a/rust-server/src/database/sql.rs b/rust-server/src/database/sql.rs index c2aaf32..2e413aa 100644 --- a/rust-server/src/database/sql.rs +++ b/rust-server/src/database/sql.rs @@ -10,9 +10,7 @@ pub struct SqlAccess { impl SqlAccess { /// Sets up access to the database and initializes all the neccessary tables pub async fn new(db_uri: &str) -> DBResult { - let pool = DBPool::connect(db_uri) - .await - .expect("Failed to connect to the database"); + let pool = DBPool::connect(db_uri).await?; let sql = Self { pool }.init().await?; Ok(sql) } diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index c26b453..a1c2809 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -12,5 +12,5 @@ async fn launch() -> _ { port: 8000, ..Default::default() }; - server::rocket(config, "postgresql://test:test@localhost:5432").await + server::rocket(config, "postgresql://test:test@postgres_db:5432/test").await } From 6c4fc47e8b6e4c95f779f1510f6ed9de9ce958af Mon Sep 17 00:00:00 2001 From: ADari-Ka Date: Thu, 16 Jun 2022 15:12:40 +0300 Subject: [PATCH 29/42] docker for python --- docker-compose.yaml | 10 ++++++++++ rust-server/Dockerfile | 4 +++- web-app/Dockerfile | 9 +++++++++ web-app/app_start.py | 4 ++++ web-app/entrypoints/flask_app.py | 7 ------- web-app/requirements.txt | 15 +++++++++++++++ 6 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 web-app/Dockerfile create mode 100644 web-app/app_start.py diff --git a/docker-compose.yaml b/docker-compose.yaml index 465a3c0..4c74fa4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,6 +8,16 @@ services: depends_on: - postgres_db + python_service: + build: web-app/ + environment: + FLASK_ENV: production + FLASK_APP: app_start.py + ports: + - 8888:8888 + depends_on: + - postgres_db + postgres_db: image: postgres container_name: database diff --git a/rust-server/Dockerfile b/rust-server/Dockerfile index 8f823b1..2338b34 100644 --- a/rust-server/Dockerfile +++ b/rust-server/Dockerfile @@ -2,4 +2,6 @@ FROM rust:1.61 COPY ./ ./ COPY Cargo.toml Cargo.toml -RUN cargo build --release \ No newline at end of file +# RUN cargo build --release + +ENTRYPOINT ["cargo", "run"] \ No newline at end of file diff --git a/web-app/Dockerfile b/web-app/Dockerfile new file mode 100644 index 0000000..952734a --- /dev/null +++ b/web-app/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9.7 + +COPY requirements.txt ./ +RUN pip install -r requirements.txt +COPY ./ ./ +EXPOSE 8080 +ENTRYPOINT ["flask", "run", "--host=0.0.0.0", "--port=8888"] + +ENV PYTHONPATH=/web-app:/web-app/adapters:/web-app/entrypoints:/web-app/domain:/web-app/service_layer:$PYTHONPATH diff --git a/web-app/app_start.py b/web-app/app_start.py new file mode 100644 index 0000000..1af5a45 --- /dev/null +++ b/web-app/app_start.py @@ -0,0 +1,4 @@ +from entrypoints.flask_app import app + +if __name__ == "__main__": + app.run(port=8888, host='0.0.0.0') diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index e734504..18eb1e2 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -14,10 +14,3 @@ app = Flask(__name__) api = api_initialization.api_initialization(app) - -def main(): - app.run(port=8080, host='127.0.0.1') - - -if __name__ == '__main__': - main() diff --git a/web-app/requirements.txt b/web-app/requirements.txt index e69de29..556c972 100644 --- a/web-app/requirements.txt +++ b/web-app/requirements.txt @@ -0,0 +1,15 @@ +aniso8601==9.0.1 +click==8.1.3 +Flask==2.1.2 +Flask-RESTful==0.3.9 +greenlet==1.1.2 +importlib-metadata==4.11.4 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +pytz==2022.1 +six==1.16.0 +SQLAlchemy==1.4.37 +waitress==2.1.2 +Werkzeug==2.1.2 +zipp==3.8.0 From 7099722dc0790ac477173e71b04339d85be56145 Mon Sep 17 00:00:00 2001 From: ADari-Ka Date: Thu, 16 Jun 2022 16:22:26 +0300 Subject: [PATCH 30/42] python docker fix --- docker-compose.yaml | 2 +- rust-server/src/main.rs | 4 ++-- web-app/Dockerfile | 2 +- web-app/app_start.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4c74fa4..1d2747b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,7 +11,7 @@ services: python_service: build: web-app/ environment: - FLASK_ENV: production + FLASK_ENV: development FLASK_APP: app_start.py ports: - 8888:8888 diff --git a/rust-server/src/main.rs b/rust-server/src/main.rs index a1c2809..d8b4f82 100644 --- a/rust-server/src/main.rs +++ b/rust-server/src/main.rs @@ -8,8 +8,8 @@ mod tests; async fn launch() -> _ { let config = rocket::Config { log_level: rocket::log::LogLevel::Debug, - address: std::net::Ipv4Addr::new(127, 0, 0, 1).into(), - port: 8000, + address: std::net::Ipv4Addr::new(0, 0, 0, 0).into(), + port: 8080, ..Default::default() }; server::rocket(config, "postgresql://test:test@postgres_db:5432/test").await diff --git a/web-app/Dockerfile b/web-app/Dockerfile index 952734a..dcfcd3a 100644 --- a/web-app/Dockerfile +++ b/web-app/Dockerfile @@ -4,6 +4,6 @@ COPY requirements.txt ./ RUN pip install -r requirements.txt COPY ./ ./ EXPOSE 8080 -ENTRYPOINT ["flask", "run", "--host=0.0.0.0", "--port=8888"] +ENTRYPOINT ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=8888"] ENV PYTHONPATH=/web-app:/web-app/adapters:/web-app/entrypoints:/web-app/domain:/web-app/service_layer:$PYTHONPATH diff --git a/web-app/app_start.py b/web-app/app_start.py index 1af5a45..7247b59 100644 --- a/web-app/app_start.py +++ b/web-app/app_start.py @@ -1,4 +1,4 @@ from entrypoints.flask_app import app if __name__ == "__main__": - app.run(port=8888, host='0.0.0.0') + app.run() From ccfd1cd17adac3ea0a07bd104e5d08c6c74a7118 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 18 Jun 2022 17:17:47 +0300 Subject: [PATCH 31/42] Orm, repository amendings (little changes). Flask app configuration (for future db) Added tests for API --- web-app/adapters/orm.py | 30 ------------------- web-app/adapters/repository.py | 2 +- web-app/config.py | 2 ++ web-app/entrypoints/api/api_initialization.py | 2 ++ web-app/entrypoints/flask_app.py | 8 +++-- web-app/requirements.txt | 1 + web-app/tests/test_api.py | 15 ++++++---- 7 files changed, 22 insertions(+), 38 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index e7c1826..130f68f 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -56,33 +56,3 @@ def start_mappers(): if __name__ == '__main__': start_mappers() -# Draft -# class User(Base): -# __tablename__ = 'user' -# -# id = Column(Integer, primary_key=True, autoincrement=True) -# nickname = Column(Text, unique=True) -# email = Column(Text, unique=True) -# password = Column(Text) -# -# -# class Quiz(Base): -# __tablename__ = 'quiz' -# -# id = Column(Integer, primary_key=True, autoincrement=True) -# user_id = Column(Integer, ForeignKey('user.id')) -# quiz_name = Column(Text) -# -# -# class Question(Base): -# __tablename__ = 'question' -# -# id = Column(Integer, primary_key=True, autoincrement=True) -# quiz_id = Column(Integer, ForeignKey('quiz.id')) -# type = Column(Enum(QuestionTypes)) -# description = Column(Text) -# possible_answers = () -# -# -# class Answers(Base): -# pass diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 5846708..f8b80cc 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -24,7 +24,7 @@ class SqlAlchemyRepository: def __init__(self, session): self.session = session - def add_user(self, user: model.Quiz): + def add_user(self, user: model.User): self.session.add(user) self.session.commit() diff --git a/web-app/config.py b/web-app/config.py index e69de29..cf9f1d3 100644 --- a/web-app/config.py +++ b/web-app/config.py @@ -0,0 +1,2 @@ +def get_postgres_uri(): + return "postgresql://test:test@postgres_db:5432/test" diff --git a/web-app/entrypoints/api/api_initialization.py b/web-app/entrypoints/api/api_initialization.py index 1d23479..099b23b 100644 --- a/web-app/entrypoints/api/api_initialization.py +++ b/web-app/entrypoints/api/api_initialization.py @@ -10,4 +10,6 @@ def api_initialization(app) -> Api: api.add_resource(QuizResource.QuizResource, '/api/user//quiz/') api.add_resource(QuizResource.QuizListResource, '/api/user//quiz') + api.add_resource(QuestionResource.QuestionResource, '/api/quiz//question/') + return api diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 18eb1e2..4c783fa 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -9,8 +9,12 @@ from entrypoints.api import api_initialization orm.start_mappers() -# get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) -# initialize_repo(get_session()) +get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) +repository.initialize_repo(get_session()) app = Flask(__name__) api = api_initialization.api_initialization(app) + +@app.route('/') +def index_page(): + return "

Hello, world

" diff --git a/web-app/requirements.txt b/web-app/requirements.txt index 556c972..287cc0f 100644 --- a/web-app/requirements.txt +++ b/web-app/requirements.txt @@ -13,3 +13,4 @@ SQLAlchemy==1.4.37 waitress==2.1.2 Werkzeug==2.1.2 zipp==3.8.0 +psycopg2==2.9.3 diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 8eb91b7..bdd2087 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -5,13 +5,12 @@ def test_quiz_post_request(): quiz_post_request_body = { - "id": 500, "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, "name": "Quiz name" } - url = "http://127.0.0.1:8080/api/user/1/quiz" - return post(url, data=quiz_post_request_body).text + url = "http://127.0.0.1:8888/api/user/1/quiz" + return post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}).text def test_quiz_get_request(): @@ -25,16 +24,22 @@ def test_user_get_request(): def test_user_post_request(): - url = "http://127.0.0.1:8080/api/user" + url = "http://127.0.0.1:8888/api/user" user_post_request_body = { 'nickname': 'Il', 'email': 'il@k.r', 'password': 'strong_pass' } - post(url, data=user_post_request_body) + return post(url, data=json.dumps(user_post_request_body), headers={"Content-Type": "application/json"}).text + + +def test_question_get_request(): + url = "http://127.0.0.1:8888/api/quiz/1/question/2" + return get(url).text # print(test_quiz_post_request()) # print(test_quiz_get_request()) print(test_user_post_request()) +# print(test_question_get_request()) From bb7639db503045ba0a904e260213446ce6c30ac7 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Mon, 20 Jun 2022 13:57:33 +0300 Subject: [PATCH 32/42] Test_api: Added tests for quizzes and questions Flask app: switches to fake repository --- web-app/entrypoints/flask_app.py | 4 +-- web-app/tests/test_api.py | 42 +++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 4c783fa..1d62f10 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -9,8 +9,8 @@ from entrypoints.api import api_initialization orm.start_mappers() -get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) -repository.initialize_repo(get_session()) +# get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) +# repository.initialize_repo(get_session()) app = Flask(__name__) api = api_initialization.api_initialization(app) diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index bdd2087..aaff181 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -1,9 +1,9 @@ import json -from requests import get, post +from requests import get, post, delete -def test_quiz_post_request(): +def test_quizzes_post_request(): quiz_post_request_body = { "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, "name": "Quiz name" @@ -14,10 +14,39 @@ def test_quiz_post_request(): def test_quiz_get_request(): - url = "http://127.0.0.1:8080/api/user/1/quiz" + url = "http://127.0.0.1:8080/api/user/1/quiz/1" return get(url).text +def test_quiz_delete_request(): + quiz_post_request_body = { + "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, + "name": "Quiz name" + } + url = "http://127.0.0.1:8888/api/user/1/quiz" + + post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}) + + return delete(url + "/1").text + + +def test_quizzes_get_request(): + url = "http://127.0.0.1:8888/api/user/1/quiz" + + return get(url).text + + +def test_quiz_put_request(): + url = "http://127.0.0.1:8888/api/user/1/quiz" + + quiz_put_request_body = { + "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, + "name": "Quiz name" + } + + return post(url, json=json.dumps(quiz_put_request_body), headers={"Content-Type": "application/json"}).text + + def test_user_get_request(): url = "http://127.0.0.1:8080/api/user/1" return get(url).text @@ -39,6 +68,13 @@ def test_question_get_request(): return get(url).text +def test_question_post_request(): + url = "http://127.0.0.1:8888/api/quiz/1/question/2" + + question_post_request_body = {"answers": ["Incorrect answer", "Correct answer"]} + return post(url, data=json.dumps(question_post_request_body), headers={"Content-Type": "application/json"}).text + + # print(test_quiz_post_request()) # print(test_quiz_get_request()) print(test_user_post_request()) From 497e75e97d6f66a6974ddd90742a7ff199474090 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Tue, 21 Jun 2022 16:12:31 +0300 Subject: [PATCH 33/42] Webhandlers for routes: '/' '/lobby' '/login' '/register' Other changes: flask_app: webhandlers for pages and tests model: Added 'to_dict' operation repository: Added debug operations ('get_users') orm: Some tables corrected Creation of tables tests: test_api - small changes test_orm - first test for orm (user_creation) --- web-app/adapters/orm.py | 17 +++++----- web-app/adapters/repository.py | 6 +++- web-app/domain/model.py | 35 +++++++++++++++++---- web-app/entrypoints/flask_app.py | 54 ++++++++++++++++++++++++++++---- web-app/tests/test_api.py | 14 +++++---- web-app/tests/test_orm.py | 16 ++++++++++ 6 files changed, 115 insertions(+), 27 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 130f68f..f48eafb 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -4,11 +4,6 @@ from domain import model -# Заглушка -# To distinguish between question types -# class QuestionTypes: -# pass - # Base = declarative_base() mapper_registry = registry() @@ -30,20 +25,21 @@ # Table for quizzes quiz_table = Table("quizzes", metadata, - Column('user_id', Integer, ForeignKey('user.id')), + Column('id', Integer, primary_key=True, autoincrement=True), + Column('user_id', Integer, ForeignKey('users.id')), Column('name', Text)) # Table for questions question_table = Table("questions", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('quiz_id', Integer, ForeignKey('quiz.id')), + Column('quiz_id', Integer, ForeignKey('quizzes.id')), Column('type', Enum(model.QuestionTypes)), Column('description', Text)) # Table for answers answer_table = Table("answers", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('question_id', ForeignKey('question.id')), + Column('question_id', ForeignKey('questions.id')), Column('text', Text), Column('correct_answer', Boolean)) @@ -54,5 +50,10 @@ def start_mappers(): answer_mapper = mapper_registry.map_imperatively(model.Answer, answer_table) +def create_all(engine): + metadata.bind = engine + metadata.create_all() + + if __name__ == '__main__': start_mappers() diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index f8b80cc..9a89147 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -29,7 +29,7 @@ def add_user(self, user: model.User): self.session.commit() def get_user(self, user_id): - return self.session.query(model.User).filter_by(id=user_id).one() + return self.session.query(model.User).filter_by(id=user_id).one().__dict__() # Working with quizzes def add_quiz(self, quiz: model.Quiz): @@ -73,6 +73,10 @@ def get_answer(self, answer_id): def list_answers(self, question_id): return self.session.query(model.Answer).filter_by(question_id=question_id) + # -- for test + def get_users(self): + return self.session.query(model.User).all() + # Testing Repository class FakeRepository: diff --git a/web-app/domain/model.py b/web-app/domain/model.py index 7418dfb..01eecd6 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -18,9 +18,17 @@ class Answer: - def __init__(self, text: QuestionAnswer, is_correct: bool): + def __init__(self, question_id: int, text: QuestionAnswer, correct_answer: bool): self.text = text - self.is_correct = is_correct + self.correct_answer = correct_answer + self.question_id = question_id + + def to_dict(self): + return { + "text": self.text, + "correct_answer": self.correct_answer, + "question_id": self.question_id + } # Possible types of question in a quiz @@ -37,10 +45,12 @@ def __init__(self, question_type: QuestionTypes, text: QuestionDescription, quiz self.text = text self.quiz_id = quiz_id - # [maybe deleted] - # return next question - def next(self): - pass + def to_dict(self): + return { + "quiz_id": self.quiz_id, + "text": self.text, + "question_type": self.question_type + } # Quiz class, @@ -49,6 +59,12 @@ def __init__(self, quiz_name: QuizName, user_id: int = 0): self.quiz_name = quiz_name self.user_id = user_id + def to_dict(self): + return { + "user_id": self.user_id, + "quiz_name": self.quiz_name + } + # User class, to manage stored in db information through nickname/email class User: @@ -56,3 +72,10 @@ def __init__(self, nickname: NickName, email: Email, password: Password): self.nickname = nickname self.email = email self.password = password + + def to_dict(self): + return { + "nickname": self.nickname, + "email": self.email, + "password": self.password + } diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 1d62f10..47a947d 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -1,4 +1,6 @@ -from flask import Flask, request +import json + +from flask import Flask, request, render_template from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -7,14 +9,54 @@ from adapters import orm, repository from service_layer import services from entrypoints.api import api_initialization +from tests import test_orm +engine = create_engine(config.get_postgres_uri()) +get_session = sessionmaker(bind=engine) +repository.initialize_repo(get_session()) orm.start_mappers() -# get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) -# repository.initialize_repo(get_session()) -app = Flask(__name__) +orm.create_all(engine) +app = Flask(__name__, template_folder='./../static/templates') api = api_initialization.api_initialization(app) @app.route('/') -def index_page(): - return "

Hello, world

" +def main_page(): + return render_template("main_page.html") + + +@app.route('/login') +def login_page(): + return render_template('authorization_page.html') + + +@app.route('/register') +def registration_page(): + return render_template("registration_page.html") + + +# [will be deleted] +@app.route('/lobby') +def lobby_page(): + return render_template("waiting_hall.html") + + +# For tests through docker +@app.route('/tests') +def test(): + repo = repository.get_repo() + + new_items = list() + for item in repo.get_users(): + new_items.append(item.to_dict()) + + return json.dumps({"items": new_items}) + + +@app.route('/tests/user_creation') +def test_user_creation( ): + repo = repository.get_repo() + + test_orm.test_add_user(repo) + + return "

OK

" diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index aaff181..9b94f6e 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -10,12 +10,13 @@ def test_quizzes_post_request(): } url = "http://127.0.0.1:8888/api/user/1/quiz" - return post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}).text + assert post(url, data=json.dumps(quiz_post_request_body), + headers={"Content-Type": "application/json"}).status_code == 200 def test_quiz_get_request(): - url = "http://127.0.0.1:8080/api/user/1/quiz/1" - return get(url).text + url = "http://127.0.0.1:8888/api/user/1/quiz/1" + assert get(url) == 200 def test_quiz_delete_request(): @@ -27,7 +28,7 @@ def test_quiz_delete_request(): post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}) - return delete(url + "/1").text + assert delete(url + "/1").status_code == 200 def test_quizzes_get_request(): @@ -48,7 +49,7 @@ def test_quiz_put_request(): def test_user_get_request(): - url = "http://127.0.0.1:8080/api/user/1" + url = "http://127.0.0.1:8888/api/user/1" return get(url).text @@ -77,5 +78,6 @@ def test_question_post_request(): # print(test_quiz_post_request()) # print(test_quiz_get_request()) -print(test_user_post_request()) +# print(test_user_post_request()) # print(test_question_get_request()) +print(test_user_get_request()) diff --git a/web-app/tests/test_orm.py b/web-app/tests/test_orm.py index e69de29..ec9a9ad 100644 --- a/web-app/tests/test_orm.py +++ b/web-app/tests/test_orm.py @@ -0,0 +1,16 @@ +from adapters import repository +from domain import model + + +def test_add_user(repo): + new_user = model.User("Fedor", "fedya@gg.ru", "CoolPass") + + repo.add_user(new_user) + db_user = repo.session.query(model.User).filter_by(nickname=new_user.nickname).one() + + assert db_user == new_user + + repo.session.query(model.User).filter_by(nickname=new_user.nickname).delete() + repo.session.commit() + + print("OK") From 8b912ccc2b485ad59aaca16a2e42a65cf51a29b3 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:21:27 +0300 Subject: [PATCH 34/42] Structure of Model changed: user: -nickname, +name, +surname question: +time_limit --- web-app/domain/model.py | 21 ++++++++++++------- .../entrypoints/api/resource/UserResource.py | 3 ++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web-app/domain/model.py b/web-app/domain/model.py index 01eecd6..c9add43 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -4,7 +4,8 @@ # Types declaration # For user -NickName = NewType('NickName', str) +Name = NewType('Name', str) +Surname = NewType('Surname', str) Email = NewType('Email', str) Password = NewType('Password', str) @@ -33,23 +34,25 @@ def to_dict(self): # Possible types of question in a quiz class QuestionTypes(enum.Enum): - poll = "polls" + poll = "poll" quiz = "quiz" # Question class, to store information about question: # question type, description, possible answers, and correct answers class Question: - def __init__(self, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int): + def __init__(self, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int, time_limit: int): self.question_type = question_type self.text = text self.quiz_id = quiz_id + self.time = time_limit def to_dict(self): return { "quiz_id": self.quiz_id, "text": self.text, - "question_type": self.question_type + "question_type": self.question_type, + "time": self.time } @@ -62,20 +65,22 @@ def __init__(self, quiz_name: QuizName, user_id: int = 0): def to_dict(self): return { "user_id": self.user_id, - "quiz_name": self.quiz_name + "name": self.quiz_name } # User class, to manage stored in db information through nickname/email class User: - def __init__(self, nickname: NickName, email: Email, password: Password): - self.nickname = nickname + def __init__(self, name: Name, surname: Surname, email: Email, password: Password): + self.name = name + self.surname = surname self.email = email self.password = password def to_dict(self): return { - "nickname": self.nickname, + "name": self.name, + "surname": self.surname, "email": self.email, "password": self.password } diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index 2e65f1e..239fcfd 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -4,7 +4,8 @@ from domain.model import User parser = reqparse.RequestParser() -parser.add_argument('nickname', required=True) +parser.add_argument('name', required=True) +parser.add_argument('surname', required=True) parser.add_argument('email', required=True) parser.add_argument('password', required=True) From e14411b43a23c6b0ddb96ec3b1150395da9fc719 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:34:08 +0300 Subject: [PATCH 35/42] Quiz and User Resources changes: \nAll requests require user credentials (email, password) --- .../entrypoints/api/resource/QuizResource.py | 22 ++++++++++++++----- .../entrypoints/api/resource/UserResource.py | 20 ++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 7f63be5..36479cb 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -3,19 +3,29 @@ from adapters.repository import SqlAlchemyRepository, get_repo from domain.model import Quiz -parser = reqparse.RequestParser() -parser.add_argument('name', required=True) -parser.add_argument('questions', required=True) +quiz_creation_parser = reqparse.RequestParser() +quiz_creation_parser.add_argument('name', required=True) +quiz_creation_parser.add_argument('questions', required=True) +quiz_creation_parser.add_argument('email', required=True) +quiz_creation_parser.add_argument('password', required=True) + +quiz_get_parser = reqparse.RequestParser() +quiz_get_parser.add_argument('email', required=True) +quiz_get_parser.add_argument('password', required=True) + + +# TODO: checking credentials class QuizResource(Resource): def get(self, quiz_id): + args = quiz_get_parser.parse_args() repo = get_repo() quiz = repo.get_quiz(quiz_id) return quiz def put(self, quiz_id): - args = parser.parse_args() + args = quiz_creation_parser.parse_args() repo = get_repo() repo.put_quiz(Quiz(quiz_id=quiz_id, name=args['name'], user_id=args['user_id'])) @@ -24,6 +34,7 @@ def put(self, quiz_id): return jsonify({"success": "OK"}) def delete(self, quiz_id): + args = quiz_get_parser.parse_args() repo = get_repo() quiz = repo.delete_quiz(quiz_id) @@ -34,12 +45,13 @@ def delete(self, quiz_id): class QuizListResource(Resource): def get(self, user_id): + args = quiz_get_parser.parse_args() repo = get_repo() quizzes = repo.list_quizzes(user_id) return quizzes def post(self, user_id): - args = parser.parse_args() + args = quiz_creation_parser.parse_args() repo = get_repo() repo.add_quiz(Quiz(quiz_name=args['name'], user_id=user_id)) diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index 239fcfd..7d4cb52 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -3,11 +3,15 @@ from adapters.repository import SqlAlchemyRepository, get_repo from domain.model import User -parser = reqparse.RequestParser() -parser.add_argument('name', required=True) -parser.add_argument('surname', required=True) -parser.add_argument('email', required=True) -parser.add_argument('password', required=True) +registration_parser = reqparse.RequestParser() +registration_parser.add_argument('name', required=True) +registration_parser.add_argument('surname', required=True) +registration_parser.add_argument('email', required=True) +registration_parser.add_argument('password', required=True) + +login_parser = reqparse.RequestParser() +login_parser.add_argument('email', required=True) +login_parser.add_argument('password', required=True) def abort_if_user_not_found(user_id): @@ -19,6 +23,10 @@ def abort_if_user_not_found(user_id): class UserResource(Resource): def get(self, user_id): + args = login_parser.parse_args() + + # check_user(user_from(args), user_id) TODO: Checking user credentials with db info and requested user page + abort_if_user_not_found(user_id) user = get_repo().get_user(user_id) return user @@ -26,6 +34,6 @@ def get(self, user_id): class UserRegistrationResource(Resource): def post(self): - args = parser.parse_args() + args = registration_parser.parse_args() repo = get_repo() repo.add_user(User(nickname=args['nickname'], email=args['email'], password=args['password'])) From ccfe5bde085c5517b2100bba377f8cc82ca3d9f7 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 25 Jun 2022 13:04:45 +0300 Subject: [PATCH 36/42] Beggining of Front-end and Back-end sides connection Amendings of Resources of API Creating algorithms to create user, quiz, and add them to DB Implemented support function to process data in services --- web-app/adapters/orm.py | 11 ++- web-app/adapters/repository.py | 18 ++++- .../entrypoints/api/resource/QuizResource.py | 65 +++++++++++++++--- .../entrypoints/api/resource/UserResource.py | 45 +++++++++--- web-app/entrypoints/flask_app.py | 29 +++++++- web-app/requirements.txt | 1 + web-app/service_layer/services.py | 68 +++++++++++++++++++ web-app/tests/test_api.py | 8 ++- 8 files changed, 216 insertions(+), 29 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index f48eafb..69596f1 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -4,7 +4,6 @@ from domain import model - # Base = declarative_base() mapper_registry = registry() metadata = mapper_registry.metadata @@ -19,7 +18,8 @@ # Table for users user_table = Table("users", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('nickname', Text, unique=True), + Column('name', Text), + Column('surname', Text), Column('email', Text, unique=True), Column('password', Text)) @@ -34,7 +34,8 @@ Column('id', Integer, primary_key=True, autoincrement=True), Column('quiz_id', Integer, ForeignKey('quizzes.id')), Column('type', Enum(model.QuestionTypes)), - Column('description', Text)) + Column('description', Text), + Column('time', Integer)) # Table for answers answer_table = Table("answers", metadata, @@ -55,5 +56,9 @@ def create_all(engine): metadata.create_all() +def delete_all(engine): + metadata.drop_all(engine) + + if __name__ == '__main__': start_mappers() diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 9a89147..eb7951f 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -28,13 +28,18 @@ def add_user(self, user: model.User): self.session.add(user) self.session.commit() - def get_user(self, user_id): - return self.session.query(model.User).filter_by(id=user_id).one().__dict__() + def get_user_by_id(self, user_id): + return self.session.query(model.User).filter_by(id=user_id).one() + + def get_user_by_email(self, email): + return self.session.query(model.User).filter_by(email=email).one() # Working with quizzes - def add_quiz(self, quiz: model.Quiz): + def add_quiz(self, quiz: model.Quiz) -> int: self.session.add(quiz) self.session.commit() + self.session.flush() + return quiz.id def get_quiz(self, quiz_id): return self.session.query(model.Quiz).filter_by(id=quiz_id).one() @@ -55,6 +60,8 @@ def list_quizzes(self, user_id): def add_question(self, question: model.Question): self.session.add(question) self.session.commit() + self.session.flush() + return question.id def get_question(self, question_id): return self.session.query(model.Question).filter_by(id=question_id).one() @@ -66,6 +73,8 @@ def list_questions(self, quiz_id): def add_answer(self, answer: model.Answer): self.session.add(answer) self.session.commit() + self.session.flush() + return answer.id def get_answer(self, answer_id): return self.session.query(model.Answer).filter_by(id=answer_id) @@ -77,6 +86,9 @@ def list_answers(self, question_id): def get_users(self): return self.session.query(model.User).all() + def get_quizzes(self): + return self.session.query(model.Quiz).all() + # Testing Repository class FakeRepository: diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 36479cb..3e514d3 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -1,7 +1,8 @@ -from flask import jsonify +from flask import jsonify, make_response from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo from domain.model import Quiz +from service_layer import services quiz_creation_parser = reqparse.RequestParser() quiz_creation_parser.add_argument('name', required=True) @@ -14,18 +15,51 @@ quiz_get_parser.add_argument('password', required=True) -# TODO: checking credentials +def check_for_credentials(args: dict): + email, password = services.email_and_pass_from(args) + + repo = get_repo() + user = repo.get_user_by_email(email) + + if not user: + abort(404, message="Unauthorized") + + if not user.password == password: + abort(404, message="Unauthorized") + + +def abort_if_user_not_found(user_id: int, args: dict): + email, password = services.email_and_pass_from(args) + + repo = get_repo() + user = repo.get_user_by_id(user_id) + if not user or user.email != email or user.password != password: + abort(404, message="Unauthorized") + + +def abort_if_quiz_not_found(quiz_id): + repo = get_repo() + quiz = repo.get_quiz(quiz_id) + + if not quiz: + abort(404, message="Quiz is not found") class QuizResource(Resource): def get(self, quiz_id): + abort_if_quiz_not_found(quiz_id) + args = quiz_get_parser.parse_args() + check_for_credentials(args) + repo = get_repo() quiz = repo.get_quiz(quiz_id) - return quiz + return make_response(quiz.to_dict(), 200) def put(self, quiz_id): args = quiz_creation_parser.parse_args() + check_for_credentials(args) + repo = get_repo() repo.put_quiz(Quiz(quiz_id=quiz_id, name=args['name'], user_id=args['user_id'])) @@ -35,28 +69,41 @@ def put(self, quiz_id): def delete(self, quiz_id): args = quiz_get_parser.parse_args() + check_for_credentials(args) + repo = get_repo() quiz = repo.delete_quiz(quiz_id) # TODO: Deleting questions + answers - return jsonify({"success": "OK"}) + return make_response("Quiz deleted", 200) class QuizListResource(Resource): def get(self, user_id): args = quiz_get_parser.parse_args() + + abort_if_user_not_found(user_id, args) + repo = get_repo() quizzes = repo.list_quizzes(user_id) - return quizzes + return make_response({"quizzes": [quiz.to_dict() for quiz in quizzes]}, 200) def post(self, user_id): args = quiz_creation_parser.parse_args() + + abort_if_user_not_found(user_id, args) + repo = get_repo() - repo.add_quiz(Quiz(quiz_name=args['name'], user_id=user_id)) + quiz = services.quiz_from(args, user_id) + quiz_id = repo.add_quiz(quiz) - # TODO: parse questions and answers to add them to SQL - # + questions_and_answers = services.questions_of_quiz_creation_from(args, quiz_id) - return jsonify({"success": "OK"}) + for (question, answers_args) in questions_and_answers: + question_id = repo.add_question(question) + for answers in services.answers_for_one_question_of_quiz_creation_from(answers_args, question_id): + repo.add_answer(answers) + + return make_response("Quiz created", 201) diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index 7d4cb52..df53426 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -1,7 +1,9 @@ -from flask import jsonify +import flask +from flask import jsonify, make_response from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo from domain.model import User +from service_layer import services registration_parser = reqparse.RequestParser() registration_parser.add_argument('name', required=True) @@ -14,26 +16,49 @@ login_parser.add_argument('password', required=True) -def abort_if_user_not_found(user_id): +def check_for_credentials(args: dict): + email, password = services.email_and_pass_from(args) + repo = get_repo() - user = repo.get_user(user_id) + user = repo.get_user_by_email(email) + if not user: - abort(404, message=f"User {user_id} not found") + abort(404, message="Unauthorized") + + if not user.password == password: + abort(404, message="Unauthorized") + + +def abort_if_user_not_found(user_id: int, args: dict): + email, password = services.email_and_pass_from(args) + + repo = get_repo() + user = repo.get_user_by_id(user_id) + if not user or user.email != email or user.password != password: + abort(404, message="Unauthorized") class UserResource(Resource): def get(self, user_id): args = login_parser.parse_args() + abort_if_user_not_found(user_id, args) + user = get_repo().get_user_by_id(user_id) + return make_response("User exists", 200) - # check_user(user_from(args), user_id) TODO: Checking user credentials with db info and requested user page - abort_if_user_not_found(user_id) - user = get_repo().get_user(user_id) - return user +class UserRegistrationResource(Resource): + def get(self): + args = login_parser.parse_args() + check_for_credentials(args) + + return make_response("User exists", 200) -class UserRegistrationResource(Resource): def post(self): args = registration_parser.parse_args() repo = get_repo() - repo.add_user(User(nickname=args['nickname'], email=args['email'], password=args['password'])) + + user = services.user_from(args) + + repo.add_user(user) + return make_response("User created", 201) diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 47a947d..62e9314 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -1,6 +1,7 @@ import json from flask import Flask, request, render_template +from flask_cors import CORS from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -15,9 +16,14 @@ get_session = sessionmaker(bind=engine) repository.initialize_repo(get_session()) orm.start_mappers() + +# To delete all tables +# orm.delete_all(engine) + orm.create_all(engine) app = Flask(__name__, template_folder='./../static/templates') api = api_initialization.api_initialization(app) +cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) @app.route('/') @@ -42,7 +48,7 @@ def lobby_page(): # For tests through docker -@app.route('/tests') +@app.route('/tests/users') def test(): repo = repository.get_repo() @@ -54,9 +60,28 @@ def test(): @app.route('/tests/user_creation') -def test_user_creation( ): +def test_user_creation(): repo = repository.get_repo() test_orm.test_add_user(repo) return "

OK

" + + +@app.route('/tests/quizzes') +def test_quizzes_list(): + repo = repository.get_repo() + + new_items = list() + for item in repo.get_quizzes(): + new_items.append(item.to_dict()) + + return json.dumps({"items": new_items}) + + +@app.route('/tests/get_user_by_email/') +def test_user_by_email(email): + repo = repository.get_repo() + + user = repo.get_user_by_email(email) + return json.dumps({"user": user.to_dict()}) diff --git a/web-app/requirements.txt b/web-app/requirements.txt index 287cc0f..60e6639 100644 --- a/web-app/requirements.txt +++ b/web-app/requirements.txt @@ -14,3 +14,4 @@ waitress==2.1.2 Werkzeug==2.1.2 zipp==3.8.0 psycopg2==2.9.3 +flask-cors==3.0.10 diff --git a/web-app/service_layer/services.py b/web-app/service_layer/services.py index e69de29..ad342f6 100644 --- a/web-app/service_layer/services.py +++ b/web-app/service_layer/services.py @@ -0,0 +1,68 @@ +from domain import model + + +# user_from gets args and creates User class +# user_args should contain following: +# name +# surname +# email +# password +def user_from(user_args: dict) -> model.User: + user_name = user_args['name'] + user_surname = user_args['surname'] + user_email = user_args['email'] + user_password = user_args['password'] + + return model.User( + name=user_name, + surname=user_surname, + email=user_email, + password=user_password + ) + + +def quiz_from(quiz_args: dict, user_id: int) -> model.Quiz: + quiz = model.Quiz( + quiz_name=quiz_args["name"], + user_id=user_id + ) + return quiz + + +def questions_of_quiz_creation_from(quiz_args: dict, quiz_id: int) -> [(model.Question, dict)]: + questions = [ + (model.Question( + question_type=question_args["question_type"], + text=question_args["text"], + time_limit=question_args["time"], + quiz_id=quiz_id + ), question_args["answers"]) + for question_args in quiz_args["questions"] + ] + + return questions + + +def answers_for_one_question_of_quiz_creation_from(answers_args: dict, question_id) -> [model.Answer]: + answers = [ + model.Answer( + text=answer_args["text"], + correct_answer=answer_args["correct_answer"], + question_id=question_id + ) + for answer_args in answers_args + ] + return answers + + +def email_and_pass_from(args: dict) -> (str, str): + return args['email'], args['password'] + + +def user_from(user_args) -> model.User: + return model.User( + name=user_args["name"], + surname=user_args["surname"], + email=user_args["email"], + password=user_args["password"] + ) diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 9b94f6e..0d84e45 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -49,8 +49,12 @@ def test_quiz_put_request(): def test_user_get_request(): - url = "http://127.0.0.1:8888/api/user/1" - return get(url).text + url = "http://127.0.0.1:8888/api/user" + + body = {"email": "vikochka_kruk@mail.ru", + "password": "123"} + + return get(url, json=json.dumps(body)).text def test_user_post_request(): From 2a4b573f72ad9f46e275828d169fd719f931f6c1 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Mon, 27 Jun 2022 21:00:56 +0300 Subject: [PATCH 37/42] Qui Resource Quiz creation process is working services: Algorithm for quiz collecting model: small amendings UserResource: Rename of api resources to UserLoginResource and UserRegistrationResource api_initialization: changes of login and registration urls test_api: changing test for quiz creations --- web-app/adapters/orm.py | 3 +- web-app/domain/model.py | 6 +-- web-app/entrypoints/api/api_initialization.py | 4 +- .../entrypoints/api/resource/QuizResource.py | 17 +++---- .../entrypoints/api/resource/UserResource.py | 28 +++++------- web-app/service_layer/services.py | 8 ++++ web-app/tests/test_api.py | 45 ++++++++++++++----- 7 files changed, 69 insertions(+), 42 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 69596f1..bfd3651 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -33,7 +33,7 @@ question_table = Table("questions", metadata, Column('id', Integer, primary_key=True, autoincrement=True), Column('quiz_id', Integer, ForeignKey('quizzes.id')), - Column('type', Enum(model.QuestionTypes)), + Column('type', Text), Column('description', Text), Column('time', Integer)) @@ -49,6 +49,7 @@ def start_mappers(): user_mapper = mapper_registry.map_imperatively(model.User, user_table) question_mapper = mapper_registry.map_imperatively(model.Question, question_table) answer_mapper = mapper_registry.map_imperatively(model.Answer, answer_table) + quiz_mapper = mapper_registry.map_imperatively(model.Quiz, quiz_table) def create_all(engine): diff --git a/web-app/domain/model.py b/web-app/domain/model.py index c9add43..b20ada2 100644 --- a/web-app/domain/model.py +++ b/web-app/domain/model.py @@ -41,7 +41,7 @@ class QuestionTypes(enum.Enum): # Question class, to store information about question: # question type, description, possible answers, and correct answers class Question: - def __init__(self, question_type: QuestionTypes, text: QuestionDescription, quiz_id: int, time_limit: int): + def __init__(self, question_type: str, text: QuestionDescription, quiz_id: int, time_limit: int): self.question_type = question_type self.text = text self.quiz_id = quiz_id @@ -59,13 +59,13 @@ def to_dict(self): # Quiz class, class Quiz: def __init__(self, quiz_name: QuizName, user_id: int = 0): - self.quiz_name = quiz_name + self.name = quiz_name self.user_id = user_id def to_dict(self): return { "user_id": self.user_id, - "name": self.quiz_name + "name": self.name } diff --git a/web-app/entrypoints/api/api_initialization.py b/web-app/entrypoints/api/api_initialization.py index 099b23b..3e49f8b 100644 --- a/web-app/entrypoints/api/api_initialization.py +++ b/web-app/entrypoints/api/api_initialization.py @@ -4,8 +4,8 @@ def api_initialization(app) -> Api: api = Api(app) - api.add_resource(UserResource.UserResource, '/api/user/') - api.add_resource(UserResource.UserRegistrationResource, '/api/user') + api.add_resource(UserResource.UserLoginResource, '/api/user/login') + api.add_resource(UserResource.UserRegistrationResource, '/api/user/register') api.add_resource(QuizResource.QuizResource, '/api/user//quiz/') api.add_resource(QuizResource.QuizListResource, '/api/user//quiz') diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 3e514d3..49e4ea7 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -6,9 +6,7 @@ quiz_creation_parser = reqparse.RequestParser() quiz_creation_parser.add_argument('name', required=True) -quiz_creation_parser.add_argument('questions', required=True) -quiz_creation_parser.add_argument('email', required=True) -quiz_creation_parser.add_argument('password', required=True) +quiz_creation_parser.add_argument('questions', type=dict, action="append", required=True) quiz_get_parser = reqparse.RequestParser() quiz_get_parser.add_argument('email', required=True) @@ -28,12 +26,10 @@ def check_for_credentials(args: dict): abort(404, message="Unauthorized") -def abort_if_user_not_found(user_id: int, args: dict): - email, password = services.email_and_pass_from(args) - +def abort_if_user_not_found(user_id: int): repo = get_repo() user = repo.get_user_by_id(user_id) - if not user or user.email != email or user.password != password: + if not user: abort(404, message="Unauthorized") @@ -83,7 +79,7 @@ class QuizListResource(Resource): def get(self, user_id): args = quiz_get_parser.parse_args() - abort_if_user_not_found(user_id, args) + abort_if_user_not_found(user_id) repo = get_repo() quizzes = repo.list_quizzes(user_id) @@ -91,8 +87,8 @@ def get(self, user_id): def post(self, user_id): args = quiz_creation_parser.parse_args() - - abort_if_user_not_found(user_id, args) + # return make_response(str(args), 404) + abort_if_user_not_found(user_id) repo = get_repo() @@ -100,6 +96,7 @@ def post(self, user_id): quiz_id = repo.add_quiz(quiz) questions_and_answers = services.questions_of_quiz_creation_from(args, quiz_id) + # return make_response(str(questions_and_answers), 404) for (question, answers_args) in questions_and_answers: question_id = repo.add_question(question) diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index df53426..d551d80 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -5,18 +5,18 @@ from domain.model import User from service_layer import services +login_parser = reqparse.RequestParser() +login_parser.add_argument('email', required=True) +login_parser.add_argument('password', required=True) + registration_parser = reqparse.RequestParser() registration_parser.add_argument('name', required=True) registration_parser.add_argument('surname', required=True) registration_parser.add_argument('email', required=True) registration_parser.add_argument('password', required=True) -login_parser = reqparse.RequestParser() -login_parser.add_argument('email', required=True) -login_parser.add_argument('password', required=True) - -def check_for_credentials(args: dict): +def check_for_credentials(args: dict) -> int: email, password = services.email_and_pass_from(args) repo = get_repo() @@ -28,6 +28,8 @@ def check_for_credentials(args: dict): if not user.password == password: abort(404, message="Unauthorized") + return user.id + def abort_if_user_not_found(user_id: int, args: dict): email, password = services.email_and_pass_from(args) @@ -38,22 +40,16 @@ def abort_if_user_not_found(user_id: int, args: dict): abort(404, message="Unauthorized") -class UserResource(Resource): - def get(self, user_id): +class UserLoginResource(Resource): + def post(self): args = login_parser.parse_args() - abort_if_user_not_found(user_id, args) - user = get_repo().get_user_by_id(user_id) - return make_response("User exists", 200) + user_id = check_for_credentials(args) -class UserRegistrationResource(Resource): - def get(self): - args = login_parser.parse_args() - - check_for_credentials(args) + return {"user_id": user_id} - return make_response("User exists", 200) +class UserRegistrationResource(Resource): def post(self): args = registration_parser.parse_args() repo = get_repo() diff --git a/web-app/service_layer/services.py b/web-app/service_layer/services.py index ad342f6..cc207d6 100644 --- a/web-app/service_layer/services.py +++ b/web-app/service_layer/services.py @@ -39,6 +39,14 @@ def questions_of_quiz_creation_from(quiz_args: dict, quiz_id: int) -> [(model.Qu ), question_args["answers"]) for question_args in quiz_args["questions"] ] + # return quiz_args["questions"] + # for question_args in quiz_args["questions"]: + # questions.append((model.Question( + # question_type=question_args["question_type"], + # text=question_args["text"], + # time_limit=question_args["time"], + # quiz_id=quiz_id), + # question_args["answers"])) return questions diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 0d84e45..067c0c3 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -4,14 +4,37 @@ def test_quizzes_post_request(): + answers1 = [ + {"text": "Hello", "correct_answer": True}, + {"text": "World", "correct_answer": False} + ] + question1 = {"question_type": "poll", + "text": "questions1", + "time": 60, + "answers": answers1} + + answers2 = [ + {"text": "Hello", "correct_answer": True}, + {"text": "World", "correct_answer": False} + ] + + question2 = { + "question_type": "quiz", + "text": "questions2", + "time": 15, + "answers": answers2 + } + quiz_post_request_body = { - "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, + "questions": [question1, question2], "name": "Quiz name" } + # print(type(quiz_post_request_body["questions"])) + url = "http://127.0.0.1:8888/api/user/1/quiz" - assert post(url, data=json.dumps(quiz_post_request_body), - headers={"Content-Type": "application/json"}).status_code == 200 + return post(url, json=quiz_post_request_body, + headers={"Content-Type": "application/json"}).text def test_quiz_get_request(): @@ -48,13 +71,13 @@ def test_quiz_put_request(): return post(url, json=json.dumps(quiz_put_request_body), headers={"Content-Type": "application/json"}).text -def test_user_get_request(): - url = "http://127.0.0.1:8888/api/user" +def test_user_login_request(): + url = "http://127.0.0.1:8888/api/user/login" body = {"email": "vikochka_kruk@mail.ru", "password": "123"} - return get(url, json=json.dumps(body)).text + return post(url, json=body).text def test_user_post_request(): @@ -65,7 +88,7 @@ def test_user_post_request(): 'email': 'il@k.r', 'password': 'strong_pass' } - return post(url, data=json.dumps(user_post_request_body), headers={"Content-Type": "application/json"}).text + return post(url, data=user_post_request_body, headers={"Content-Type": "application/json"}).text def test_question_get_request(): @@ -80,8 +103,10 @@ def test_question_post_request(): return post(url, data=json.dumps(question_post_request_body), headers={"Content-Type": "application/json"}).text -# print(test_quiz_post_request()) +print(test_quizzes_post_request()) # print(test_quiz_get_request()) -# print(test_user_post_request()) +# print(test_user_login_request()) # print(test_question_get_request()) -print(test_user_get_request()) +# print(test_user_get_request()) + +# print(str([{"Hello": True, "World": False}])) From 4b7f5972a6494d3f9d9cdd045d33ba8b04e9f655 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:55:29 +0300 Subject: [PATCH 38/42] Repository: quiz, question, answer delete functions Quiz Resource: deletion, creation test_api: quiz_deletion test orm: mappers for foreign_key update --- web-app/adapters/orm.py | 28 +++++++--- web-app/adapters/repository.py | 23 ++++++-- .../entrypoints/api/resource/QuizResource.py | 52 ++++++++++++++----- web-app/entrypoints/flask_app.py | 12 ++++- web-app/tests/test_api.py | 19 +++++-- 5 files changed, 103 insertions(+), 31 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index bfd3651..41c8967 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -1,6 +1,6 @@ from sqlalchemy import Column, MetaData, Integer, String, ForeignKey, Text, Enum, Table, Boolean from sqlalchemy.orm import registry -from sqlalchemy.orm import mapper +from sqlalchemy.orm import mapper, relationship from domain import model @@ -33,23 +33,37 @@ question_table = Table("questions", metadata, Column('id', Integer, primary_key=True, autoincrement=True), Column('quiz_id', Integer, ForeignKey('quizzes.id')), - Column('type', Text), - Column('description', Text), + Column('question_type', Text), + Column('text', Text), Column('time', Integer)) # Table for answers answer_table = Table("answers", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('question_id', ForeignKey('questions.id')), + Column('question_id', Integer, ForeignKey('questions.id')), Column('text', Text), Column('correct_answer', Boolean)) def start_mappers(): - user_mapper = mapper_registry.map_imperatively(model.User, user_table) - question_mapper = mapper_registry.map_imperatively(model.Question, question_table) + user_mapper = mapper_registry.map_imperatively(model.User, user_table, + properties={ + 'user_id': relationship(model.Quiz, + cascade="all,delete") + }) + question_mapper = mapper_registry.map_imperatively(model.Question, question_table, + properties={ + 'question_id': relationship(model.Answer, + cascade="all,delete") + }) answer_mapper = mapper_registry.map_imperatively(model.Answer, answer_table) - quiz_mapper = mapper_registry.map_imperatively(model.Quiz, quiz_table) + + quiz_mapper = mapper_registry.map_imperatively(model.Quiz, quiz_table, + properties={ + 'quiz_id': relationship(model.Question, + cascade="all,delete") + } + ) def create_all(engine): diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index eb7951f..f028769 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -48,9 +48,8 @@ def put_quiz(self, quiz: model.Quiz): # self.session.query(model.Quiz).f raise NotImplemented - def delete_quiz(self, quiz_id): - self.session.query(model.Quiz).filter_by(id=quiz_id).delete(synchronize_session=False) - # TODO: deletion of questions and answers + def delete_quiz(self, quiz: model.Quiz): + self.session.delete(quiz) self.session.commit() def list_quizzes(self, user_id): @@ -66,6 +65,10 @@ def add_question(self, question: model.Question): def get_question(self, question_id): return self.session.query(model.Question).filter_by(id=question_id).one() + def delete_question(self, question: model.Question): + self.session.delete(question) + self.session.commit() + def list_questions(self, quiz_id): return self.session.query(model.Question).filter_by(quiz_id=quiz_id).all() @@ -77,10 +80,14 @@ def add_answer(self, answer: model.Answer): return answer.id def get_answer(self, answer_id): - return self.session.query(model.Answer).filter_by(id=answer_id) + return self.session.query(model.Answer).filter_by(id=answer_id).one() + + def delete_answer(self, answer: model.Answer): + self.session.delete(answer) + self.session.commit() def list_answers(self, question_id): - return self.session.query(model.Answer).filter_by(question_id=question_id) + return self.session.query(model.Answer).filter_by(question_id=question_id).all() # -- for test def get_users(self): @@ -89,6 +96,12 @@ def get_users(self): def get_quizzes(self): return self.session.query(model.Quiz).all() + def get_answers(self): + return self.session.query(model.Answer).all() + + def get_questions(self): + return self.session.query(model.Question).all() + # Testing Repository class FakeRepository: diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 49e4ea7..4d39f3c 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -42,17 +42,34 @@ def abort_if_quiz_not_found(quiz_id): class QuizResource(Resource): - def get(self, quiz_id): + def get(self, user_id, quiz_id): abort_if_quiz_not_found(quiz_id) - args = quiz_get_parser.parse_args() - check_for_credentials(args) - repo = get_repo() quiz = repo.get_quiz(quiz_id) - return make_response(quiz.to_dict(), 200) - def put(self, quiz_id): + if quiz.user_id != user_id: + abort(403, message="You are not allowed to get this information") + + questions = repo.list_questions(quiz_id) + questions_list = [] + # assert len(questions) != 0 + for question in questions: + questions_list.append(question.to_dict()) + + answers_list = [] + answers = repo.list_answers(question.id) + for answer in answers: + answers_list.append(answer.to_dict()) + questions_list[-1]["answers"] = answers_list + + quiz_dict = quiz.to_dict() + quiz_dict["questions"] = questions_list + return make_response(quiz_dict, 200) + + def put(self, user_id, quiz_id): + abort_if_quiz_not_found(quiz_id) + args = quiz_creation_parser.parse_args() check_for_credentials(args) @@ -63,22 +80,31 @@ def put(self, quiz_id): return jsonify({"success": "OK"}) - def delete(self, quiz_id): - args = quiz_get_parser.parse_args() - check_for_credentials(args) + def delete(self, user_id, quiz_id): + abort_if_quiz_not_found(quiz_id) repo = get_repo() - quiz = repo.delete_quiz(quiz_id) - # TODO: Deleting questions + answers + quiz = repo.get_quiz(quiz_id) + + if quiz.user_id != user_id: + abort(403, message="You are not allowed to perform this action") + + repo.delete_quiz(quiz) + # questions = repo.list_questions(quiz_id) + # + # for question in questions: + # + # answers = repo.list_answers(question.id) + # for answer in answers: + # repo.delete_answer(answer) + # repo.delete_question(question) return make_response("Quiz deleted", 200) class QuizListResource(Resource): def get(self, user_id): - args = quiz_get_parser.parse_args() - abort_if_user_not_found(user_id) repo = get_repo() diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 62e9314..6c57473 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -18,7 +18,7 @@ orm.start_mappers() # To delete all tables -# orm.delete_all(engine) +orm.delete_all(engine) orm.create_all(engine) app = Flask(__name__, template_folder='./../static/templates') @@ -85,3 +85,13 @@ def test_user_by_email(email): user = repo.get_user_by_email(email) return json.dumps({"user": user.to_dict()}) + + +@app.route('/tests/answers') +def test_answers_list(): + repo = repository.get_repo() + + dict_items = [] + for item in repo.get_answers(): + dict_items.append(item.to_dict()) + return json.dumps({"answers": dict_items}) diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 067c0c3..64a6741 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -81,14 +81,15 @@ def test_user_login_request(): def test_user_post_request(): - url = "http://127.0.0.1:8888/api/user" + url = "http://127.0.0.1:8888/api/user/register" user_post_request_body = { - 'nickname': 'Il', + 'name': 'Il', + 'surname': 'hello', 'email': 'il@k.r', 'password': 'strong_pass' } - return post(url, data=user_post_request_body, headers={"Content-Type": "application/json"}).text + return post(url, json=user_post_request_body, headers={"Content-Type": "application/json"}).text def test_question_get_request(): @@ -100,10 +101,18 @@ def test_question_post_request(): url = "http://127.0.0.1:8888/api/quiz/1/question/2" question_post_request_body = {"answers": ["Incorrect answer", "Correct answer"]} - return post(url, data=json.dumps(question_post_request_body), headers={"Content-Type": "application/json"}).text + return post(url, json=question_post_request_body, headers={"Content-Type": "application/json"}).text -print(test_quizzes_post_request()) +def test_quiz_delete_request(): + url = "http://127.0.0.1:8888/api/user/1/quiz/2" + + return delete(url).text + + +# print(test_user_post_request()) +print(test_quiz_delete_request()) +# print(test_quizzes_post_request()) # print(test_quiz_get_request()) # print(test_user_login_request()) # print(test_question_get_request()) From 3dc1f225c0062f6f1187b4f53684958cb97bdd08 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:28:08 +0300 Subject: [PATCH 39/42] Orm: Rewrited foreign key mode (To CASCADE mode) QuizResource: Creation of quiz extracted into a function Quiz put request (recreation of quiz) Test_api: quiz put request added --- web-app/adapters/orm.py | 26 ++------- .../entrypoints/api/resource/QuizResource.py | 56 ++++++++----------- web-app/tests/test_api.py | 36 +++++++++--- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 41c8967..2d379ef 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -26,13 +26,13 @@ # Table for quizzes quiz_table = Table("quizzes", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('user_id', Integer, ForeignKey('users.id')), + Column('user_id', Integer, ForeignKey('users.id', ondelete="CASCADE")), Column('name', Text)) # Table for questions question_table = Table("questions", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('quiz_id', Integer, ForeignKey('quizzes.id')), + Column('quiz_id', Integer, ForeignKey('quizzes.id', ondelete="CASCADE")), Column('question_type', Text), Column('text', Text), Column('time', Integer)) @@ -40,30 +40,16 @@ # Table for answers answer_table = Table("answers", metadata, Column('id', Integer, primary_key=True, autoincrement=True), - Column('question_id', Integer, ForeignKey('questions.id')), + Column('question_id', Integer, ForeignKey('questions.id', ondelete="CASCADE")), Column('text', Text), Column('correct_answer', Boolean)) def start_mappers(): - user_mapper = mapper_registry.map_imperatively(model.User, user_table, - properties={ - 'user_id': relationship(model.Quiz, - cascade="all,delete") - }) - question_mapper = mapper_registry.map_imperatively(model.Question, question_table, - properties={ - 'question_id': relationship(model.Answer, - cascade="all,delete") - }) + question_mapper = mapper_registry.map_imperatively(model.Question, question_table) + user_mapper = mapper_registry.map_imperatively(model.User, user_table) answer_mapper = mapper_registry.map_imperatively(model.Answer, answer_table) - - quiz_mapper = mapper_registry.map_imperatively(model.Quiz, quiz_table, - properties={ - 'quiz_id': relationship(model.Question, - cascade="all,delete") - } - ) + quiz_mapper = mapper_registry.map_imperatively(model.Quiz, quiz_table) def create_all(engine): diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 4d39f3c..e7108a4 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -8,10 +8,6 @@ quiz_creation_parser.add_argument('name', required=True) quiz_creation_parser.add_argument('questions', type=dict, action="append", required=True) -quiz_get_parser = reqparse.RequestParser() -quiz_get_parser.add_argument('email', required=True) -quiz_get_parser.add_argument('password', required=True) - def check_for_credentials(args: dict): email, password = services.email_and_pass_from(args) @@ -41,6 +37,22 @@ def abort_if_quiz_not_found(quiz_id): abort(404, message="Quiz is not found") +def create_quiz(user_id, args): + abort_if_user_not_found(user_id) + + repo = get_repo() + + quiz = services.quiz_from(args, user_id) + quiz_id = repo.add_quiz(quiz) + + questions_and_answers = services.questions_of_quiz_creation_from(args, quiz_id) + + for (question, answers_args) in questions_and_answers: + question_id = repo.add_question(question) + for answers in services.answers_for_one_question_of_quiz_creation_from(answers_args, question_id): + repo.add_answer(answers) + + class QuizResource(Resource): def get(self, user_id, quiz_id): abort_if_quiz_not_found(quiz_id) @@ -71,14 +83,16 @@ def put(self, user_id, quiz_id): abort_if_quiz_not_found(quiz_id) args = quiz_creation_parser.parse_args() - check_for_credentials(args) - repo = get_repo() - repo.put_quiz(Quiz(quiz_id=quiz_id, name=args['name'], user_id=args['user_id'])) + quiz = repo.get_quiz(quiz_id) - # TODO: Putting questions + answers + if quiz.user_id != user_id: + abort(403, message="You are not allowed to perform this action") - return jsonify({"success": "OK"}) + repo.delete_quiz(quiz) + create_quiz(user_id, args) + + return make_response("Quiz recreated", 200) def delete(self, user_id, quiz_id): abort_if_quiz_not_found(quiz_id) @@ -91,14 +105,6 @@ def delete(self, user_id, quiz_id): abort(403, message="You are not allowed to perform this action") repo.delete_quiz(quiz) - # questions = repo.list_questions(quiz_id) - # - # for question in questions: - # - # answers = repo.list_answers(question.id) - # for answer in answers: - # repo.delete_answer(answer) - # repo.delete_question(question) return make_response("Quiz deleted", 200) @@ -113,20 +119,6 @@ def get(self, user_id): def post(self, user_id): args = quiz_creation_parser.parse_args() - # return make_response(str(args), 404) - abort_if_user_not_found(user_id) - - repo = get_repo() - - quiz = services.quiz_from(args, user_id) - quiz_id = repo.add_quiz(quiz) - - questions_and_answers = services.questions_of_quiz_creation_from(args, quiz_id) - # return make_response(str(questions_and_answers), 404) - - for (question, answers_args) in questions_and_answers: - question_id = repo.add_question(question) - for answers in services.answers_for_one_question_of_quiz_creation_from(answers_args, question_id): - repo.add_answer(answers) + create_quiz(user_id, args) return make_response("Quiz created", 201) diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 64a6741..be674da 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -1,6 +1,6 @@ import json -from requests import get, post, delete +from requests import get, post, delete, put def test_quizzes_post_request(): @@ -61,14 +61,35 @@ def test_quizzes_get_request(): def test_quiz_put_request(): - url = "http://127.0.0.1:8888/api/user/1/quiz" + url = "http://127.0.0.1:8888/api/user/1/quiz/1" + + answers1 = [ + {"text": "Hello", "correct_answer": False}, + {"text": "Worldzzz", "correct_answer": False} + ] + question1 = {"question_type": "poll", + "text": "questions1", + "time": 60, + "answers": answers1} + + answers2 = [ + {"text": "Hello", "correct_answer": True}, + {"text": "World", "correct_answer": False} + ] + + question2 = { + "question_type": "quiz", + "text": "questions2zzz", + "time": 15, + "answers": answers2 + } quiz_put_request_body = { - "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, - "name": "Quiz name" + "questions": [question1, question2], + "name": "Quiz recreated name" } - return post(url, json=json.dumps(quiz_put_request_body), headers={"Content-Type": "application/json"}).text + return put(url, json=quiz_put_request_body, headers={"Content-Type": "application/json"}).text def test_user_login_request(): @@ -105,13 +126,14 @@ def test_question_post_request(): def test_quiz_delete_request(): - url = "http://127.0.0.1:8888/api/user/1/quiz/2" + url = "http://127.0.0.1:8888/api/user/1/quiz/1" return delete(url).text # print(test_user_post_request()) -print(test_quiz_delete_request()) +# print(test_quiz_delete_request()) +print(test_quiz_put_request()) # print(test_quizzes_post_request()) # print(test_quiz_get_request()) # print(test_user_login_request()) From f22670d7e016954165fce516bb136e171303f8f4 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:57:12 +0300 Subject: [PATCH 40/42] User reigtration: Catching error if user creates several accounts with the same email test_orm: Created more tests test_api: added assertions repository: fixed bugs --- web-app/adapters/repository.py | 9 +-- .../entrypoints/api/resource/QuizResource.py | 6 +- .../entrypoints/api/resource/UserResource.py | 20 +++++- web-app/tests/test_api.py | 42 ++++++------- web-app/tests/test_orm.py | 61 +++++++++++++++++-- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index f028769..0d249e9 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -34,11 +34,14 @@ def get_user_by_id(self, user_id): def get_user_by_email(self, email): return self.session.query(model.User).filter_by(email=email).one() + def delete_user(self, user: model.User): + self.session.delete(user) + self.session.commit() + # Working with quizzes def add_quiz(self, quiz: model.Quiz) -> int: self.session.add(quiz) self.session.commit() - self.session.flush() return quiz.id def get_quiz(self, quiz_id): @@ -53,13 +56,12 @@ def delete_quiz(self, quiz: model.Quiz): self.session.commit() def list_quizzes(self, user_id): - return self.session.query(model.User).filter_by(user_id=user_id).all() + return self.session.query(model.Quiz).filter_by(user_id=user_id).all() # Working with questions def add_question(self, question: model.Question): self.session.add(question) self.session.commit() - self.session.flush() return question.id def get_question(self, question_id): @@ -76,7 +78,6 @@ def list_questions(self, quiz_id): def add_answer(self, answer: model.Answer): self.session.add(answer) self.session.commit() - self.session.flush() return answer.id def get_answer(self, answer_id): diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index e7108a4..8df03b5 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -115,7 +115,11 @@ def get(self, user_id): repo = get_repo() quizzes = repo.list_quizzes(user_id) - return make_response({"quizzes": [quiz.to_dict() for quiz in quizzes]}, 200) + + user = repo.get_user_by_id(user_id) + + return make_response( + {"quizzes": [quiz.to_dict() for quiz in quizzes], "quiz_number": len(quizzes), "user": user.to_dict()}, 200) def post(self, user_id): args = quiz_creation_parser.parse_args() diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index d551d80..8808f8c 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -4,6 +4,7 @@ from adapters.repository import SqlAlchemyRepository, get_repo from domain.model import User from service_layer import services +from sqlalchemy.exc import IntegrityError login_parser = reqparse.RequestParser() login_parser.add_argument('email', required=True) @@ -31,6 +32,15 @@ def check_for_credentials(args: dict) -> int: return user.id +def abort_if_user_already_exists(email): + repo = get_repo() + + user = repo.get_user_by_email(email) + + if user: + abort(403, message="User already exists") + + def abort_if_user_not_found(user_id: int, args: dict): email, password = services.email_and_pass_from(args) @@ -56,5 +66,11 @@ def post(self): user = services.user_from(args) - repo.add_user(user) - return make_response("User created", 201) + # abort_if_user_already_exists(user.email) + try: + repo.add_user(user) + return make_response("User created", 201) + + except IntegrityError: + repo.session.rollback() + return make_response("User already exists", 403) diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index be674da..9968a06 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -33,31 +33,31 @@ def test_quizzes_post_request(): # print(type(quiz_post_request_body["questions"])) url = "http://127.0.0.1:8888/api/user/1/quiz" - return post(url, json=quiz_post_request_body, - headers={"Content-Type": "application/json"}).text + assert post(url, json=quiz_post_request_body, + headers={"Content-Type": "application/json"}).status_code == 201 def test_quiz_get_request(): url = "http://127.0.0.1:8888/api/user/1/quiz/1" - assert get(url) == 200 + assert get(url).status_code == 200 -def test_quiz_delete_request(): - quiz_post_request_body = { - "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, - "name": "Quiz name" - } - url = "http://127.0.0.1:8888/api/user/1/quiz" - - post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}) - - assert delete(url + "/1").status_code == 200 +# def test_quiz_delete_request(): +# quiz_post_request_body = { +# "questions": {"question1": {"Hello": True, "World": True}, "question2": {"Hello": True, "World": True}}, +# "name": "Quiz name" +# } +# url = "http://127.0.0.1:8888/api/user/1/quiz" +# +# post(url, data=json.dumps(quiz_post_request_body), headers={"Content-Type": "application/json"}) +# +# assert delete(url + "/1").status_code == 200 def test_quizzes_get_request(): url = "http://127.0.0.1:8888/api/user/1/quiz" - return get(url).text + assert get(url).status_code == 200 def test_quiz_put_request(): @@ -89,7 +89,7 @@ def test_quiz_put_request(): "name": "Quiz recreated name" } - return put(url, json=quiz_put_request_body, headers={"Content-Type": "application/json"}).text + assert put(url, json=quiz_put_request_body, headers={"Content-Type": "application/json"}).status_code == 200 def test_user_login_request(): @@ -98,7 +98,7 @@ def test_user_login_request(): body = {"email": "vikochka_kruk@mail.ru", "password": "123"} - return post(url, json=body).text + assert post(url, json=body).status_code == 200 def test_user_post_request(): @@ -110,30 +110,30 @@ def test_user_post_request(): 'email': 'il@k.r', 'password': 'strong_pass' } - return post(url, json=user_post_request_body, headers={"Content-Type": "application/json"}).text + assert post(url, json=user_post_request_body, headers={"Content-Type": "application/json"}).status_code == 201 def test_question_get_request(): url = "http://127.0.0.1:8888/api/quiz/1/question/2" - return get(url).text + assert get(url).status_code == 200 def test_question_post_request(): url = "http://127.0.0.1:8888/api/quiz/1/question/2" question_post_request_body = {"answers": ["Incorrect answer", "Correct answer"]} - return post(url, json=question_post_request_body, headers={"Content-Type": "application/json"}).text + assert post(url, json=question_post_request_body, headers={"Content-Type": "application/json"}).status_code == 201 def test_quiz_delete_request(): url = "http://127.0.0.1:8888/api/user/1/quiz/1" - return delete(url).text + assert delete(url).status_code == 200 # print(test_user_post_request()) # print(test_quiz_delete_request()) -print(test_quiz_put_request()) +# print(test_quiz_put_request()) # print(test_quizzes_post_request()) # print(test_quiz_get_request()) # print(test_user_login_request()) diff --git a/web-app/tests/test_orm.py b/web-app/tests/test_orm.py index ec9a9ad..6f682e7 100644 --- a/web-app/tests/test_orm.py +++ b/web-app/tests/test_orm.py @@ -2,15 +2,66 @@ from domain import model +# Unit tests can be written only for user or quiz (With user) +# Because we use foreign key to link all info in db + def test_add_user(repo): - new_user = model.User("Fedor", "fedya@gg.ru", "CoolPass") + new_user = model.User("Fedor", "Surname", "fedya@gg.ru", "CoolPass") repo.add_user(new_user) - db_user = repo.session.query(model.User).filter_by(nickname=new_user.nickname).one() + db_user = repo.session.query(model.User).filter_by(email=new_user.email).one() assert db_user == new_user - repo.session.query(model.User).filter_by(nickname=new_user.nickname).delete() - repo.session.commit() + repo.delete_user(new_user) + + +def test_add_and_quiz(repo): + new_user = model.User("Fedor", "Surname", "fedya@gg.ru", "CoolPass") + user_id = repo.add_user(new_user) + + new_quiz = model.Quiz(quiz_name="Quiz name", user_id=user_id) + quiz_id = repo.add_quiz(new_quiz) + + db_quiz = repo.get_quiz(quiz_id) + + assert db_quiz == new_quiz + + repo.delete_user(new_user) + + +def test_add_and_question(repo): + new_user = model.User("Fedor", "Surname", "fedya@gg.ru", "CoolPass") + user_id = repo.add_user(new_user) + + new_quiz = model.Quiz(quiz_name="Quiz name", user_id=user_id) + quiz_id = repo.add_quiz(new_quiz) + + question = model.Question(question_type="poll", text="questions1", time_limit=60, quiz_id=quiz_id) + question_id = repo.add_questions(question1) + + db_question = repo.get_question(question_id) + + assert db_question == question + + repo.delete_user(new_user) + + +def test_add_and_answer(repo): + new_user = model.User("Fedor", "Surname", "fedya@gg.ru", "CoolPass") + user_id = repo.add_user(new_user) + + new_quiz = model.Quiz(quiz_name="Quiz name", user_id=user_id) + quiz_id = repo.add_quiz(new_quiz) + + question = model.Question(question_type="poll", text="questions1", time_limit=60, quiz_id=quiz_id) + question_id = repo.add_questions(question) + + answer = model.Answer(text="Hello", correct_answer=True, question_id=question_id) + answer_id = repo.add_answer(answer) + + db_answer = repo.get_answer(answer_id) + + assert db_answer == answer - print("OK") + repo.delete_user(new_user) From 8c44e5cbd26e0a64aeee3a15ee311c750d88db7f Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Thu, 30 Jun 2022 17:16:06 +0300 Subject: [PATCH 41/42] pyanalyze code --- web-app/adapters/__init__.py | 1 + web-app/adapters/orm.py | 2 ++ web-app/adapters/repository.py | 2 +- web-app/app_start.py | 1 + web-app/domain/__init__.py | 1 + web-app/entrypoints/__init__.py | 1 + web-app/entrypoints/api/__init__.py | 1 + web-app/entrypoints/api/api_initialization.py | 2 ++ web-app/entrypoints/api/resource/AnswerResource.py | 1 + web-app/entrypoints/api/resource/QuestionResource.py | 1 + web-app/entrypoints/api/resource/QuizResource.py | 4 ++++ web-app/entrypoints/api/resource/UserResource.py | 4 ++++ web-app/entrypoints/api/resource/__init__.py | 1 + web-app/entrypoints/flask_app.py | 1 + web-app/main.py | 1 + web-app/service_layer/__init__.py | 1 + web-app/service_layer/services.py | 1 + web-app/tests/test_api.py | 1 + web-app/tests/test_orm.py | 1 + 19 files changed, 27 insertions(+), 1 deletion(-) diff --git a/web-app/adapters/__init__.py b/web-app/adapters/__init__.py index eec1779..e5034e2 100644 --- a/web-app/adapters/__init__.py +++ b/web-app/adapters/__init__.py @@ -1 +1,2 @@ +# static analysis: ignore[import_failed] from . import orm, repository \ No newline at end of file diff --git a/web-app/adapters/orm.py b/web-app/adapters/orm.py index 2d379ef..a340c2a 100644 --- a/web-app/adapters/orm.py +++ b/web-app/adapters/orm.py @@ -1,9 +1,11 @@ +# static analysis: ignore[import_failed] from sqlalchemy import Column, MetaData, Integer, String, ForeignKey, Text, Enum, Table, Boolean from sqlalchemy.orm import registry from sqlalchemy.orm import mapper, relationship from domain import model + # Base = declarative_base() mapper_registry = registry() metadata = mapper_registry.metadata diff --git a/web-app/adapters/repository.py b/web-app/adapters/repository.py index 0d249e9..2fc2bf6 100644 --- a/web-app/adapters/repository.py +++ b/web-app/adapters/repository.py @@ -1,4 +1,4 @@ -# from abc import ABC, abstractmethod +# static analysis: ignore[import_failed] from domain import model diff --git a/web-app/app_start.py b/web-app/app_start.py index 7247b59..eb07319 100644 --- a/web-app/app_start.py +++ b/web-app/app_start.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from entrypoints.flask_app import app if __name__ == "__main__": diff --git a/web-app/domain/__init__.py b/web-app/domain/__init__.py index 36ec720..48d0fae 100644 --- a/web-app/domain/__init__.py +++ b/web-app/domain/__init__.py @@ -1 +1,2 @@ +# static analysis: ignore[import_failed] from . import model \ No newline at end of file diff --git a/web-app/entrypoints/__init__.py b/web-app/entrypoints/__init__.py index e69de29..1a975b4 100644 --- a/web-app/entrypoints/__init__.py +++ b/web-app/entrypoints/__init__.py @@ -0,0 +1 @@ +# static analysis: ignore[import_failed] diff --git a/web-app/entrypoints/api/__init__.py b/web-app/entrypoints/api/__init__.py index e69de29..1a975b4 100644 --- a/web-app/entrypoints/api/__init__.py +++ b/web-app/entrypoints/api/__init__.py @@ -0,0 +1 @@ +# static analysis: ignore[import_failed] diff --git a/web-app/entrypoints/api/api_initialization.py b/web-app/entrypoints/api/api_initialization.py index 3e49f8b..0660342 100644 --- a/web-app/entrypoints/api/api_initialization.py +++ b/web-app/entrypoints/api/api_initialization.py @@ -1,4 +1,6 @@ +# static analysis: ignore[import_failed] from entrypoints.api.resource import AnswerResource, QuizResource, QuestionResource, UserResource + from flask_restful import Api diff --git a/web-app/entrypoints/api/resource/AnswerResource.py b/web-app/entrypoints/api/resource/AnswerResource.py index c46472e..474772f 100644 --- a/web-app/entrypoints/api/resource/AnswerResource.py +++ b/web-app/entrypoints/api/resource/AnswerResource.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from flask import jsonify from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo diff --git a/web-app/entrypoints/api/resource/QuestionResource.py b/web-app/entrypoints/api/resource/QuestionResource.py index 74ee5fe..edbc3ea 100644 --- a/web-app/entrypoints/api/resource/QuestionResource.py +++ b/web-app/entrypoints/api/resource/QuestionResource.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from flask import jsonify from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index 8df03b5..d574bc1 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -1,9 +1,13 @@ +# static analysis: ignore[import_failed] from flask import jsonify, make_response from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo + from domain.model import Quiz + from service_layer import services + quiz_creation_parser = reqparse.RequestParser() quiz_creation_parser.add_argument('name', required=True) quiz_creation_parser.add_argument('questions', type=dict, action="append", required=True) diff --git a/web-app/entrypoints/api/resource/UserResource.py b/web-app/entrypoints/api/resource/UserResource.py index 8808f8c..e85f051 100644 --- a/web-app/entrypoints/api/resource/UserResource.py +++ b/web-app/entrypoints/api/resource/UserResource.py @@ -1,9 +1,13 @@ +# static analysis: ignore[import_failed] import flask from flask import jsonify, make_response from flask_restful import reqparse, abort, Resource from adapters.repository import SqlAlchemyRepository, get_repo + from domain.model import User + from service_layer import services + from sqlalchemy.exc import IntegrityError login_parser = reqparse.RequestParser() diff --git a/web-app/entrypoints/api/resource/__init__.py b/web-app/entrypoints/api/resource/__init__.py index e69de29..1a975b4 100644 --- a/web-app/entrypoints/api/resource/__init__.py +++ b/web-app/entrypoints/api/resource/__init__.py @@ -0,0 +1 @@ +# static analysis: ignore[import_failed] diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index 6c57473..d9747da 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] import json from flask import Flask, request, render_template diff --git a/web-app/main.py b/web-app/main.py index 695cac7..18dac90 100644 --- a/web-app/main.py +++ b/web-app/main.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from entrypoints import flask_app flask_app.main() diff --git a/web-app/service_layer/__init__.py b/web-app/service_layer/__init__.py index e69de29..1a975b4 100644 --- a/web-app/service_layer/__init__.py +++ b/web-app/service_layer/__init__.py @@ -0,0 +1 @@ +# static analysis: ignore[import_failed] diff --git a/web-app/service_layer/services.py b/web-app/service_layer/services.py index cc207d6..aeef318 100644 --- a/web-app/service_layer/services.py +++ b/web-app/service_layer/services.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from domain import model diff --git a/web-app/tests/test_api.py b/web-app/tests/test_api.py index 9968a06..8410451 100644 --- a/web-app/tests/test_api.py +++ b/web-app/tests/test_api.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] import json from requests import get, post, delete, put diff --git a/web-app/tests/test_orm.py b/web-app/tests/test_orm.py index 6f682e7..40e5d27 100644 --- a/web-app/tests/test_orm.py +++ b/web-app/tests/test_orm.py @@ -1,3 +1,4 @@ +# static analysis: ignore[import_failed] from adapters import repository from domain import model From ee0a1220e38817164375b1a7dacfc91993e19f15 Mon Sep 17 00:00:00 2001 From: IlnurHA <53339563+IlnurHA@users.noreply.github.com> Date: Sat, 2 Jul 2022 14:34:25 +0300 Subject: [PATCH 42/42] quiz_id info for api request --- web-app/entrypoints/api/resource/QuizResource.py | 10 ++++++++-- web-app/entrypoints/flask_app.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web-app/entrypoints/api/resource/QuizResource.py b/web-app/entrypoints/api/resource/QuizResource.py index d574bc1..827cdee 100644 --- a/web-app/entrypoints/api/resource/QuizResource.py +++ b/web-app/entrypoints/api/resource/QuizResource.py @@ -7,7 +7,6 @@ from service_layer import services - quiz_creation_parser = reqparse.RequestParser() quiz_creation_parser.add_argument('name', required=True) quiz_creation_parser.add_argument('questions', type=dict, action="append", required=True) @@ -122,8 +121,15 @@ def get(self, user_id): user = repo.get_user_by_id(user_id) + quizzes_as_dict = [] + + for quiz in quizzes: + quiz_as_dict = quiz.to_dict + quiz_as_dict["quiz_id"] = quiz.id + quizzes_as_dict.append(dicted_quiz) + return make_response( - {"quizzes": [quiz.to_dict() for quiz in quizzes], "quiz_number": len(quizzes), "user": user.to_dict()}, 200) + {"quizzes": dicted_quizzes, "quiz_number": len(quizzes), "user": user.to_dict()}, 200) def post(self, user_id): args = quiz_creation_parser.parse_args() diff --git a/web-app/entrypoints/flask_app.py b/web-app/entrypoints/flask_app.py index d9747da..a701e53 100644 --- a/web-app/entrypoints/flask_app.py +++ b/web-app/entrypoints/flask_app.py @@ -19,7 +19,7 @@ orm.start_mappers() # To delete all tables -orm.delete_all(engine) +# orm.delete_all(engine) orm.create_all(engine) app = Flask(__name__, template_folder='./../static/templates')