diff --git a/.env b/.env new file mode 100644 index 0000000..1b1faaf --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DEBUG=True +FLASK_ENV=development +DATABASE_URL=postgresql://app:1234@127.0.0.1:5435/app +JWT_KEY=1234 +CORS_ORIGIN=* diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..69cb543 --- /dev/null +++ b/.env.local @@ -0,0 +1,7 @@ +# commentaire +# exemple d'une db de prod +# le .env.local sur une vraie application ne sera jamais push sur un git et ne servira qu'aux configurations locale +# d'un serveur de production ou d'acceptance/test. +# DATABASE_URL=postgresql://proddbuser:ASkjkmKAMM19os88!@*mamksjdui@prod url and port/proddb +JWT_KEY=asdkj9AK2j1ak5jsmnAs152MAJnKJAMskjad53 +# CORS_ORIGIN=https://www.odoo.front.com/ diff --git a/.gitignore b/.gitignore index 0fdefb4..76779a7 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,6 @@ celerybeat-schedule *.sage.py # Environments -.env .venv env/ venv/ diff --git a/README.md b/README.md index ead5504..1318854 100644 --- a/README.md +++ b/README.md @@ -1 +1,84 @@ # pythonORM + +## 1) models + from app import db + from app.models.base_entity import BaseEntity + + class Role(BaseEntity, db.Model): + __tablename__ = "roles" + roleid = db.Column(db.Integer, primary_key=True) + rolename = db.Column(db.String(50), nullable=False, unique=True, index=True) + self.data = data + +### 1.2) relationship one to many + class Parent(BaseEntity, db.Model): + __tablename__ = "parents" + parentid = db.Column(db.Integer, primary_key=True) + childid = db.Column(db.ForeignKey('children.childid')) + children = db.relationship("Child", cascade='all, delete-orphan') + + class Child(BaseEntity, db.Model): + __tablename__ = "children" + childid = db.Column(db.Integer, primary_key=True) + parent = db.relationship("User", back_populates="children") + +## 2) formulares + class ExampleForm(FlaskForm): + # désactiver CSRF pour les api + class Meta: + csrf = False + exampleData = StringField('exampleData', validators=[]) + exampleData2 = StringField('exampleData2', validators=[DataRequired()]) + +## 3) mappers + Créer les mapper pour passer des entités au dtos, et mettre à jours les entité + à partir des forms. + +## 4) services + - des fonctions pour contacter la db : + - findOne => retourne une entité en fonction de son id + - findOneBy => retourne une entité en fonction d'un + paramètre passé (données d'une des colonnes) + - findAll => retourne toutes les data + - update => met à jours les donnés en DB + - insert => insert des nouvelles donnée en DB + - delete => delete une entrée en DB + +## 5) controllers + class ExampleController: + # http://servername/path + @app.route('/path', methods=['GET', 'POST']) + def getAllData(): + return jsonify([example.get_json_parseable() for example in service.findAll()]) + # http://servername/path/1 + @app.route('/path/', methods=['GET', 'POST']) + def getAllData(dataid): + return jsonify(service.findOne().get_json_parseable()) + + +## 6) Dépendence + les mappers dépendent des modèles, des DTOs et des forms + les services dépendent des modèles et des mappers + les controllers dépendent des services. + +## 7) database + la connection string doit être modifiée dans le .env ou .env.local + pour initialiser la db utiliser : + $ ./sqlAlchemy.sh -i + + pour créer de nouvelles migrations : + $ ./sqlAlchemy.sh -m nom_de_la_migration + + Le fichier de la migration peut être modifier si nécéssaire pour mettre à jours ou rajouter + des données dans la DB. + + une fois les migrations créées on peut les appliquer avec la commande : + $ ./sqlAlchemy.sh -u + +## 8) lancer le serveur + $ python3 runserver.py + + pour lancer le serveur en debug depuis votre ide, il faudra soit créer un launch.json + via l'onglet de debugging de VSCode. + Soit on peut créer une nouvelle configuration de debugging dans Pycharm et mettre le runserver.py + comme programme de lancement. diff --git a/app/__init__.py b/app/__init__.py index 9b6f896..d4d4b6f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,19 +1,34 @@ +import os +from pathlib import Path + +import wtforms_json from flask import Flask +from flask_cors import CORS from flask_debugtoolbar import DebugToolbarExtension from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_wtf import CSRFProtect +from dotenv import load_dotenv + +envvars = Path().cwd() / '.env.local' +load_dotenv() + +if os.path.exists(envvars): + load_dotenv(envvars, override=True, verbose=True) app = Flask('app') -app.debug = True -app.secret_key = 'superSecretKey01@' +app.debug = os.environ.get("DEBUG") +app.secret_key = os.environ.get("JWT_KEY") +wtforms_json.init() toolbar = DebugToolbarExtension(app) app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False -app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://app:1234@127.0.0.1:5435/app' +app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("DATABASE_URL") +print(os.environ.get("DATABASE_URL")) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # csrf_protect = CSRFProtect(app) +cors = CORS(app, resources={'/api/*': {'origins': os.environ.get("CORS_ORIGIN")}}) db = SQLAlchemy(app) migrate = Migrate(app, db) diff --git a/app/controllers/basket_controller.py b/app/controllers/basket_controller.py index fddb49e..5d015ab 100644 --- a/app/controllers/basket_controller.py +++ b/app/controllers/basket_controller.py @@ -1,47 +1,48 @@ -from flask import render_template, session, request, redirect, url_for - +from flask import render_template, session, request, redirect, url_for, jsonify from app import app from app.framework.decorators.auth_required import auth_required from app.forms.basket.basket_add_item_form import BasketAddItemForm +from app.framework.decorators.inject import inject +from app.services.auth_service import AuthService from app.services.basket_service import BasketService -from app.services.item_service import ItemService -from app.services.user_service import UserService - -basketService = BasketService() -itemService = ItemService() -userService = UserService() -@app.route('/basket/all') +@app.route('/api/basket/all') @auth_required(level="ADMIN") -def getAllBaskets(): - return render_template('baskets/list.html', baskets=basketService.find_all()) +@inject +@inject +def getAllBaskets(basketService: BasketServicebasketService: BasketService): + return jsonify([basket.get_json_parsable() for basket in basketService.find_all()]) -@app.route('/basket') +@app.route('/api/basket') @auth_required() -def getBasketDetail(basketService: BasketService): - basket = basketService.find_one_by(userid=session.get('userid'), basketclosed=False) - return render_template('baskets/details.html', - basket=basket, items=basket.items, is_basket=True) +@inject +def getBasketDetail(basketService: BasketService, authService: AuthService): + user = authService.get_current_user() + basket = basketService.find_one_by(userid=user.userid, basketclosed=False) + return jsonify(basket.get_json_parsable()) -@app.route('/basket/add', methods=['POST']) +@app.route('/api/basket/', methods=['PUT']) @auth_required() -def add_item_to_basket(): - item_to_add = BasketAddItemForm(request.form) +@inject +def add_item_to_basket(basketService: BasketService): + item_to_add = BasketAddItemForm.from_json(request.json) - basketService.add_item(item_to_add) + basket = basketService.add_item(item_to_add) - return redirect(url_for('getBasketDetail')) + return jsonify({ 'status': 'added' }) -@app.route('/basket/remove/', methods=['POST']) +@app.route('/api/basket/', methods=['DELETE']) @auth_required() -def remove_item_to_basket(itemid: int): +@inject +def remove_item_to_basket(itemid: int, basketService: BasketService): basketService.remove_item(itemid) - return redirect(url_for('getBasketDetail')) + return jsonify({ 'status': 'removed' }) -@app.route('/basket/checkout', methods=['POST']) +@app.route('/api/basket/checkout', methods=['POST']) @auth_required() -def checkout_basket(): +@inject +def checkout_basket(basketService: BasketService): basketService.checkout_basket() - return redirect(url_for('getBasketDetail')) + return jsonify({ 'status': 'checkout' }) \ No newline at end of file diff --git a/app/controllers/home_controller.py b/app/controllers/home_controller.py index 5303bdd..dcb8a91 100644 --- a/app/controllers/home_controller.py +++ b/app/controllers/home_controller.py @@ -1,7 +1,7 @@ from app import app -from flask import render_template, redirect +from flask import render_template, redirect, jsonify @app.route('/', methods=['GET']) def index(): - return render_template('home/home.html') + return jsonify({'error': 'invalid credentials'}), 500 diff --git a/app/controllers/item_controller.py b/app/controllers/item_controller.py index 0cd5926..d7fda9a 100644 --- a/app/controllers/item_controller.py +++ b/app/controllers/item_controller.py @@ -1,56 +1,52 @@ -from flask import render_template, request, redirect, url_for +from flask import render_template, request, redirect, url_for, jsonify from app import app -from app.forms.basket.basket_add_item_form import BasketAddItemForm from app.forms.item.item_form import ItemForm +from app.framework.decorators.auth_required import auth_required +from app.framework.decorators.inject import inject from app.services.item_service import ItemService -itemService = ItemService() - -@app.route('/items') -def getItemList(): - return render_template('items/list.html', items=itemService.find_all()) - -@app.route('/items/') -def getItemDetails(itemid): - form = BasketAddItemForm() - - return render_template('items/details.html', item=itemService.find_one(itemid), form=form) - -@app.route('/items/add', methods=['GET','POST']) -def addItem(): - form = ItemForm(request.form) - - if request.method == 'POST': - if form.validate(): - item = itemService.insert(form) - - return redirect(url_for('getItemList')) - - print(form.errors) - form.itemname.data = '' - form.itemdescription.data = '' - form.itemstock.data = 1 - - return render_template('items/add_or_update.html', form=form) - -@app.route('/items/update/', methods=['GET','POST']) -def updateItem(itemid: int): +@app.route('/api/items') +@inject +def getItemListAsJson(itemService: ItemService): + return jsonify([item.get_json_parsable() for item in itemService.find_all()]) + +@app.route('/api/items/') +@inject +def getItemDetails(itemid, itemService: ItemService): + return jsonify(itemService.find_one(itemid).get_json_parsable()) + +@app.route('/api/items/add', methods=['POST']) +@inject +@auth_required(level="ADMIN") +@inject +def addItem(itemService: ItemServiceitemService: ItemService): + form = ItemForm.from_json(request.json) + error = { + "code": 0, + "message": "" + } + + if form.validate(): + item = itemService.insert(form) + + return jsonify(item.get_json_parsable()) + + return jsonify(form.errors) + +@app.route('/api/items/', methods=['PUT']) +@auth_required(level="ADMIN") +@inject +def updateItem(itemid: int, itemService: ItemService): item = itemService.find_one(itemid) if item is None: - return redirect(url_for('getItemList')) + return jsonify({ 'errors': "item not found" }) form = ItemForm(request.form) - if request.method == 'POST': - if form.validate(): - item = itemService.update(itemid, form) - - return redirect(url_for('getItemList')) + if form.validate(): + item = itemService.update(itemid, form) - print(form.errors) - form.itemname.data = item.itemname - form.itemdescription.data = item.itemdescription - form.itemstock.data = item.itemquantity + return jsonify(item.get_json_parsable()) - return render_template('items/add_or_update.html', form=form) + return jsonify(form.errors) diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py index 1c95f60..6c1d1be 100644 --- a/app/controllers/user_controller.py +++ b/app/controllers/user_controller.py @@ -1,3 +1,8 @@ +import datetime + +import jwt + +from app.dtos.user_dto import UserDTO from app.framework.decorators.auth_required import auth_required from app.framework.decorators.inject import inject from app.services.auth_service import AuthService @@ -12,96 +17,74 @@ # http://localhost:8080/users -> GET -@app.route('/users') -@inject -def getUserList(userService: UserService): - form = UserRegisterForm() - - return render_template('users/list.html', users=userService.find_all(), form=form) - @app.route('/api/users') +@auth_required() @inject -def getUsersAsJson(user_service: UserService): +def getUserList(user_service: UserService): return jsonify([user.get_json_parsable() for user in user_service.find_all()]) # http://localhost:8080/users/5 -> GET -@app.route('/users/', methods=["GET"]) +@app.route('/api/users/', methods=["GET"]) @auth_required() @inject -def getOneUser(userid: int, userService: UserService): - user = userService.find_one(userid) +def getOneUser(userid: int, user_service: UserService): + user = user_service.find_one(userid) - return render_template('users/profile.html', user=user) + return jsonify(user.get_json_parsable()) # http://localhost:8080/users/register -> GET | POST -@app.route('/users/register', methods=["GET", "POST"]) +@app.route('/api/users/register', methods=["POST"]) @inject def register(userService: UserService): - form = UserRegisterForm(request.form) + form = UserRegisterForm.from_json(request.json) - if request.method == 'POST': - if form.validate(): - user = userService.insert(form) + if form.validate(): + user = userService.insert(form) - return redirect(url_for('getOneUser', userid=user.userid)) + return jsonify(user.get_json_parsable()) - return render_template('users/register.html', form=form) + return jsonify(form.errors) -@app.route('/users/update/', methods=["GET", "POST"]) +@app.route('/api/users/', methods=["PUT"]) @auth_required(level="ADMIN", or_is_current_user=True) @inject def userUpdate(userid: int, userService: UserService, roleService: RoleService): - form = UserUpdateForm(request.form) - roles = roleService.find_all() + form = UserUpdateForm.from_json(request.json) - if request.method == 'POST': - if form.validate(): - user = userService.update(userid, form) + if form.validate(): + user = userService.update(userid, form) - return redirect(url_for('getOneUser', userid=userid)) + return jsonify(user.get_json_parsable()) - user = userService.find_one(userid) - return render_template('users/update.html', form=form, user=user, roles=roles) + return jsonify(form.errors) -@app.route('/login', methods=["GET", "POST"]) +@app.route('/api/login', methods=["POST"]) @inject def login(userService: UserService): - form = UserLoginForm(request.form) - - if request.method == 'POST': - if form.validate(): - user = userService.login(form) + form = UserLoginForm.from_json(request.json) - if user != None: - session['username'] = user.username - session['userid'] = user.userid - session['userroles'] = user.get_roles() - return redirect(url_for('getOneUser', userid=user.userid)) + if form.validate(): + user = userService.login(form) - errors = {} - errors['authentication'] = 'Wrong user or password!' + if user != None: + token = jwt.encode({ + 'user': user.get_json_parsable(), + 'userid' : user.userid, + 'username' : user.username, + 'roles' : user.get_roles(), + 'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=45) + }, + app.config['SECRET_KEY'], + "HS256") - return render_template('users/login.html', form=form, errors=errors) + return jsonify({ 'token': token }) - return render_template('users/login.html', form=form, errors=form.errors) + errors = {} + errors['authentication'] = 'Wrong user or password!' + return jsonify(errors) -@app.route('/logout', methods=["GET"]) -@inject -def logout(): - session.pop('userid', None) - session.pop('username', None) - session.pop('userroles', None) - return redirect(url_for('index')) - - -@app.route('/profile', methods=["GET"]) -@auth_required() -@inject -def profile(authService: AuthService): - userid = authService.get_current_user().userid - - return redirect(url_for('getOneUser', userid=userid)) + return jsonify(form.errors) diff --git a/app/dtos/basket_dto.py b/app/dtos/basket_dto.py index 1c7f01d..acef7b3 100644 --- a/app/dtos/basket_dto.py +++ b/app/dtos/basket_dto.py @@ -2,6 +2,7 @@ from app.dtos.item_dto import ItemDTO from app.dtos.user_dto import UserDTO from app.models.basket import Basket +from copy import deepcopy class BasketDTO(AbstractDTO): @@ -25,4 +26,8 @@ def build_from_entity(basket: Basket): return basket_dto def get_json_parsable(self): - pass \ No newline at end of file + items = [item.__dict__ for item in self.items] + self.items = items + self.user = self.user.get_json_parsable() + + return self.__dict__ \ No newline at end of file diff --git a/app/dtos/user_dto.py b/app/dtos/user_dto.py index c38fbc0..2ec41bd 100644 --- a/app/dtos/user_dto.py +++ b/app/dtos/user_dto.py @@ -27,8 +27,9 @@ def build_from_entity(user): user_dto.useremail = user.useremail user_dto.userdescription = user.userdescription user_dto.userroles = [] + for role in user.roles: - user_dto.userroles.append(RoleDTO.build_from_entity(role.rel_role).__dict__) + user_dto.userroles.append(RoleDTO.build_from_entity(role.rel_role)) return user_dto diff --git a/app/forms/item/item_form.py b/app/forms/item/item_form.py index 5baaefa..c51cb00 100644 --- a/app/forms/item/item_form.py +++ b/app/forms/item/item_form.py @@ -1,9 +1,9 @@ from flask_wtf import FlaskForm from wtforms import StringField, IntegerField -from wtforms.validators import DataRequired, NumberRange +from wtforms.validators import DataRequired, NumberRange, Length class ItemForm(FlaskForm): - itemname = StringField('itemname', validators=[DataRequired()]) + itemname = StringField('itemname', validators=[DataRequired(), Length(min=2, max=255)]) itemdescription = StringField('itemdescription', validators=[DataRequired()]) itemstock = IntegerField('itemstock', validators=[DataRequired(), NumberRange(min=0)]) \ No newline at end of file diff --git a/app/forms/user/user_login_form.py b/app/forms/user/user_login_form.py index 4c38720..65644f3 100644 --- a/app/forms/user/user_login_form.py +++ b/app/forms/user/user_login_form.py @@ -4,5 +4,8 @@ from app.models.user import User class UserLoginForm(FlaskForm): + class Meta: + csrf = False + username = StringField('username', validators=[DataRequired()]) userpassword = StringField('userpassword', validators=[DataRequired()]) \ No newline at end of file diff --git a/app/framework/decorators/auth_required.py b/app/framework/decorators/auth_required.py index c23897b..e4c4dee 100644 --- a/app/framework/decorators/auth_required.py +++ b/app/framework/decorators/auth_required.py @@ -1,8 +1,11 @@ from functools import wraps -from flask import redirect, session, url_for +from flask import redirect, session, url_for, request, jsonify from app.framework.decorators.inject import inject from app.services.auth_service import AuthService +from jwt import PyJWTError +import jwt +from app import app def auth_required(level="USER", or_is_current_user=False): @@ -10,11 +13,31 @@ def auth_required_decorator(func): @wraps(func) @inject def function_wrapper(authService: AuthService, *args, **kwargs): - current_user = authService.get_current_user() - if level in current_user.get_roles(): + token = None + + if 'x-access-tokens' in request.headers: + token = request.headers['x-access-tokens'] + + if 'Authorization' in request.headers: + token = request.headers['Authorization'] + + if not token: + return redirect(url_for('index')) + + try: + token = token.replace('Bearer ', '') + current_user = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"]) + print(current_user) + print(type(current_user)) + except PyJWTError as e: + print(e) + return redirect(url_for('index')) + + authService.set_current_user(current_user) + if level in current_user['roles']: return func(*args, **kwargs) - if or_is_current_user and current_user.userid == kwargs['userid']: + if or_is_current_user and current_user['userid'] == kwargs['userid']: return func(*args, **kwargs) return redirect(url_for('index')) diff --git a/app/framework/injector.py b/app/framework/injector.py index 563423e..97d94a6 100644 --- a/app/framework/injector.py +++ b/app/framework/injector.py @@ -1,6 +1,6 @@ from enum import Enum -from flask import Flask +from flask import Flask, session, request class Scope(Enum): @@ -39,13 +39,21 @@ def __init__(self, app: Flask, config): app.before_request(self.__request_start) app.after_request(self.__request_end) + def __get_session_id(self): + for cookie, value in request.cookies.items(): + if cookie == 'session': + return value + def __request_start(self, *args, **kwargs): # print("START SCOPED") - self.__scoped = {} + # print(session.get('csrf_token')) + sessionid = self.__get_session_id() + self.__scoped[sessionid] = {} def __request_end(self, response): # print("SCOPE END") - self.__scoped = {} + sessionid = self.__get_session_id() + self.__scoped[sessionid] = {} return response def __del__(self): @@ -71,10 +79,11 @@ def __get_singleton(self, dependency: DependencyConfig): return self.__singleton[dependency.base.__name__] def __get_scoped(self, dependency: DependencyConfig): - if self.__scoped.get(dependency.base.__name__) is None: - self.__scoped[dependency.base.__name__] = dependency.implement() + sessionid = self.__get_session_id() + if self.__scoped.get(sessionid).get(dependency.base.__name__) is None: + self.__scoped.get(sessionid)[dependency.base.__name__] = dependency.implement() - return self.__scoped[dependency.base.__name__] + return self.__scoped.get(sessionid)[dependency.base.__name__] def __get_transient(self, dependency: DependencyConfig): return dependency.implement() diff --git a/app/services/auth_service.py b/app/services/auth_service.py index f644ef2..8936d79 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -12,4 +12,7 @@ def __del__(self): @abstractmethod def get_current_user(self) -> UserDTO: + pass + + def set_current_user(self, user): pass \ No newline at end of file diff --git a/app/services/auth_service_impl.py b/app/services/auth_service_impl.py index 8855f41..575bdf5 100644 --- a/app/services/auth_service_impl.py +++ b/app/services/auth_service_impl.py @@ -19,4 +19,5 @@ def __init__(self, userService: UserService): def get_current_user(self) -> UserDTO: return self.__current_user - + def set_current_user(self, user): + self.__current_user = self.__user_service.find_one(user['userid']) diff --git a/app/services/basket_service.py b/app/services/basket_service.py index fde6d39..94243d2 100644 --- a/app/services/basket_service.py +++ b/app/services/basket_service.py @@ -18,7 +18,9 @@ def find_one(self, entity_id: int): return BasketDTO.build_from_entity(Basket.query.filter_by(basketid=entity_id).one()) def find_one_by(self, **kwargs): - return BasketDTO.build_from_entity(Basket.query.filter_by(**kwargs).one()) + basket = Basket.query.filter_by(**kwargs).one() + print(basket) + return BasketDTO.build_from_entity(basket) def insert(self, data): basket = Basket() @@ -27,7 +29,8 @@ def insert(self, data): try: db.session.add(basket) db.session.commit() - except: + except Exception as e: + print(e) db.session.rollback() return self.find_one(basket.basketid) @@ -40,7 +43,8 @@ def update(self, entity_id: int, data): BasketMapper.form_to_entity(data, basket) try: db.session.commit() - except: + except Exception as e: + print(e) db.session.rollback() return self.find_one(entity_id) @@ -53,7 +57,8 @@ def delete(self, entity_id: int): try: db.session.delete(basket) db.session.commit() - except: + except Exception as e: + print(e) db.session.rollback() return basket.basketid diff --git a/app/services/item_service.py b/app/services/item_service.py index 2c13d73..8d259f9 100644 --- a/app/services/item_service.py +++ b/app/services/item_service.py @@ -1,3 +1,5 @@ +import datetime + from app import db from app.dtos.item_dto import ItemDTO from app.mappers.item_mapper import ItemMapper @@ -22,7 +24,9 @@ def insert(self, data): try: db.session.add(item) db.session.commit() - except: + except Exception as e: + print(e) + raise e db.session.rollback() return self.find_one(item.itemid) @@ -36,7 +40,7 @@ def update(self, entity_id: int, data): ItemMapper.form_to_entity(data, item) try: db.session.commit() - except: + except Exception as e: db.session.rollback() return self.find_one(entity_id) @@ -50,7 +54,9 @@ def delete(self, entity_id: int): try: db.session.delete(item) db.session.commit() - except: + except Exception as e: + print(e) + raise e db.session.rollback() return item.itemid \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py index aad7392..2937d90 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,6 +1,7 @@ from flask import session from app import db +from app.dtos.user_dto import UserDTO from app.forms.user.user_login_form import UserLoginForm from app.forms.user.user_register_form import UserRegisterForm from app.forms.user.user_update_form import UserUpdateForm @@ -42,7 +43,8 @@ def insert(self, form: UserRegisterForm): try: db.session.add(user) db.session.commit() - except: + except Exception as e: + print(e) db.session.rollback() return self.find_one(user.userid) @@ -62,7 +64,8 @@ def update(self, entity_id: int, form: UserUpdateForm): try: db.session.commit() - except: + except Exception as e: + print(e) db.session.rollback() return None @@ -83,6 +86,6 @@ def login(self, form: UserLoginForm): to_login = self.find_one_by(username=user.username) if to_login is not None and bcrypt.checkpw(user.userpassword.encode('utf-8'), to_login.userpassword.encode('utf-8')): - return to_login + return UserDTO.build_from_entity(to_login) return None \ No newline at end of file diff --git a/app/static/js/ajax-tools.js b/app/static/js/ajax-tools.js index 040e25c..75acfde 100644 --- a/app/static/js/ajax-tools.js +++ b/app/static/js/ajax-tools.js @@ -1,3 +1,7 @@ +export let ajaxConfig = { + headers: [] +}; + export function sendAjax(url, method='get', data = null) { return new Promise((resolve, reject) => @@ -21,6 +25,19 @@ export function sendAjax(url, method='get', data = null) }); request.open(method, url, true); + + if(ajaxConfig.headers.length > 0) + { + for(let header of ajaxConfig.headers) + { + const methods = header.methods; + if(methods.includes(method) || methods.includes('ALL')) + { + request.setRequestHeader(header.key, header.value); + } + } + } + request.send(data); }) } \ No newline at end of file diff --git a/app/static/js/components/table-component.js b/app/static/js/components/table-component.js index 217de38..40fa5c2 100644 --- a/app/static/js/components/table-component.js +++ b/app/static/js/components/table-component.js @@ -2,7 +2,9 @@ export class TableComponent { constructor(tableConfig) { this.html = document.createElement('table'); - + this.form = []; + this.tableConfig = tableConfig; + this.data = []; this.initTable(tableConfig); } @@ -14,41 +16,133 @@ export class TableComponent this.thead = document.createElement('thead'); this.tbody = document.createElement('tbody'); - let first = true; let thead_tr = document.createElement('tr'); - for(const row of tableConfig.data) + for(const col of tableConfig.columns) { + const th = document.createElement('th'); + th.scope = 'col'; + th.innerText = col.columnName; + thead_tr.appendChild(th); + } + this.thead.appendChild(thead_tr); + this.html.appendChild(this.thead); + this.html.appendChild(this.tbody); + + if(tableConfig.findDataCb) + { + this.refreshData(); + } else if(tableConfig.data) { - let tr = document.createElement('tr'); - for(const col of tableConfig.columns) + this.data = tableConfig.data; + this.processData(); + } + } + + processData() + { + if(this.data.length > 0) + { + for(const row of this.data) { - if(first) - { - const th = document.createElement('th'); - th.scope = 'col'; - th.innerText = col.columnName; - thead_tr.appendChild(th); - } - if(col.value) - { - const value = this.processRow(row, col.value); - const td = document.createElement('td'); - td.innerText = value; - tr.appendChild(td); - } else if(col.valueCb) + let tr = document.createElement('tr'); + + this.processRowData(row, this.tableConfig, tr); + + this.tbody.appendChild(tr); + } + + } + } + + processRowData(row, tableConfig, tr) + { + for(const col of tableConfig.columns) + { + if(col.value) + { + const value = this.processRow(row, col.value); + const td = document.createElement('td'); + td.innerText = value; + tr.appendChild(td); + } else if(col.valueCb) + { + const value = col.valueCb(row); + const td = document.createElement('td'); + td.innerText = value; + tr.appendChild(td); + } else if(col.type === 'FORM') + { + this.processForm(row, tableConfig, tr); + } else if(col.type === 'ACTIONS') + { + this.processAction(row, tableConfig, tr); + } + } + } + + processForm(row, tableConfig, tr) + { + if(!tableConfig.form) + { + return; + } + let td = document.createElement('td'); + const formId = tableConfig.form.formName + row[tableConfig.form.formIdValue]; + this.form[formId] = document.createElement('form'); + td.appendChild(this.form[formId]); + + for(const field of tableConfig.form.formFields) + { + console.log(field); + const input = document.createElement('input'); + input.name = field.fieldName; + input.value = this.processRow(row, field.value); + input.type = field.type; + + this.form[formId].appendChild(input); + } + tr.appendChild(td); + } + + processAction(row, tableConfig, tr) + { + if(!tableConfig.actions) + { + return; + } + + let actionsTd = document.createElement('td'); + let div = document.createElement('div'); + div.classList.add('btn-group'); + + for(const action of tableConfig.actions) + { + // { actionName: '', actionHref: '', actionCb: (event) => T, buttonType: } + let actionBtn = document.createElement('button'); + actionBtn.innerText = action.actionName; + + actionBtn.addEventListener('click', (event) => + { + event.preventDefault(); + + let data = row; + + if(action.buttonType === 3) { - const value = col.valueCb(row); - const td = document.createElement('td'); - td.innerText = value; - tr.appendChild(td); + const formId = tableConfig.form.formName + row[tableConfig.form.formIdValue]; + data = new FormData(this.form[formId]); } - } - this.tbody.appendChild(tr); - first = false; + + action.actionCb(data); + }); + + + actionBtn.classList.add('btn'); + actionBtn.classList.add(action.buttonType === 1 ? 'btn-success' : action.buttonType === 2 ? 'btn-danger' : 'btn-primary'); + div.appendChild(actionBtn); + actionsTd.appendChild(div); } - this.thead.appendChild(thead_tr); - this.html.appendChild(this.thead); - this.html.appendChild(this.tbody); + tr.appendChild(actionsTd); } /** @@ -58,15 +152,43 @@ export class TableComponent processRow(row, value) { console.log(row, value) + // itemtype.itemtypename if(value.indexOf('.') !== -1) { let objMemberName = value.split('.'); - for(const val of objMemberName) + const len = objMemberName.length; + for(let i = 0; i < len; i++) { - row = this.processRow(row, val); + row = this.processRow(row, objMemberName[i]); } - return row; + return row[objMemberName[i]]; } return row[value]; } + + refreshData() + { + this.tableConfig.findDataCb().then((data) => + { + while(this.tbody.firstChild) + { + this.tbody.removeChild(this.tbody.firstChild); + } + this.data = JSON.parse(data); + + if(this.tableConfig.sortData) + { + const sortData = this.tableConfig.sortData; + this.data.sort((a, b) => + { + if(sortData.direction === 'ASC') + { + return a[sortData.sortColumnName] - b[sortData.sortColumnName]; + } + return b[sortData.sortColumnName] - a[sortData.sortColumnName]; + }) + } + this.processData(); + }) + } } diff --git a/app/static/js/components/table_component-v2.js b/app/static/js/components/table_component-v2.js new file mode 100644 index 0000000..7e75b29 --- /dev/null +++ b/app/static/js/components/table_component-v2.js @@ -0,0 +1,54 @@ +export class TableComponentV2 { + constructor(tableConfig) { + this.table = document.createElement('table'); + this.tableCaption = document.createElement('caption'); + this.tableHead = document.createElement('thead'); + this.tableBody = document.createElement('tbody'); + + this.initTable(tableConfig); + } + + initTable(tableConfig) { + + if (tableConfig.classList && tableConfig.classList.length != 0) { + const tableClasslist = this.table.classList; + tableConfig.classList.array.forEach(c => { + tableClasslist.add(c); + }); + } + + if (tableConfig.caption && tableConfig.caption != '') { + this.tableCaption.innerText = tableConfig.caption; + this.table.appendChild(this.tableCaption); + } + + if (tableConfig.columns && tableConfig.columns.length != 0) { + const tableHead_tr = document.createElement('tr'); + + } + } +} + +conf = { + data: data, + caption: "List of items", + classList: [ + 'table', + 'table-stripped', + 'table-hover' + ], + columns: [ + {name: 'ID', value: 'userid'}, + {name: 'Name', value: 'username'}, + {name: 'Email', value: 'useremail'}, + {name: 'Descritpion', value: 'userdescritpion'}, + {name: 'test', value: 'usertest', input: 'text'}, + {name: 'Roles', value: () => {}}, + ], + actions: [ + {name: 'update', type: 'button', classList: ['btn btn-primary'], cb: () => {}, event: 'click', displayType: 'row'}, + {name: 'remove', type: 'button', classList: ['btn btn-primary'], cb: () => {}, event: 'click', displayType: 'row'}, + {name: 'test', type: 'a', classList: ['btn btn-primary'], cb: "href", displayType: 'row'} + ] +} + diff --git a/app/templates/baskets/details.html b/app/templates/baskets/details.html index 54fb226..6e6bb54 100644 --- a/app/templates/baskets/details.html +++ b/app/templates/baskets/details.html @@ -3,56 +3,80 @@ {{ basket.user.username }}
- {% include 'items/item_list.html' %} +
-{# href="{{ url_for('checkout_basket') }}"#} - Checkout + Checkout
- {% endblock %} \ No newline at end of file diff --git a/app/templates/items/add_or_update.html b/app/templates/items/add_or_update.html index d2c7ea4..1269524 100644 --- a/app/templates/items/add_or_update.html +++ b/app/templates/items/add_or_update.html @@ -15,11 +15,15 @@ {% endif %} - {% if form.errors and "itemstock" in form.errors.keys() %} - {{ form.errors['itemstock'] }} - {% endif %} +
+

+ {% if error %} + error code {{ error.code }} : {{ error.message }} + {% endif %} +

+
{% endblock %} \ No newline at end of file diff --git a/app/templates/items/item_list.html b/app/templates/items/item_list.html index 4d7ebf3..0969a97 100644 --- a/app/templates/items/item_list.html +++ b/app/templates/items/item_list.html @@ -1,33 +1,24 @@ {% block body %} - - - - - - - - - - - - {% for item in items %} - - - - - - diff --git a/app/templates/items/list.html b/app/templates/items/list.html index 04dae22..648f3c6 100644 --- a/app/templates/items/list.html +++ b/app/templates/items/list.html @@ -1,7 +1,114 @@ {% extends "layout/layout.html" %} {% block body %} - {% include 'items/item_list.html' %} +
+
{% if "ADMIN" in session.get('userroles') %} - Add item + + {{ form.csrf_token }} + + {% if form.errors and "itemname" in form.errors.keys() %} + {{ form.errors['itemnames'] }} + {% endif %} + + {% if form.errors and "itemdescription" in form.errors.keys() %} + {{ form.errors['itemdescription'] }} + {% endif %} + + {% if form.errors and "itemstock" in form.errors.keys() %} + {{ form.errors['itemstock'] }} + {% endif %} + + + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/app/templates/layout/layout.html b/app/templates/layout/layout.html index 455a091..9c97127 100644 --- a/app/templates/layout/layout.html +++ b/app/templates/layout/layout.html @@ -9,6 +9,17 @@ + + diff --git a/app/templates/users/list.html b/app/templates/users/list.html index 4cf5386..c6b74de 100644 --- a/app/templates/users/list.html +++ b/app/templates/users/list.html @@ -28,7 +28,7 @@ { refreshUserTable(); document.forms['addUser'].reset(); - }) + }); }) refreshUserTable(); @@ -39,6 +39,7 @@ .then((res) => { const data = JSON.parse(res); + console.log(res); console.log(data); const tableConfig = { @@ -54,11 +55,10 @@ roles.push(role.rolename); } return roles.join(', '); - }}, + } }, ] } - let table = new TableComponent(tableConfig); const userTable = document.getElementById('userTable'); while(userTable.firstChild) @@ -66,6 +66,9 @@ userTable.removeChild(userTable.firstChild); } userTable.appendChild(table.html); + }, (err) => + { + console.log(err) }) } diff --git a/requirements.txt b/requirements.txt index c25119b..d33ea31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,15 @@ Flask Flask-WTF +WTForms-JSON Jinja2 Flask-DebugToolbar psycopg2 bcrypt Flask-SQLAlchemy Flask-Migrate +flask-cors dependency-injector -pyyaml \ No newline at end of file +pyyaml +pyjwt +uuid +python-dotenv diff --git a/sqlAlchemy.sh b/sqlAlchemy.sh index a0c3e0f..b709bb8 100755 --- a/sqlAlchemy.sh +++ b/sqlAlchemy.sh @@ -1,11 +1,12 @@ export FLASK_APP=app -while getopts 'm:iu' flag +while getopts 'm:iue' flag do case "${flag}" in m) DB_CMD="flask db migrate -m ${OPTARG}";; i) DB_CMD="flask db init";; u) DB_CMD="flask db upgrade";; + e) DB_CMD="flask db revision --autogenerate";; esac done @@ -17,4 +18,4 @@ then echo -e '-i \t init project database' else $DB_CMD -fi \ No newline at end of file +fi
#NameDescriptionQuantityActions
{{ item.itemid }} {{ item.itemname }} {{ item.itemdescription }} {{ item.itemquantity }} - {% if is_basket %} -
-
- - - - -
-
- {% endif %}