From 5c5799206fc63dcb59f95461d8c2b37698b1c748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 11 Apr 2019 23:56:55 +0200 Subject: [PATCH 001/111] new start --- geruecht/__init__.py | 17 +- geruecht/controller/__init__.py | 0 geruecht/controller/accesTokenController.py | 25 ++ geruecht/forms.py | 23 -- geruecht/model.py | 341 ------------------- geruecht/model/__init__.py | 0 geruecht/model/accessToken.py | 27 ++ geruecht/model/creditList.py | 45 +++ geruecht/model/priceList.py | 13 + geruecht/model/user.py | 24 ++ geruecht/routes.py | 201 +++--------- geruecht/site.db | Bin 0 -> 28672 bytes geruecht/static/img/logo.png | Bin 329633 -> 0 bytes geruecht/static/img/logo_selected.png | Bin 112669 -> 0 bytes geruecht/static/master.css | 194 ----------- geruecht/templates/about.html | 4 - geruecht/templates/finanzer.html | 119 ------- geruecht/templates/home.html | 53 --- geruecht/templates/layout.html | 89 ----- geruecht/templates/login.html | 41 --- geruecht/templates/test.html | 346 -------------------- run.py | 4 - 22 files changed, 195 insertions(+), 1371 deletions(-) create mode 100644 geruecht/controller/__init__.py create mode 100644 geruecht/controller/accesTokenController.py delete mode 100644 geruecht/forms.py delete mode 100644 geruecht/model.py create mode 100644 geruecht/model/__init__.py create mode 100644 geruecht/model/accessToken.py create mode 100644 geruecht/model/creditList.py create mode 100644 geruecht/model/priceList.py create mode 100644 geruecht/model/user.py create mode 100644 geruecht/site.db delete mode 100644 geruecht/static/img/logo.png delete mode 100644 geruecht/static/img/logo_selected.png delete mode 100644 geruecht/static/master.css delete mode 100644 geruecht/templates/about.html delete mode 100644 geruecht/templates/finanzer.html delete mode 100644 geruecht/templates/home.html delete mode 100644 geruecht/templates/layout.html delete mode 100644 geruecht/templates/login.html delete mode 100644 geruecht/templates/test.html diff --git a/geruecht/__init__.py b/geruecht/__init__.py index a514acd..6591d64 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -1,17 +1,18 @@ -from flask import Flask, g +from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt -from flask_login import LoginManager - +from .controller.accesTokenController import AccesTokenController +# from flask_login import LoginManager app = Flask(__name__) -app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' +# app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' db = SQLAlchemy(app) bcrypt = Bcrypt(app) -login_manager = LoginManager(app) -login_manager.login_view = 'login' -login_manager.login_message_category = 'info' +accesTokenController = AccesTokenController() +# login_manager = LoginManager(app) +# login_manager.login_view = 'login' +# login_manager.login_message_category = 'info' -from geruecht import routes \ No newline at end of file +from geruecht import routes diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py new file mode 100644 index 0000000..1dfc13c --- /dev/null +++ b/geruecht/controller/accesTokenController.py @@ -0,0 +1,25 @@ +from geruecht.model.accessToken import AccessToken +from datetime import datetime +import hashlib + +class AccesTokenController(): + tokenList = None + + def __init__(self): + self.tokenList = [] + + def findAccesToken(self, token): + for accToken in self.tokenList: + if accToken == token: + return accToken + return None + + def createAccesToken(self, user): + time = datetime.ctime(datetime.now()) + token = hashlib.md5((time + user.password).encode('utf-8')).hexdigest() + self.tokenList.append(AccessToken(user, token)) + print(self.tokenList) + return token + + def isSameGroup(self, accToken, group): + return True if accToken.user.group == group else False diff --git a/geruecht/forms.py b/geruecht/forms.py deleted file mode 100644 index c13803b..0000000 --- a/geruecht/forms.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, PasswordField; -from wtforms.validators import DataRequired, Length, ValidationError -from geruecht.model import User - - -class RegistrationForm(FlaskForm): - username = StringField('Name', validators=[DataRequired(), Length(min=2, max=20)]) - submit = SubmitField('Create') - - def validate_username(self, username): - - user = User.query.filter_by(username=username.data).first() - - if user: - raise ValidationError('Bist du behindert!? Der Name ist vergeben!!') - - -class FinanzerLogin(FlaskForm): - username = StringField('Name', validators=[DataRequired(), Length(min=2, max=20)]) - password = PasswordField('Password', validators=[DataRequired()]) - submit = SubmitField('Login') - diff --git a/geruecht/model.py b/geruecht/model.py deleted file mode 100644 index 9e73343..0000000 --- a/geruecht/model.py +++ /dev/null @@ -1,341 +0,0 @@ -from geruecht import db, login_manager -from threading import Timer -from flask_login import UserMixin -from datetime import date - - -@login_manager.user_loader -def load_finanzer(finanzer_id): - return Finanzer.query.get(int(finanzer_id)) - - -class User(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(20), unique=True, nullable=False) - sum = db.Column(db.Float, nullable=False, default=0.0) - - jan = db.Column(db.Float, nullable=False, default=0) - jan_sub = db.Column(db.Float, nullable=False, default=0) - jan_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - feb = db.Column(db.Float, nullable=False, default=0) - feb_sub = db.Column(db.Float, nullable=False, default=0) - feb_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - maer = db.Column(db.Float, nullable=False, default=0) - maer_sub = db.Column(db.Float, nullable=False, default=0) - maer_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - apr = db.Column(db.Float, nullable=False, default=0) - apr_sub = db.Column(db.Float, nullable=False, default=0) - apr_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - mai = db.Column(db.Float, nullable=False, default=0) - mai_sub = db.Column(db.Float, nullable=False, default=0) - mai_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - jun = db.Column(db.Float, nullable=False, default=0) - jun_sub = db.Column(db.Float, nullable=False, default=0) - jun_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - jul = db.Column(db.Float, nullable=False, default=0) - jul_sub = db.Column(db.Float, nullable=False, default=0) - jul_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - aug = db.Column(db.Float, nullable=False, default=0) - aug_sub = db.Column(db.Float, nullable=False, default=0) - aug_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - sep = db.Column(db.Float, nullable=False, default=0) - sep_sub = db.Column(db.Float, nullable=False, default=0) - sep_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - okt = db.Column(db.Float, nullable=False, default=0) - okt_sub = db.Column(db.Float, nullable=False, default=0) - okt_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - nov = db.Column(db.Float, nullable=False, default=0) - nov_sub = db.Column(db.Float, nullable=False, default=0) - nov_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - dez = db.Column(db.Float, nullable=False, default=0) - dez_sub = db.Column(db.Float, nullable=False, default=0) - dez_year = db.Column(db.Integer, nullable=False, default=date.today().year) - - - - def getsum(self): - jan = self.jan - self.jan_sub - feb = self.feb - self.feb_sub - maer = self.maer - self.maer_sub - apr = self.apr - self.apr_sub - mai = self.mai - self.mai_sub - jun = self.jun - self.jun_sub - jul = self.jul - self.jul_sub - aug = self.aug - self.aug_sub - sep = self.sep - self.sep_sub - okt = self.okt - self.okt_sub - nov = self.nov - self.nov_sub - dez = self.dez - self.dez_sub - - result = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez - return result - - def add_sub(self, value): - year = date.today().year - month = date.today().month - - if month == 1: - if year == self.jan_year: - self.jan_sub += value - else: - self.jan = 0 - self.jan_sub = value - self.jan_year = year; - elif month == 2: - if year == self.feb_year: - self.feb_sub += value - else: - self.feb = 0 - self.feb_sub = value - self.feb_year = year - elif month == 3: - if year == self.maer_year: - self.maer_sub += value - else: - self.maer = 0 - self.maer_sub = value - self.maer_year = year - elif month == 4: - if year == self.apr_year: - self.apr_sub += value - else: - self.apr = 0 - self.apr_sub = value - self.apr_year = year - elif month == 5: - if year == self.mai_year: - self.mai_sub += value - else: - self.mai = 0 - self.mai_sub = value - self.mai_year = year - elif month == 6: - if year == self.jun_year: - self.jun_sub += value - else: - self.jun = 0 - self.jun_sub = value - self.jun_year = year - elif month == 7: - if year == self.jul_year: - self.jul_sub += value - else: - self.jul = 0 - self.jul_sub = value - self.jul_year = year - elif month == 8: - if year == self.aug_year: - self.aug_sub += value - else: - self.aug = 0 - self.aug_sub = value - self.aug_year = year - elif month == 9: - if year == self.sep_year: - self.sep_sub += value - else: - self.sep = 0 - self.sep_sub = value - self.sep_year = year - elif month == 10: - if year == self.okt_year: - self.okt_sub += value - else: - self.okt = 0 - self.okt_sub = value - self.okt_year = year - elif month == 11: - if year == self.nov_year: - self.nov_sub += value - else: - self.nov = 0 - self.nov_sub = value - self.nov_year = year - elif month == 12: - if year == self.dez_year: - self.dez_sub += value - else: - self.dez = 0 - self.dez_sub = value - self.dez_year = year - else: - raise IndexError('Mehr monate gibt es nicht') - - - def add(self, value): - - year = date.today().year - month = date.today().month - - if month == 1 : - if year == self.jan_year: - self.jan += value - else: - self.jan = value - self.jan_sub = 0 - self.jan_year = year - elif month == 2: - if year == self.feb_year: - self.feb += value - else: - self.feb = value - self.feb_sub = 0 - self.feb_year = year - elif month == 3: - if year == self.maer_year: - self.maer += value - else: - self.maer = value - self.mear_sub = 0 - self.mear_year = year - elif month == 4: - if year == self.apr_year: - self.apr += value - else: - self.apr = value - self.apr_sub = 0 - self.apr_year = year - elif month == 5: - if year == self.mai_year: - self.mai += value - else: - self.mai = value - self.mai_sub = 0 - self.mai_year = year - elif month == 6: - if year == self.jun_year: - self.jun += value - else: - self.jun = value - self.jun_sub = 0 - self.jun_year = year - elif month == 7: - if year == self.jul_year: - self.jul += value - else: - self.jul = value - self.jul_sub = 0 - self.jul_year = year - elif month == 8: - if year == self.aug_year: - self.aug += value - else: - self.aug = value - self.aug_sub = 0 - self.aug_year = year - elif month == 9: - if year == self.sep_year: - self.sep += value - else: - self.sep = value - self.sep_sub = 0 - self.sep_year = year - elif month == 10: - if year == self.okt_year: - self.okt += value - else: - self.okt = value - self.okt_sub = 0 - self.okt_year = year - elif month == 11: - if year == self.nov_year: - self.nov += value - else: - self.nov = value - self.nov_sub = 0 - self.nov_year = year - elif month == 12: - if year == self.dez_year: - self.dez += value - else: - self.dez = value - self.dez_sub = 0 - self.dez_year = year - else: - raise IndexError('Mehr monate gibt es nicht') - - - def storner(self, value, month): - - if month == 1: - self.jan = self.jan - value - elif month == 2: - self.feb = self.feb - value - elif month == 3: - self.maer = self.maer - value - elif month == 4: - self.apr = self.apr - value - elif month == 5: - self.mai = self.mai - value - elif month == 6: - self.jun = self.jun - value - elif month == 7: - self.jul = self.jul - value - elif month == 8: - self.aug = self.aug - value - elif month == 9: - self.sep = self.sep - value - elif month == 10: - self.okt = self.okt - value - elif month == 11: - self.nov = self.nov - value - elif month == 12: - self.dez = self.dez - value - else: - raise IndexError('Mehr monate gibt es nicht') - - def __repr__(self): - return f"User('{self.username}', '{self.sum}')" - - -class Finanzer(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(20), unique=True, nullable=False) - password = db.Column(db.String(60), nullable=False) - - def __repr__(self): - return f"Finanzer('{self.username}')" - - -class History: - history = [] - user = None - - def __init__(self, history, user, value): - self.history = history - self.history.append(self) - self.user = user - self.month = date.today().month - self.value = value - self.timer = Timer(60, self.kill) - self.timer.start() - - def exec(self): - self.timer.cancel() - User.query.filter_by(username=self.user.username).first().storner(self.value, self.month) - self.kill() - - def kill(self): - print("{} deledet from history".format(self.user)) - self.history.remove(self) - print(self.history) - - -''' - def __repr__(self): - print(f'self:{self}, history:{self.history}, user:{self.user}, value:{self.value}') - - def __str__(self): - print(f'self:{self}, history:{self.history}, user:{self.user}, value:{self.value}') -''' diff --git a/geruecht/model/__init__.py b/geruecht/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py new file mode 100644 index 0000000..078c1bb --- /dev/null +++ b/geruecht/model/accessToken.py @@ -0,0 +1,27 @@ +from datetime import datetime + +class AccessToken(): + + timestamp = None + user = None + token = None + + def __init__(self, user, token, timestamp=datetime.now()): + self.user = user + self.timestamp = timestamp + self.token = token + + def updateTimestamp(self): + self.timestamp = datetime.now() + + def __eq__(self, token): + return True if self.token == token else False + + def __sub__(self, other): + return other - self.timestamp + + def __str__(self): + return f"AccessToken({self.user}, {self.token}, {self.timestamp}" + + def __repr__(self): + return f"AccessToken({self.user}, {self.token}, {self.timestamp}" diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py new file mode 100644 index 0000000..a8e97a2 --- /dev/null +++ b/geruecht/model/creditList.py @@ -0,0 +1,45 @@ +from geruecht import db +from datetime import datetime + +class CreditList(db.Model): + id = db.Column(db.Integer, primary_key=True) + + jan_guthaben = db.Column(db.Integer, nullable=False, default=0) + jan_schulden = db.Column(db.Integer, nullable=False, default=0) + + feb_guthaben = db.Column(db.Integer, nullable=False, default=0) + feb_schulden = db.Column(db.Integer, nullable=False, default=0) + + maer_guthaben = db.Column(db.Integer, nullable=False, default=0) + maer_schulden = db.Column(db.Integer, nullable=False, default=0) + + apr_guthaben = db.Column(db.Integer, nullable=False, default=0) + apr_schulden = db.Column(db.Integer, nullable=False, default=0) + + mai_guthaben = db.Column(db.Integer, nullable=False, default=0) + mai_schulden = db.Column(db.Integer, nullable=False, default=0) + + jun_guthaben = db.Column(db.Integer, nullable=False, default=0) + jun_schulden = db.Column(db.Integer, nullable=False, default=0) + + jul_guthaben = db.Column(db.Integer, nullable=False, default=0) + jul_schulden = db.Column(db.Integer, nullable=False, default=0) + + aug_guthaben = db.Column(db.Integer, nullable=False, default=0) + aug_schulden = db.Column(db.Integer, nullable=False, default=0) + + sep_guthaben = db.Column(db.Integer, nullable=False, default=0) + sep_schulden = db.Column(db.Integer, nullable=False, default=0) + + okt_guthaben = db.Column(db.Integer, nullable=False, default=0) + okt_schulden = db.Column(db.Integer, nullable=False, default=0) + + nov_guthaben = db.Column(db.Integer, nullable=False, default=0) + nov_schulden = db.Column(db.Integer, nullable=False, default=0) + + dez_guthaben = db.Column(db.Integer, nullable=False, default=0) + dez_schulden = db.Column(db.Integer, nullable=False, default=0) + + last_schulden = db.Column(db.Integer, nullable=False, default=0) + + year = db.Column(db.Integer, nullable=False, default=datetime.now().year) diff --git a/geruecht/model/priceList.py b/geruecht/model/priceList.py new file mode 100644 index 0000000..7616dda --- /dev/null +++ b/geruecht/model/priceList.py @@ -0,0 +1,13 @@ +from geruecht import db + +class PriceList(db.Model): + id = db.Column(db.Integer, primary_key=True) + + name = db.Column(db.String, nullable=False, unique=True) + price = db.Column(db.Integer, nullable=False) + price_club = db.Column(db.Integer, nullable=False) + price_ext_club = db.Column(db.Integer, nullable=False) + category = db.Column(db.Integer, nullable=False) + upPrice = db.Column(db.Integer) + upPrice_club = db.Column(db.Integer) + upPrice_ext_club = db.Column(db.Integer) diff --git a/geruecht/model/user.py b/geruecht/model/user.py new file mode 100644 index 0000000..557e434 --- /dev/null +++ b/geruecht/model/user.py @@ -0,0 +1,24 @@ +from geruecht import db +from geruecht import bcrypt + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + userID = db.Column(db.String, nullable=False, unique=True) + username = db.Column(db.String, nullable=False, unique=True) + firstname = db.Column(db.String, nullable=False) + lastname = db.Column(db.String, nullable=False) + group = db.Column(db.String, nullable=False) + password = db.Column(db.String, nullable=False) + + def toJSON(self): + dic = { + "username": self.username, + "firstname": self.firstname, + "lastname": self.lastname, + "group": self.group, + } + return dic + + def login(self, password): + return True if bcrypt.check_password_hash(self.password, password) else False + diff --git a/geruecht/routes.py b/geruecht/routes.py index f828660..c3efb3a 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,156 +1,59 @@ -from flask import render_template, url_for, redirect, flash, jsonify, g, request -from flask_login import login_user, current_user, logout_user, login_required -from geruecht import app, db, bcrypt -from geruecht.forms import RegistrationForm, FinanzerLogin -from geruecht.model import User, History, Finanzer -import flask_sijax -import os, sys +from geruecht import app, db, accesTokenController +from geruecht.model.user import User +from geruecht.model.creditList import CreditList +from geruecht.model.priceList import PriceList +from flask import request, jsonify -path = os.path.join('.', os.path.dirname(__file__), '../') -sys.path.append(path) +MONEY = "moneymaster" +GASTRO = "gastro" +USER = "user" -# The path where you want the extension to create the needed javascript files -# DON'T put any of your files in this directory, because they'll be deleted! -app.config["SIJAX_STATIC_PATH"] = os.path.join('.', os.path.dirname(__file__), 'static/js/sijax/') +def verifyAccessToken(token, group): + accToken = accesTokenController.findAccesToken(token) + print(accToken) + if accToken is not None: + if accesTokenController.isSameGroup(accToken, group): + accToken.updateTimestamp() + return accToken + return None -# You need to point Sijax to the json2.js library if you want to support -# browsers that don't support JSON natively (like IE <= 7) -app.config["SIJAX_JSON_URI"] = '/static/js/sijax/json2.js' +@app.route("/getFinanzerMain", methods=['POST']) +def _getFinanzer(): + data = request.get_json() + token = data["token"] -flask_sijax.Sijax(app) + accToken = verifyAccessToken(token, MONEY) + if accToken is not None: + users = User.query.all() + dic = {} + for user in users: + dic["userID"] = user.toJSON() + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 -history = [] - - -@flask_sijax.route(app, "/", methods=['GET', 'POST']) -@flask_sijax.route(app, "/home", methods=['GET', 'POST']) -def home(): - def add(obj_response, username, value): - user_2 = User.query.filter_by(username=username).first() - print(user_2) - print(user_2.id) - user_2.add(value) - print(user_2) - db.session.commit() - print(obj_response, username, value) - History(history=history, user=user_2, value=value) - print(history) - obj_response.html('div#flash', f'{user_2.username} wurde {"%.2f"%value} € hinzugefügt') - obj_response.attr('div#flash', "class", "alert alert-success") - obj_response.html(f'#{user_2.id}-sum', str("%.2f" % user_2.getsum() + " €")) - - def storner(obj_response): - try: - obj = history[len(history)-1] - user_2 = User.query.filter_by(username=obj.user.username).first() - print("{} {}".format(obj.user, obj.value)) - obj.exec() - print(history) - print(user_2) - db.session.commit() - obj_response.html('div#flash', f'{"%.2f"%obj.value} wurden von {user_2.username} storniert') - obj_response.attr('div#flash', "class", "alert alert-success") - obj_response.html(f'#{user_2.id}-sum', str("%.2f" % user_2.getsum() + " €")) - - except IndexError: - print("history: {} is empty".format(history)) - obj_response.html('div#flash', "Der Timer ist abgelaufen!! Die Stornierliste ist leer!! Falls es was wichtiges ist, melde dich beim Finanzer oder Administrator!!") - obj_response.attr('div#flash', "class", "alert alert-error") - - if g.sijax.is_sijax_request: - g.sijax.register_callback('add', add) - g.sijax.register_callback('storner', storner) - return g.sijax.process_request() - - form = RegistrationForm() - if form.validate_on_submit(): - user = User(username=form.username.data) - db.session.add(user) - db.session.commit() - flash(f'Person wurde angelegt: {form.username.data}', 'success') - return redirect(url_for('home')) - return render_template('home.html', users=User.query.all(), form=form) - - -@flask_sijax.route(app, "/return") -def to_home(): - return home() - - -@app.route("/login", methods=['GET', 'POST']) -def login(): - if current_user.is_authenticated: - return redirect(url_for('home')) - login_form = FinanzerLogin() - if login_form.validate_on_submit(): - user = Finanzer.query.filter_by(username=login_form.username.data).first() - if user: - if user and bcrypt.check_password_hash(user.password, login_form.password.data): - login_user(user) - next_page = request.args.get('next') - return redirect(next_page) if next_page else redirect(url_for('home')) - else: - flash('Passwort ist falsch', 'error') +@app.route("/login", methods=['POST']) +def _login(): + data = request.get_json() + print(data) + username = data['username'] + password = data['password'] + user = User.query.filter_by(username=username).first() + if user: + if user.login(password): + token = accesTokenController.createAccesToken(user) + dic = user.toJSON() + dic["token"] = token + return jsonify({user.userID: dic}) else: - flash('nur der finanzer kann sich einloggen !!', 'error') - return render_template('login.html', title='Login', form=login_form) + return jsonify({"error": "wrong password"}), 401 + return jsonify({"error": "wrong username"}), 402 + - -@app.route("/logout") -def logout(): - logout_user() - return redirect(url_for('home')) - - -@flask_sijax.route(app, "/uebersicht") -@login_required -def finanzen(): - - def supply(obj_response, arg1, arg2): - - list = zip(arg1, arg2) - try: - for value in arg2: - if value: - float(value) - - for user_id, value in list: - if user_id and value: - user_ = User.query.get(user_id) - user_.add_sub(float(value)) - db.session.commit() - obj_response.attr(f'#{user_id}-input', 'value', '') - - obj_response.html('div#flash', "Alle Werte wurden übernommen") - obj_response.attr('div#flash', 'class', "alert alert-success") - - for user_ in User.query.all(): - obj_response.html(f'p#{user_.id}-jan-sub', "%.2f"%user_.jan_sub + " €") - obj_response.html(f'p#{user_.id}-feb-sub', "%.2f"%user_.feb_sub + " €") - obj_response.html(f'p#{user_.id}-maer-sub', "%.2f"%user_.maer_sub + " €") - obj_response.html(f'p#{user_.id}-apr-sub', "%.2f"%user_.apr_sub + " €") - obj_response.html(f'p#{user_.id}-mai-sub', "%.2f"%user_.mai_sub + " €") - obj_response.html(f'p#{user_.id}-jun-sub', "%.2f"%user_.jun_sub + " €") - obj_response.html(f'p#{user_.id}-jul-sub', "%.2f"%user_.jul_sub + " €") - obj_response.html(f'p#{user_.id}-aug-sub', "%.2f"%user_.aug_sub + " €") - obj_response.html(f'p#{user_.id}-sep-sub', "%.2f"%user_.sep_sub + " €") - obj_response.html(f'p#{user_.id}-okt-sub', "%.2f"%user_.okt_sub + " €") - obj_response.html(f'p#{user_.id}-nov-sub', "%.2f"%user_.nov_sub + " €") - obj_response.html(f'p#{user_.id}-dez-sub', "%.2f"%user_.dez_sub + " €") - obj_response.html(f'p#{user_.id}-sum', "%.2f"%user_.getsum() + " €") - - except ValueError: - obj_response.html('div#flash', - "Du hast irgendwo keine Zahl eingegeb!!") - obj_response.attr('div#flash', "class", "alert alert-error") - - if g.sijax.is_sijax_request: - g.sijax.register_callback('supply', supply) - return g.sijax.process_request() - - return render_template('finanzer.html', users=User.query.all()) - - -@flask_sijax.route(app, "/about") -def about(): - return render_template('about.html', title='about') +@app.route("/getFinanzer") +def getFinanzer(): + users = User.query.all() + dic = {} + for user in users: + dic["userID"] = user.toJSON() + print(dic) + return jsonify(dic) diff --git a/geruecht/site.db b/geruecht/site.db new file mode 100644 index 0000000000000000000000000000000000000000..ae21fbe68089f050a3ef470ccc1be0faaf206144 GIT binary patch literal 28672 zcmeI(T~FFj7zglEK^R>eOBTb0GdZG(5||4!Oc0iMMG8G)SzoX(PQ9Bm z+1>8;6?VPLeT#jWne1{sXoDCy+${_FH&kn%rw`}+jy>91_sd1yWAxB=Ox2?~@{mXp zc}*!HBpR&oU=5c@u)+6*ixh4MK1a#Z$3Lc$e@HB`Odgyjez{} zfB*y_0D=ES;G#AePtDCqm%gVq4Ayb-Dx`n-wKrRcJN8QB-JeKV6a&x<(77 zs1pY5y?g%pE_3)g(Rar3J%Up;&Etiw z^uXzp$A-?Whq~i>L!-Q-Lxbs-WBZ-K@7YmZ_oMA-0~>VL;Py-y_M~rm40{kvk&BU7 z>gDvXDUv7Fv;1RpabjfRDo^Cs^7r@;)~R5XlHW-1!U6#ZKmY;|fB*y_009U<00Izz zz`qpGr39Isc{DTsY0_h^$1m8Mj&8cl@^mw}|G-+zF>Q;TnZcb3<}73z3#-|M@>Y5+V)ya+1XlAw2HRd_FL6sIp~*^{7r%v z76?E90uX=z1Rwwb2tWV=5P$###z^3PWHu4rSGd0Zzaq)2G3p??0s#m>00Izz00bZa z0SG_<0uX=zA#!9Uaq~a{fBrxCr++LEfB*y_009U<00Izz00bZa0SJt-0IvVXcyQ4* z2tWV=5P$##AOHafKmY;|fWVCafBqkT|Hroj1Rwwb2tWV=5P$##AOHafKw!KD{sP93 BK)V0{ literal 0 HcmV?d00001 diff --git a/geruecht/static/img/logo.png b/geruecht/static/img/logo.png deleted file mode 100644 index abd2e09c2f6b5b00226460f8463accc4068386f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 329633 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 z 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 Q;9rO|R6(Nfg@Nz?0rV6|)&Kwi diff --git a/geruecht/static/img/logo_selected.png b/geruecht/static/img/logo_selected.png deleted file mode 100644 index f27650dbfc74ebe61dcd005900c0e2fa77d2760b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112669 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

*5$nxrq9JUCiaE4OL`_-nx7 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 diff --git a/geruecht/static/master.css b/geruecht/static/master.css deleted file mode 100644 index 3c3c336..0000000 --- a/geruecht/static/master.css +++ /dev/null @@ -1,194 +0,0 @@ -body { - margin: 0 auto; -} - -.topnav { - background-color: #077BCB; - height: 61; - overflow: hidden; -} - -.blue-button { - color: white; - background-color: #055288; - text-align: center; - padding: 5px 5px; - margin: 7px 17px; - text-decoration: none; - font-size: 17px; - border: 5px solid #055288; - border-style: solid; - border-radius: 10px; - transition-duration: 0.4s; -/*box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);*/ -} - -.no-mg { - margin: 0; -} - -.no-pd { - padding: 0; -} - -.blue-button:hover { - background: white; - border: 5px solid #055288; - color: black; -} - -.blue-button:active { - transform: translateY(4px); -} - -.first { - float: left;; - margin-left: 10%; -} - -.last { - float: right; -} - -.logo-nav { - height: 61px; - width: auto; - transition-duration: 0.4s; -} -.logo-nav:active { - transform: translateY(4px) -} - -.logo-nav img { - position: absolute; - transition-duration: 0.4s; -} - -.logo-nav img.main-logo:hover { - opacity: 0; -} - - - -.geruecht { - margin: 5px auto; - display: table; - border: 0px; - border-top: 1px gray; - border-bottom: 1px gray; - border-radius: 15px; - border-style: solid; - width: 100%; - border-spacing: 5px; - height: 30px; -} - -.g-item { - display: table-cell; - text-align: center; - vertical-align: middle; - width: 20%; -} - -.auto-width { - width:6.66%; -} - -.width-auto { - width:80%; -} - -.g-item p { - font-size: 15px; -} - -.button { - width: 70px; - heigt: 30px; - border-radius: 10px; - border: 5px solid green; - background: green; - color: white; - transition-duration: 0.4s; -} - -.button:hover { - color: green; - background: white; -} - -.button:active { - transform: translateY(4px); -} - -.bottombar { - background-color: #077BCB; - overflow: scroll; - position: fixed; - bottom: 0; - width: 100%; - height: 62px; - display: table; -} - -.bottombar-element { - float: left; - line-height: auto; - vertical-align: middle; -} - -.right { - float: right; -} - -.form-group { - float: left; - border: 0; - height: auto; - padding: 5px; - margin: auto; - float: left; - vertical-align: middle; -} - -.reg-label { - color: white; -} - -.m { - margin-top: 10px; -} - -.alert { - color: black; - font-size: 15px; - text-align: center; -} - -.alert-success { - background: lightgreen; - border: 1px solid darkgreen; - color: darkgreen; - border-radius: 5px; - padding: 6px; -} - -.alert-error { - background: darksalmon; - border: 1px solid darkred; - color: darkred; - border-radius: 5px; - padding: 6px; -} - -.container { - margin-bottom: 50%; -} - -.schulden { - color: red; -} - -.bezahlt { - color: green; -} diff --git a/geruecht/templates/about.html b/geruecht/templates/about.html deleted file mode 100644 index 941af53..0000000 --- a/geruecht/templates/about.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "layout.html" %} -{% block content %} -

About Page

-{% endblock %} \ No newline at end of file diff --git a/geruecht/templates/finanzer.html b/geruecht/templates/finanzer.html deleted file mode 100644 index def5748..0000000 --- a/geruecht/templates/finanzer.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends 'layout.html' %} - -{% block content %} -
-
- Username -
-
- Januar -
-
- Februar -
-
- März -
-
- April -
-
- Mai -
-
- Juni -
-
- July -
-
- August -
-
- September -
-
- Oktober -
-
- November -
-
- Dezember -
-
- Schulden -
-
- Ausgleich -
-
- {% for user in users %} -
-
-

{{ user.username }}

-
-
-

{{ "%.2f"%user.jan }} €

-

{{ "%.2f"%user.jan_sub }} €

-
-
-

{{ "%.2f"%user.feb }} €

-

{{ "%.2f"%user.feb_sub }} €

-
-
-

{{ "%.2f"%user.maer }} €

-

{{ "%.2f"%user.maer_sub }} €

-
-
-

{{ "%.2f"%user.apr }} €

-

{{ "%.2f"%user.apr_sub }} €

-
-
-

{{ "%.2f"%user.mai }} €

-

{{ "%.2f"%user.mai_sub }} €

-
-
-

{{ "%.2f"%user.jun }} €

-

{{ "%.2f"%user.jun_sub }} €

-
-
-

{{ "%.2f"%user.jul }} €

-

{{ "%.2f"%user.jul_sub }} €

-
-
-

{{ "%.2f"%user.aug }} €

-

{{ "%.2f"%user.aug_sub }} €

-
-
-

{{ "%.2f"%user.sep }} €

-

{{ "%.2f"%user.sep_sub }} €

-
-
-

{{ "%.2f"%user.okt }} €

-

{{ "%.2f"%user.okt_sub }} €

-
-
-

{{ "%.2f"%user.nov }} €

-

{{ "%.2f"%user.nov_sub }} €

-
-
-

{{ "%.2f"%user.dez }} €

-

{{ "%.2f"%user.dez_sub }} €

-
-
-

{{ "%.2f"%user.getsum() }} €

-
-
- -
-
- {% endfor %} -{% endblock %} -{% block bottombar %} - - -
- Anwenden -
-{% endblock %} diff --git a/geruecht/templates/home.html b/geruecht/templates/home.html deleted file mode 100644 index 7045b52..0000000 --- a/geruecht/templates/home.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "layout.html" %} -{% block content %} - {% for user in users %} -
-
-

{{ user.username }}

-
-
- +2 € -
-
- +1 € -
-
- +0,5 € -
-
- +0,4 € -
-
- +0,2 € -
-
- +0,1 € -
-
-

{{ "%.2f"%user.getsum() }} €

-
-
- {% endfor %} -{% endblock %} -{% block bottombar %} -
- {% if form %} -
- {{ form.hidden_tag() }} -
- -
- {{ form.username.label(class="reg-label") }} - {{ form.username() }} -
-
-
- {{ form.submit(class="blue-button no-mg") }} -
-
- {% endif %} -
-
- Stornieren -
-{% endblock %} diff --git a/geruecht/templates/layout.html b/geruecht/templates/layout.html deleted file mode 100644 index a115b51..0000000 --- a/geruecht/templates/layout.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - {% if title %} - {{ title }} - {% else %} - Gerüchteküche - {% endif %} - - - - - - - - - - - - - -
-
- {% block content %} - - {% endblock %} - -
- - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
- {{ message }} -
- {% endfor %} - {% endif %} - {% if form %} - {% if form.username.errors %} - {% for error in form.username.errors %} -
- {{ error }} -
- {% endfor %} - {% endif %} - {% if form.password %} - {% if form.password.errors %} - {% for error in form.password.errors %} -
- {{ error }} -
- {% endfor %} - {% endif %} - {% endif %} - {% endif %} - {% endwith %} -
- {% block bottombar %} - - {% endblock %} -
- - - - - - - diff --git a/geruecht/templates/login.html b/geruecht/templates/login.html deleted file mode 100644 index ab7f0ec..0000000 --- a/geruecht/templates/login.html +++ /dev/null @@ -1,41 +0,0 @@ - -{% extends "layout.html" %} -{% block content %} - -{% endblock %} diff --git a/geruecht/templates/test.html b/geruecht/templates/test.html deleted file mode 100644 index 1d754ff..0000000 --- a/geruecht/templates/test.html +++ /dev/null @@ -1,346 +0,0 @@ - - - Home - - - -
- Finanzer -
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-

dummy 1

-
-
-

+2 €

-
-
-

+1 €

-
-
-

+0,5 €

-
-
-

+0,4 €

-
-
-

+0,2 €

-
-
-

+0,1 €

-
-
-

Gesamt

-
-
- -
-
-
- Name: - -
-
-
-

Stornieren

-
-
- diff --git a/run.py b/run.py index 1d9622b..0de5915 100644 --- a/run.py +++ b/run.py @@ -1,9 +1,5 @@ from geruecht import app - - - - if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') From 535b9cbc12c3ab7c9a6095d9b4ca0ad30cd93beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 12 Apr 2019 14:51:37 +0200 Subject: [PATCH 002/111] AccesTokenController ist ein Thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccesTokenController schaut immer wieder nach, ob ein AccesToken noch valid ist. Zeitabstand beträgt bis jetzt 10 SeKunden ValidLifeTime beträgt bis jetzt 60 Sekunden --- geruecht/__init__.py | 3 ++ geruecht/controller/accesTokenController.py | 32 ++++++++++++++++++-- geruecht/model/user.py | 1 + geruecht/routes.py | 14 +++++++-- geruecht/site.db | Bin 28672 -> 28672 bytes 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 6591d64..671f980 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -1,15 +1,18 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt +from flask_cors import CORS from .controller.accesTokenController import AccesTokenController # from flask_login import LoginManager app = Flask(__name__) +CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' db = SQLAlchemy(app) bcrypt = Bcrypt(app) accesTokenController = AccesTokenController() +accesTokenController.start() # login_manager = LoginManager(app) # login_manager.login_view = 'login' # login_manager.login_message_category = 'info' diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 1dfc13c..e7a5c77 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,25 +1,51 @@ from geruecht.model.accessToken import AccessToken from datetime import datetime +import time +from threading import Thread import hashlib -class AccesTokenController(): +class AccesTokenController(Thread): tokenList = None + self.lifetime = 60 def __init__(self): + print("init AccesTokenControlle") + print("init threading") + Thread.__init__(self) self.tokenList = [] def findAccesToken(self, token): + print("search for AccesToken", token) for accToken in self.tokenList: if accToken == token: + print("find AccesToken", accToken, "with token", token) return accToken + print("no AccesToken with", token) return None def createAccesToken(self, user): - time = datetime.ctime(datetime.now()) - token = hashlib.md5((time + user.password).encode('utf-8')).hexdigest() + print("create AccesToken") + now = datetime.ctime(datetime.now()) + token = hashlib.md5((now + user.password).encode('utf-8')).hexdigest() self.tokenList.append(AccessToken(user, token)) print(self.tokenList) + print("finished create AccesToken", token) return token def isSameGroup(self, accToken, group): + print("controll if", accToken, "hase group", group) return True if accToken.user.group == group else False + + def run(self): + while True: + print("start allocate") + for accToken in self.tokenList: + print("controle", accToken) + if (datetime.now() - accToken.timestamp).seconds > self.lifetime: + print("delete", accToken) + self.tokenList.remove(accToken) + else: + print("time is only", (datetime.now() - accToken.timestamp).seconds) + print(self.tokenList) + print("wait") + time.sleep(10) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 557e434..6c8c9de 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -12,6 +12,7 @@ class User(db.Model): def toJSON(self): dic = { + "userId": self.userID, "username": self.username, "firstname": self.firstname, "lastname": self.lastname, diff --git a/geruecht/routes.py b/geruecht/routes.py index c3efb3a..967be95 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -7,6 +7,7 @@ from flask import request, jsonify MONEY = "moneymaster" GASTRO = "gastro" USER = "user" +BAR = "bar" def verifyAccessToken(token, group): accToken = accesTokenController.findAccesToken(token) @@ -31,6 +32,15 @@ def _getFinanzer(): return jsonify(dic) return jsonify({"error": "permission denied"}), 401 +@app.route("/valid", methods=['POST']) +def _valid(): + data = request.get_json() + token = data["token"] + accToken = verifyAccessToken(token, MONEY) + if accToken is not None: + return jsonify(accToken.user.toJSON()) + return jsonify({"error": "permission denied"}), 401 + @app.route("/login", methods=['POST']) def _login(): data = request.get_json() @@ -43,11 +53,11 @@ def _login(): token = accesTokenController.createAccesToken(user) dic = user.toJSON() dic["token"] = token - return jsonify({user.userID: dic}) + return jsonify(dic) else: return jsonify({"error": "wrong password"}), 401 return jsonify({"error": "wrong username"}), 402 - + @app.route("/getFinanzer") def getFinanzer(): diff --git a/geruecht/site.db b/geruecht/site.db index ae21fbe68089f050a3ef470ccc1be0faaf206144..172ae01ada83bfeb1859bd052b769c271b5ec6ed 100644 GIT binary patch delta 206 zcmZp8z}WDBae_1>>qHr6M%Il9OZYjM_)Qr2r}CR@78KCnPmE{cWDu2>loW1cEzL|! zEzZj?&rMBDFG@|%FG?%QEU8LON-R<_N>VX2Qi(`*t|~Y1_p>N73ARk}af@*Cb`DL= z%gomgDX{eM_b*DXw9N9;^T{>Oa*gy!j7)RPj4+x!RX!hR=W7Q3FZ{1J3o6{_SL9$8 zWdvEq#LX%S0JxMwX2UOZeFs`KL1QPu(o2(9J)2s(e0>|Am47%Vt4`SNvk! j%(9#%sl_Fw#i>PH3=9m6{9hUPzXIi6^KbrY&ustz)O8rO From 2ba7240611ab24a3a3800549c622b75d2a4a9d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Apr 2019 14:46:46 +0200 Subject: [PATCH 003/111] =?UTF-8?q?Erste=20Doku=20hinzugef=C3=BCgt=20im=20?= =?UTF-8?q?Ticket=20#41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/__init__.py | 7 +++ geruecht/controller/accesTokenController.py | 51 ++++++++++++++++++++- geruecht/model/accessToken.py | 26 ++++++++++- geruecht/model/creditList.py | 12 +++++ geruecht/model/priceList.py | 4 ++ geruecht/model/user.py | 25 ++++++++++ geruecht/routes.py | 28 +++++++++++ run.py | 4 ++ 8 files changed, 153 insertions(+), 4 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 671f980..5a7316f 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -1,3 +1,10 @@ +""" Server-package + + Initialize app, cors, database and bcrypt (for passwordhashing) and added it to the application. + Initialize also a singelton for the AccesTokenControler and start the Thread. + +""" + from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index e7a5c77..7c77680 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -5,16 +5,38 @@ from threading import Thread import hashlib class AccesTokenController(Thread): + """ Control all createt AccesToken + + This Class create, delete, find and manage AccesToken. + + Attributes: + tokenList: List of currents AccessToken + lifetime: Variable for the Lifetime of one AccessToken in seconds. + """ tokenList = None - self.lifetime = 60 + lifetime = 60 def __init__(self): + """ Initialize AccessTokenController + + Initialize Thread and set tokenList empty. + """ print("init AccesTokenControlle") print("init threading") Thread.__init__(self) self.tokenList = [] def findAccesToken(self, token): + """ Find a Token in current AccessTokens + + Iterate throw all availables AccesTokens and retrieve one, if they are the same. + + Args: + token: Token to find + + Returns: + An AccessToken if found or None if not found. + """ print("search for AccesToken", token) for accToken in self.tokenList: if accToken == token: @@ -24,6 +46,16 @@ class AccesTokenController(Thread): return None def createAccesToken(self, user): + """ Create an AccessToken + + Create an AccessToken for an User and add it to the tokenList. + + Args: + user: For wich User is to create an AccessToken + + Returns: + A created Token for User + """ print("create AccesToken") now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.password).encode('utf-8')).hexdigest() @@ -33,15 +65,30 @@ class AccesTokenController(Thread): return token def isSameGroup(self, accToken, group): + """ Verify group in AccessToken + + Verify if the User in the AccesToken has the right group. + + Args: + accToken: AccessToken to verify. + group: Group to verify. + + Returns: + A Bool. If the same then True else False + """ print("controll if", accToken, "hase group", group) return True if accToken.user.group == group else False def run(self): + """ Starting Controll-Thread + + Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. + """ while True: print("start allocate") for accToken in self.tokenList: print("controle", accToken) - if (datetime.now() - accToken.timestamp).seconds > self.lifetime: + if (datetime.now() - accToken.timestamp).seconds > 60: print("delete", accToken) self.tokenList.remove(accToken) else: diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 078c1bb..916b4ab 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -1,17 +1,38 @@ from datetime import datetime class AccessToken(): + """ Model for an AccessToken + + Attributes: + timestamp: Is a Datetime from current Time. + user: Is an User. + token: String to verify access later. + """ timestamp = None user = None token = None def __init__(self, user, token, timestamp=datetime.now()): + """ Initialize Class AccessToken + + No more to say. + + Args: + User: Is an User to set. + token: Is a String to verify later + timestamp: Default current time, but can set to an other datetime-Object. + """ + self.user = user self.timestamp = timestamp self.token = token def updateTimestamp(self): + """ Update the Timestamp + + Update the Timestamp to the current Time. + """ self.timestamp = datetime.now() def __eq__(self, token): @@ -21,7 +42,8 @@ class AccessToken(): return other - self.timestamp def __str__(self): - return f"AccessToken({self.user}, {self.token}, {self.timestamp}" + return "AccessToken({}, {}, {}".format(self.user, self.token, self.timestamp) def __repr__(self): - return f"AccessToken({self.user}, {self.token}, {self.timestamp}" + return "AccessToken({}, {}, {}".format(self.user, self.token, self.timestamp) + diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index a8e97a2..a99b5cb 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -2,6 +2,18 @@ from geruecht import db from datetime import datetime class CreditList(db.Model): + """ DataBase Object Credit List: + + Attributes: + id: id in Database. Is the Primary Key + _guthaben: Credit of the Month. + _schulden: Debt of the Month. + + last_schulden: Debt or Credit of last Year. + year: Year of all Credits and Debts. + + TODO: link to user??? + """ id = db.Column(db.Integer, primary_key=True) jan_guthaben = db.Column(db.Integer, nullable=False, default=0) diff --git a/geruecht/model/priceList.py b/geruecht/model/priceList.py index 7616dda..fa1864e 100644 --- a/geruecht/model/priceList.py +++ b/geruecht/model/priceList.py @@ -1,6 +1,10 @@ from geruecht import db class PriceList(db.Model): + """ Database Model for PriceList + + PriceList has lots of Drinks and safe all Prices (normal, for club, for other clubs, which catagory, etc) + """ id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False, unique=True) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 6c8c9de..591b0a3 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -2,6 +2,19 @@ from geruecht import db from geruecht import bcrypt class User(db.Model): + """ Database Object for User + + Table for all safed User + + Attributes: + id: Id in Database as Primary Key. + userID: ID for the User maybe to Link? + username: Username of the User to Login + firstname: Firstname of the User + Lastname: Lastname of the User + group: Which group is the User? moneymaster, gastro, user or bar? + password: salted hashed password for the User. + """ id = db.Column(db.Integer, primary_key=True) userID = db.Column(db.String, nullable=False, unique=True) username = db.Column(db.String, nullable=False, unique=True) @@ -11,6 +24,11 @@ class User(db.Model): password = db.Column(db.String, nullable=False) def toJSON(self): + """ Create Dic to dump in JSON + + Returns: + A Dic with static Attributes. + """ dic = { "userId": self.userID, "username": self.username, @@ -21,5 +39,12 @@ class User(db.Model): return dic def login(self, password): + """ Login for the User + + Only check the given Password: + + Returns: + A Bool. True if the password is correct and False if it isn't. + """ return True if bcrypt.check_password_hash(self.password, password) else False diff --git a/geruecht/routes.py b/geruecht/routes.py index 967be95..a413bbb 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -10,6 +10,17 @@ USER = "user" BAR = "bar" def verifyAccessToken(token, group): + """ Verify Accestoken + + Verify an Accestoken and Group so if the User has permission or not. + Retrieves the accestoken if valid else retrieves None + + Args: + token: Token to verify. + group: Group like 'moneymaster', 'gastro', 'user' or 'bar' + Returns: + An the AccesToken for this given Token or None. + """ accToken = accesTokenController.findAccesToken(token) print(accToken) if accToken is not None: @@ -20,6 +31,15 @@ def verifyAccessToken(token, group): @app.route("/getFinanzerMain", methods=['POST']) def _getFinanzer(): + """ Function for /getFinanzerMain + + Retrieves all User for the groupe 'moneymaster' + + Returns: + A JSON-File with Users or an Error. + example: + + """ data = request.get_json() token = data["token"] @@ -43,6 +63,14 @@ def _valid(): @app.route("/login", methods=['POST']) def _login(): + """ Login User + + Nothing to say. + Login in User and create an AccessToken for the User. + + Returns: + A JSON-File with createt Token or Errors + """ data = request.get_json() print(data) username = data['username'] diff --git a/run.py b/run.py index 0de5915..b6d40ab 100644 --- a/run.py +++ b/run.py @@ -1,5 +1,9 @@ from geruecht import app +""" Main + + Start the backend +""" if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') From 7bec023d57d6e05939089e6037b2bf8dc769da5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 23 Apr 2019 20:26:20 +0200 Subject: [PATCH 004/111] =?UTF-8?q?relationship=20zwischen=20user=20und=20?= =?UTF-8?q?creditlist=20fixed=20bug:=20json=20mit=20['userID']=20wird=20ni?= =?UTF-8?q?cht=20mehr=20=C3=BCberschrieben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/model/creditList.py | 2 ++ geruecht/model/user.py | 2 ++ geruecht/routes.py | 4 ++-- geruecht/site.db | Bin 28672 -> 28672 bytes 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index a99b5cb..039f486 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -55,3 +55,5 @@ class CreditList(db.Model): last_schulden = db.Column(db.Integer, nullable=False, default=0) year = db.Column(db.Integer, nullable=False, default=datetime.now().year) + + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 591b0a3..011d4d7 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -23,6 +23,8 @@ class User(db.Model): group = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) + geruechte = db.relationship('CreditList', backref='user', lazy=True) + def toJSON(self): """ Create Dic to dump in JSON diff --git a/geruecht/routes.py b/geruecht/routes.py index a413bbb..6497a1a 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -48,7 +48,7 @@ def _getFinanzer(): users = User.query.all() dic = {} for user in users: - dic["userID"] = user.toJSON() + dic[user.userID] = user.toJSON() return jsonify(dic) return jsonify({"error": "permission denied"}), 401 @@ -92,6 +92,6 @@ def getFinanzer(): users = User.query.all() dic = {} for user in users: - dic["userID"] = user.toJSON() + dic[user.userID] = user.toJSON() print(dic) return jsonify(dic) diff --git a/geruecht/site.db b/geruecht/site.db index 172ae01ada83bfeb1859bd052b769c271b5ec6ed..26c840d443926f4c28ad54a0ffccbd27c140ec75 100644 GIT binary patch delta 573 zcmZp8z}WDBaY80P$0G*53jXu_%XnV%IdeSP*qFtk*1^WcF0QQ1*dJVyn3R*6T$GxU zSrVUt=sWQ$|)+HgR{w$!qu}CNJa?V`ST0!Pmpc z#mx7JfqxFa1>d91f&y3g>O+`08H7Qgku@o?2pOpuC8-!1srX0chGa&iI~#cxcw|KS zx+OcNXXd7s=X$!BxkV+F8mAU{rCU^`XQWiRl;tL8hE^2>rkW)&aWY6sOG=7@OicoM zxi~MsJU2Dfy(l$3zsRj9v!p5&g&Bb%#mvPl38X>Z yV`5_#<^;L`qWUod|6~5w{NI79uky>nRC6;+GeU!biGx`jq&zouQGmjt0098BjkA>i delta 402 zcmZp8z}WDBaY80Pr#1s$1^;>eWn4QrwYi!%3kq!JU~UR!o&1hRfst)9Kd&hxD;t}* zyW-?E{1THB`NX){+ih9c#g&yA`y3~~;gDuz-ORz+!8~~lj{p;w=H?2%9!3r(eiH`% zsr)9J1r;>->*JX?8APQeC50PVOEVKwi}Ui!b5m2(i&E3`i_(fROR7?n5{p!fl2iW7g{74nSxZujOG=AVi;{~nbBj~+N-}fPBa8A=(^HFb^YcH3vznHk0WjSH?LzKN_;Qz}15~%DhzXUh4G$YjO VOkB*;oKP3?Ze|SlFTW^2007#Bc7p%_ From 360766bd35eb7b6e30d07edf513f3c030f52b7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 24 Apr 2019 00:08:25 +0200 Subject: [PATCH 005/111] task #23 und #22 wurden bearbeitet. --- geruecht/controller/accesTokenController.py | 2 +- geruecht/model/creditList.py | 137 ++++++++++++++++++++ geruecht/model/user.py | 33 +++++ geruecht/routes.py | 90 +++++++++++-- geruecht/site.db | Bin 28672 -> 28672 bytes 5 files changed, 253 insertions(+), 9 deletions(-) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 7c77680..d8a6917 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -88,7 +88,7 @@ class AccesTokenController(Thread): print("start allocate") for accToken in self.tokenList: print("controle", accToken) - if (datetime.now() - accToken.timestamp).seconds > 60: + if (datetime.now() - accToken.timestamp).seconds > 7200: print("delete", accToken) self.tokenList.remove(accToken) else: diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 039f486..515a2ad 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -11,6 +11,7 @@ class CreditList(db.Model): last_schulden: Debt or Credit of last Year. year: Year of all Credits and Debts. + user_id: id from the User. TODO: link to user??? """ @@ -57,3 +58,139 @@ class CreditList(db.Model): year = db.Column(db.Integer, nullable=False, default=datetime.now().year) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def getSchulden(self): + jan = self.jan_guthaben - self.jan_schulden + feb = self.feb_guthaben - self.feb_schulden + maer = self.maer_guthaben - self.maer_schulden + apr = self.apr_guthaben - self.apr_schulden + mai = self.mai_guthaben - self.mai_schulden + jun = self.jun_guthaben - self.jun_schulden + jul = self.jul_guthaben - self.jul_schulden + aug = self.aug_guthaben - self.aug_schulden + sep = self.sep_guthaben - self.sep_schulden + okt = self.okt_guthaben - self.okt_schulden + nov = self.nov_guthaben - self.nov_schulden + dez = self.dez_guthaben - self.dez_schulden + + sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez + self.last_schulden + return sum + + def getMonth(self, month): + retValue = None + + if month == 1: + retValue = (self.jan_guthaben, self.jan_schulden) + elif month == 2: + retValue = (self.feb_guthaben, self.feb_schulden) + elif month == 3: + retValue = (self.mear_guthaben, self.maer_schulden) + elif month == 4: + retValue = (self.apr_guthaben, self.apr_schulden) + elif month == 5: + retValue = (self.mai_guthaben, self.mai_schulden) + elif month == 6: + retValue = (self.jun_guthaben, self.jun_schulden) + elif month == 7: + retValue = (self.jul_guthaben, self.jul_schulden) + elif month == 8: + retValue = (self.aug_guthaben, self.aug_schulden) + elif month == 9: + retValue = (self.sep_guthaben, self.sep_schulden) + elif month == 10: + retValue = (self.okt_guthaben, self.okt_schulden) + elif month == 11: + retValue = (self.nov_guthaben, self.nov_schulden) + elif month == 12: + retValue = (self.dez_guthaben, self.dez_schulden) + + return retValue + + def addAmount(self, amount): + month = datetime.now().month + + if month == 1: + self.jan_schulden += amount + retValue = (self.jan_guthaben, self.jan_schulden) + elif month == 2: + self.feb_schulden += amount + retValue = (self.feb_guthaben, self.feb_schulden) + elif month == 3: + self.maer_schulden += amount + retValue = (self.maer_guthaben, self.maer_schulden) + elif month == 4: + self.apr_schulden += amount + retValue = (self.apr_guthaben, self.apr_schulden) + elif month == 5: + self.mai_schulden += amount + retValue = (self.mai_guthaben, self.mai_schulden) + elif month == 6: + self.jun_schulden += amount + retValue = (self.jun_guthaben, self.jun_schulden) + elif month == 7: + self.jul_schulden += amount + retValue = (self.jul_guthaben, self.jul_schulden) + elif month == 8: + self.aug_schulden += amount + retValue = (self.aug_guthaben, self.aug_schulden) + elif month == 9: + self.sep_schulden += amount + retValue = (self.sep_guthaben, self.sep_schulden) + elif month == 10: + self.okt_schulden += amount + retValue = (self.okt_guthaben, self.okt_schulden) + elif month == 11: + self.nov_schulden += amount + retValue = (self.nov_guthaben, self.nov_schulden) + elif month == 12: + self.dez_schulden += amount + retValue = (self.dez_guthaben, self.dez_schulden) + + return retValue + + def toJSON(self): + """ Create Dic to dump in JSON + + Returns: + A Dic with static Attributes. + """ + dic = { + "year": self.year, + "jan": { + "credit": self.jan_guthaben, + "depts": self.jan_schulden}, + "feb": { + "credit": self.feb_guthaben, + "depts": self.feb_schulden}, + "maer": { + "credit": self.maer_guthaben, + "depts": self.maer_schulden}, + "apr": { + "credit": self.apr_guthaben, + "depts": self.apr_schulden}, + "mai": { + "credit": self.mai_guthaben, + "depts": self.mai_schulden}, + "jun": { + "credit": self.jun_guthaben, + "depts": self.jun_schulden}, + "jul": { + "credit": self.jul_guthaben, + "depts": self.jul_schulden}, + "aug": { + "credit": self.aug_guthaben, + "depts": self.aug_schulden}, + "sep": { + "credit": self.sep_guthaben, + "depts": self.sep_schulden}, + "okt": { + "credit": self.okt_guthaben, + "depts": self.okt_schulden}, + "nov": { + "credit": self.nov_guthaben, + "depts": self.nov_schulden}, + "dez": { + "credit": self.dez_guthaben, + "depts": self.dez_schulden}, + } + return dic \ No newline at end of file diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 011d4d7..6d31e60 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,5 +1,7 @@ from geruecht import db from geruecht import bcrypt +from geruecht.model.creditList import CreditList +from datetime import datetime class User(db.Model): """ Database Object for User @@ -25,6 +27,37 @@ class User(db.Model): geruechte = db.relationship('CreditList', backref='user', lazy=True) + def getCurrentGeruecht(self): + print('search currentGeruecht in user {}'.format(self.userID)) + if len(self.geruechte) == 0: + print('user {} has no geruechte'.format(self.userID)) + return self.createCurrentGeruecht() + print('iterate throw geruechte') + last = None + for geruecht in self.geruechte: + print('geruecht {}'.format(geruecht)) + if geruecht.year == datetime.now().year: + print('found geruecht {}'.format(geruecht)) + return geruecht + if geruecht.year == datetime.now().year - 1: + print('fonud last geruecht {}'.format(geruecht)) + last = geruecht + + if last: + amount = last.getSchulden() + return self.createCurrentGeruecht(amount=amount) + else: + print('error, no geruecht found and no geruecht created') + + def createCurrentGeruecht(self, amount=0): + print('create currentgeruecht for user {} in year {}'.format(self.userID, datetime.now().year)) + credit = CreditList(user_id=self.id, last_schulden=amount) + db.session.add(credit) + db.session.commit() + credit = CreditList.query.filter_by(year=datetime.now().year).first() + print('reated currentgeruecht {}'.format(credit)) + return credit + def toJSON(self): """ Create Dic to dump in JSON diff --git a/geruecht/routes.py b/geruecht/routes.py index 6497a1a..442f59b 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -2,6 +2,7 @@ from geruecht import app, db, accesTokenController from geruecht.model.user import User from geruecht.model.creditList import CreditList from geruecht.model.priceList import PriceList +from datetime import datetime from flask import request, jsonify MONEY = "moneymaster" @@ -29,7 +30,7 @@ def verifyAccessToken(token, group): return accToken return None -@app.route("/getFinanzerMain", methods=['POST']) +@app.route("/getFinanzerMain") def _getFinanzer(): """ Function for /getFinanzerMain @@ -40,8 +41,7 @@ def _getFinanzer(): example: """ - data = request.get_json() - token = data["token"] + token = request.headers.get("Token") accToken = verifyAccessToken(token, MONEY) if accToken is not None: @@ -52,11 +52,38 @@ def _getFinanzer(): return jsonify(dic) return jsonify({"error": "permission denied"}), 401 -@app.route("/valid", methods=['POST']) -def _valid(): - data = request.get_json() - token = data["token"] +@app.route("/getFinanzerYears", methods=['POST']) +def _getFinanzerYear(): + print(request.headers) + token = request.headers.get("Token") + print(token) accToken = verifyAccessToken(token, MONEY) + + dic = {} + if accToken is not None: + data = request.get_json() + userID = data['userId'] + + user = User.query.filter_by(userID=userID).first() + dic[user.userID] = {} + for geruecht in user.geruechte: + dic[user.userID][geruecht.year] = geruecht.toJSON() + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 + +@app.route("/valid") +def _valid(): + token = request.headers.get("Token") + accToken = verifyAccessToken(token, MONEY) + if accToken is not None: + return jsonify(accToken.user.toJSON()) + accToken = verifyAccessToken(token, BAR) + if accToken is not None: + return jsonify(accToken.user.toJSON()) + accToken = verifyAccessToken(token, GASTRO) + if accToken is not None: + return jsonify(accToken.user.toJSON()) + accToken = verifyAccessToken(token, USER) if accToken is not None: return jsonify(accToken.user.toJSON()) return jsonify({"error": "permission denied"}), 401 @@ -84,7 +111,54 @@ def _login(): return jsonify(dic) else: return jsonify({"error": "wrong password"}), 401 - return jsonify({"error": "wrong username"}), 402 + return jsonify({"error": "wrong username"}), 402 + +@app.route("/bar") +def _bar(): + print(request.headers) + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + dic = {} + if accToken is not None: + users = User.query.all() + for user in users: + geruecht = None + geruecht = user.getCurrentGeruecht() + if geruecht is not None: + month = geruecht.getMonth(datetime.now().month) + amount = abs(month[0] - month[1]) + if amount != 0: + dic[user.userID] = {"username": user.username, + "firstname": user.firstname, + "lastname": user.lastname, + "amount": abs(month[0] - month[1]) + } + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 + +@app.route("/baradd", methods=['POST']) +def _baradd(): + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + if accToken is not None: + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) + + user = User.query.filter_by(userID=userID).first() + geruecht = user.getCurrentGeruecht() + month = geruecht.addAmount(amount) + amount = abs(month[0] - month[1]) + + db.session.add(geruecht) + db.session.commit() + + return jsonify({"userId": user.userID, "amount": amount}) + return jsonify({"error", "permission denied"}), 401 @app.route("/getFinanzer") diff --git a/geruecht/site.db b/geruecht/site.db index 26c840d443926f4c28ad54a0ffccbd27c140ec75..6ae59fa770a70d436179f51031102a369bab043e 100644 GIT binary patch delta 234 zcmZp8z}WDBae_3X&_o$$Mxl)fOXB%h`Ry6_uko+qZ{knnx8E$Npvzycz$(kYfeSD( zvOi{)XThg}ll`$8GfV*!6S_%EOpFXC*xxa~V1LY{#U#rBQvsu$U>bl5nO`t|Vf?~+ Whw%&hBPI<-OvNy@90;}S519ah-5_=V delta 24 gcmZp8z}WDBae_1>>qHr6M%Il9OX4@PIQ-!U0BffRfB*mh From cb58f1269808db77b2682d0a956ca8b1fb64f029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 1 May 2019 22:10:29 +0200 Subject: [PATCH 006/111] deleted year in toJson in creditList --- geruecht/model/creditList.py | 1 - geruecht/site.db | Bin 28672 -> 28672 bytes 2 files changed, 1 deletion(-) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 515a2ad..b6fda39 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -155,7 +155,6 @@ class CreditList(db.Model): A Dic with static Attributes. """ dic = { - "year": self.year, "jan": { "credit": self.jan_guthaben, "depts": self.jan_schulden}, diff --git a/geruecht/site.db b/geruecht/site.db index 6ae59fa770a70d436179f51031102a369bab043e..30785b79eb676034d4ab6f610a4331b6e0ad4d59 100644 GIT binary patch delta 26 icmZp8z}WDBae_3XZ From 52ca1caa52de629a1729667189456460605b3519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 1 May 2019 22:43:28 +0200 Subject: [PATCH 007/111] clean up code, extra routes for finanzer and baruser --- geruecht/__init__.py | 10 ++++ geruecht/baruser/__init__.py | 0 geruecht/baruser/routes.py | 54 ++++++++++++++++++ geruecht/finanzer/__init__.py | 0 geruecht/finanzer/routes.py | 48 ++++++++++++++++ geruecht/model/creditList.py | 6 +- geruecht/model/user.py | 7 +-- geruecht/routes.py | 102 ++-------------------------------- 8 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 geruecht/baruser/__init__.py create mode 100644 geruecht/baruser/routes.py create mode 100644 geruecht/finanzer/__init__.py create mode 100644 geruecht/finanzer/routes.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 5a7316f..535c04e 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -24,5 +24,15 @@ accesTokenController.start() # login_manager.login_view = 'login' # login_manager.login_message_category = 'info' +MONEY = "moneymaster" +GASTRO = "gastro" +USER = "user" +BAR = "bar" + from geruecht import routes +from geruecht.baruser.routes import baruser +from geruecht.finanzer.routes import finanzer + +app.register_blueprint(baruser) +app.register_blueprint(finanzer) diff --git a/geruecht/baruser/__init__.py b/geruecht/baruser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py new file mode 100644 index 0000000..151a65f --- /dev/null +++ b/geruecht/baruser/routes.py @@ -0,0 +1,54 @@ +from flask import Blueprint, request, jsonify +from geruecht import BAR, db +from geruecht.routes import verifyAccessToken +from geruecht.model.user import User +from datetime import datetime + +baruser = Blueprint("baruser", __name__) + +@baruser.route("/bar") +def _bar(): + print(request.headers) + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + dic = {} + if accToken is not None: + users = User.query.all() + for user in users: + geruecht = None + geruecht = user.getCurrentGeruecht() + if geruecht is not None: + month = geruecht.getMonth(datetime.now().month) + amount = abs(month[0] - month[1]) + if amount != 0: + dic[user.userID] = {"username": user.username, + "firstname": user.firstname, + "lastname": user.lastname, + "amount": abs(month[0] - month[1]) + } + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 + +@baruser.route("/baradd", methods=['POST']) +def _baradd(): + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + if accToken is not None: + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) + + user = User.query.filter_by(userID=userID).first() + geruecht = user.getCurrentGeruecht() + month = geruecht.addAmount(amount) + amount = abs(month[0] - month[1]) + + db.session.add(geruecht) + db.session.commit() + + return jsonify({"userId": user.userID, "amount": amount}) + return jsonify({"error", "permission denied"}), 401 diff --git a/geruecht/finanzer/__init__.py b/geruecht/finanzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py new file mode 100644 index 0000000..aae886b --- /dev/null +++ b/geruecht/finanzer/routes.py @@ -0,0 +1,48 @@ +from flask import Blueprint, request, jsonify +from geruecht import MONEY +from geruecht.routes import verifyAccessToken +from geruecht.model.user import User + +finanzer = Blueprint("finanzer", __name__) + + +@finanzer.route("/getFinanzerMain") +def _getFinanzer(): + """ Function for /getFinanzerMain + + Retrieves all User for the groupe 'moneymaster' + + Returns: + A JSON-File with Users or an Error. + example: + + """ + token = request.headers.get("Token") + + accToken = verifyAccessToken(token, MONEY) + if accToken is not None: + users = User.query.all() + dic = {} + for user in users: + dic[user.userID] = user.toJSON() + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/getFinanzerYears", methods=['POST']) +def _getFinanzerYear(): + print(request.headers) + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, MONEY) + + dic = {} + if accToken is not None: + data = request.get_json() + userID = data['userId'] + + user = User.query.filter_by(userID=userID).first() + dic[user.userID] = {} + for geruecht in user.geruechte: + dic[user.userID][geruecht.year] = geruecht.toJSON() + return jsonify(dic) + return jsonify({"error": "permission denied"}), 401 \ No newline at end of file diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index b6fda39..e0bad3b 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -3,7 +3,7 @@ from datetime import datetime class CreditList(db.Model): """ DataBase Object Credit List: - + Attributes: id: id in Database. Is the Primary Key _guthaben: Credit of the Month. @@ -150,7 +150,7 @@ class CreditList(db.Model): def toJSON(self): """ Create Dic to dump in JSON - + Returns: A Dic with static Attributes. """ @@ -192,4 +192,4 @@ class CreditList(db.Model): "credit": self.dez_guthaben, "depts": self.dez_schulden}, } - return dic \ No newline at end of file + return dic diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 6d31e60..930e528 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -5,7 +5,7 @@ from datetime import datetime class User(db.Model): """ Database Object for User - + Table for all safed User Attributes: @@ -42,7 +42,7 @@ class User(db.Model): if geruecht.year == datetime.now().year - 1: print('fonud last geruecht {}'.format(geruecht)) last = geruecht - + if last: amount = last.getSchulden() return self.createCurrentGeruecht(amount=amount) @@ -60,7 +60,7 @@ class User(db.Model): def toJSON(self): """ Create Dic to dump in JSON - + Returns: A Dic with static Attributes. """ @@ -82,4 +82,3 @@ class User(db.Model): A Bool. True if the password is correct and False if it isn't. """ return True if bcrypt.check_password_hash(self.password, password) else False - diff --git a/geruecht/routes.py b/geruecht/routes.py index 442f59b..62506a8 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,15 +1,10 @@ -from geruecht import app, db, accesTokenController +from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO from geruecht.model.user import User from geruecht.model.creditList import CreditList from geruecht.model.priceList import PriceList from datetime import datetime from flask import request, jsonify -MONEY = "moneymaster" -GASTRO = "gastro" -USER = "user" -BAR = "bar" - def verifyAccessToken(token, group): """ Verify Accestoken @@ -20,7 +15,7 @@ def verifyAccessToken(token, group): token: Token to verify. group: Group like 'moneymaster', 'gastro', 'user' or 'bar' Returns: - An the AccesToken for this given Token or None. + An the AccesToken for this given Token or None. """ accToken = accesTokenController.findAccesToken(token) print(accToken) @@ -30,47 +25,6 @@ def verifyAccessToken(token, group): return accToken return None -@app.route("/getFinanzerMain") -def _getFinanzer(): - """ Function for /getFinanzerMain - - Retrieves all User for the groupe 'moneymaster' - - Returns: - A JSON-File with Users or an Error. - example: - - """ - token = request.headers.get("Token") - - accToken = verifyAccessToken(token, MONEY) - if accToken is not None: - users = User.query.all() - dic = {} - for user in users: - dic[user.userID] = user.toJSON() - return jsonify(dic) - return jsonify({"error": "permission denied"}), 401 - -@app.route("/getFinanzerYears", methods=['POST']) -def _getFinanzerYear(): - print(request.headers) - token = request.headers.get("Token") - print(token) - accToken = verifyAccessToken(token, MONEY) - - dic = {} - if accToken is not None: - data = request.get_json() - userID = data['userId'] - - user = User.query.filter_by(userID=userID).first() - dic[user.userID] = {} - for geruecht in user.geruechte: - dic[user.userID][geruecht.year] = geruecht.toJSON() - return jsonify(dic) - return jsonify({"error": "permission denied"}), 401 - @app.route("/valid") def _valid(): token = request.headers.get("Token") @@ -91,7 +45,7 @@ def _valid(): @app.route("/login", methods=['POST']) def _login(): """ Login User - + Nothing to say. Login in User and create an AccessToken for the User. @@ -111,55 +65,7 @@ def _login(): return jsonify(dic) else: return jsonify({"error": "wrong password"}), 401 - return jsonify({"error": "wrong username"}), 402 - -@app.route("/bar") -def _bar(): - print(request.headers) - token = request.headers.get("Token") - print(token) - accToken = verifyAccessToken(token, BAR) - - dic = {} - if accToken is not None: - users = User.query.all() - for user in users: - geruecht = None - geruecht = user.getCurrentGeruecht() - if geruecht is not None: - month = geruecht.getMonth(datetime.now().month) - amount = abs(month[0] - month[1]) - if amount != 0: - dic[user.userID] = {"username": user.username, - "firstname": user.firstname, - "lastname": user.lastname, - "amount": abs(month[0] - month[1]) - } - return jsonify(dic) - return jsonify({"error": "permission denied"}), 401 - -@app.route("/baradd", methods=['POST']) -def _baradd(): - token = request.headers.get("Token") - print(token) - accToken = verifyAccessToken(token, BAR) - - if accToken is not None: - data = request.get_json() - userID = data['userId'] - amount = int(data['amount']) - - user = User.query.filter_by(userID=userID).first() - geruecht = user.getCurrentGeruecht() - month = geruecht.addAmount(amount) - amount = abs(month[0] - month[1]) - - db.session.add(geruecht) - db.session.commit() - - return jsonify({"userId": user.userID, "amount": amount}) - return jsonify({"error", "permission denied"}), 401 - + return jsonify({"error": "wrong username"}), 402 @app.route("/getFinanzer") def getFinanzer(): From 2427d94626adf5186ad96783e04cd9f08b6aec41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 2 May 2019 02:21:50 +0200 Subject: [PATCH 008/111] =?UTF-8?q?task=20#22=20und=20task=20#26=20abgesch?= =?UTF-8?q?lossen=20es=20werden=20dynamisch=20neue=20geruechte=20erstellt,?= =?UTF-8?q?=20wenn=20n=C3=B6tig.=20Finanzer=20kann=20sowohl=20Guthaben=20a?= =?UTF-8?q?ls=20auch=20Schulden=20hinzuf=C3=BCgen,=20egal=20zu=20welcher?= =?UTF-8?q?=20Zeit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/baruser/routes.py | 9 ++-- geruecht/finanzer/routes.py | 61 ++++++++++++++++++++++++- geruecht/model/creditList.py | 50 +++++++++++++++++++-- geruecht/model/user.py | 83 +++++++++++++++++++++++------------ geruecht/site.db | Bin 28672 -> 28672 bytes 5 files changed, 166 insertions(+), 37 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 151a65f..352cdba 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -18,7 +18,7 @@ def _bar(): users = User.query.all() for user in users: geruecht = None - geruecht = user.getCurrentGeruecht() + geruecht = user.getGeruecht() if geruecht is not None: month = geruecht.getMonth(datetime.now().month) amount = abs(month[0] - month[1]) @@ -43,12 +43,9 @@ def _baradd(): amount = int(data['amount']) user = User.query.filter_by(userID=userID).first() - geruecht = user.getCurrentGeruecht() - month = geruecht.addAmount(amount) - amount = abs(month[0] - month[1]) + month = user.addAmount(amount) - db.session.add(geruecht) - db.session.commit() + amount = abs(month[0] - month[1]) return jsonify({"userId": user.userID, "amount": amount}) return jsonify({"error", "permission denied"}), 401 diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index aae886b..638c0fc 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,4 +1,5 @@ from flask import Blueprint, request, jsonify +from datetime import datetime from geruecht import MONEY from geruecht.routes import verifyAccessToken from geruecht.model.user import User @@ -45,4 +46,62 @@ def _getFinanzerYear(): for geruecht in user.geruechte: dic[user.userID][geruecht.year] = geruecht.toJSON() return jsonify(dic) - return jsonify({"error": "permission denied"}), 401 \ No newline at end of file + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerAddAmount", methods=['POST']) +def _addAmount(): + print(request.headers) + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, MONEY) + + if accToken is not None: + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) + + try: + year = int(data['year']) + except KeyError as er: + print("Error: ", er) + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + print("Error: ", er) + month = datetime.now().month + + user = User.query.filter_by(userID=userID).first() + user.addAmount(amount, year=year, month=month) + retVal = user.getGeruecht(year=year).toJSON() + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerAddCredit", methods=['POST']) +def _addCredit(): + print(request.headers) + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, MONEY) + + if accToken is not None: + data = request.get_json() + userID = data['userId'] + credit = int(data['credit']) + + try: + year = int(data['year']) + except KeyError as er: + print("Error: ", er) + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + print("Error: ", er) + month = datetime.now().month + + user = User.query.filter_by(userID=userID).first() + user.addCredit(credit, year=year, month=month) + retVal = user.getGeruecht(year=year).toJSON() + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index e0bad3b..191c6c6 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -73,7 +73,7 @@ class CreditList(db.Model): nov = self.nov_guthaben - self.nov_schulden dez = self.dez_guthaben - self.dez_schulden - sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez + self.last_schulden + sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez - self.last_schulden return sum def getMonth(self, month): @@ -106,8 +106,7 @@ class CreditList(db.Model): return retValue - def addAmount(self, amount): - month = datetime.now().month + def addAmount(self, amount, month=datetime.now().month): if month == 1: self.jan_schulden += amount @@ -146,6 +145,51 @@ class CreditList(db.Model): self.dez_schulden += amount retValue = (self.dez_guthaben, self.dez_schulden) + db.session.commit() + + return retValue + + def addCredit(self, credit, month=datetime.now().month): + + if month == 1: + self.jan_guthaben += credit + retValue = (self.jan_guthaben, self.jan_schulden) + elif month == 2: + self.feb_guthaben += credit + retValue = (self.feb_guthaben, self.feb_schulden) + elif month == 3: + self.maer_guthaben += credit + retValue = (self.maer_guthaben, self.maer_schulden) + elif month == 4: + self.apr_guthaben += credit + retValue = (self.apr_guthaben, self.apr_schulden) + elif month == 5: + self.mai_guthaben += credit + retValue = (self.mai_guthaben, self.mai_schulden) + elif month == 6: + self.jun_guthaben += credit + retValue = (self.jun_guthaben, self.jun_schulden) + elif month == 7: + self.jul_guthaben += credit + retValue = (self.jul_guthaben, self.jul_schulden) + elif month == 8: + self.aug_guthaben += credit + retValue = (self.aug_guthaben, self.aug_schulden) + elif month == 9: + self.sep_guthaben += credit + retValue = (self.sep_guthaben, self.sep_schulden) + elif month == 10: + self.okt_guthaben += credit + retValue = (self.okt_guthaben, self.okt_schulden) + elif month == 11: + self.nov_guthaben += credit + retValue = (self.nov_guthaben, self.nov_schulden) + elif month == 12: + self.dez_guthaben += credit + retValue = (self.dez_guthaben, self.dez_schulden) + + db.session.commit() + return retValue def toJSON(self): diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 930e528..fbb2f20 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -27,37 +27,66 @@ class User(db.Model): geruechte = db.relationship('CreditList', backref='user', lazy=True) - def getCurrentGeruecht(self): - print('search currentGeruecht in user {}'.format(self.userID)) - if len(self.geruechte) == 0: - print('user {} has no geruechte'.format(self.userID)) - return self.createCurrentGeruecht() - print('iterate throw geruechte') - last = None - for geruecht in self.geruechte: - print('geruecht {}'.format(geruecht)) - if geruecht.year == datetime.now().year: - print('found geruecht {}'.format(geruecht)) - return geruecht - if geruecht.year == datetime.now().year - 1: - print('fonud last geruecht {}'.format(geruecht)) - last = geruecht - - if last: - amount = last.getSchulden() - return self.createCurrentGeruecht(amount=amount) - else: - print('error, no geruecht found and no geruecht created') - - def createCurrentGeruecht(self, amount=0): - print('create currentgeruecht for user {} in year {}'.format(self.userID, datetime.now().year)) - credit = CreditList(user_id=self.id, last_schulden=amount) + def createGeruecht(self, amount=0, year=datetime.now().year): + print('create geruecht for user {} in year {}'.format(self.userID, year)) + credit = CreditList(user_id=self.id, last_schulden=amount, year=year) db.session.add(credit) db.session.commit() - credit = CreditList.query.filter_by(year=datetime.now().year).first() - print('reated currentgeruecht {}'.format(credit)) + credit = CreditList.query.filter_by(year=year, user_id=self.id).first() + print('reated geruecht {}'.format(credit)) return credit + def getGeruecht(self, year=datetime.now().year): + for geruecht in self.geruechte: + if geruecht.year == year: + print("find geruecht {} for user {}".format(geruecht, self.id)) + return geruecht + print("no geruecht found for user {}. Will create one".format(self.id)) + geruecht = self.createGeruecht(year=year) + + self.updateGeruecht() + + return geruecht + + def addAmount(self, amount, year=datetime.now().year, month=datetime.now().month): + geruecht = self.getGeruecht(year=year) + retVal = geruecht.addAmount(amount, month=month) + + db.session.add(geruecht) + db.session.commit() + + self.updateGeruecht() + + return retVal + + def addCredit(self, credit, year=datetime.now().year, month=datetime.now().month): + geruecht = self.getGeruecht(year=year) + retVal = geruecht.addCredit(credit, month=month) + + db.session.add(geruecht) + db.session.commit() + + self.updateGeruecht() + + return retVal + + def updateGeruecht(self): + """ Update list of geruechte + + """ + self.geruechte.sort(key=self.sortYear) + + for index, geruecht in enumerate(self.geruechte): + if index == 0 or index == len(self.geruechte) - 1: + geruecht.last_schulden = 0 + if index != 0: + geruecht.last_schulden = (self.geruechte[index - 1].getSchulden() * -1) + + db.session.commit() + + def sortYear(self, geruecht): + return geruecht.year + def toJSON(self): """ Create Dic to dump in JSON diff --git a/geruecht/site.db b/geruecht/site.db index 30785b79eb676034d4ab6f610a4331b6e0ad4d59..fe9f624634a1ba6063c3a071a79f8af094a6da47 100644 GIT binary patch delta 572 zcmZvYu}cFn6vmTWYOY%I>`>&;_DYc|9dvPUD_Rkui(|pXb|_TEg55f3b#OkpQAcM{ z?mtk3f{RP1cBpqyDuUC2hzKU_1ugmlc}d?Bk^EFRP=!@0Is+!$=dVy#*#+1uu-<0eu#LZOY|4SLXq z4=0*@go$y@z{6YA?kyz*(L&%s3tr(}5D&m7H8UWD9Cf^GB5Ck@6y|xtU*HL1P=qYc z!x_KeHQ|f#V_riUUDwHAh{f+91Y-*|QPbaKU78+?BNCKVRo2u-M*82I1u zKjZ(oSk)R<)%CNInr Date: Thu, 2 May 2019 15:39:53 +0200 Subject: [PATCH 009/111] =?UTF-8?q?task=20#23=20erledigt=20es=20k=C3=B6nne?= =?UTF-8?q?n=20die=20User=20abgefragt=20werden,=20die=20diesen=20Monat=20n?= =?UTF-8?q?och=20keine=20Schulden=20haben.=20es=20kann=20der=20user=20abge?= =?UTF-8?q?fragt=20werden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/baruser/routes.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 352cdba..2321268 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -49,3 +49,35 @@ def _baradd(): return jsonify({"userId": user.userID, "amount": amount}) return jsonify({"error", "permission denied"}), 401 + +@baruser.route("/barGetUsers") +def _getUsers(): + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + retVal = {} + if accToken is not None: + users = User.query.all() + for user in users: + month = user.getGeruecht().getMonth() + if month == 0: + retVal[user.userID] = {user.toJSON()} + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 + +@baruser.route("/barGetUser", methods=['POST']) +def _getUser(): + token = request.headers.get("Token") + print(token) + accToken = verifyAccessToken(token, BAR) + + if accToken is not None: + data = request.get_json() + userID = data['userId'] + + user = User.query.filter_by(userID=userID) + month = user.getGeruecht().getMonth() + + return jsonify({"userId": user.userID, "amount": month[1], "credit": month[0]}) + return jsonify({"error": "permission denied"}), 401 From d591705835abf1c0fbaada0a511440ae3ced4eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 2 May 2019 18:50:59 +0200 Subject: [PATCH 010/111] =?UTF-8?q?Dokumentation=20vervollst=C3=A4ndigt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/baruser/routes.py | 32 +++++++++++ geruecht/controller/accesTokenController.py | 12 ++-- geruecht/finanzer/routes.py | 33 ++++++++++- geruecht/model/accessToken.py | 7 +-- geruecht/model/creditList.py | 49 ++++++++++++++++- geruecht/model/priceList.py | 2 +- geruecht/model/user.py | 61 +++++++++++++++++++++ 7 files changed, 179 insertions(+), 17 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 2321268..7cea2d6 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -8,6 +8,14 @@ baruser = Blueprint("baruser", __name__) @baruser.route("/bar") def _bar(): + """ Main function for Baruser + + Returns JSON-file with all Users, who hast amounts in this month. + + Returns: + JSON-File with Users, who has amounts in this month + or ERROR 401 Permission Denied + """ print(request.headers) token = request.headers.get("Token") print(token) @@ -33,6 +41,14 @@ def _bar(): @baruser.route("/baradd", methods=['POST']) def _baradd(): + """ Function for Baruser to add amount + + This function added to the user with the posted userID the posted amount. + + Returns: + JSON-File with userID and the amount + or ERROR 401 Permission Denied + """ token = request.headers.get("Token") print(token) accToken = verifyAccessToken(token, BAR) @@ -52,6 +68,14 @@ def _baradd(): @baruser.route("/barGetUsers") def _getUsers(): + """ Get Users without amount + + This Function returns all Users, who hasn't an amount in this month. + + Returns: + JSON-File with Users + or ERROR 401 Permission Denied + """ token = request.headers.get("Token") print(token) accToken = verifyAccessToken(token, BAR) @@ -68,6 +92,14 @@ def _getUsers(): @baruser.route("/barGetUser", methods=['POST']) def _getUser(): + """ Get specified User + + This function returns the user with posted userID and them amount and credit. + + Returns: + JSON-File with userID, amount and credit + or ERROR 401 Permission Denied + """ token = request.headers.get("Token") print(token) accToken = verifyAccessToken(token, BAR) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index d8a6917..1597ee1 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -6,7 +6,7 @@ import hashlib class AccesTokenController(Thread): """ Control all createt AccesToken - + This Class create, delete, find and manage AccesToken. Attributes: @@ -18,7 +18,7 @@ class AccesTokenController(Thread): def __init__(self): """ Initialize AccessTokenController - + Initialize Thread and set tokenList empty. """ print("init AccesTokenControlle") @@ -28,7 +28,7 @@ class AccesTokenController(Thread): def findAccesToken(self, token): """ Find a Token in current AccessTokens - + Iterate throw all availables AccesTokens and retrieve one, if they are the same. Args: @@ -66,13 +66,13 @@ class AccesTokenController(Thread): def isSameGroup(self, accToken, group): """ Verify group in AccessToken - + Verify if the User in the AccesToken has the right group. Args: accToken: AccessToken to verify. group: Group to verify. - + Returns: A Bool. If the same then True else False """ @@ -81,7 +81,7 @@ class AccesTokenController(Thread): def run(self): """ Starting Controll-Thread - + Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. """ while True: diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 638c0fc..5da9bfb 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -14,9 +14,8 @@ def _getFinanzer(): Retrieves all User for the groupe 'moneymaster' Returns: - A JSON-File with Users or an Error. - example: - + A JSON-File with Users + or ERROR 401 Permission Denied. """ token = request.headers.get("Token") @@ -31,6 +30,14 @@ def _getFinanzer(): @finanzer.route("/getFinanzerYears", methods=['POST']) def _getFinanzerYear(): + """ Get all geruechte from User + + This function returns all geruechte from user with posted userID + + Returns: + JSON-File with geruechte of special user + or ERROR 401 Permission Denied + """ print(request.headers) token = request.headers.get("Token") print(token) @@ -50,6 +57,16 @@ def _getFinanzerYear(): @finanzer.route("/finanzerAddAmount", methods=['POST']) def _addAmount(): + """ Add Amount to User + + This Function add an amount to the user with posted userID. + If year is not posted the default is the actual Year. + If month is not posted the default is the actual Month. + + Returns: + JSON-File with geruecht of year + or ERROR 401 Permission Denied + """ print(request.headers) token = request.headers.get("Token") print(token) @@ -79,6 +96,16 @@ def _addAmount(): @finanzer.route("/finanzerAddCredit", methods=['POST']) def _addCredit(): + """ Add Credit to User + + This Function add an credit to the user with posted userID. + If year is not posted the default is the actual Year. + If month is not posted the default is the actual Month. + + Returns: + JSON-File with geruecht of year + or ERROR 401 Permission Denied + """ print(request.headers) token = request.headers.get("Token") print(token) diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 916b4ab..24212d7 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -2,7 +2,7 @@ from datetime import datetime class AccessToken(): """ Model for an AccessToken - + Attributes: timestamp: Is a Datetime from current Time. user: Is an User. @@ -15,7 +15,7 @@ class AccessToken(): def __init__(self, user, token, timestamp=datetime.now()): """ Initialize Class AccessToken - + No more to say. Args: @@ -23,7 +23,7 @@ class AccessToken(): token: Is a String to verify later timestamp: Default current time, but can set to an other datetime-Object. """ - + self.user = user self.timestamp = timestamp self.token = token @@ -46,4 +46,3 @@ class AccessToken(): def __repr__(self): return "AccessToken({}, {}, {}".format(self.user, self.token, self.timestamp) - diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 191c6c6..0ce3499 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -12,8 +12,6 @@ class CreditList(db.Model): last_schulden: Debt or Credit of last Year. year: Year of all Credits and Debts. user_id: id from the User. - - TODO: link to user??? """ id = db.Column(db.Integer, primary_key=True) @@ -60,6 +58,18 @@ class CreditList(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) def getSchulden(self): + """ Get Schulden + + This function calculate the total amount of them self. + From the Credit of the Month will the Amount of the Month subtract. + Finaly all Month will added together. + At Last the amount from last year will be subtract. + + If the Return Value is negativ, the User has an Credit, else the User has an amount. + + Returns: + double of the calculated amount + """ jan = self.jan_guthaben - self.jan_schulden feb = self.feb_guthaben - self.feb_schulden maer = self.maer_guthaben - self.maer_schulden @@ -76,7 +86,18 @@ class CreditList(db.Model): sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez - self.last_schulden return sum - def getMonth(self, month): + def getMonth(self, month=datetime.now().month): + """ Get Amount from month + + This function returns the amount and credit of the month. + By default is month the actual month + + Args: + month: which month you want to get the amount(1-12) + + Returns: + double (credit, amount) + """ retValue = None if month == 1: @@ -107,7 +128,18 @@ class CreditList(db.Model): return retValue def addAmount(self, amount, month=datetime.now().month): + """ Add Amount + This function add an amount to a month and returns the credit and amount of the month. + By default is month the actual month. + + Args: + amount: the amount which is to add + month: in which month to add the amount (1-12) + + Returns: + double (credit, amount) + """ if month == 1: self.jan_schulden += amount retValue = (self.jan_guthaben, self.jan_schulden) @@ -150,7 +182,18 @@ class CreditList(db.Model): return retValue def addCredit(self, credit, month=datetime.now().month): + """ Add Credit + This function add an credit to a month and returns the credit and amount of the month. + By default is month the actual month. + + Args: + credit: the credit which is to add + month: in which month to add the credit (1-12) + + Returns: + double (credit, amount) + """ if month == 1: self.jan_guthaben += credit retValue = (self.jan_guthaben, self.jan_schulden) diff --git a/geruecht/model/priceList.py b/geruecht/model/priceList.py index fa1864e..abfe406 100644 --- a/geruecht/model/priceList.py +++ b/geruecht/model/priceList.py @@ -2,7 +2,7 @@ from geruecht import db class PriceList(db.Model): """ Database Model for PriceList - + PriceList has lots of Drinks and safe all Prices (normal, for club, for other clubs, which catagory, etc) """ id = db.Column(db.Integer, primary_key=True) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index fbb2f20..db2cd98 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -28,6 +28,18 @@ class User(db.Model): geruechte = db.relationship('CreditList', backref='user', lazy=True) def createGeruecht(self, amount=0, year=datetime.now().year): + """ Create Geruecht + + This function create a geruecht for the user for an year. + By default is amount zero and year the actual year. + + Args: + amount: is the last_schulden of the geruecht + year: is the year of the geruecht + + Returns: + the created geruecht + """ print('create geruecht for user {} in year {}'.format(self.userID, year)) credit = CreditList(user_id=self.id, last_schulden=amount, year=year) db.session.add(credit) @@ -37,6 +49,17 @@ class User(db.Model): return credit def getGeruecht(self, year=datetime.now().year): + """ Get Geruecht + + This function returns the geruecht of an year. + By default is the year the actual year. + + Args: + year: the year of the geruecht + + Returns: + the geruecht of the year + """ for geruecht in self.geruechte: if geruecht.year == year: print("find geruecht {} for user {}".format(geruecht, self.id)) @@ -49,6 +72,19 @@ class User(db.Model): return geruecht def addAmount(self, amount, year=datetime.now().year, month=datetime.now().month): + """ Add Amount + + This function add an amount to a geruecht with an spezified year and month to the user. + By default the year is the actual year. + By default the month is the actual month. + + Args: + year: year of the geruecht + month: month for the amount + + Returns: + double (credit, amount) + """ geruecht = self.getGeruecht(year=year) retVal = geruecht.addAmount(amount, month=month) @@ -60,6 +96,19 @@ class User(db.Model): return retVal def addCredit(self, credit, year=datetime.now().year, month=datetime.now().month): + """ Add Credit + + This function add an credit to a geruecht with an spezified year and month to the user. + By default the year is the actual year. + By default the month is the actual month. + + Args: + year: year of the geruecht + month: month for the amount + + Returns: + double (credit, amount) + """ geruecht = self.getGeruecht(year=year) retVal = geruecht.addCredit(credit, month=month) @@ -73,6 +122,7 @@ class User(db.Model): def updateGeruecht(self): """ Update list of geruechte + This function iterate through the geruechte, which sorted by year and update the last_schulden of the geruecht. """ self.geruechte.sort(key=self.sortYear) @@ -85,6 +135,17 @@ class User(db.Model): db.session.commit() def sortYear(self, geruecht): + """ Sort Year + + This function is only an helperfunction to sort the list of geruechte by years. + It only returns the year of the geruecht. + + Args: + geruecht: geruecht which year you want + + Returns: + int year of the geruecht + """ return geruecht.year def toJSON(self): From 83c6974d5a9fcb94588f0d3d6bb69d63838c5403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 2 May 2019 21:51:01 +0200 Subject: [PATCH 011/111] =?UTF-8?q?python-packages=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit einfach mit pip alle packages installieren --- packages.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages.txt diff --git a/packages.txt b/packages.txt new file mode 100644 index 0000000..c852dc5 --- /dev/null +++ b/packages.txt @@ -0,0 +1,20 @@ +bcrypt==3.1.6 +cffi==1.12.3 +Click==7.0 +entrypoints==0.3 +flake8==3.7.7 +Flask==1.0.2 +Flask-Bcrypt==0.7.1 +Flask-Cors==3.0.7 +Flask-SQLAlchemy==2.4.0 +itsdangerous==1.1.0 +Jinja2==2.10.1 +MarkupSafe==1.1.1 +mccabe==0.6.1 +pkg-resources==0.0.0 +pycodestyle==2.5.0 +pycparser==2.19 +pyflakes==2.1.1 +six==1.12.0 +SQLAlchemy==1.3.3 +Werkzeug==0.15.2 From cd0def0c1b1ba3925f7758916dc87cf3fae0f6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 3 May 2019 01:40:13 +0200 Subject: [PATCH 012/111] Task #60 erledigt. Es wird jetzt per StreamHandler geloggt und per File gespeichert. Vielleicht wird auch ein bisschen zuviel geloggt. --- geruecht/__init__.py | 30 +++++++++- geruecht/baruser/routes.py | 2 +- geruecht/controller/__init__.py | 3 + geruecht/controller/accesTokenController.py | 63 +++++++++++++++----- geruecht/finanzer/__init__.py | 3 + geruecht/finanzer/routes.py | 54 ++++++++++++----- geruecht/model/accessToken.py | 6 +- geruecht/model/creditList.py | 18 +++++- geruecht/model/user.py | 22 +++++-- geruecht/routes.py | 17 +++++- geruecht/site.db | Bin 28672 -> 28672 bytes 11 files changed, 173 insertions(+), 45 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 535c04e..2e93a13 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -4,21 +4,46 @@ Initialize also a singelton for the AccesTokenControler and start the Thread. """ +import logging +from logging.handlers import WatchedFileHandler +import sys + +FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") + +logFileHandler = WatchedFileHandler("testlog.log") +logFileHandler.setFormatter(FORMATTER) + +logStreamHandler = logging.StreamHandler(stream=sys.stdout) +logStreamHandler.setFormatter(FORMATTER) + +def getLogger(logger_name): + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG) + logger.addHandler(logFileHandler) + logger.addHandler(logStreamHandler) + + logger.propagate = False + + return logger + +LOGGER = getLogger(__name__) +LOGGER.info("Initialize App") from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_cors import CORS from .controller.accesTokenController import AccesTokenController -# from flask_login import LoginManager +# from flask_login import LoginManager +LOGGER.info("Build APP") app = Flask(__name__) CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' db = SQLAlchemy(app) bcrypt = Bcrypt(app) -accesTokenController = AccesTokenController() +accesTokenController = AccesTokenController("GERUECHT") accesTokenController.start() # login_manager = LoginManager(app) # login_manager.login_view = 'login' @@ -34,5 +59,6 @@ from geruecht import routes from geruecht.baruser.routes import baruser from geruecht.finanzer.routes import finanzer +LOGGER.info("Registrate bluebrints") app.register_blueprint(baruser) app.register_blueprint(finanzer) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 7cea2d6..1852a36 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -9,7 +9,7 @@ baruser = Blueprint("baruser", __name__) @baruser.route("/bar") def _bar(): """ Main function for Baruser - + Returns JSON-file with all Users, who hast amounts in this month. Returns: diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index e69de29..6464ff6 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -0,0 +1,3 @@ +from geruecht import getLogger + +LOGGER = getLogger(__name__) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 1597ee1..93e7e32 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,8 +1,11 @@ from geruecht.model.accessToken import AccessToken +from geruecht.controller import LOGGER from datetime import datetime import time from threading import Thread import hashlib +import logging +from logging.handlers import WatchedFileHandler class AccesTokenController(Thread): """ Control all createt AccesToken @@ -13,16 +16,40 @@ class AccesTokenController(Thread): tokenList: List of currents AccessToken lifetime: Variable for the Lifetime of one AccessToken in seconds. """ + class __OnlyOne: + def __init__(self, arg): + self.val = arg + + def __str__(self): + return repr(self) + self.val + instance = None tokenList = None lifetime = 60 - def __init__(self): + def __init__(self, arg): """ Initialize AccessTokenController Initialize Thread and set tokenList empty. """ - print("init AccesTokenControlle") - print("init threading") + LOGGER.info("Initialize AccessTokenController") + if not AccesTokenController.instance: + AccesTokenController.instance = AccesTokenController.__OnlyOne(arg) + else: + AccesTokenController.instance.val = arg + + LOGGER.debug("Build Logger for VerificationThread") + + FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") + + logFileHandler = WatchedFileHandler("Verification.log") + logFileHandler.setFormatter(FORMATTER) + + self.LOGGER = logging.getLogger("VerificationThread") + self.LOGGER.setLevel(logging.DEBUG) + self.LOGGER.addHandler(logFileHandler) + self.LOGGER.propagate = False + + LOGGER.debug("Initialize Threading") Thread.__init__(self) self.tokenList = [] @@ -37,12 +64,14 @@ class AccesTokenController(Thread): Returns: An AccessToken if found or None if not found. """ - print("search for AccesToken", token) + LOGGER.info("Search for Token: {}".format(token)) + LOGGER.debug("Iterate through List of current Tokens") for accToken in self.tokenList: + LOGGER.debug("Check if AccessToken {} has Token {}".format(accToken, token)) if accToken == token: - print("find AccesToken", accToken, "with token", token) + LOGGER.info("Find AccessToken {} with Token {}".format(accToken, token)) return accToken - print("no AccesToken with", token) + LOGGER.info("no AccesToken found with Token {}".format(token)) return None def createAccesToken(self, user): @@ -56,12 +85,13 @@ class AccesTokenController(Thread): Returns: A created Token for User """ - print("create AccesToken") + LOGGER.info("Create AccessToken") now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.password).encode('utf-8')).hexdigest() - self.tokenList.append(AccessToken(user, token)) - print(self.tokenList) - print("finished create AccesToken", token) + accToken = AccessToken(user, token) + LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) + self.tokenList.append(accToken) + LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) return token def isSameGroup(self, accToken, group): @@ -77,6 +107,7 @@ class AccesTokenController(Thread): A Bool. If the same then True else False """ print("controll if", accToken, "hase group", group) + LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, group)) return True if accToken.user.group == group else False def run(self): @@ -84,15 +115,17 @@ class AccesTokenController(Thread): Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. """ + LOGGER.info("Start Thread for verification that the AccessToken are not out of date.") while True: - print("start allocate") + self.LOGGER.debug("Start to iterate through List of current Tokens") for accToken in self.tokenList: - print("controle", accToken) + self.LOGGER.debug("Check if AccessToken {} is out of date".format(accToken)) if (datetime.now() - accToken.timestamp).seconds > 7200: print("delete", accToken) + self.LOGGER.info("Delete AccessToken {} from List of current Tokens".format(accToken)) self.tokenList.remove(accToken) else: - print("time is only", (datetime.now() - accToken.timestamp).seconds) - print(self.tokenList) - print("wait") + self.LOGGER.debug("AccessToken {} is up to date. {} seconds left".format(accToken, 7200 - (datetime.now() - accToken.timestamp).seconds)) + self.LOGGER.debug("List of current Tokens: {}".format(self.tokenList)) + self.LOGGER.info("Wait 10 Seconds") time.sleep(10) diff --git a/geruecht/finanzer/__init__.py b/geruecht/finanzer/__init__.py index e69de29..6464ff6 100644 --- a/geruecht/finanzer/__init__.py +++ b/geruecht/finanzer/__init__.py @@ -0,0 +1,3 @@ +from geruecht import getLogger + +LOGGER = getLogger(__name__) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 5da9bfb..b134e1e 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,4 +1,5 @@ from flask import Blueprint, request, jsonify +from geruecht.finanzer import LOGGER from datetime import datetime from geruecht import MONEY from geruecht.routes import verifyAccessToken @@ -17,15 +18,21 @@ def _getFinanzer(): A JSON-File with Users or ERROR 401 Permission Denied. """ + LOGGER.info("Get main for Finanzer") token = request.headers.get("Token") - + LOGGER.debug("Verify AccessToken with Token {}".format(token)) accToken = verifyAccessToken(token, MONEY) if accToken is not None: + LOGGER.debug("Get all Useres") users = User.query.all() dic = {} for user in users: + LOGGER.debug("Add User {} to ReturnValue".format(user)) dic[user.userID] = user.toJSON() + LOGGER.debug("ReturnValue is {}".format(dic)) + LOGGER.info("Send main for Finanzer") return jsonify(dic) + LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 @finanzer.route("/getFinanzerYears", methods=['POST']) @@ -38,21 +45,28 @@ def _getFinanzerYear(): JSON-File with geruechte of special user or ERROR 401 Permission Denied """ - print(request.headers) + LOGGER.info("Get all Geruechte from User.") token = request.headers.get("Token") - print(token) + LOGGER.debug("Verify AccessToken with Token {}".format(token)) accToken = verifyAccessToken(token, MONEY) dic = {} if accToken is not None: data = request.get_json() + LOGGER.debug("Get data {}".format(data)) userID = data['userId'] - + LOGGER.debug("UserID is {}".format(userID)) user = User.query.filter_by(userID=userID).first() + LOGGER.debug("User is {}".format(user)) dic[user.userID] = {} + LOGGER.debug("Build ReturnValue") for geruecht in user.geruechte: + LOGGER.debug("Add Geruecht {} to ReturnValue".format(geruecht)) dic[user.userID][geruecht.year] = geruecht.toJSON() + LOGGER.debug("ReturnValue is {}".format(dic)) + LOGGER.info("Send Geruechte from User {}".format(user)) return jsonify(dic) + LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 @finanzer.route("/finanzerAddAmount", methods=['POST']) @@ -67,31 +81,36 @@ def _addAmount(): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - print(request.headers) + LOGGER.info("Add Amount") token = request.headers.get("Token") - print(token) + LOGGER.debug("Verify AccessToken with Token {}".format(token)) accToken = verifyAccessToken(token, MONEY) if accToken is not None: data = request.get_json() + LOGGER.debug("Get data {}".format(data)) userID = data['userId'] amount = int(data['amount']) - + LOGGER.debug("UserID is {} and amount is {}".format(userID, amount)) try: year = int(data['year']) except KeyError as er: - print("Error: ", er) + LOGGER.error("KeyError in year. Year is set to default.") year = datetime.now().year try: month = int(data['month']) except KeyError as er: - print("Error: ", er) + LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month - + LOGGER.debug("Year is {} and Month is {}".format(year, month)) user = User.query.filter_by(userID=userID).first() + LOGGER.debug("User is {}".format(user)) + LOGGER.debug("Add amount to User {} in year {} and month {}".format(user, year, month)) user.addAmount(amount, year=year, month=month) retVal = user.getGeruecht(year=year).toJSON() + LOGGER.info("Send updated Geruecht") return jsonify(retVal) + LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 @finanzer.route("/finanzerAddCredit", methods=['POST']) @@ -106,29 +125,36 @@ def _addCredit(): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - print(request.headers) + LOGGER.info("Add Amount") token = request.headers.get("Token") - print(token) + LOGGER.debug("Verify AccessToken with Token {}".format(token)) accToken = verifyAccessToken(token, MONEY) if accToken is not None: data = request.get_json() + LOGGER.debug("Get data {}".format(data)) userID = data['userId'] credit = int(data['credit']) + LOGGER.debug("UserID is {} and credit is {}".format(userID, credit)) try: year = int(data['year']) except KeyError as er: - print("Error: ", er) + LOGGER.error("KeyError in year. Year is set to default.") year = datetime.now().year try: month = int(data['month']) except KeyError as er: - print("Error: ", er) + LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month + LOGGER.debug("Year is {} and Month is {}".format(year, month)) user = User.query.filter_by(userID=userID).first() + LOGGER.debug("User is {}".format(user)) + LOGGER.debug("Add credit to User {} in year {} and month {}".format(user, year, month)) user.addCredit(credit, year=year, month=month) retVal = user.getGeruecht(year=year).toJSON() + LOGGER.info("Send updated Geruecht") return jsonify(retVal) + LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 24212d7..0718dc4 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -1,4 +1,7 @@ from datetime import datetime +from geruecht import getLogger + +LOGGER = getLogger(__name__) class AccessToken(): """ Model for an AccessToken @@ -23,7 +26,7 @@ class AccessToken(): token: Is a String to verify later timestamp: Default current time, but can set to an other datetime-Object. """ - + LOGGER.debug("Initialize AccessToken") self.user = user self.timestamp = timestamp self.token = token @@ -33,6 +36,7 @@ class AccessToken(): Update the Timestamp to the current Time. """ + LOGGER.debug("Update Timestamp") self.timestamp = datetime.now() def __eq__(self, token): diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 0ce3499..ddcc877 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,5 +1,8 @@ from geruecht import db from datetime import datetime +from geruecht import getLogger + +LOGGER = getLogger(__name__) class CreditList(db.Model): """ DataBase Object Credit List: @@ -13,6 +16,7 @@ class CreditList(db.Model): year: Year of all Credits and Debts. user_id: id from the User. """ + LOGGER.debug("Initialize Geruecht") id = db.Column(db.Integer, primary_key=True) jan_guthaben = db.Column(db.Integer, nullable=False, default=0) @@ -70,6 +74,7 @@ class CreditList(db.Model): Returns: double of the calculated amount """ + LOGGER.debug("Calculate amount") jan = self.jan_guthaben - self.jan_schulden feb = self.feb_guthaben - self.feb_schulden maer = self.maer_guthaben - self.maer_schulden @@ -84,6 +89,7 @@ class CreditList(db.Model): dez = self.dez_guthaben - self.dez_schulden sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez - self.last_schulden + LOGGER.debug("Calculated amount is {}".format(sum)) return sum def getMonth(self, month=datetime.now().month): @@ -98,6 +104,7 @@ class CreditList(db.Model): Returns: double (credit, amount) """ + LOGGER.debug("Get Credit and Amount from Month {}".format(month)) retValue = None if month == 1: @@ -124,7 +131,7 @@ class CreditList(db.Model): retValue = (self.nov_guthaben, self.nov_schulden) elif month == 12: retValue = (self.dez_guthaben, self.dez_schulden) - + LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue def addAmount(self, amount, month=datetime.now().month): @@ -140,6 +147,7 @@ class CreditList(db.Model): Returns: double (credit, amount) """ + LOGGER.debug("Add Amount in Month {}".format(month)) if month == 1: self.jan_schulden += amount retValue = (self.jan_guthaben, self.jan_schulden) @@ -178,7 +186,7 @@ class CreditList(db.Model): retValue = (self.dez_guthaben, self.dez_schulden) db.session.commit() - + LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue def addCredit(self, credit, month=datetime.now().month): @@ -194,6 +202,7 @@ class CreditList(db.Model): Returns: double (credit, amount) """ + LOGGER.debug("Add Credit in Month {}".format(month)) if month == 1: self.jan_guthaben += credit retValue = (self.jan_guthaben, self.jan_schulden) @@ -232,7 +241,7 @@ class CreditList(db.Model): retValue = (self.dez_guthaben, self.dez_schulden) db.session.commit() - + LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue def toJSON(self): @@ -280,3 +289,6 @@ class CreditList(db.Model): "depts": self.dez_schulden}, } return dic + + def __repr__(self): + return "CreditList(year: {}, userID: {}, amount: {})".format(self.year, self.user_id, self.toJSON()) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index db2cd98..4ba9d7c 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,8 +1,9 @@ -from geruecht import db -from geruecht import bcrypt +from geruecht import db, bcrypt, getLogger from geruecht.model.creditList import CreditList from datetime import datetime +LOGGER = getLogger(__name__) + class User(db.Model): """ Database Object for User @@ -40,12 +41,12 @@ class User(db.Model): Returns: the created geruecht """ - print('create geruecht for user {} in year {}'.format(self.userID, year)) + LOGGER.debug("Create Geruecht for user {} in year {}".format(self, year)) credit = CreditList(user_id=self.id, last_schulden=amount, year=year) db.session.add(credit) db.session.commit() credit = CreditList.query.filter_by(year=year, user_id=self.id).first() - print('reated geruecht {}'.format(credit)) + LOGGER.debug("Created Geruecht {}".format(credit)) return credit def getGeruecht(self, year=datetime.now().year): @@ -60,11 +61,13 @@ class User(db.Model): Returns: the geruecht of the year """ + LOGGER.debug("Iterate through Geruechte of User {}".format(self)) for geruecht in self.geruechte: + LOGGER.debug("Check if Geruecht {} has year {}".format(geruecht, year)) if geruecht.year == year: - print("find geruecht {} for user {}".format(geruecht, self.id)) + LOGGER.debug("Find Geruecht {} for User {}".format(geruecht, self)) return geruecht - print("no geruecht found for user {}. Will create one".format(self.id)) + LOGGER.debug("No Geruecht found for User {}. Will create one".format(self)) geruecht = self.createGeruecht(year=year) self.updateGeruecht() @@ -85,6 +88,7 @@ class User(db.Model): Returns: double (credit, amount) """ + LOGGER.debug("Add amount to User {} in year {} and month {}".format(self, year, month)) geruecht = self.getGeruecht(year=year) retVal = geruecht.addAmount(amount, month=month) @@ -109,6 +113,7 @@ class User(db.Model): Returns: double (credit, amount) """ + LOGGER.debug("Add credit to User {} in year {} and month {}".format(self, year, month)) geruecht = self.getGeruecht(year=year) retVal = geruecht.addCredit(credit, month=month) @@ -124,6 +129,7 @@ class User(db.Model): This function iterate through the geruechte, which sorted by year and update the last_schulden of the geruecht. """ + LOGGER.debug("Update all Geruechte ") self.geruechte.sort(key=self.sortYear) for index, geruecht in enumerate(self.geruechte): @@ -171,4 +177,8 @@ class User(db.Model): Returns: A Bool. True if the password is correct and False if it isn't. """ + LOGGER.debug("Login User {}".format(self)) return True if bcrypt.check_password_hash(self.password, password) else False + + def __repr__(self): + return "User({}, {}, {})".format(self.userID, self.username, self.group) diff --git a/geruecht/routes.py b/geruecht/routes.py index 62506a8..cd192cb 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,4 +1,4 @@ -from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO +from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO, LOGGER from geruecht.model.user import User from geruecht.model.creditList import CreditList from geruecht.model.priceList import PriceList @@ -17,12 +17,16 @@ def verifyAccessToken(token, group): Returns: An the AccesToken for this given Token or None. """ + LOGGER.info("Verify AccessToken with token: {} and group: {}".format(token, group)) accToken = accesTokenController.findAccesToken(token) - print(accToken) + LOGGER.debug("AccessToken is {}".format(accToken)) if accToken is not None: + LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) if accesTokenController.isSameGroup(accToken, group): accToken.updateTimestamp() + LOGGER.info("Found AccessToken {} with token: {} and group: {}".format(accToken, token, group)) return accToken + LOGGER.info("No AccessToken with token: {} and group: {} found".format(token, group)) return None @app.route("/valid") @@ -52,19 +56,26 @@ def _login(): Returns: A JSON-File with createt Token or Errors """ + LOGGER.info("Start log in.") data = request.get_json() - print(data) + LOGGER.debug("JSON from request: {}".format(data)) username = data['username'] password = data['password'] + LOGGER.info("{} try to log in".format(username)) user = User.query.filter_by(username=username).first() + LOGGER.debug("User is {}".format(user)) if user: + LOGGER.debug("Check login for User {}".format(user)) if user.login(password): token = accesTokenController.createAccesToken(user) dic = user.toJSON() dic["token"] = token + LOGGER.info("User {} success login.".format(username)) return jsonify(dic) else: + LOGGER.info("User {} failed login.".format(username)) return jsonify({"error": "wrong password"}), 401 + LOGGER.info("User {} does not exist.".format(username)) return jsonify({"error": "wrong username"}), 402 @app.route("/getFinanzer") diff --git a/geruecht/site.db b/geruecht/site.db index fe9f624634a1ba6063c3a071a79f8af094a6da47..041487dde91ac01863bb9d3719696c7612d32e86 100644 GIT binary patch delta 224 zcmZp8z}WDBae_2s>_i!7#@LMsi{d4G7&!S98TnrFU*H$zPvp1fE976vH=9p!v!DPw zpMeIaECUAz2NM_q8Bo9krkNNSCor5~IKeo9{Q(Of-(dz-j>!Re;zo=>i7z7Tk61L= zWEmhzK}NsJJ433QBqorU;etN Wf_PAXFa^Q_TgB7>v~2SEd_w?c0xC!V delta 226 zcmZp8z}WDBae_2s#6%fq#)ypxi{d2?GjQ^GGx5FTzrZibpU7{|=gr^7H=D0`v!DPw zpFUqPgBAw|2NM_q8OVT%iIH&v!wH5HUp&|!u&A-gGB8aJ%o8^TiZj1p{KBlj{*XnD zQ Date: Thu, 19 Dec 2019 08:12:29 +0100 Subject: [PATCH 013/111] mysql adapter and ldap adapter start adapetr for mysql not sqllite authenfication with ldap problem.. ldap and db is initialize in __init__.py when you initialize db, you initialize User and that requires ldap from __init__.py. But ldap is not initialize. if you initialize ldap, you initialize User and that requires db from __init__.py. But db is not initialize. --- geruecht/__init__.py | 34 ++++++++--- geruecht/controller/accesTokenController.py | 22 +++---- geruecht/controller/databaseController.py | 68 +++++++++++++++++++++ geruecht/controller/ldapController.py | 56 +++++++++++++++++ geruecht/model/creditList.py | 5 +- geruecht/model/user.py | 47 ++++++++------ geruecht/routes.py | 23 ++++++- 7 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 geruecht/controller/databaseController.py create mode 100644 geruecht/controller/ldapController.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 2e93a13..09d744b 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -8,6 +8,11 @@ import logging from logging.handlers import WatchedFileHandler import sys +MONEY = "moneymaster" +GASTRO = "gastro" +USER = "user" +BAR = "bar" + FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") logFileHandler = WatchedFileHandler("testlog.log") @@ -29,6 +34,18 @@ def getLogger(logger_name): LOGGER = getLogger(__name__) LOGGER.info("Initialize App") +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] +from .controller.databaseController import DatabaseController +db = DatabaseController() +from .controller.ldapController import LDAPController +ldapController = LDAPController() + + from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt @@ -41,7 +58,7 @@ app = Flask(__name__) CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' -db = SQLAlchemy(app) +#db = SQLAlchemy(app) bcrypt = Bcrypt(app) accesTokenController = AccesTokenController("GERUECHT") accesTokenController.start() @@ -49,16 +66,13 @@ accesTokenController.start() # login_manager.login_view = 'login' # login_manager.login_message_category = 'info' -MONEY = "moneymaster" -GASTRO = "gastro" -USER = "user" -BAR = "bar" + from geruecht import routes -from geruecht.baruser.routes import baruser -from geruecht.finanzer.routes import finanzer +#from geruecht.baruser.routes import baruser +#from geruecht.finanzer.routes import finanzer -LOGGER.info("Registrate bluebrints") -app.register_blueprint(baruser) -app.register_blueprint(finanzer) +#LOGGER.info("Registrate bluebrints") +#app.register_blueprint(baruser) +#app.register_blueprint(finanzer) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 93e7e32..c8594e1 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -6,8 +6,9 @@ from threading import Thread import hashlib import logging from logging.handlers import WatchedFileHandler +from geruecht import Singleton -class AccesTokenController(Thread): +class AccesTokenController(Thread, metaclass=Singleton): """ Control all createt AccesToken This Class create, delete, find and manage AccesToken. @@ -16,12 +17,6 @@ class AccesTokenController(Thread): tokenList: List of currents AccessToken lifetime: Variable for the Lifetime of one AccessToken in seconds. """ - class __OnlyOne: - def __init__(self, arg): - self.val = arg - - def __str__(self): - return repr(self) + self.val instance = None tokenList = None lifetime = 60 @@ -32,10 +27,6 @@ class AccesTokenController(Thread): Initialize Thread and set tokenList empty. """ LOGGER.info("Initialize AccessTokenController") - if not AccesTokenController.instance: - AccesTokenController.instance = AccesTokenController.__OnlyOne(arg) - else: - AccesTokenController.instance.val = arg LOGGER.debug("Build Logger for VerificationThread") @@ -87,7 +78,7 @@ class AccesTokenController(Thread): """ LOGGER.info("Create AccessToken") now = datetime.ctime(datetime.now()) - token = hashlib.md5((now + user.password).encode('utf-8')).hexdigest() + token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() accToken = AccessToken(user, token) LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) self.tokenList.append(accToken) @@ -115,17 +106,20 @@ class AccesTokenController(Thread): Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. """ + valid_time=120 LOGGER.info("Start Thread for verification that the AccessToken are not out of date.") while True: + self.LOGGER.debug("Name: {}".format(self.getName())) self.LOGGER.debug("Start to iterate through List of current Tokens") for accToken in self.tokenList: + self.LOGGER.debug("Check if AccessToken {} is out of date".format(accToken)) - if (datetime.now() - accToken.timestamp).seconds > 7200: + if (datetime.now() - accToken.timestamp).seconds > valid_time: print("delete", accToken) self.LOGGER.info("Delete AccessToken {} from List of current Tokens".format(accToken)) self.tokenList.remove(accToken) else: - self.LOGGER.debug("AccessToken {} is up to date. {} seconds left".format(accToken, 7200 - (datetime.now() - accToken.timestamp).seconds)) + self.LOGGER.debug("AccessToken {} is up to date. {} seconds left".format(accToken, valid_time - (datetime.now() - accToken.timestamp).seconds)) self.LOGGER.debug("List of current Tokens: {}".format(self.tokenList)) self.LOGGER.info("Wait 10 Seconds") time.sleep(10) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py new file mode 100644 index 0000000..c73ecb0 --- /dev/null +++ b/geruecht/controller/databaseController.py @@ -0,0 +1,68 @@ +import pymysql +from geruecht import Singleton +from geruecht.model.user import User + +class DatabaseController(metaclass=Singleton): + ''' + DatabaesController + + Connect to the Database and execute sql-executions + ''' + + def __init__(self, url='192.168.5.108', user='wu5', password='E1n$tein', database='geruecht'): + self.url = url + self.user = user + self.password = password + self.database = database + self.connect() + + + def connect(self): + try: + self.db = pymysql.connect(self.url, self.user, self.password, self.database, cursorclass=pymysql.cursors.DictCursor) + except Exception as err: + raise err + + def getAllUser(self): + cursor = self.db.cursor() + + def getUser(self, username): + self.connect() + retVal = None + cursor = self.db.cursor() + cursor.execute("select * from user where cn='{}'".format(username)) + data = cursor.fetchone() + if data: + retVal = User(data) + self.db.close() + return retVal + + + def insertUser(self, data): + self.connect() + cursor = self.db.cursor() + try: + cursor.execute("insert into user (cn, dn, firstname, lastname, `group`) VALUES ('{}','{}','{}','{}','{}')".format( + data['cn'], data['dn'], data['givenName'], data['sn'], data['group'])) + self.db.commit() + except Exception as err: + self.db.rollback() + self.db.close() + raise err + self.db.close() + + def updateUser(self, data): + self.connect() + cursor = self.db.cursor() + try: + cursor.execute("update user dn='{}', firstname='{}', lastname='{}', group='{}' where cn='{}'".format( + data['dn'], data['givenName'], data['sn'], data['group'], data['cn'])) + self.db.commit() + except Exception as err: + self.db.rollback() + self.db.close() + raise err + self.db.close() + +if __name__ == '__main__': + db = DatabaseController(user='tim') diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py new file mode 100644 index 0000000..dbd5a40 --- /dev/null +++ b/geruecht/controller/ldapController.py @@ -0,0 +1,56 @@ +import ldap +from geruecht import MONEY, USER, GASTRO, BAR, Singleton + +class LDAPController(metaclass=Singleton): + ''' + Authentification over LDAP. Create Account on-the-fly + ''' + + def __init__(self, url="ldap://192.168.5.108", dn='dc=ldap,dc=example,dc=local'): + self.url = url + self.dn = dn + self.connect() + + def connect(self): + try: + self.client = ldap.initialize(self.url, bytes_mode=False) + except Exception as err: + raise err + + def login(self, username, password): + self.connect() + try: + self.client.bind_s("cn={},ou=user,{}".format(username, self.dn), password) + self.client.unbind_s() + except: + self.client.unbind_s() + raise Exception("Invalid Password or Username") + + def getUserData(self, username): + self.connect() + search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'cn={}'.format(username), ['cn', 'givenName', 'sn']) + retVal = search_data[0][1] + for k,v in retVal.items(): + retVal[k] = v[0].decode('utf-8') + retVal['dn'] = self.dn + return retVal + + + def getGroup(self, username): + self.connect() + groups_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'memberUID={}'.format(username), ['cn']) + if len(groups_data) == 0: + return USER + else: + data = groups_data[0][1]['cn'][0].decode('utf-8') + if data == 'finanzer': + return MONEY + elif data == 'gastro': + return GASTRO + elif data == 'bar': + return BAR + + +if __name__ == '__main__': + a = LDAPController() + a.getUserData('jhille') diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index ddcc877..2370001 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,10 +1,9 @@ -from geruecht import db from datetime import datetime from geruecht import getLogger LOGGER = getLogger(__name__) -class CreditList(db.Model): +class CreditList(): """ DataBase Object Credit List: Attributes: @@ -17,7 +16,7 @@ class CreditList(db.Model): user_id: id from the User. """ LOGGER.debug("Initialize Geruecht") - id = db.Column(db.Integer, primary_key=True) + id = db.Colum(db.Integer, primary_key=True) jan_guthaben = db.Column(db.Integer, nullable=False, default=0) jan_schulden = db.Column(db.Integer, nullable=False, default=0) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 4ba9d7c..89d63f5 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,10 +1,11 @@ -from geruecht import db, bcrypt, getLogger -from geruecht.model.creditList import CreditList +from geruecht import getLogger +from geruecht import db +#from geruecht.model.creditList import CreditList from datetime import datetime LOGGER = getLogger(__name__) -class User(db.Model): +class User(): """ Database Object for User Table for all safed User @@ -18,16 +19,16 @@ class User(db.Model): group: Which group is the User? moneymaster, gastro, user or bar? password: salted hashed password for the User. """ - id = db.Column(db.Integer, primary_key=True) - userID = db.Column(db.String, nullable=False, unique=True) - username = db.Column(db.String, nullable=False, unique=True) - firstname = db.Column(db.String, nullable=False) - lastname = db.Column(db.String, nullable=False) - group = db.Column(db.String, nullable=False) - password = db.Column(db.String, nullable=False) - - geruechte = db.relationship('CreditList', backref='user', lazy=True) + def __init__(self, data): + self.id = int(data['id']) + self.cn = data['cn'] + self.dn = data['dn'] + self.firstname = data['firstname'] + self.lastname = data['lastname'] + self.group = data['group'] + #geruechte = db.relationship('CreditList', backref='user', lazy=True) + ''' def createGeruecht(self, amount=0, year=datetime.now().year): """ Create Geruecht @@ -153,7 +154,7 @@ class User(db.Model): int year of the geruecht """ return geruecht.year - + ''' def toJSON(self): """ Create Dic to dump in JSON @@ -161,14 +162,19 @@ class User(db.Model): A Dic with static Attributes. """ dic = { - "userId": self.userID, - "username": self.username, + "cn": self.cn, + "dn": self.dn, "firstname": self.firstname, "lastname": self.lastname, "group": self.group, } return dic + def update(self): + data = ldap.getUserData(self.cn) + data['group'] = ldap.getGroup(self.cn) + db.updateUser(data) + def login(self, password): """ Login for the User @@ -178,7 +184,14 @@ class User(db.Model): A Bool. True if the password is correct and False if it isn't. """ LOGGER.debug("Login User {}".format(self)) - return True if bcrypt.check_password_hash(self.password, password) else False + try: + from geruecht import ldapController as ldap + ldap.login(self.cn, password) + + self.update() + return True + except: + return False def __repr__(self): - return "User({}, {}, {})".format(self.userID, self.username, self.group) + return "User({}, {}, {})".format(self.cn, self.dn, self.group) diff --git a/geruecht/routes.py b/geruecht/routes.py index cd192cb..2685794 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,7 +1,8 @@ from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO, LOGGER +from geruecht import ldapController as ldap from geruecht.model.user import User -from geruecht.model.creditList import CreditList -from geruecht.model.priceList import PriceList +#from geruecht.model.creditList import CreditList +#from geruecht.model.priceList import PriceList from datetime import datetime from flask import request, jsonify @@ -61,8 +62,24 @@ def _login(): LOGGER.debug("JSON from request: {}".format(data)) username = data['username'] password = data['password'] + LOGGER.info("search {} in database".format(username)) + user = db.getUser(username) + if user is None: + LOGGER.info("User {} not found. Authenticate over LDAP and create User.") + try: + ldap.login(username, password) + LOGGER.info("Authentification successfull. Search Group") + group = ldap.getGroup(username) + LOGGER.info("Get userdata from LDAP") + user_data = ldap.getUserData(username) + user_data['group'] = group + LOGGER.info('Insert user {} into database') + db.insertUser(user_data) + + except Exception as err: + raise err LOGGER.info("{} try to log in".format(username)) - user = User.query.filter_by(username=username).first() + user = db.getUser(username) LOGGER.debug("User is {}".format(user)) if user: LOGGER.debug("Check login for User {}".format(user)) From 33333561f3c049030cce7c4bef9e0079dd884cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 19 Dec 2019 18:26:41 +0100 Subject: [PATCH 014/111] add creditList support but can't update database --- .gitignore | 5 +- geruecht/__init__.py | 22 +++-- geruecht/controller/accesTokenController.py | 2 +- geruecht/controller/databaseController.py | 90 +++++++++++++++++-- geruecht/finanzer/routes.py | 16 ++-- geruecht/model/creditList.py | 99 ++++++++++++++------- geruecht/model/user.py | 55 +++++++----- geruecht/routes.py | 8 +- 8 files changed, 217 insertions(+), 80 deletions(-) diff --git a/.gitignore b/.gitignore index 716695e..037f03d 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,7 @@ dmypy.json .pyre/ #ide -.idea \ No newline at end of file +.idea + +.vscode/ +*.log \ No newline at end of file diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 09d744b..39b790e 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -40,15 +40,28 @@ class Singleton(type): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + from .controller.databaseController import DatabaseController db = DatabaseController() from .controller.ldapController import LDAPController ldapController = LDAPController() +def getDatabesController(): + if db is not None: + return db + else: + return DatabaseController() +def getLDAPController(): + if ldapController is not None: + return ldapController + else: + return LDAPController() + + + from flask import Flask from flask_sqlalchemy import SQLAlchemy -from flask_bcrypt import Bcrypt from flask_cors import CORS from .controller.accesTokenController import AccesTokenController @@ -59,7 +72,6 @@ CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' #db = SQLAlchemy(app) -bcrypt = Bcrypt(app) accesTokenController = AccesTokenController("GERUECHT") accesTokenController.start() # login_manager = LoginManager(app) @@ -71,8 +83,8 @@ accesTokenController.start() from geruecht import routes #from geruecht.baruser.routes import baruser -#from geruecht.finanzer.routes import finanzer +from geruecht.finanzer.routes import finanzer -#LOGGER.info("Registrate bluebrints") +LOGGER.info("Registrate bluebrints") #app.register_blueprint(baruser) -#app.register_blueprint(finanzer) +app.register_blueprint(finanzer) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index c8594e1..6943371 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -106,7 +106,7 @@ class AccesTokenController(Thread, metaclass=Singleton): Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. """ - valid_time=120 + valid_time=7200 LOGGER.info("Start Thread for verification that the AccessToken are not out of date.") while True: self.LOGGER.debug("Name: {}".format(self.getName())) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index c73ecb0..3d5f98f 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -1,6 +1,8 @@ import pymysql from geruecht import Singleton from geruecht.model.user import User +from geruecht.model.creditList import CreditList +from datetime import datetime class DatabaseController(metaclass=Singleton): ''' @@ -24,17 +26,31 @@ class DatabaseController(metaclass=Singleton): raise err def getAllUser(self): + self.connect() cursor = self.db.cursor() + try: + cursor.execute("select * from user") + data = cursor.fetchall() + self.db.close() + except Exception as err: + raise err + + if data: + return [User(value) for value in data] def getUser(self, username): self.connect() retVal = None cursor = self.db.cursor() - cursor.execute("select * from user where cn='{}'".format(username)) - data = cursor.fetchone() + try: + cursor.execute("select * from user where cn='{}'".format(username)) + data = cursor.fetchone() + self.db.close() + except Exception as err: + raise err if data: retVal = User(data) - self.db.close() + return retVal @@ -42,7 +58,7 @@ class DatabaseController(metaclass=Singleton): self.connect() cursor = self.db.cursor() try: - cursor.execute("insert into user (cn, dn, firstname, lastname, `group`) VALUES ('{}','{}','{}','{}','{}')".format( + cursor.execute("insert into user (cn, dn, firstname, lastname, gruppe) VALUES ('{}','{}','{}','{}','{}')".format( data['cn'], data['dn'], data['givenName'], data['sn'], data['group'])) self.db.commit() except Exception as err: @@ -55,7 +71,7 @@ class DatabaseController(metaclass=Singleton): self.connect() cursor = self.db.cursor() try: - cursor.execute("update user dn='{}', firstname='{}', lastname='{}', group='{}' where cn='{}'".format( + cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}' where cn='{}'".format( data['dn'], data['givenName'], data['sn'], data['group'], data['cn'])) self.db.commit() except Exception as err: @@ -64,5 +80,67 @@ class DatabaseController(metaclass=Singleton): raise err self.db.close() + def getCreditListFromUser(self, user, **kwargs): + self.connect() + cursor = self.db.cursor() + try: + if 'year' in kwargs: + sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) + else: + sql = "select * from creditList where user_id={}".format(user.id) + cursor.execute(sql) + data = cursor.fetchall() + self.db.close() + except Exception as err: + self.db.close() + raise err + if len(data) == 1: + return CreditList(data[0]) + else: + return [CreditList(value) for value in data] + + def createCreditList(self, user_id, year=datetime.now().year): + self.connect() + cursor = self.db.cursor() + try: + cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) + self.db.close() + except Exception as err: + self.db.close() + raise err + + def updateCreditList(self, creditlist): + self.connect() + cursor = self.db.cursor() + try: + cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) + data = cursor.fetchall() + self.db.close() + if len(data) == 0: + self.createCreditList(creditlist.user_id, creditlist.year) + self.connect() + cursor = self.db.cursor() + cursor.execute("update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, + creditlist.feb_guthaben, creditlist.feb_schulden, + creditlist.maer_guthaben, creditlist.maer_schulden, + creditlist.apr_guthaben, creditlist.apr_schulden, + creditlist.mai_guthaben, creditlist.mai_schulden, + creditlist.jun_guthaben, creditlist.jun_schulden, + creditlist.jul_guthaben, creditlist.jul_schulden, + creditlist.aug_guthaben, creditlist.aug_schulden, + creditlist.sep_guthaben, creditlist.sep_schulden, + creditlist.okt_guthaben, creditlist.okt_schulden, + creditlist.nov_guthaben, creditlist.nov_schulden, + creditlist.dez_guthaben, creditlist.dez_schulden, + creditlist.last_schulden, creditlist.year, creditlist.user_id)) + self.db.close() + except Exception as err: + self.db.rollback() + self.db.close() + raise err + + if __name__ == '__main__': - db = DatabaseController(user='tim') + db = DatabaseController() + user = db.getUser('jhille') + db.getCreditListFromUser(user, year=2018) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index b134e1e..bf2c5c2 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,7 +1,7 @@ from flask import Blueprint, request, jsonify from geruecht.finanzer import LOGGER from datetime import datetime -from geruecht import MONEY +from geruecht import MONEY, db from geruecht.routes import verifyAccessToken from geruecht.model.user import User @@ -24,11 +24,11 @@ def _getFinanzer(): accToken = verifyAccessToken(token, MONEY) if accToken is not None: LOGGER.debug("Get all Useres") - users = User.query.all() + users = db.getAllUser() dic = {} for user in users: LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.userID] = user.toJSON() + dic[user.cn] = user.toJSON() LOGGER.debug("ReturnValue is {}".format(dic)) LOGGER.info("Send main for Finanzer") return jsonify(dic) @@ -56,13 +56,13 @@ def _getFinanzerYear(): LOGGER.debug("Get data {}".format(data)) userID = data['userId'] LOGGER.debug("UserID is {}".format(userID)) - user = User.query.filter_by(userID=userID).first() + user = db.getUser(userID) LOGGER.debug("User is {}".format(user)) - dic[user.userID] = {} + dic[user.cn] = {} LOGGER.debug("Build ReturnValue") for geruecht in user.geruechte: LOGGER.debug("Add Geruecht {} to ReturnValue".format(geruecht)) - dic[user.userID][geruecht.year] = geruecht.toJSON() + dic[user.cn][geruecht.year] = geruecht.toJSON() LOGGER.debug("ReturnValue is {}".format(dic)) LOGGER.info("Send Geruechte from User {}".format(user)) return jsonify(dic) @@ -103,7 +103,7 @@ def _addAmount(): LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - user = User.query.filter_by(userID=userID).first() + user = db.getUser(userID) LOGGER.debug("User is {}".format(user)) LOGGER.debug("Add amount to User {} in year {} and month {}".format(user, year, month)) user.addAmount(amount, year=year, month=month) @@ -149,7 +149,7 @@ def _addCredit(): month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - user = User.query.filter_by(userID=userID).first() + user = db.getUser(userID) LOGGER.debug("User is {}".format(user)) LOGGER.debug("Add credit to User {} in year {} and month {}".format(user, year, month)) user.addCredit(credit, year=year, month=month) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 2370001..70fcaa8 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,7 +1,38 @@ from datetime import datetime from geruecht import getLogger +import geruecht LOGGER = getLogger(__name__) +def create_empty_data(): + empty_data = {'id': 0, + 'jan_guthaben': 0, + 'jan_schulden': 0, + 'feb_guthaben': 0, + 'feb_schulden': 0, + 'maer_guthaben': 0, + 'maer_schulden': 0, + 'apr_guthaben': 0, + 'apr_schulden': 0, + 'mai_guthaben': 0, + 'mai_schulden': 0, + 'jun_guthaben': 0, + 'jun_schulden': 0, + 'jul_guthaben': 0, + 'jul_schulden': 0, + 'aug_guthaben': 0, + 'aug_schulden': 0, + 'sep_guthaben': 0, + 'sep_schulden': 0, + 'okt_guthaben': 0, + 'okt_schulden': 0, + 'nov_guthaben': 0, + 'nov_schulden': 0, + 'dez_guthaben': 0, + 'dez_schulden': 0, + 'last_schulden': 0, + 'year_date': datetime.now().year, + 'user_id': 0} + return empty_data class CreditList(): """ DataBase Object Credit List: @@ -15,50 +46,53 @@ class CreditList(): year: Year of all Credits and Debts. user_id: id from the User. """ - LOGGER.debug("Initialize Geruecht") - id = db.Colum(db.Integer, primary_key=True) + def __init__(self, data): + LOGGER.debug("Initialize Geruecht") + self.id = int(data['id']) - jan_guthaben = db.Column(db.Integer, nullable=False, default=0) - jan_schulden = db.Column(db.Integer, nullable=False, default=0) + self.jan_guthaben = int(data['jan_guthaben']) + self.jan_schulden = int(data['jan_schulden']) - feb_guthaben = db.Column(db.Integer, nullable=False, default=0) - feb_schulden = db.Column(db.Integer, nullable=False, default=0) + self.feb_guthaben = int(data['feb_guthaben']) + self.feb_schulden = int(data['feb_schulden']) - maer_guthaben = db.Column(db.Integer, nullable=False, default=0) - maer_schulden = db.Column(db.Integer, nullable=False, default=0) + self.maer_guthaben = int(data['maer_guthaben']) + self.maer_schulden = int(data['maer_schulden']) - apr_guthaben = db.Column(db.Integer, nullable=False, default=0) - apr_schulden = db.Column(db.Integer, nullable=False, default=0) + self.apr_guthaben = int(data['apr_guthaben']) + self.apr_schulden = int(data['apr_schulden']) - mai_guthaben = db.Column(db.Integer, nullable=False, default=0) - mai_schulden = db.Column(db.Integer, nullable=False, default=0) + self.mai_guthaben = int(data['mai_guthaben']) + self.mai_schulden = int(data['mai_schulden']) - jun_guthaben = db.Column(db.Integer, nullable=False, default=0) - jun_schulden = db.Column(db.Integer, nullable=False, default=0) + self.jun_guthaben = int(data['jun_guthaben']) + self.jun_schulden = int(data['jun_schulden']) - jul_guthaben = db.Column(db.Integer, nullable=False, default=0) - jul_schulden = db.Column(db.Integer, nullable=False, default=0) + self.jul_guthaben = int(data['jul_guthaben']) + self.jul_schulden = int(data['jul_schulden']) - aug_guthaben = db.Column(db.Integer, nullable=False, default=0) - aug_schulden = db.Column(db.Integer, nullable=False, default=0) + self.aug_guthaben = int(data['aug_guthaben']) + self.aug_schulden = int(data['aug_schulden']) - sep_guthaben = db.Column(db.Integer, nullable=False, default=0) - sep_schulden = db.Column(db.Integer, nullable=False, default=0) + self.sep_guthaben = int(data['sep_guthaben']) + self.sep_schulden = int(data['sep_schulden']) - okt_guthaben = db.Column(db.Integer, nullable=False, default=0) - okt_schulden = db.Column(db.Integer, nullable=False, default=0) + self.okt_guthaben = int(data['okt_guthaben']) + self.okt_schulden = int(data['okt_schulden']) - nov_guthaben = db.Column(db.Integer, nullable=False, default=0) - nov_schulden = db.Column(db.Integer, nullable=False, default=0) + self.nov_guthaben = int(data['nov_guthaben']) + self.nov_schulden = int(data['nov_schulden']) - dez_guthaben = db.Column(db.Integer, nullable=False, default=0) - dez_schulden = db.Column(db.Integer, nullable=False, default=0) + self.dez_guthaben = int(data['dez_guthaben']) + self.dez_schulden = int(data['dez_schulden']) - last_schulden = db.Column(db.Integer, nullable=False, default=0) + self.last_schulden = int(data['last_schulden']) - year = db.Column(db.Integer, nullable=False, default=datetime.now().year) + self.year = int(data['year_date']) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + self.user_id = int(data['user_id']) + + self.db = geruecht.getDatabesController() def getSchulden(self): """ Get Schulden @@ -184,7 +218,8 @@ class CreditList(): self.dez_schulden += amount retValue = (self.dez_guthaben, self.dez_schulden) - db.session.commit() + #db.session.commit() + self.db.updateCreditList(self) LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue @@ -238,8 +273,8 @@ class CreditList(): elif month == 12: self.dez_guthaben += credit retValue = (self.dez_guthaben, self.dez_schulden) - - db.session.commit() + self.db.updateCreditList(self) + #db.session.commit() LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 89d63f5..d549732 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,10 +1,11 @@ from geruecht import getLogger -from geruecht import db -#from geruecht.model.creditList import CreditList +import geruecht +from geruecht.model.creditList import CreditList, create_empty_data from datetime import datetime LOGGER = getLogger(__name__) + class User(): """ Database Object for User @@ -25,10 +26,17 @@ class User(): self.dn = data['dn'] self.firstname = data['firstname'] self.lastname = data['lastname'] - self.group = data['group'] - + self.group = data['gruppe'] + self.db = geruecht.getDatabesController() + self.ldap = geruecht.getLDAPController() + self.geruechte = [] + geruechte = self.db.getCreditListFromUser(self) + if type(geruechte) == list: + self.geruechte = geruechte + elif type(geruechte) == CreditList: + self.geruechte.append(geruechte) #geruechte = db.relationship('CreditList', backref='user', lazy=True) - ''' + def createGeruecht(self, amount=0, year=datetime.now().year): """ Create Geruecht @@ -43,10 +51,14 @@ class User(): the created geruecht """ LOGGER.debug("Create Geruecht for user {} in year {}".format(self, year)) - credit = CreditList(user_id=self.id, last_schulden=amount, year=year) - db.session.add(credit) - db.session.commit() - credit = CreditList.query.filter_by(year=year, user_id=self.id).first() + data = create_empty_data() + data['user_id'] = self.id, + data['last_schulden'] = amount, + data['year_date'] = year + credit = CreditList(data) + self.geruechte.append(credit) + self.db.updateCreditList(credit) + credit = self.db.getCreditListFromUser(self, year=year) LOGGER.debug("Created Geruecht {}".format(credit)) return credit @@ -93,8 +105,7 @@ class User(): geruecht = self.getGeruecht(year=year) retVal = geruecht.addAmount(amount, month=month) - db.session.add(geruecht) - db.session.commit() + self.db.updateCreditList(geruecht) self.updateGeruecht() @@ -118,8 +129,7 @@ class User(): geruecht = self.getGeruecht(year=year) retVal = geruecht.addCredit(credit, month=month) - db.session.add(geruecht) - db.session.commit() + self.db.updateCreditList(geruecht) self.updateGeruecht() @@ -138,8 +148,7 @@ class User(): geruecht.last_schulden = 0 if index != 0: geruecht.last_schulden = (self.geruechte[index - 1].getSchulden() * -1) - - db.session.commit() + self.db.updateCreditList(geruecht) def sortYear(self, geruecht): """ Sort Year @@ -154,7 +163,7 @@ class User(): int year of the geruecht """ return geruecht.year - ''' + def toJSON(self): """ Create Dic to dump in JSON @@ -170,10 +179,10 @@ class User(): } return dic - def update(self): - data = ldap.getUserData(self.cn) - data['group'] = ldap.getGroup(self.cn) - db.updateUser(data) + def updateUser(self): + data = self.ldap.getUserData(self.cn) + data['group'] = self.ldap.getGroup(self.cn) + self.db.updateUser(data) def login(self, password): """ Login for the User @@ -185,13 +194,13 @@ class User(): """ LOGGER.debug("Login User {}".format(self)) try: - from geruecht import ldapController as ldap - ldap.login(self.cn, password) + self.ldap.login(self.cn, password) - self.update() + self.updateUser() return True except: return False def __repr__(self): return "User({}, {}, {})".format(self.cn, self.dn, self.group) + diff --git a/geruecht/routes.py b/geruecht/routes.py index 2685794..6aa742f 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,11 +1,11 @@ from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO, LOGGER from geruecht import ldapController as ldap from geruecht.model.user import User -#from geruecht.model.creditList import CreditList -#from geruecht.model.priceList import PriceList -from datetime import datetime from flask import request, jsonify +def login(user, password): + return user.login(password) + def verifyAccessToken(token, group): """ Verify Accestoken @@ -83,7 +83,7 @@ def _login(): LOGGER.debug("User is {}".format(user)) if user: LOGGER.debug("Check login for User {}".format(user)) - if user.login(password): + if login(user, password): token = accesTokenController.createAccesToken(user) dic = user.toJSON() dic["token"] = token From c6508fd516916d84ff3a696e550b7f25415e2cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 19 Dec 2019 19:46:04 +0100 Subject: [PATCH 015/111] test with databaseController --- geruecht/controller/databaseController.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 3d5f98f..10fbb6d 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -115,12 +115,9 @@ class DatabaseController(metaclass=Singleton): try: cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) data = cursor.fetchall() - self.db.close() if len(data) == 0: self.createCreditList(creditlist.user_id, creditlist.year) - self.connect() - cursor = self.db.cursor() - cursor.execute("update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, + sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, creditlist.feb_guthaben, creditlist.feb_schulden, creditlist.maer_guthaben, creditlist.maer_schulden, creditlist.apr_guthaben, creditlist.apr_schulden, @@ -132,7 +129,9 @@ class DatabaseController(metaclass=Singleton): creditlist.okt_guthaben, creditlist.okt_schulden, creditlist.nov_guthaben, creditlist.nov_schulden, creditlist.dez_guthaben, creditlist.dez_schulden, - creditlist.last_schulden, creditlist.year, creditlist.user_id)) + creditlist.last_schulden, creditlist.year, creditlist.user_id) + print(sql) + cursor.execute(sql) self.db.close() except Exception as err: self.db.rollback() From 23db38690e3bbdd795cef47a5a66dc6583a977ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 22 Dec 2019 22:27:39 +0100 Subject: [PATCH 016/111] fixed #79 and other bugs --- geruecht/__init__.py | 4 +-- geruecht/baruser/routes.py | 23 +++++++++------- geruecht/controller/accesTokenController.py | 2 +- geruecht/controller/databaseController.py | 20 ++++++++++++-- geruecht/controller/ldapController.py | 30 ++++++++++++++------- geruecht/finanzer/routes.py | 1 - geruecht/model/user.py | 13 ++++++--- geruecht/routes.py | 8 +++--- 8 files changed, 70 insertions(+), 31 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 39b790e..4c08b28 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -82,9 +82,9 @@ accesTokenController.start() from geruecht import routes -#from geruecht.baruser.routes import baruser +from geruecht.baruser.routes import baruser from geruecht.finanzer.routes import finanzer LOGGER.info("Registrate bluebrints") -#app.register_blueprint(baruser) +app.register_blueprint(baruser) app.register_blueprint(finanzer) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 1852a36..3f25633 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -23,18 +23,23 @@ def _bar(): dic = {} if accToken is not None: - users = User.query.all() + users = db.getAllUser() for user in users: geruecht = None geruecht = user.getGeruecht() if geruecht is not None: month = geruecht.getMonth(datetime.now().month) - amount = abs(month[0] - month[1]) + amount = month[0] - month[1] if amount != 0: - dic[user.userID] = {"username": user.username, + if amount >= 0: + type = 'credit' + else: + type = 'amount' + dic[user.cn] = {"username": user.cn, "firstname": user.firstname, "lastname": user.lastname, - "amount": abs(month[0] - month[1]) + "amount": abs(month[0] - month[1]), + "type": type } return jsonify(dic) return jsonify({"error": "permission denied"}), 401 @@ -58,12 +63,12 @@ def _baradd(): userID = data['userId'] amount = int(data['amount']) - user = User.query.filter_by(userID=userID).first() + user = db.getUser(userID) month = user.addAmount(amount) amount = abs(month[0] - month[1]) - return jsonify({"userId": user.userID, "amount": amount}) + return jsonify({"userId": user.cn, "amount": amount}) return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") @@ -82,11 +87,11 @@ def _getUsers(): retVal = {} if accToken is not None: - users = User.query.all() + users = db.getAllUser() for user in users: month = user.getGeruecht().getMonth() if month == 0: - retVal[user.userID] = {user.toJSON()} + retVal[user.cn] = {user.toJSON()} return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 @@ -108,7 +113,7 @@ def _getUser(): data = request.get_json() userID = data['userId'] - user = User.query.filter_by(userID=userID) + user = db.getUser(userID) month = user.getGeruecht().getMonth() return jsonify({"userId": user.userID, "amount": month[1], "credit": month[0]}) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 6943371..ca92c9b 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -99,7 +99,7 @@ class AccesTokenController(Thread, metaclass=Singleton): """ print("controll if", accToken, "hase group", group) LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, group)) - return True if accToken.user.group == group else False + return True if group in accToken.user.group else False def run(self): """ Starting Controll-Thread diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 10fbb6d..20f5a89 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -53,13 +53,21 @@ class DatabaseController(metaclass=Singleton): return retVal + def _convertGroupToString(self, groups): + retVal = '' + for group in groups: + if len(retVal) != 0: + retVal += ',' + retVal += group + return retVal def insertUser(self, data): self.connect() cursor = self.db.cursor() + groups = self._convertGroupToString(data['group']) try: cursor.execute("insert into user (cn, dn, firstname, lastname, gruppe) VALUES ('{}','{}','{}','{}','{}')".format( - data['cn'], data['dn'], data['givenName'], data['sn'], data['group'])) + data['cn'], data['dn'], data['givenName'], data['sn'], groups)) self.db.commit() except Exception as err: self.db.rollback() @@ -70,14 +78,17 @@ class DatabaseController(metaclass=Singleton): def updateUser(self, data): self.connect() cursor = self.db.cursor() + groups = self._convertGroupToString(data['group']) try: cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}' where cn='{}'".format( - data['dn'], data['givenName'], data['sn'], data['group'], data['cn'])) + data['dn'], data['givenName'], data['sn'], groups, data['cn'])) self.db.commit() except Exception as err: self.db.rollback() self.db.close() + print(err.__traceback__) raise err + self.db.close() def getCreditListFromUser(self, user, **kwargs): @@ -104,6 +115,7 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() try: cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) + self.db.commit() self.db.close() except Exception as err: self.db.close() @@ -115,6 +127,7 @@ class DatabaseController(metaclass=Singleton): try: cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) data = cursor.fetchall() + self.db.close() if len(data) == 0: self.createCreditList(creditlist.user_id, creditlist.year) sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, @@ -131,7 +144,10 @@ class DatabaseController(metaclass=Singleton): creditlist.dez_guthaben, creditlist.dez_schulden, creditlist.last_schulden, creditlist.year, creditlist.user_id) print(sql) + self.connect() + cursor = self.db.cursor() cursor.execute(sql) + self.db.commit() self.db.close() except Exception as err: self.db.rollback() diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index dbd5a40..a07b3aa 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -37,18 +37,28 @@ class LDAPController(metaclass=Singleton): def getGroup(self, username): + retVal = [] self.connect() + main_group_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'cn={}'.format(username), ['gidNumber']) + if main_group_data: + main_group_number = main_group_data[0][1]['gidNumber'][0].decode('utf-8') + group_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'gidNumber={}'.format(main_group_number), ['cn']) + if group_data: + group_name = group_data[0][1]['cn'][0].decode('utf-8') + if group_name == 'ldap-user': + retVal.append(USER) + groups_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'memberUID={}'.format(username), ['cn']) - if len(groups_data) == 0: - return USER - else: - data = groups_data[0][1]['cn'][0].decode('utf-8') - if data == 'finanzer': - return MONEY - elif data == 'gastro': - return GASTRO - elif data == 'bar': - return BAR + for data in groups_data: + print(data[1]['cn'][0].decode('utf-8')) + group_name = data[1]['cn'][0].decode('utf-8') + if group_name == 'finanzer': + retVal.append(MONEY) + elif group_name == 'gastro': + retVal.append(GASTRO) + elif group_name == 'bar': + retVal.append(BAR) + return retVal if __name__ == '__main__': diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index bf2c5c2..f1178cd 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -3,7 +3,6 @@ from geruecht.finanzer import LOGGER from datetime import datetime from geruecht import MONEY, db from geruecht.routes import verifyAccessToken -from geruecht.model.user import User finanzer = Blueprint("finanzer", __name__) diff --git a/geruecht/model/user.py b/geruecht/model/user.py index d549732..cec7dcc 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -27,6 +27,11 @@ class User(): self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] + if type(data['gruppe']) == list: + self.group = data['gruppe'] + elif type(data['gruppe']) == str: + self.group = data['gruppe'].split(',') + self.db = geruecht.getDatabesController() self.ldap = geruecht.getLDAPController() self.geruechte = [] @@ -52,8 +57,8 @@ class User(): """ LOGGER.debug("Create Geruecht for user {} in year {}".format(self, year)) data = create_empty_data() - data['user_id'] = self.id, - data['last_schulden'] = amount, + data['user_id'] = self.id + data['last_schulden'] = amount data['year_date'] = year credit = CreditList(data) self.geruechte.append(credit) @@ -85,7 +90,7 @@ class User(): self.updateGeruecht() - return geruecht + return self.getGeruecht(year=year) def addAmount(self, amount, year=datetime.now().year, month=datetime.now().month): """ Add Amount @@ -171,11 +176,13 @@ class User(): A Dic with static Attributes. """ dic = { + "userId": self.cn, "cn": self.cn, "dn": self.dn, "firstname": self.firstname, "lastname": self.lastname, "group": self.group, + "username": self.cn } return dic diff --git a/geruecht/routes.py b/geruecht/routes.py index 6aa742f..349d575 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -59,6 +59,7 @@ def _login(): """ LOGGER.info("Start log in.") data = request.get_json() + print(data) LOGGER.debug("JSON from request: {}".format(data)) username = data['username'] password = data['password'] @@ -69,15 +70,15 @@ def _login(): try: ldap.login(username, password) LOGGER.info("Authentification successfull. Search Group") - group = ldap.getGroup(username) + groups = ldap.getGroup(username) LOGGER.info("Get userdata from LDAP") user_data = ldap.getUserData(username) - user_data['group'] = group + user_data['group'] = groups LOGGER.info('Insert user {} into database') db.insertUser(user_data) except Exception as err: - raise err + return jsonify({"error": str(err)}), 401 LOGGER.info("{} try to log in".format(username)) user = db.getUser(username) LOGGER.debug("User is {}".format(user)) @@ -87,6 +88,7 @@ def _login(): token = accesTokenController.createAccesToken(user) dic = user.toJSON() dic["token"] = token + dic["accessToken"] = token LOGGER.info("User {} success login.".format(username)) return jsonify(dic) else: From 589ae3e3a823ad18631f93f8ce2825a69b61d5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 22 Dec 2019 23:09:18 +0100 Subject: [PATCH 017/111] ldap search --- geruecht/baruser/routes.py | 24 +++++++------------ geruecht/controller/ldapController.py | 33 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 1852a36..62f3170 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify -from geruecht import BAR, db +from geruecht import BAR, db, ldapController as ldap from geruecht.routes import verifyAccessToken from geruecht.model.user import User from datetime import datetime @@ -90,26 +90,18 @@ def _getUsers(): return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 -@baruser.route("/barGetUser", methods=['POST']) -def _getUser(): - """ Get specified User - - This function returns the user with posted userID and them amount and credit. - - Returns: - JSON-File with userID, amount and credit - or ERROR 401 Permission Denied - """ +@baruser.route("/search", methods=['POST']) +def _search(): token = request.headers.get("Token") - print(token) accToken = verifyAccessToken(token, BAR) + if accToken is not None: data = request.get_json() - userID = data['userId'] - user = User.query.filter_by(userID=userID) - month = user.getGeruecht().getMonth() + searchString = data['searchString'] - return jsonify({"userId": user.userID, "amount": month[1], "credit": month[0]}) + retVal = ldap.searchUser(searchString) + + return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index dbd5a40..3646d8e 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -50,6 +50,39 @@ class LDAPController(metaclass=Singleton): elif data == 'bar': return BAR + def __isUserInList(self, list, username): + help_list = [] + for user in list: + help_list.append(user[1]['cn'][0].decode('utf-8')) + if username in help_list: + return True + return False + + def searchUser(self, searchString): + self.connect() + + name = searchString.split(" ") + name_result = [] + + if len(name) == 1: + name_result[0] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn']) + name_result[1] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['cn', 'givenName', 'sn']) + else: + name_result[2] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, + 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn']) + name_result[3] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]), + ['cn', 'givenName', 'sn']) + retVal = [] + + for user in name_result: + username = user[1]['cn'][0].decode('utf-8') + if not self.__isUserInList(retVal, username): + firstname = user[1]['givenName'][0].decode('utf-8') + lastname = user[1]['givenName'][0].decode('utf-8') + retVal.append({username: username, firstname: firstname, lastname: lastname}) + + return retVal + if __name__ == '__main__': a = LDAPController() From d0f665cc8b9a18cc869961bd81877785350f77a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 26 Dec 2019 10:28:30 +0100 Subject: [PATCH 018/111] first usable site only 2 groups exists: bar and finanzer person can be in more than 1 group site for baruser first try with autocomplete from ldap site for finanzer ; overview; specialview for 1 Person --- geruecht/baruser/routes.py | 12 +++--- geruecht/controller/databaseController.py | 2 +- geruecht/controller/ldapController.py | 51 +++++++++++++++++------ geruecht/finanzer/routes.py | 4 ++ geruecht/model/creditList.py | 1 + geruecht/model/user.py | 1 + 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index dc2579b..4feb597 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -64,6 +64,12 @@ def _baradd(): amount = int(data['amount']) user = db.getUser(userID) + if user is None: + groups = ldap.getGroup(userID) + user_data = ldap.getUserData(userID) + user_data['group'] = groups + db.insertUser(user_data) + user = db.getUser(userID) month = user.addAmount(amount) amount = abs(month[0] - month[1]) @@ -87,11 +93,7 @@ def _getUsers(): retVal = {} if accToken is not None: - users = db.getAllUser() - for user in users: - month = user.getGeruecht().getMonth() - if month == 0: - retVal[user.cn] = {user.toJSON()} + retVal = ldap.getAllUser() return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 20f5a89..2e239f4 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -106,7 +106,7 @@ class DatabaseController(metaclass=Singleton): self.db.close() raise err if len(data) == 1: - return CreditList(data[0]) + return [CreditList(data[0])] else: return [CreditList(value) for value in data] diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 593c4e6..ebb7de8 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -63,33 +63,58 @@ class LDAPController(metaclass=Singleton): def __isUserInList(self, list, username): help_list = [] for user in list: - help_list.append(user[1]['cn'][0].decode('utf-8')) + help_list.append(user['username']) if username in help_list: return True return False + def getAllUser(self): + self.connect() + retVal = [] + data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['cn', 'givenName', 'sn']) + for user in data: + if 'cn' in user[1]: + username = user[1]['cn'][0].decode('utf-8') + firstname = user[1]['givenName'][0].decode('utf-8') + lastname = user[1]['sn'][0].decode('utf-8') + retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) + return retVal + def searchUser(self, searchString): self.connect() name = searchString.split(" ") + + for i in range(len(name)): + name[i] = "*"+name[i]+"*" + + + print(name) + name_result = [] if len(name) == 1: - name_result[0] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn']) - name_result[1] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['cn', 'givenName', 'sn']) + if name[0] == "**": + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, + attrlist=['cn', 'givenName', 'sn'])) + else: + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['cn', 'givenName', 'sn'])) else: - name_result[2] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, - 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn']) - name_result[3] = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]), - ['cn', 'givenName', 'sn']) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, + 'givenName={}'.format(name[1]), ['cn', 'givenName', 'sn'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[1]), + ['cn', 'givenName', 'sn'])) retVal = [] - for user in name_result: - username = user[1]['cn'][0].decode('utf-8') - if not self.__isUserInList(retVal, username): - firstname = user[1]['givenName'][0].decode('utf-8') - lastname = user[1]['givenName'][0].decode('utf-8') - retVal.append({username: username, firstname: firstname, lastname: lastname}) + for names in name_result: + for user in names: + if 'cn' in user[1]: + username = user[1]['cn'][0].decode('utf-8') + if not self.__isUserInList(retVal, username): + firstname = user[1]['givenName'][0].decode('utf-8') + lastname = user[1]['sn'][0].decode('utf-8') + retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) return retVal diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index f1178cd..57b13a5 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -28,6 +28,8 @@ def _getFinanzer(): for user in users: LOGGER.debug("Add User {} to ReturnValue".format(user)) dic[user.cn] = user.toJSON() + creditList = db.getCreditListFromUser(user) + dic[user.cn]['creditList'] = {credit.year: credit.toJSON() for credit in creditList} LOGGER.debug("ReturnValue is {}".format(dic)) LOGGER.info("Send main for Finanzer") return jsonify(dic) @@ -130,7 +132,9 @@ def _addCredit(): accToken = verifyAccessToken(token, MONEY) if accToken is not None: + data = request.get_json() + print(data) LOGGER.debug("Get data {}".format(data)) userID = data['userId'] credit = int(data['credit']) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 70fcaa8..5c30a9c 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -321,6 +321,7 @@ class CreditList(): "dez": { "credit": self.dez_guthaben, "depts": self.dez_schulden}, + "last": self.last_schulden } return dic diff --git a/geruecht/model/user.py b/geruecht/model/user.py index cec7dcc..a9e61b2 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -40,6 +40,7 @@ class User(): self.geruechte = geruechte elif type(geruechte) == CreditList: self.geruechte.append(geruechte) + self.updateGeruecht() #geruechte = db.relationship('CreditList', backref='user', lazy=True) def createGeruecht(self, amount=0, year=datetime.now().year): From 5b37e3d15b0c1d0bb6f48547586b997d172c7c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 28 Dec 2019 11:31:45 +0100 Subject: [PATCH 019/111] update token validatition no second thread to validate for token ... only on access it will be validate --- geruecht/__init__.py | 8 --- geruecht/baruser/routes.py | 20 +++--- geruecht/controller/accesTokenController.py | 73 ++++++++------------- geruecht/finanzer/routes.py | 19 +++--- geruecht/routes.py | 49 +++----------- 5 files changed, 54 insertions(+), 115 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 4c08b28..d6a806d 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -61,22 +61,14 @@ def getLDAPController(): from flask import Flask -from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS from .controller.accesTokenController import AccesTokenController -# from flask_login import LoginManager LOGGER.info("Build APP") app = Flask(__name__) CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' -#db = SQLAlchemy(app) accesTokenController = AccesTokenController("GERUECHT") -accesTokenController.start() -# login_manager = LoginManager(app) -# login_manager.login_view = 'login' -# login_manager.login_message_category = 'info' diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 4feb597..a3858eb 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,7 +1,5 @@ from flask import Blueprint, request, jsonify -from geruecht import BAR, db, ldapController as ldap -from geruecht.routes import verifyAccessToken -from geruecht.model.user import User +from geruecht import BAR, db, ldapController as ldap, accesTokenController from datetime import datetime baruser = Blueprint("baruser", __name__) @@ -19,10 +17,10 @@ def _bar(): print(request.headers) token = request.headers.get("Token") print(token) - accToken = verifyAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, BAR) dic = {} - if accToken is not None: + if accToken: users = db.getAllUser() for user in users: geruecht = None @@ -56,9 +54,9 @@ def _baradd(): """ token = request.headers.get("Token") print(token) - accToken = verifyAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, BAR) - if accToken is not None: + if accToken: data = request.get_json() userID = data['userId'] amount = int(data['amount']) @@ -89,10 +87,10 @@ def _getUsers(): """ token = request.headers.get("Token") print(token) - accToken = verifyAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, BAR) retVal = {} - if accToken is not None: + if accToken: retVal = ldap.getAllUser() return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 @@ -101,9 +99,9 @@ def _getUsers(): def _search(): token = request.headers.get("Token") print(token) - accToken = verifyAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, BAR) - if accToken is not None: + if accToken: data = request.get_json() searchString = data['searchString'] diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index ca92c9b..17f6e3c 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,14 +1,12 @@ from geruecht.model.accessToken import AccessToken from geruecht.controller import LOGGER -from datetime import datetime -import time -from threading import Thread +from datetime import datetime, timedelta import hashlib import logging from logging.handlers import WatchedFileHandler from geruecht import Singleton -class AccesTokenController(Thread, metaclass=Singleton): +class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken This Class create, delete, find and manage AccesToken. @@ -19,7 +17,7 @@ class AccesTokenController(Thread, metaclass=Singleton): """ instance = None tokenList = None - lifetime = 60 + lifetime = 1800 def __init__(self, arg): """ Initialize AccessTokenController @@ -39,31 +37,39 @@ class AccesTokenController(Thread, metaclass=Singleton): self.LOGGER.setLevel(logging.DEBUG) self.LOGGER.addHandler(logFileHandler) self.LOGGER.propagate = False - - LOGGER.debug("Initialize Threading") - Thread.__init__(self) self.tokenList = [] - def findAccesToken(self, token): - """ Find a Token in current AccessTokens + def validateAccessToken(self, token, group): + """ Verify Accestoken - Iterate throw all availables AccesTokens and retrieve one, if they are the same. + Verify an Accestoken and Group so if the User has permission or not. + Retrieves the accestoken if valid else retrieves False Args: - token: Token to find - + token: Token to verify. + group: Group like 'moneymaster', 'gastro', 'user' or 'bar' Returns: - An AccessToken if found or None if not found. + An the AccesToken for this given Token or False. """ - LOGGER.info("Search for Token: {}".format(token)) - LOGGER.debug("Iterate through List of current Tokens") + LOGGER.info("Verify AccessToken with token: {} and group: {}".format(token, group)) for accToken in self.tokenList: - LOGGER.debug("Check if AccessToken {} has Token {}".format(accToken, token)) + LOGGER.debug("Check is token {} same as in AccessToken {}".format(token, accToken)) if accToken == token: - LOGGER.info("Find AccessToken {} with Token {}".format(accToken, token)) - return accToken - LOGGER.info("no AccesToken found with Token {}".format(token)) - return None + LOGGER.debug("AccessToken is {}".format(accToken)) + endTime = accToken.timestamp + timedelta(seconds=self.lifetime) + now = datetime.now() + LOGGER.debug("Check if AccessToken's Endtime {} is bigger then now {}".format(endTime, now)) + if now <= endTime: + LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) + if self.isSameGroup(accToken, group): + accToken.updateTimestamp() + LOGGER.info("Found AccessToken {} with token: {} and group: {}".format(accToken, token, group)) + return accToken + else: + LOGGER.debug("AccessToken {} is no longer valid and will removed".format(accToken)) + self.tokenList.remove(accToken) + LOGGER.info("Found no valid AccessToken with token: {} and group: {}".format(token, group)) + return False def createAccesToken(self, user): """ Create an AccessToken @@ -79,7 +85,7 @@ class AccesTokenController(Thread, metaclass=Singleton): LOGGER.info("Create AccessToken") now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() - accToken = AccessToken(user, token) + accToken = AccessToken(user, token, datetime.now()) LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) self.tokenList.append(accToken) LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) @@ -100,26 +106,3 @@ class AccesTokenController(Thread, metaclass=Singleton): print("controll if", accToken, "hase group", group) LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, group)) return True if group in accToken.user.group else False - - def run(self): - """ Starting Controll-Thread - - Verify that the AccesToken are not out of date. If one AccessToken out of date it will be deletet from tokenList. - """ - valid_time=7200 - LOGGER.info("Start Thread for verification that the AccessToken are not out of date.") - while True: - self.LOGGER.debug("Name: {}".format(self.getName())) - self.LOGGER.debug("Start to iterate through List of current Tokens") - for accToken in self.tokenList: - - self.LOGGER.debug("Check if AccessToken {} is out of date".format(accToken)) - if (datetime.now() - accToken.timestamp).seconds > valid_time: - print("delete", accToken) - self.LOGGER.info("Delete AccessToken {} from List of current Tokens".format(accToken)) - self.tokenList.remove(accToken) - else: - self.LOGGER.debug("AccessToken {} is up to date. {} seconds left".format(accToken, valid_time - (datetime.now() - accToken.timestamp).seconds)) - self.LOGGER.debug("List of current Tokens: {}".format(self.tokenList)) - self.LOGGER.info("Wait 10 Seconds") - time.sleep(10) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 57b13a5..d0fdfa1 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,8 +1,7 @@ from flask import Blueprint, request, jsonify from geruecht.finanzer import LOGGER from datetime import datetime -from geruecht import MONEY, db -from geruecht.routes import verifyAccessToken +from geruecht import MONEY, db, accesTokenController finanzer = Blueprint("finanzer", __name__) @@ -20,8 +19,8 @@ def _getFinanzer(): LOGGER.info("Get main for Finanzer") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = verifyAccessToken(token, MONEY) - if accToken is not None: + accToken = accesTokenController.validateAccessToken(token, MONEY) + if accToken: LOGGER.debug("Get all Useres") users = db.getAllUser() dic = {} @@ -49,10 +48,10 @@ def _getFinanzerYear(): LOGGER.info("Get all Geruechte from User.") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = verifyAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, MONEY) dic = {} - if accToken is not None: + if accToken: data = request.get_json() LOGGER.debug("Get data {}".format(data)) userID = data['userId'] @@ -85,9 +84,9 @@ def _addAmount(): LOGGER.info("Add Amount") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = verifyAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, MONEY) - if accToken is not None: + if accToken: data = request.get_json() LOGGER.debug("Get data {}".format(data)) userID = data['userId'] @@ -129,9 +128,9 @@ def _addCredit(): LOGGER.info("Add Amount") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = verifyAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, MONEY) - if accToken is not None: + if accToken: data = request.get_json() print(data) diff --git a/geruecht/routes.py b/geruecht/routes.py index 349d575..3c026cc 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -6,44 +6,20 @@ from flask import request, jsonify def login(user, password): return user.login(password) -def verifyAccessToken(token, group): - """ Verify Accestoken - - Verify an Accestoken and Group so if the User has permission or not. - Retrieves the accestoken if valid else retrieves None - - Args: - token: Token to verify. - group: Group like 'moneymaster', 'gastro', 'user' or 'bar' - Returns: - An the AccesToken for this given Token or None. - """ - LOGGER.info("Verify AccessToken with token: {} and group: {}".format(token, group)) - accToken = accesTokenController.findAccesToken(token) - LOGGER.debug("AccessToken is {}".format(accToken)) - if accToken is not None: - LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) - if accesTokenController.isSameGroup(accToken, group): - accToken.updateTimestamp() - LOGGER.info("Found AccessToken {} with token: {} and group: {}".format(accToken, token, group)) - return accToken - LOGGER.info("No AccessToken with token: {} and group: {} found".format(token, group)) - return None - @app.route("/valid") def _valid(): token = request.headers.get("Token") - accToken = verifyAccessToken(token, MONEY) - if accToken is not None: + accToken = accesTokenController.validateAccessToken(token, MONEY) + if accToken: return jsonify(accToken.user.toJSON()) - accToken = verifyAccessToken(token, BAR) - if accToken is not None: + accToken = accesTokenController.validateAccessToken(token, BAR) + if accToken: return jsonify(accToken.user.toJSON()) - accToken = verifyAccessToken(token, GASTRO) - if accToken is not None: + accToken = accesTokenController.validateAccessToken(token, GASTRO) + if accToken: return jsonify(accToken.user.toJSON()) - accToken = verifyAccessToken(token, USER) - if accToken is not None: + accToken = accesTokenController.validateAccessToken(token, USER) + if accToken: return jsonify(accToken.user.toJSON()) return jsonify({"error": "permission denied"}), 401 @@ -96,12 +72,3 @@ def _login(): return jsonify({"error": "wrong password"}), 401 LOGGER.info("User {} does not exist.".format(username)) return jsonify({"error": "wrong username"}), 402 - -@app.route("/getFinanzer") -def getFinanzer(): - users = User.query.all() - dic = {} - for user in users: - dic[user.userID] = user.toJSON() - print(dic) - return jsonify(dic) From 6ee6c1d44a47adf0b17be618994ae13cea3e8a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 28 Dec 2019 21:52:49 +0100 Subject: [PATCH 020/111] update for UnitTests new controller: userController so routes don't have to import users or creditlist and don't do logics. --- geruecht/__init__.py | 58 +---------------- geruecht/baruser/routes.py | 24 +++----- geruecht/controller/__init__.py | 29 ++++++++- geruecht/controller/accesTokenController.py | 15 +---- geruecht/controller/databaseController.py | 22 ++++--- geruecht/controller/ldapController.py | 33 +++++----- geruecht/controller/userController.py | 46 ++++++++++++++ geruecht/exceptions/__init__.py | 2 + geruecht/finanzer/routes.py | 56 +++-------------- geruecht/logger.py | 21 +++++++ geruecht/model/__init__.py | 4 ++ geruecht/model/creditList.py | 8 --- geruecht/model/priceList.py | 2 +- geruecht/model/user.py | 65 ++++---------------- geruecht/routes.py | 50 +++++---------- geruecht/site.db | Bin 28672 -> 0 bytes 16 files changed, 183 insertions(+), 252 deletions(-) create mode 100644 geruecht/controller/userController.py create mode 100644 geruecht/exceptions/__init__.py create mode 100644 geruecht/logger.py delete mode 100644 geruecht/site.db diff --git a/geruecht/__init__.py b/geruecht/__init__.py index d6a806d..12caada 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -4,74 +4,18 @@ Initialize also a singelton for the AccesTokenControler and start the Thread. """ -import logging -from logging.handlers import WatchedFileHandler -import sys - -MONEY = "moneymaster" -GASTRO = "gastro" -USER = "user" -BAR = "bar" - -FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") - -logFileHandler = WatchedFileHandler("testlog.log") -logFileHandler.setFormatter(FORMATTER) - -logStreamHandler = logging.StreamHandler(stream=sys.stdout) -logStreamHandler.setFormatter(FORMATTER) - -def getLogger(logger_name): - logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG) - logger.addHandler(logFileHandler) - logger.addHandler(logStreamHandler) - - logger.propagate = False - - return logger +from .logger import getLogger LOGGER = getLogger(__name__) LOGGER.info("Initialize App") -class Singleton(type): - _instances = {} - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - -from .controller.databaseController import DatabaseController -db = DatabaseController() -from .controller.ldapController import LDAPController -ldapController = LDAPController() - -def getDatabesController(): - if db is not None: - return db - else: - return DatabaseController() -def getLDAPController(): - if ldapController is not None: - return ldapController - else: - return LDAPController() - - - - from flask import Flask from flask_cors import CORS -from .controller.accesTokenController import AccesTokenController LOGGER.info("Build APP") app = Flask(__name__) CORS(app) # app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' -accesTokenController = AccesTokenController("GERUECHT") - - - from geruecht import routes from geruecht.baruser.routes import baruser diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index a3858eb..5aba339 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,6 +1,7 @@ from flask import Blueprint, request, jsonify -from geruecht import BAR, db, ldapController as ldap, accesTokenController +from geruecht.controller import ldapController as ldap, accesTokenController, userController from datetime import datetime +from geruecht.model import BAR baruser = Blueprint("baruser", __name__) @@ -21,10 +22,10 @@ def _bar(): dic = {} if accToken: - users = db.getAllUser() + users = userController.getAllUsersfromDB() for user in users: geruecht = None - geruecht = user.getGeruecht() + geruecht = user.getGeruecht(datetime.now().year) if geruecht is not None: month = geruecht.getMonth(datetime.now().month) amount = month[0] - month[1] @@ -33,7 +34,7 @@ def _bar(): type = 'credit' else: type = 'amount' - dic[user.cn] = {"username": user.cn, + dic[user.uid] = {"username": user.uid, "firstname": user.firstname, "lastname": user.lastname, "amount": abs(month[0] - month[1]), @@ -61,18 +62,13 @@ def _baradd(): userID = data['userId'] amount = int(data['amount']) - user = db.getUser(userID) - if user is None: - groups = ldap.getGroup(userID) - user_data = ldap.getUserData(userID) - user_data['group'] = groups - db.insertUser(user_data) - user = db.getUser(userID) - month = user.addAmount(amount) - + date = datetime.now() + userController.addAmount(userID, amount, year=date.year, month=date.month) + user = userController.getUser(userID) + month = user.getGeruecht(year=date.year).getMonth(month=date.month) amount = abs(month[0] - month[1]) - return jsonify({"userId": user.cn, "amount": amount}) + return jsonify({"userId": user.uid, "amount": amount}) return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index 6464ff6..fa32eb5 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -1,3 +1,30 @@ -from geruecht import getLogger +from geruecht.logger import getLogger LOGGER = getLogger(__name__) + +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +from .databaseController import DatabaseController +def getDatabesController(): + if db is not None: + return db + else: + return DatabaseController() +from .ldapController import LDAPController +def getLDAPController(): + if ldapController is not None: + return ldapController + else: + return LDAPController() +from .accesTokenController import AccesTokenController + +db = DatabaseController() +ldapController = LDAPController() +accesTokenController = AccesTokenController("GERUECHT") +from . userController import UserController +userController = UserController() \ No newline at end of file diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 17f6e3c..1c6e9ec 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -2,9 +2,7 @@ from geruecht.model.accessToken import AccessToken from geruecht.controller import LOGGER from datetime import datetime, timedelta import hashlib -import logging -from logging.handlers import WatchedFileHandler -from geruecht import Singleton +from . import Singleton class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken @@ -26,17 +24,6 @@ class AccesTokenController(metaclass=Singleton): """ LOGGER.info("Initialize AccessTokenController") - LOGGER.debug("Build Logger for VerificationThread") - - FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") - - logFileHandler = WatchedFileHandler("Verification.log") - logFileHandler.setFormatter(FORMATTER) - - self.LOGGER = logging.getLogger("VerificationThread") - self.LOGGER.setLevel(logging.DEBUG) - self.LOGGER.addHandler(logFileHandler) - self.LOGGER.propagate = False self.tokenList = [] def validateAccessToken(self, token, group): diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 2e239f4..522f5ac 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -1,5 +1,5 @@ import pymysql -from geruecht import Singleton +from . import Singleton from geruecht.model.user import User from geruecht.model.creditList import CreditList from datetime import datetime @@ -36,20 +36,28 @@ class DatabaseController(metaclass=Singleton): raise err if data: - return [User(value) for value in data] + retVal = [] + for value in data: + user = User(value) + creditLists = self.getCreditListFromUser(user) + user.initGeruechte(creditLists) + retVal.append(user) + return retVal def getUser(self, username): self.connect() retVal = None cursor = self.db.cursor() try: - cursor.execute("select * from user where cn='{}'".format(username)) + cursor.execute("select * from user where uid='{}'".format(username)) data = cursor.fetchone() self.db.close() except Exception as err: raise err if data: retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) return retVal @@ -66,8 +74,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(data['group']) try: - cursor.execute("insert into user (cn, dn, firstname, lastname, gruppe) VALUES ('{}','{}','{}','{}','{}')".format( - data['cn'], data['dn'], data['givenName'], data['sn'], groups)) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe) VALUES ('{}','{}','{}','{}','{}')".format( + data['uid'], data['dn'], data['givenName'], data['sn'], groups)) self.db.commit() except Exception as err: self.db.rollback() @@ -80,8 +88,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(data['group']) try: - cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}' where cn='{}'".format( - data['dn'], data['givenName'], data['sn'], groups, data['cn'])) + cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}' where uid='{}'".format( + data['dn'], data['givenName'], data['sn'], groups, data['uid'])) self.db.commit() except Exception as err: self.db.rollback() diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index ebb7de8..059db9b 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -1,5 +1,7 @@ import ldap -from geruecht import MONEY, USER, GASTRO, BAR, Singleton +from geruecht.model import MONEY, USER, GASTRO, BAR +from geruecht.exceptions import PermissionDenied +from . import Singleton class LDAPController(metaclass=Singleton): ''' @@ -20,15 +22,16 @@ class LDAPController(metaclass=Singleton): def login(self, username, password): self.connect() try: - self.client.bind_s("cn={},ou=user,{}".format(username, self.dn), password) + cn = self.client.search_s("ou=user,{}".format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username),['cn'])[0][1]['cn'][0].decode('utf-8') + self.client.bind_s("cn={},ou=user,{}".format(cn, self.dn), password) self.client.unbind_s() except: self.client.unbind_s() - raise Exception("Invalid Password or Username") + raise PermissionDenied("Invalid Password or Username") def getUserData(self, username): self.connect() - search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'cn={}'.format(username), ['cn', 'givenName', 'sn']) + search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn']) retVal = search_data[0][1] for k,v in retVal.items(): retVal[k] = v[0].decode('utf-8') @@ -39,7 +42,7 @@ class LDAPController(metaclass=Singleton): def getGroup(self, username): retVal = [] self.connect() - main_group_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'cn={}'.format(username), ['gidNumber']) + main_group_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['gidNumber']) if main_group_data: main_group_number = main_group_data[0][1]['gidNumber'][0].decode('utf-8') group_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'gidNumber={}'.format(main_group_number), ['cn']) @@ -71,10 +74,10 @@ class LDAPController(metaclass=Singleton): def getAllUser(self): self.connect() retVal = [] - data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['cn', 'givenName', 'sn']) + data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['uid', 'givenName', 'sn']) for user in data: - if 'cn' in user[1]: - username = user[1]['cn'][0].decode('utf-8') + if 'uid' in user[1]: + username = user[1]['uid'][0].decode('utf-8') firstname = user[1]['givenName'][0].decode('utf-8') lastname = user[1]['sn'][0].decode('utf-8') retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) @@ -96,21 +99,21 @@ class LDAPController(metaclass=Singleton): if len(name) == 1: if name[0] == "**": name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, - attrlist=['cn', 'givenName', 'sn'])) + attrlist=['uid', 'givenName', 'sn'])) else: - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['cn', 'givenName', 'sn'])) - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['cn', 'givenName', 'sn'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['uid', 'givenName', 'sn'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['uid', 'givenName', 'sn'])) else: name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, - 'givenName={}'.format(name[1]), ['cn', 'givenName', 'sn'])) + 'givenName={}'.format(name[1]), ['uid', 'givenName', 'sn'])) name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[1]), - ['cn', 'givenName', 'sn'])) + ['uid', 'givenName', 'sn'])) retVal = [] for names in name_result: for user in names: - if 'cn' in user[1]: - username = user[1]['cn'][0].decode('utf-8') + if 'uid' in user[1]: + username = user[1]['uid'][0].decode('utf-8') if not self.__isUserInList(retVal, username): firstname = user[1]['givenName'][0].decode('utf-8') lastname = user[1]['sn'][0].decode('utf-8') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py new file mode 100644 index 0000000..6584ef8 --- /dev/null +++ b/geruecht/controller/userController.py @@ -0,0 +1,46 @@ +from . import LOGGER, Singleton, db, ldapController as ldap +from geruecht.exceptions import PermissionDenied + +class UserController(metaclass=Singleton): + + def __init__(self): + pass + + def addAmount(self, username, amount, year, month): + user = self.getUser(username) + user.addAmount(amount, year=year, month=month) + creditLists = user.updateGeruecht() + for creditList in creditLists: + db.updateCreditList(creditList) + return user.getGeruecht(year) + + def addCredit(self, username, credit, year, month): + user = self.getUser(username) + user.addCredit(credit, year=year, month=month) + creditLists = user.updateGeruecht() + for creditList in creditLists: + db.updateCreditList(creditList) + return user.getGeruecht(year) + + def getAllUsersfromDB(self): + return db.getAllUser() + + def getUser(self, username): + user = db.getUser(username) + groups = ldap.getGroup(username) + user_data = ldap.getUserData(username) + user_data['group'] = groups + if user is None: + db.insertUser(user_data) + else: + db.updateUser(user_data) + user = db.getUser(username) + return user + + def loginUser(self, username, password): + try: + user = self.getUser(username) + ldap.login(username, password) + return user + except PermissionDenied as err: + raise err diff --git a/geruecht/exceptions/__init__.py b/geruecht/exceptions/__init__.py new file mode 100644 index 0000000..30bba52 --- /dev/null +++ b/geruecht/exceptions/__init__.py @@ -0,0 +1,2 @@ +class PermissionDenied(Exception): + pass \ No newline at end of file diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index d0fdfa1..9363617 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,7 +1,8 @@ from flask import Blueprint, request, jsonify from geruecht.finanzer import LOGGER from datetime import datetime -from geruecht import MONEY, db, accesTokenController +from geruecht.controller import accesTokenController, userController +from geruecht.model import MONEY finanzer = Blueprint("finanzer", __name__) @@ -22,53 +23,18 @@ def _getFinanzer(): accToken = accesTokenController.validateAccessToken(token, MONEY) if accToken: LOGGER.debug("Get all Useres") - users = db.getAllUser() + users = userController.getAllUsersfromDB() dic = {} for user in users: LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.cn] = user.toJSON() - creditList = db.getCreditListFromUser(user) - dic[user.cn]['creditList'] = {credit.year: credit.toJSON() for credit in creditList} + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} LOGGER.debug("ReturnValue is {}".format(dic)) LOGGER.info("Send main for Finanzer") return jsonify(dic) LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 -@finanzer.route("/getFinanzerYears", methods=['POST']) -def _getFinanzerYear(): - """ Get all geruechte from User - - This function returns all geruechte from user with posted userID - - Returns: - JSON-File with geruechte of special user - or ERROR 401 Permission Denied - """ - LOGGER.info("Get all Geruechte from User.") - token = request.headers.get("Token") - LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, MONEY) - - dic = {} - if accToken: - data = request.get_json() - LOGGER.debug("Get data {}".format(data)) - userID = data['userId'] - LOGGER.debug("UserID is {}".format(userID)) - user = db.getUser(userID) - LOGGER.debug("User is {}".format(user)) - dic[user.cn] = {} - LOGGER.debug("Build ReturnValue") - for geruecht in user.geruechte: - LOGGER.debug("Add Geruecht {} to ReturnValue".format(geruecht)) - dic[user.cn][geruecht.year] = geruecht.toJSON() - LOGGER.debug("ReturnValue is {}".format(dic)) - LOGGER.info("Send Geruechte from User {}".format(user)) - return jsonify(dic) - LOGGER.info("Permission Denied") - return jsonify({"error": "permission denied"}), 401 - @finanzer.route("/finanzerAddAmount", methods=['POST']) def _addAmount(): """ Add Amount to User @@ -103,11 +69,7 @@ def _addAmount(): LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - user = db.getUser(userID) - LOGGER.debug("User is {}".format(user)) - LOGGER.debug("Add amount to User {} in year {} and month {}".format(user, year, month)) - user.addAmount(amount, year=year, month=month) - retVal = user.getGeruecht(year=year).toJSON() + retVal = userController.addAmount(userID, amount, year=year, month=month).toJSON() LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") @@ -151,11 +113,7 @@ def _addCredit(): month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - user = db.getUser(userID) - LOGGER.debug("User is {}".format(user)) - LOGGER.debug("Add credit to User {} in year {} and month {}".format(user, year, month)) - user.addCredit(credit, year=year, month=month) - retVal = user.getGeruecht(year=year).toJSON() + retVal = userController.addCredit(userID, credit, year=year, month=month).toJSON() LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") diff --git a/geruecht/logger.py b/geruecht/logger.py new file mode 100644 index 0000000..5fbe2dd --- /dev/null +++ b/geruecht/logger.py @@ -0,0 +1,21 @@ +import logging +from logging.handlers import WatchedFileHandler +import sys + +FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") + +logFileHandler = WatchedFileHandler("testlog.log") +logFileHandler.setFormatter(FORMATTER) + +logStreamHandler = logging.StreamHandler(stream=sys.stdout) +logStreamHandler.setFormatter(FORMATTER) + +def getLogger(logger_name): + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG) + logger.addHandler(logFileHandler) + logger.addHandler(logStreamHandler) + + logger.propagate = False + + return logger \ No newline at end of file diff --git a/geruecht/model/__init__.py b/geruecht/model/__init__.py index e69de29..a0d2bbb 100644 --- a/geruecht/model/__init__.py +++ b/geruecht/model/__init__.py @@ -0,0 +1,4 @@ +MONEY = "moneymaster" +GASTRO = "gastro" +USER = "user" +BAR = "bar" \ No newline at end of file diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 5c30a9c..5bb8688 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,6 +1,5 @@ from datetime import datetime from geruecht import getLogger -import geruecht LOGGER = getLogger(__name__) def create_empty_data(): @@ -92,8 +91,6 @@ class CreditList(): self.user_id = int(data['user_id']) - self.db = geruecht.getDatabesController() - def getSchulden(self): """ Get Schulden @@ -217,9 +214,6 @@ class CreditList(): elif month == 12: self.dez_schulden += amount retValue = (self.dez_guthaben, self.dez_schulden) - - #db.session.commit() - self.db.updateCreditList(self) LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue @@ -273,8 +267,6 @@ class CreditList(): elif month == 12: self.dez_guthaben += credit retValue = (self.dez_guthaben, self.dez_schulden) - self.db.updateCreditList(self) - #db.session.commit() LOGGER.debug("Credit and Amount is {}".format(retValue)) return retValue diff --git a/geruecht/model/priceList.py b/geruecht/model/priceList.py index abfe406..0f8c6ef 100644 --- a/geruecht/model/priceList.py +++ b/geruecht/model/priceList.py @@ -1,4 +1,4 @@ -from geruecht import db +from geruecht.controller import db class PriceList(db.Model): """ Database Model for PriceList diff --git a/geruecht/model/user.py b/geruecht/model/user.py index a9e61b2..70dde36 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,5 +1,4 @@ -from geruecht import getLogger -import geruecht +from geruecht.logger import getLogger from geruecht.model.creditList import CreditList, create_empty_data from datetime import datetime @@ -22,7 +21,7 @@ class User(): """ def __init__(self, data): self.id = int(data['id']) - self.cn = data['cn'] + self.uid = data['uid'] self.dn = data['dn'] self.firstname = data['firstname'] self.lastname = data['lastname'] @@ -31,17 +30,12 @@ class User(): self.group = data['gruppe'] elif type(data['gruppe']) == str: self.group = data['gruppe'].split(',') + if 'creditLists' in data: + self.geruechte = data['creditLists'] - self.db = geruecht.getDatabesController() - self.ldap = geruecht.getLDAPController() - self.geruechte = [] - geruechte = self.db.getCreditListFromUser(self) - if type(geruechte) == list: - self.geruechte = geruechte - elif type(geruechte) == CreditList: - self.geruechte.append(geruechte) - self.updateGeruecht() - #geruechte = db.relationship('CreditList', backref='user', lazy=True) + def initGeruechte(self, creditLists): + if type(creditLists) == list: + self.geruechte = creditLists def createGeruecht(self, amount=0, year=datetime.now().year): """ Create Geruecht @@ -63,8 +57,6 @@ class User(): data['year_date'] = year credit = CreditList(data) self.geruechte.append(credit) - self.db.updateCreditList(credit) - credit = self.db.getCreditListFromUser(self, year=year) LOGGER.debug("Created Geruecht {}".format(credit)) return credit @@ -89,8 +81,6 @@ class User(): LOGGER.debug("No Geruecht found for User {}. Will create one".format(self)) geruecht = self.createGeruecht(year=year) - self.updateGeruecht() - return self.getGeruecht(year=year) def addAmount(self, amount, year=datetime.now().year, month=datetime.now().month): @@ -111,10 +101,6 @@ class User(): geruecht = self.getGeruecht(year=year) retVal = geruecht.addAmount(amount, month=month) - self.db.updateCreditList(geruecht) - - self.updateGeruecht() - return retVal def addCredit(self, credit, year=datetime.now().year, month=datetime.now().month): @@ -135,10 +121,6 @@ class User(): geruecht = self.getGeruecht(year=year) retVal = geruecht.addCredit(credit, month=month) - self.db.updateCreditList(geruecht) - - self.updateGeruecht() - return retVal def updateGeruecht(self): @@ -154,7 +136,8 @@ class User(): geruecht.last_schulden = 0 if index != 0: geruecht.last_schulden = (self.geruechte[index - 1].getSchulden() * -1) - self.db.updateCreditList(geruecht) + + return self.geruechte def sortYear(self, geruecht): """ Sort Year @@ -177,38 +160,16 @@ class User(): A Dic with static Attributes. """ dic = { - "userId": self.cn, - "cn": self.cn, + "userId": self.uid, + "uid": self.uid, "dn": self.dn, "firstname": self.firstname, "lastname": self.lastname, "group": self.group, - "username": self.cn + "username": self.uid } return dic - def updateUser(self): - data = self.ldap.getUserData(self.cn) - data['group'] = self.ldap.getGroup(self.cn) - self.db.updateUser(data) - - def login(self, password): - """ Login for the User - - Only check the given Password: - - Returns: - A Bool. True if the password is correct and False if it isn't. - """ - LOGGER.debug("Login User {}".format(self)) - try: - self.ldap.login(self.cn, password) - - self.updateUser() - return True - except: - return False - def __repr__(self): - return "User({}, {}, {})".format(self.cn, self.dn, self.group) + return "User({}, {}, {})".format(self.uid, self.dn, self.group) diff --git a/geruecht/routes.py b/geruecht/routes.py index 3c026cc..4343f19 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,8 +1,10 @@ -from geruecht import app, db, accesTokenController, MONEY, BAR, USER, GASTRO, LOGGER -from geruecht import ldapController as ldap -from geruecht.model.user import User +from geruecht import app, LOGGER +from geruecht.exceptions import PermissionDenied +from geruecht.controller import accesTokenController, userController +from geruecht.model import MONEY, BAR, USER, GASTRO from flask import request, jsonify + def login(user, password): return user.login(password) @@ -40,35 +42,15 @@ def _login(): username = data['username'] password = data['password'] LOGGER.info("search {} in database".format(username)) - user = db.getUser(username) - if user is None: - LOGGER.info("User {} not found. Authenticate over LDAP and create User.") - try: - ldap.login(username, password) - LOGGER.info("Authentification successfull. Search Group") - groups = ldap.getGroup(username) - LOGGER.info("Get userdata from LDAP") - user_data = ldap.getUserData(username) - user_data['group'] = groups - LOGGER.info('Insert user {} into database') - db.insertUser(user_data) - - except Exception as err: - return jsonify({"error": str(err)}), 401 - LOGGER.info("{} try to log in".format(username)) - user = db.getUser(username) - LOGGER.debug("User is {}".format(user)) - if user: - LOGGER.debug("Check login for User {}".format(user)) - if login(user, password): - token = accesTokenController.createAccesToken(user) - dic = user.toJSON() - dic["token"] = token - dic["accessToken"] = token - LOGGER.info("User {} success login.".format(username)) - return jsonify(dic) - else: - LOGGER.info("User {} failed login.".format(username)) - return jsonify({"error": "wrong password"}), 401 + try: + user = userController.loginUser(username, password) + token = accesTokenController.createAccesToken(user) + dic = user.toJSON() + dic["token"] = token + dic["accessToken"] = token + LOGGER.info("User {} success login.".format(username)) + return jsonify(dic) + except PermissionDenied as err: + return jsonify({"error": str(err)}), 401 LOGGER.info("User {} does not exist.".format(username)) - return jsonify({"error": "wrong username"}), 402 + return jsonify({"error": "wrong username"}), 401 diff --git a/geruecht/site.db b/geruecht/site.db deleted file mode 100644 index 041487dde91ac01863bb9d3719696c7612d32e86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI3&rjQC9LD|HPMl=Jp2SduScUn~61J%lFjbv)SW`%7HVP$#NmQyLJ2AmDPEyA% zkkddt?6%(5!%mZS>uG<$df~QRdTI|VNL8mwJ0We;G->ba#00j)9Dqyryu|VA_xt#H z-}m#{M(>MRTAVW-LtU-fmgcBa+&dg0+%;9@IPMPJB)a)Ypc&CWpnGy8-z|HGyZqkc z9_0@%9NFXKP30%$ySHD<)ARrd1V8`;KmY_l00hpBz*ph06dxHOUoSbDUN#E0Q8b-= z*{nN9veI-mmCU8oT=M!{N<9)#6S1gSR5SCr)NCrN&M)ND`Q^E}57k(7U903vuCu1; zMy0EuURZO>#V&$XLl1Uff@mzKS=5D1O3~P?-z(GkWL|Tfs&|}kL(EUK^WmKjh!pSZBM(0I27?U~+xTMx z&>Xz358z=I&&&*W#Lr?~$7a>E>rU4wyL`I_hf8+Vt#z)mrq$~kRlC@+K|v(_o_LqE zkJs-3MLZ^iu z=)#JXGjErsCo{DhYb!U?g=ERJj14O@b0vLScc%>dv(nW@X|1?5bKfeM%Z=KiaU~~2 zxn7pGL%L=k{hY>C=V|K)`ul0?bxo)1%JoWh!!nFnn=QX3XPokz@=|%G{CTEB3^Nb_0T2KI5C8!X z009sH0T2KI5CDPmCJ+<)Npnp_l!RVFS3U$*)LgFM!$NPAE-5tU|4%t(UwNv$R(?P4 zM!_EhKmY_l00ck)1V8`;KmY_l00cnbj0p4ygRPlAeTm|5^8c1FKw57p@DU--X8)GK z-v6in^p6ArAOHd&00JNY0w4eaAOHd&00JQJ1`}X^|Hu6Q4IW^04Fo^{1V8`;KmY_l z00ck)1V8`;D1jb%j*H4eME+IzS?NZBFgd;?&9&de-w!jDm{2t+7lDu3taPL zW_p;sEYB0+6*(Z+#K+PL@lwRA*^}@`q^y7{L(ir1U*AgmqO!wX^27eV?dv&GxAzIH zw-s*f+u~Ne2chTWKzhL^1ixmgY^z!=A>3~C5&1Flk+jdNAzyh|V>`nWX Date: Sat, 28 Dec 2019 22:40:33 +0100 Subject: [PATCH 021/111] update finanzer routes if finanzer add credit or amount to a user, he get all creditList of all years of the user --- geruecht/finanzer/routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 9363617..a7cb678 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -69,7 +69,8 @@ def _addAmount(): LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - retVal = userController.addAmount(userID, amount, year=year, month=month).toJSON() + userController.addAmount(userID, amount, year=year, month=month) + retVal = {geruecht.year: geruecht.toJSON() for geruecht in userController.getUser(userID).geruechte} LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") @@ -114,6 +115,7 @@ def _addCredit(): LOGGER.debug("Year is {} and Month is {}".format(year, month)) retVal = userController.addCredit(userID, credit, year=year, month=month).toJSON() + retVal = {geruecht.year: geruecht.toJSON() for geruecht in userController.getUser(userID).geruechte} LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") From 5607ec72f708c79fd4e7a361fcb5cf9160a158fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 28 Dec 2019 23:34:09 +0100 Subject: [PATCH 022/111] update userControler, databaseController, ldapControlle and user for locking user --- geruecht/controller/databaseController.py | 16 +++++------ geruecht/controller/ldapController.py | 2 ++ geruecht/controller/userController.py | 13 +++++++-- geruecht/model/user.py | 33 ++++++++++++++++++++++- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 522f5ac..54f5f15 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -69,13 +69,13 @@ class DatabaseController(metaclass=Singleton): retVal += group return retVal - def insertUser(self, data): + def insertUser(self, user): self.connect() cursor = self.db.cursor() - groups = self._convertGroupToString(data['group']) + groups = self._convertGroupToString(user.group) try: - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe) VALUES ('{}','{}','{}','{}','{}')".format( - data['uid'], data['dn'], data['givenName'], data['sn'], groups)) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, limit, locked, autoLock) VALUES ('{}','{}','{}','{}','{}',{},{},{})".format( + user.uid, user.dn, user.firstname, user.lastname, groups)) self.db.commit() except Exception as err: self.db.rollback() @@ -83,13 +83,13 @@ class DatabaseController(metaclass=Singleton): raise err self.db.close() - def updateUser(self, data): + def updateUser(self, user): self.connect() cursor = self.db.cursor() - groups = self._convertGroupToString(data['group']) + groups = self._convertGroupToString(user.group) try: - cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}' where uid='{}'".format( - data['dn'], data['givenName'], data['sn'], groups, data['uid'])) + cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}, limit={}, locked={}, autoLock={}' where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.uid)) self.db.commit() except Exception as err: self.db.rollback() diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 059db9b..b543e64 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -36,6 +36,8 @@ class LDAPController(metaclass=Singleton): for k,v in retVal.items(): retVal[k] = v[0].decode('utf-8') retVal['dn'] = self.dn + retVal['firstname'] = retVal['givenName'] + retVal['lastname'] = retVal['sn'] return retVal diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 6584ef8..21ee2f5 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,4 +1,5 @@ from . import LOGGER, Singleton, db, ldapController as ldap +from geruecht.model.user import User from geruecht.exceptions import PermissionDenied class UserController(metaclass=Singleton): @@ -6,6 +7,12 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def updateConfig(self, username, data): + user = self.getUser(username) + user.updateData(data) + db.updateUser(user) + return self.getUser(username) + def addAmount(self, username, amount, year, month): user = self.getUser(username) user.addAmount(amount, year=year, month=month) @@ -31,9 +38,11 @@ class UserController(metaclass=Singleton): user_data = ldap.getUserData(username) user_data['group'] = groups if user is None: - db.insertUser(user_data) + user = User(user_data) + db.insertUser(user) else: - db.updateUser(user_data) + user.updateData(user_data) + db.updateUser(user) user = db.getUser(username) return user diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 70dde36..d709939 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -26,6 +26,18 @@ class User(): self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] + if 'limit' in data: + self.limit = data['limit'] + else: + self.limit = 4200 + if 'locked' in data: + self.locked = bool(data['locked']) + else: + self.locked = False + if 'autoLock' in data: + self.autoLock = bool(data['autoLock']) + else: + self.autoLock = True if type(data['gruppe']) == list: self.group = data['gruppe'] elif type(data['gruppe']) == str: @@ -33,6 +45,22 @@ class User(): if 'creditLists' in data: self.geruechte = data['creditLists'] + def updateData(self, data): + if 'dn' in data: + self.dn = data['dn'] + if 'firstname' in data: + self.firstname = data['firstname'] + if 'lastname' in data: + self.lastname = data['lastname'] + if 'gruppe' in data: + self.group = data['gruppe'] + if 'limit' in data: + self.limit = data['limit'] + if 'locked' in data: + self.locked = bool(data['locked']) + if 'autoLock' in data: + self.autoLock = bool(data['autoLock']) + def initGeruechte(self, creditLists): if type(creditLists) == list: self.geruechte = creditLists @@ -166,7 +194,10 @@ class User(): "firstname": self.firstname, "lastname": self.lastname, "group": self.group, - "username": self.uid + "username": self.uid, + "locked": self.locked, + "autoLock": self.autoLock, + "limit": self.limit } return dic From 92c2c95a3447415014dc55dce67a1c4b846d2aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 29 Dec 2019 17:55:21 +0100 Subject: [PATCH 023/111] update to lock user and if bar user add a locked user, he see it --- geruecht/baruser/routes.py | 14 ++++++++++- geruecht/controller/databaseController.py | 10 ++++---- geruecht/controller/userController.py | 29 +++++++++++++++++++---- geruecht/finanzer/routes.py | 29 ++++++++++++++++++++++- geruecht/model/user.py | 11 +++++---- 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 5aba339..155ae6c 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -38,6 +38,7 @@ def _bar(): "firstname": user.firstname, "lastname": user.lastname, "amount": abs(month[0] - month[1]), + "locked": user.locked, "type": type } return jsonify(dic) @@ -68,7 +69,7 @@ def _baradd(): month = user.getGeruecht(year=date.year).getMonth(month=date.month) amount = abs(month[0] - month[1]) - return jsonify({"userId": user.uid, "amount": amount}) + return jsonify({"userId": user.uid, "amount": amount, 'locked': user.locked}) return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") @@ -91,6 +92,17 @@ def _getUsers(): return jsonify(retVal) return jsonify({"error": "permission denied"}), 401 +@baruser.route("/barGetUser", methods=['POST']) +def _getUser(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, BAR) + if accToken: + data = request.get_json() + username = data['userId'] + retVal = userController.getUser(username).toJSON() + return jsonify(retVal) + return jsonify("error", "permission denied"), 401 + @baruser.route("/search", methods=['POST']) def _search(): token = request.headers.get("Token") diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 54f5f15..9d7fb68 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -74,8 +74,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, limit, locked, autoLock) VALUES ('{}','{}','{}','{}','{}',{},{},{})".format( - user.uid, user.dn, user.firstname, user.lastname, groups)) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock) VALUES ('{}','{}','{}','{}','{}',{},{},{})".format( + user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock)) self.db.commit() except Exception as err: self.db.rollback() @@ -88,8 +88,10 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: - cursor.execute("update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}, limit={}, locked={}, autoLock={}' where uid='{}'".format( - user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.uid)) + sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={} where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.uid) + print(sql) + cursor.execute(sql) self.db.commit() except Exception as err: self.db.rollback() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 21ee2f5..fcf3d7e 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,24 +1,41 @@ from . import LOGGER, Singleton, db, ldapController as ldap from geruecht.model.user import User from geruecht.exceptions import PermissionDenied +from datetime import datetime class UserController(metaclass=Singleton): def __init__(self): pass + def lockUser(self, username, locked): + user = self.getUser(username) + user.updateData({'locked': locked}) + db.updateUser(user) + return self.getUser(username) + def updateConfig(self, username, data): user = self.getUser(username) user.updateData(data) db.updateUser(user) return self.getUser(username) - def addAmount(self, username, amount, year, month): + def autoLock(self, user): + if user.autoLock: + if user.getGeruecht(year=datetime.now().year).getSchulden() <= (-1*user.limit): + user.updateData({'locked': True}) + else: + user.updateData({'locked': False}) + db.updateUser(user) + + def addAmount(self, username, amount, year, month, finanzer=False): user = self.getUser(username) - user.addAmount(amount, year=year, month=month) - creditLists = user.updateGeruecht() - for creditList in creditLists: - db.updateCreditList(creditList) + if not user.locked or finanzer: + user.addAmount(amount, year=year, month=month) + creditLists = user.updateGeruecht() + for creditList in creditLists: + db.updateCreditList(creditList) + self.autoLock(user) return user.getGeruecht(year) def addCredit(self, username, credit, year, month): @@ -27,6 +44,7 @@ class UserController(metaclass=Singleton): creditLists = user.updateGeruecht() for creditList in creditLists: db.updateCreditList(creditList) + self.autoLock(user) return user.getGeruecht(year) def getAllUsersfromDB(self): @@ -36,6 +54,7 @@ class UserController(metaclass=Singleton): user = db.getUser(username) groups = ldap.getGroup(username) user_data = ldap.getUserData(username) + user_data['gruppe'] = groups user_data['group'] = groups if user is None: user = User(user_data) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index a7cb678..f81ce0b 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -69,7 +69,7 @@ def _addAmount(): LOGGER.error("KeyError in month. Month is set to default.") month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - userController.addAmount(userID, amount, year=year, month=month) + userController.addAmount(userID, amount, year=year, month=month, finanzer=True) retVal = {geruecht.year: geruecht.toJSON() for geruecht in userController.getUser(userID).geruechte} LOGGER.info("Send updated Geruecht") return jsonify(retVal) @@ -120,3 +120,30 @@ def _addCredit(): return jsonify(retVal) LOGGER.info("Permission Denied") return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerLock", methods=['POST']) +def _finanzerLock(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, MONEY) + + if accToken: + data = request.get_json() + username = data['userId'] + locked = bool(data['locked']) + retVal = userController.lockUser(username, locked).toJSON() + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerSetConfig", methods=['POST']) +def _finanzerSetConfig(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, MONEY) + + if accToken: + data = request.get_json() + username = data['userId'] + autoLock = bool(data['autoLock']) + limit = int(data['limit']) + retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 \ No newline at end of file diff --git a/geruecht/model/user.py b/geruecht/model/user.py index d709939..85c6b7a 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -20,14 +20,15 @@ class User(): password: salted hashed password for the User. """ def __init__(self, data): - self.id = int(data['id']) + if 'id' in data: + self.id = int(data['id']) self.uid = data['uid'] self.dn = data['dn'] self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] - if 'limit' in data: - self.limit = data['limit'] + if 'lockLimit' in data: + self.limit = int(data['lockLimit']) else: self.limit = 4200 if 'locked' in data: @@ -54,8 +55,8 @@ class User(): self.lastname = data['lastname'] if 'gruppe' in data: self.group = data['gruppe'] - if 'limit' in data: - self.limit = data['limit'] + if 'lockLimit' in data: + self.limit = int(data['lockLimit']) if 'locked' in data: self.locked = bool(data['locked']) if 'autoLock' in data: From 16e50ea75162203055780a4620f1969d7759c80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 29 Dec 2019 21:36:42 +0100 Subject: [PATCH 024/111] finanzer can add user from LDAP to DB bug fixes --- geruecht/baruser/routes.py | 5 +++-- geruecht/finanzer/routes.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 155ae6c..5b8da89 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,7 +1,7 @@ from flask import Blueprint, request, jsonify from geruecht.controller import ldapController as ldap, accesTokenController, userController from datetime import datetime -from geruecht.model import BAR +from geruecht.model import BAR, MONEY baruser = Blueprint("baruser", __name__) @@ -108,8 +108,9 @@ def _search(): token = request.headers.get("Token") print(token) accToken = accesTokenController.validateAccessToken(token, BAR) + accToken2 = accesTokenController.validateAccessToken(token, MONEY) - if accToken: + if accToken or accToken2: data = request.get_json() searchString = data['searchString'] diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index f81ce0b..d72e2fc 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -70,7 +70,9 @@ def _addAmount(): month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) userController.addAmount(userID, amount, year=year, month=month, finanzer=True) - retVal = {geruecht.year: geruecht.toJSON() for geruecht in userController.getUser(userID).geruechte} + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") @@ -114,8 +116,10 @@ def _addCredit(): month = datetime.now().month LOGGER.debug("Year is {} and Month is {}".format(year, month)) - retVal = userController.addCredit(userID, credit, year=year, month=month).toJSON() - retVal = {geruecht.year: geruecht.toJSON() for geruecht in userController.getUser(userID).geruechte} + userController.addCredit(userID, credit, year=year, month=month).toJSON() + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked LOGGER.info("Send updated Geruecht") return jsonify(retVal) LOGGER.info("Permission Denied") @@ -146,4 +150,24 @@ def _finanzerSetConfig(): limit = int(data['limit']) retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 \ No newline at end of file + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerAddUser", methods=['POST']) +def _finanzerAddUser(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, MONEY) + + if accToken: + data = request.get_json() + username = data['userId'] + userController.getUser(username) + LOGGER.debug("Get all Useres") + users = userController.getAllUsersfromDB() + dic = {} + for user in users: + LOGGER.debug("Add User {} to ReturnValue".format(user)) + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} + LOGGER.debug("ReturnValue is {}".format(dic)) + return jsonify(dic), 200 + return jsonify("error:" "permission denied"), 401 \ No newline at end of file From 3a5d7d7d0f76c00fd5e1ae403de06e00f3662563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 29 Dec 2019 21:57:59 +0100 Subject: [PATCH 025/111] fixed that baruser get endsum not only from month --- geruecht/baruser/routes.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 5b8da89..2ecfaff 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -29,15 +29,16 @@ def _bar(): if geruecht is not None: month = geruecht.getMonth(datetime.now().month) amount = month[0] - month[1] + all = geruecht.getSchulden() if amount != 0: - if amount >= 0: + if all >= 0: type = 'credit' else: type = 'amount' dic[user.uid] = {"username": user.uid, "firstname": user.firstname, "lastname": user.lastname, - "amount": abs(month[0] - month[1]), + "amount": abs(all), "locked": user.locked, "type": type } @@ -66,10 +67,16 @@ def _baradd(): date = datetime.now() userController.addAmount(userID, amount, year=date.year, month=date.month) user = userController.getUser(userID) - month = user.getGeruecht(year=date.year).getMonth(month=date.month) + geruecht = user.getGeruecht(year=date.year) + month = geruecht.getMonth(month=date.month) amount = abs(month[0] - month[1]) + all = geruecht.getSchulden() + if all >= 0: + type = 'credit' + else: + type = 'amount' - return jsonify({"userId": user.uid, "amount": amount, 'locked': user.locked}) + return jsonify({"userId": user.uid, "amount": abs(all), 'locked': user.locked, 'type': type}) return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") From 7a95fb9d32c9d313ab0d1692e41da164c1a002d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 30 Dec 2019 09:22:43 +0100 Subject: [PATCH 026/111] added a Configparser --- geruecht/config.yml | 9 ++++++++ geruecht/configparser.py | 24 +++++++++++++++++++++ geruecht/controller/__init__.py | 20 ++++++++++++----- geruecht/controller/accesTokenController.py | 4 ++-- 4 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 geruecht/config.yml create mode 100644 geruecht/configparser.py diff --git a/geruecht/config.yml b/geruecht/config.yml new file mode 100644 index 0000000..2b3e45b --- /dev/null +++ b/geruecht/config.yml @@ -0,0 +1,9 @@ +AccessTokenLifeTime: 1800 +Database: + URL: 192.168.5.108 + user: wu5 + passwd: E1n$tein + database: geruecht +LDAP: + URL: ldap://192.168.5.108 + dn: dc=ldap,dc=example,dc=local \ No newline at end of file diff --git a/geruecht/configparser.py b/geruecht/configparser.py new file mode 100644 index 0000000..ee945e0 --- /dev/null +++ b/geruecht/configparser.py @@ -0,0 +1,24 @@ +import yaml + +class ConifgParser(): + def __init__(self, file='config.yml'): + self.file = file + with open(file, 'r') as f: + self.config = yaml.load(f) + + print(self.config) + self.db = self.config['Database'] + self.ldap = self.config['LDAP'] + self.accessTokenLifeTime = self.config['AccessTokenLifeTime'] + + def getLDAP(self): + return self.ldap + + def getDatabase(self): + return self.db + + def getAccessToken(self): + return self.accessTokenLifeTime + +if __name__ == '__main__': + ConifgParser() \ No newline at end of file diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index fa32eb5..efcc892 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -1,4 +1,10 @@ from geruecht.logger import getLogger +from geruecht.configparser import ConifgParser +import os + +print(os.getcwd()) + +config = ConifgParser('geruecht/config.yml') LOGGER = getLogger(__name__) @@ -14,17 +20,21 @@ def getDatabesController(): if db is not None: return db else: - return DatabaseController() + return DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) from .ldapController import LDAPController def getLDAPController(): if ldapController is not None: return ldapController else: - return LDAPController() + return LDAPController(ldapConfig['URL'], ldapConfig['dn']) from .accesTokenController import AccesTokenController -db = DatabaseController() -ldapController = LDAPController() -accesTokenController = AccesTokenController("GERUECHT") +dbConfig = config.getDatabase() +ldapConfig = config.getLDAP() +accConfig = config.getAccessToken() + +db = DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) +ldapController = LDAPController(ldapConfig['URL'], ldapConfig['dn']) +accesTokenController = AccesTokenController(accConfig) from . userController import UserController userController = UserController() \ No newline at end of file diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 1c6e9ec..281ad37 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -15,14 +15,14 @@ class AccesTokenController(metaclass=Singleton): """ instance = None tokenList = None - lifetime = 1800 - def __init__(self, arg): + def __init__(self, lifetime=1800): """ Initialize AccessTokenController Initialize Thread and set tokenList empty. """ LOGGER.info("Initialize AccessTokenController") + self.lifetime = lifetime self.tokenList = [] From 9f40d2c93b44f63cf169c212be3ddadc5012fabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 31 Dec 2019 13:06:40 +0100 Subject: [PATCH 027/111] fixed #107 getUserData "from ldap" is now surrounded with try --- geruecht/controller/ldapController.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index b543e64..67ba4c0 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -30,15 +30,18 @@ class LDAPController(metaclass=Singleton): raise PermissionDenied("Invalid Password or Username") def getUserData(self, username): - self.connect() - search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn']) - retVal = search_data[0][1] - for k,v in retVal.items(): - retVal[k] = v[0].decode('utf-8') - retVal['dn'] = self.dn - retVal['firstname'] = retVal['givenName'] - retVal['lastname'] = retVal['sn'] - return retVal + try: + self.connect() + search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn']) + retVal = search_data[0][1] + for k,v in retVal.items(): + retVal[k] = v[0].decode('utf-8') + retVal['dn'] = self.dn + retVal['firstname'] = retVal['givenName'] + retVal['lastname'] = retVal['sn'] + return retVal + except: + raise PermissionDenied("No User exists with this uid.") def getGroup(self, username): From d0a9345d68044ffb3c7ef9864d0f01d2f37f747c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 5 Jan 2020 14:15:02 +0100 Subject: [PATCH 028/111] added EmailController with the emailController you can send automaticly emails, if a user is over the limit or if the finanzer send the emails the configparser is updated. So you have to set database and ldapconfig. if accestokenlifetime and mailconfig is not set, the parser set default values. --- geruecht/baruser/routes.py | 13 ++++- geruecht/config.yml | 4 +- geruecht/configparser.py | 66 +++++++++++++++++++++-- geruecht/controller/__init__.py | 3 ++ geruecht/controller/databaseController.py | 8 +-- geruecht/controller/emailController.py | 49 +++++++++++++++++ geruecht/controller/ldapController.py | 10 ++-- geruecht/controller/userController.py | 28 +++++++++- geruecht/finanzer/routes.py | 24 ++++++++- geruecht/model/user.py | 6 +++ 10 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 geruecht/controller/emailController.py diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 2ecfaff..f809622 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -30,7 +30,7 @@ def _bar(): month = geruecht.getMonth(datetime.now().month) amount = month[0] - month[1] all = geruecht.getSchulden() - if amount != 0: + if all != 0: if all >= 0: type = 'credit' else: @@ -106,7 +106,16 @@ def _getUser(): if accToken: data = request.get_json() username = data['userId'] - retVal = userController.getUser(username).toJSON() + user = userController.getUser(username) + amount = user.getGeruecht(datetime.now().year).getSchulden() + if amount >= 0: + type = 'credit' + else: + type = 'amount' + + retVal = user.toJSON() + retVal['amount'] = amount + retVal['type'] = type return jsonify(retVal) return jsonify("error", "permission denied"), 401 diff --git a/geruecht/config.yml b/geruecht/config.yml index 2b3e45b..3df779a 100644 --- a/geruecht/config.yml +++ b/geruecht/config.yml @@ -1,9 +1,9 @@ AccessTokenLifeTime: 1800 Database: - URL: 192.168.5.108 + URL: 192.168.5.128 user: wu5 passwd: E1n$tein database: geruecht LDAP: - URL: ldap://192.168.5.108 + URL: ldap://192.168.5.128 dn: dc=ldap,dc=example,dc=local \ No newline at end of file diff --git a/geruecht/configparser.py b/geruecht/configparser.py index ee945e0..247f23c 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -1,15 +1,68 @@ import yaml +import sys +from . import LOGGER + +default = { + 'AccessTokenLifeTime': 1800, + 'Mail': { + 'URL': '', + 'port': 0, + 'user': '', + 'passwd': '', + 'email': '' + } +} class ConifgParser(): def __init__(self, file='config.yml'): self.file = file with open(file, 'r') as f: - self.config = yaml.load(f) + self.config = yaml.safe_load(f) + + if 'Database' not in self.config: + self.__error__('Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') + if 'URL' not in self.config['Database'] or 'user' not in self.config['Database'] or 'passwd' not in self.config['Database'] or 'database' not in self.config['Database']: + self.__error__('Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') - print(self.config) self.db = self.config['Database'] + LOGGER.debug("Set Databaseconfig: {}".format(self.db)) + + if 'LDAP' not in self.config: + self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') + if 'URL' not in self.config['LDAP'] or 'dn' not in self.config['LDAP']: + self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') self.ldap = self.config['LDAP'] - self.accessTokenLifeTime = self.config['AccessTokenLifeTime'] + LOGGER.info("Set LDAPconfig: {}".format(self.ldap)) + if 'AccessTokenLifeTime' in self.config: + self.accessTokenLifeTime = self.config['AccessTokenLifeTime'] + LOGGER.info("Set AccessTokenLifeTime: {}".format(self.accessTokenLifeTime)) + else: + self.accessTokenLifeTime = default['AccessTokenLifeTime'] + LOGGER.info("No Config for AccessTokenLifetime found. Set it to default: {}".format(self.accessTokenLifeTime)) + + if 'Mail' not in self.config: + self.config['Mail'] = default['Mail'] + LOGGER.info('No Conifg for Mail found. Set it to defaul: {}'.format(self.config['Mail'])) + if 'URL' not in self.config['Mail']: + self.config['Mail']['URL'] = default['Mail']['URL'] + LOGGER.info("No Config for URL in Mail found. Set it to default") + if 'port' not in self.config['Mail']: + self.config['Mail']['port'] = default['Mail']['port'] + LOGGER.info("No Config for port in Mail found. Set it to default") + else: + self.config['Mail']['port'] = int(self.config['Mail']['port']) + if 'user' not in self.config['Mail']: + self.config['Mail']['user'] = default['Mail']['user'] + LOGGER.info("No Config for user in Mail found. Set it to default") + if 'passwd' not in self.config['Mail']: + self.config['Mail']['passwd'] = default['Mail']['passwd'] + LOGGER.info("No Config for passwd in Mail found. Set it to default") + if 'email' not in self.config['Mail']: + self.config['Mail']['email'] = default['Mail']['email'] + LOGGER.info("No Config for email in Mail found. Set it to default") + self.mail = self.config['Mail'] + LOGGER.info('Set Mailconfig: {}'.format(self.mail)) + def getLDAP(self): return self.ldap @@ -20,5 +73,12 @@ class ConifgParser(): def getAccessToken(self): return self.accessTokenLifeTime + def getMail(self): + return self.mail + + def __error__(self, msg): + LOGGER.error(msg) + sys.exit(-1) + if __name__ == '__main__': ConifgParser() \ No newline at end of file diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index efcc892..ed03c81 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -32,9 +32,12 @@ from .accesTokenController import AccesTokenController dbConfig = config.getDatabase() ldapConfig = config.getLDAP() accConfig = config.getAccessToken() +mailConfig = config.getMail() db = DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) ldapController = LDAPController(ldapConfig['URL'], ldapConfig['dn']) accesTokenController = AccesTokenController(accConfig) +from . emailController import EmailController +emailController = EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) from . userController import UserController userController = UserController() \ No newline at end of file diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 9d7fb68..f58cd6c 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -74,8 +74,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock) VALUES ('{}','{}','{}','{}','{}',{},{},{})".format( - user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock)) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( + user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) self.db.commit() except Exception as err: self.db.rollback() @@ -88,8 +88,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: - sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={} where uid='{}'".format( - user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.uid) + sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) print(sql) cursor.execute(sql) self.db.commit() diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py new file mode 100644 index 0000000..23882d8 --- /dev/null +++ b/geruecht/controller/emailController.py @@ -0,0 +1,49 @@ +import smtplib +from datetime import datetime +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.header import Header +from . import LOGGER + +class EmailController(): + + def __init__(self, smtpServer, user, passwd, port = 587, email = ""): + self.smtpServer = smtpServer + self.port = port + self.user = user + self.passwd = passwd + if email: + self.email = email + else: + self.email = user + + def __connect__(self): + self.smtp = smtplib.SMTP(self.smtpServer, self.port) + self.smtp.starttls() + self.smtp.login(self.user, self.passwd) + + def sendMail(self, user): + try: + if user.mail == 'None' or not user.mail: + LOGGER.debug("cant send email to {}. Has no email-address. {}".format(user.uid, {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) + raise Exception("no valid Email") + msg = MIMEMultipart() + msg['From'] = self.email + msg['To'] = user.mail + msg['Subject'] = Header('Gerücht, bezahle deine ￿Schulden!', 'utf-8') + sum = user.getGeruecht(datetime.now().year).getSchulden() + if sum < 0: + type = '￿Schulden' + add = 'Bezahle diese umgehend an den Finanzer.' + else: + type = 'Guthaben' + add = '' + text = MIMEText("Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format(user.firstname, user.lastname, type, abs(sum)/100, add), 'plain', 'utf-8') + msg.attach(text) + LOGGER.debug("Send email to {}: '{}'".format(user.uid, msg.as_string())) + self.__connect__() + self.smtp.sendmail(self.email, user.mail, msg.as_string()) + LOGGER.debug("Sended email to {}. {}".format(user.uid, {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) + return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} + except Exception: + return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} \ No newline at end of file diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 67ba4c0..7002f44 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -32,7 +32,7 @@ class LDAPController(metaclass=Singleton): def getUserData(self, username): try: self.connect() - search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn']) + search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn', 'mail']) retVal = search_data[0][1] for k,v in retVal.items(): retVal[k] = v[0].decode('utf-8') @@ -79,7 +79,7 @@ class LDAPController(metaclass=Singleton): def getAllUser(self): self.connect() retVal = [] - data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['uid', 'givenName', 'sn']) + data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['uid', 'givenName', 'sn', 'mail']) for user in data: if 'uid' in user[1]: username = user[1]['uid'][0].decode('utf-8') @@ -106,13 +106,13 @@ class LDAPController(metaclass=Singleton): name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['uid', 'givenName', 'sn'])) else: - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['uid', 'givenName', 'sn'])) - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['uid', 'givenName', 'sn'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['uid', 'givenName', 'sn', 'mail'])) + name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['uid', 'givenName', 'sn'], 'mail')) else: name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[1]), ['uid', 'givenName', 'sn'])) name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[1]), - ['uid', 'givenName', 'sn'])) + ['uid', 'givenName', 'sn', 'mail'])) retVal = [] for names in name_result: diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index fcf3d7e..f027816 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,4 +1,4 @@ -from . import LOGGER, Singleton, db, ldapController as ldap +from . import LOGGER, Singleton, db, ldapController as ldap, emailController from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime @@ -24,6 +24,7 @@ class UserController(metaclass=Singleton): if user.autoLock: if user.getGeruecht(year=datetime.now().year).getSchulden() <= (-1*user.limit): user.updateData({'locked': True}) + emailController.sendMail(user) else: user.updateData({'locked': False}) db.updateUser(user) @@ -48,6 +49,9 @@ class UserController(metaclass=Singleton): return user.getGeruecht(year) def getAllUsersfromDB(self): + users = db.getAllUser() + for user in users: + self.__updateGeruechte(user) return db.getAllUser() def getUser(self, username): @@ -63,8 +67,30 @@ class UserController(metaclass=Singleton): user.updateData(user_data) db.updateUser(user) user = db.getUser(username) + self.__updateGeruechte(user) return user + def __updateGeruechte(self, user): + user.getGeruecht(datetime.now().year) + creditLists = user.updateGeruecht() + if user.getGeruecht(datetime.now().year).getSchulden() != 0: + for creditList in creditLists: + db.updateCreditList(creditList) + + def sendMail(self, username): + if type(username) == User: + user = username + if type(username) == str: + user = db.getUser(username) + return emailController.sendMail(user) + + def sendAllMail(self): + retVal = [] + users = db.getAllUser() + for user in users: + retVal.append(self.sendMail(user)) + return retVal + def loginUser(self, username, password): try: user = self.getUser(username) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index d72e2fc..4f1894d 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -170,4 +170,26 @@ def _finanzerAddUser(): dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} LOGGER.debug("ReturnValue is {}".format(dic)) return jsonify(dic), 200 - return jsonify("error:" "permission denied"), 401 \ No newline at end of file + return jsonify({"error": "permission denied"}), 401 + +@finanzer.route("/finanzerSendOneMail", methods=['POST']) +def _finanzerSendOneMail(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, MONEY) + + if accToken: + data = request.get_json() + username = data['userId'] + retVal = userController.sendMail(username) + return jsonify(retVal) + return jsonify({"error:", "permission denied"}), 401 + +@finanzer.route("/finanzerSendAllMail", methods=['GET']) +def _finanzerSendAllMail(): + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, MONEY) + + if accToken: + retVal = userController.sendAllMail() + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 \ No newline at end of file diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 85c6b7a..925ca37 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -27,6 +27,10 @@ class User(): self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] + if 'mail' in data: + self.mail = data['mail'] + else: + self.mail = '' if 'lockLimit' in data: self.limit = int(data['lockLimit']) else: @@ -61,6 +65,8 @@ class User(): self.locked = bool(data['locked']) if 'autoLock' in data: self.autoLock = bool(data['autoLock']) + if 'mail' in data: + self.mail = data['mail'] def initGeruechte(self, creditLists): if type(creditLists) == list: From e42a97eca46647a63c3991dbc9774ec4a47adc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 14 Jan 2020 20:00:37 +0100 Subject: [PATCH 029/111] change return of addAmount in baruser for new frontend --- geruecht/baruser/routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index f809622..591281c 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -75,8 +75,10 @@ def _baradd(): type = 'credit' else: type = 'amount' + dic = user.toJSON() + dic['amount'] = abs(all) - return jsonify({"userId": user.uid, "amount": abs(all), 'locked': user.locked, 'type': type}) + return jsonify(dic) return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") From 214f389fe21e7acb97be55c403549a9e1976d383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 17 Jan 2020 01:05:58 +0100 Subject: [PATCH 030/111] added normal user --- geruecht/__init__.py | 2 ++ geruecht/baruser/routes.py | 1 + geruecht/routes.py | 3 +++ geruecht/user/__init__.py | 0 geruecht/user/routes.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 geruecht/user/__init__.py create mode 100644 geruecht/user/routes.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 12caada..943228c 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -20,7 +20,9 @@ CORS(app) from geruecht import routes from geruecht.baruser.routes import baruser from geruecht.finanzer.routes import finanzer +from geruecht.user.routes import user LOGGER.info("Registrate bluebrints") app.register_blueprint(baruser) app.register_blueprint(finanzer) +app.register_blueprint(user) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 591281c..bc704a7 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -77,6 +77,7 @@ def _baradd(): type = 'amount' dic = user.toJSON() dic['amount'] = abs(all) + dic['type'] = type return jsonify(dic) return jsonify({"error", "permission denied"}), 401 diff --git a/geruecht/routes.py b/geruecht/routes.py index 4343f19..6db7239 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -8,6 +8,7 @@ from flask import request, jsonify def login(user, password): return user.login(password) + @app.route("/valid") def _valid(): token = request.headers.get("Token") @@ -25,6 +26,7 @@ def _valid(): return jsonify(accToken.user.toJSON()) return jsonify({"error": "permission denied"}), 401 + @app.route("/login", methods=['POST']) def _login(): """ Login User @@ -44,6 +46,7 @@ def _login(): LOGGER.info("search {} in database".format(username)) try: user = userController.loginUser(username, password) + user.password = password token = accesTokenController.createAccesToken(user) dic = user.toJSON() dic["token"] = token diff --git a/geruecht/user/__init__.py b/geruecht/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py new file mode 100644 index 0000000..af4341b --- /dev/null +++ b/geruecht/user/routes.py @@ -0,0 +1,34 @@ +from flask import Blueprint, request, jsonify +from geruecht.controller import userController, accesTokenController +from geruecht.model import USER +from datetime import datetime + +user = Blueprint("user", __name__) + +@user.route("/user/main") +def _main(): + + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, USER) + if accToken: + accToken.user = userController.getUser(accToken.user.uid) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 + +@user.route("/user/addAmount", methods=['POST']) +def _addAmount(): + + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, USER) + if accToken: + data = request.get_json() + amount = int(data['amount']) + date = datetime.now() + userController.addAmount(accToken.user.uid, amount, year=date.year, month=date.month) + accToken.user = userController.getUser(accToken.user.uid) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + return jsonify(retVal) + return jsonify({"error": "permission denied"}), 401 \ No newline at end of file From 77949107543b5c2e562397c7a4656a1f373f5248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 17 Jan 2020 22:12:08 +0100 Subject: [PATCH 031/111] add certs for ssl in development mode --- server.crt | 21 +++++++++++++++++++++ server.key | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 server.crt create mode 100644 server.key diff --git a/server.crt b/server.crt new file mode 100644 index 0000000..e804409 --- /dev/null +++ b/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIJAJGH2ozWvd1RMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV +BAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAOBgNVBAcMB0RyZXNkZW4xITAfBgNV +BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAxMTcwOTA0MDFaFw0z +MDAxMDQwOTA0MDFaMEQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAO +BgNVBAcMB0RyZXNkZW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALlkr1UOQypLKicESRnse52d5mAX9MjZQpH0/Y5u +V5WxpPSasmOpt4MRj5MWTfTK2ukj/jLtPAMsggUh7wMXb1uytHj7T5mtiahXBM0H +1sUi2nScXR6doQZlmqKWDGrVS7WHULM01WhirsnxI8S8e6Evpk4F5/RafKA8FgYI +Ongg6S1B16+7T0e/FnILoMjKr1jpgzXnVkPFIneu/qVevSNco5/aw+bc6sjeS/ZA +65dXFGpDlw0lPRHLT5/CgNyMyiLYov7KwMycZw7uxa1ynO+73tqe5tvO/DiMpAPJ +EkrSz/StYBsGJxDhwq5RT31tHVtHhTf0rk1BmaoQJ0Aq7iECAwEAAaNRME8wHwYD +VR0jBBgwFoAUt8P5gBfN9hCUAiWhtPH5fTWnctAwCQYDVR0TBAIwADALBgNVHQ8E +BAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCD +fBByVq8AbV1DMrY+MElb/nZA5/cuGnUpBpjSlk5OnYHWtywuQk6veiiJ0S2fNfqf +RzwOFuZDHKmIcH0574VssLfUynMKP3w3xb2ZNic3AxAdhzZ6LXLx6+qF5tYcL7oC +UWmj5Mo9SkX5HZLEGamQlVyGOGKNatxep4liyoSeKXr0AOHYfB4AkDhVZn7yQc/v +But42fLBg4mE+rk4UBYOHA4XdoFwqgTCNZq2RxKzvG9LIcok6lOc6gDnfTsH8GqE +byGpfIIQAXF8aftCm4dGXxtzMh8C5d0t2Ell9g+Rr8i/enebT2nJ9B9ptldDjhcZ +7I0ywGsXwrh0EwFsX74/ +-----END CERTIFICATE----- diff --git a/server.key b/server.key new file mode 100644 index 0000000..153fd6b --- /dev/null +++ b/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZK9VDkMqSyon +BEkZ7HudneZgF/TI2UKR9P2ObleVsaT0mrJjqbeDEY+TFk30ytrpI/4y7TwDLIIF +Ie8DF29bsrR4+0+ZrYmoVwTNB9bFItp0nF0enaEGZZqilgxq1Uu1h1CzNNVoYq7J +8SPEvHuhL6ZOBef0WnygPBYGCDp4IOktQdevu09HvxZyC6DIyq9Y6YM151ZDxSJ3 +rv6lXr0jXKOf2sPm3OrI3kv2QOuXVxRqQ5cNJT0Ry0+fwoDcjMoi2KL+ysDMnGcO +7sWtcpzvu97anubbzvw4jKQDyRJK0s/0rWAbBicQ4cKuUU99bR1bR4U39K5NQZmq +ECdAKu4hAgMBAAECggEABoMQ3Y34sf2d52zxHGYAGZM4SlvND1kCS5otZdleXjW1 +M5pTdci6V3JAdswrxNNzSQkonqVSnFHt5zw/5v3lvXTTfgRl0WIVGcKkuobx9k65 +Gat8YdzrkQv0mI1otj/zvtaX8ROEA3yj4xgDR5/PP+QqlUcD1MNw6TfzFhcn5pxB +/RDPmvarMhzMdDW60Uub6Z7e/kVPuXWrW4bDyULd1d1NoSibnFZi+vGY0Lc1ctDW +2Vl7A8RFTcQi6Cjx/FwgPGJTBE4UMjIBO3wnoPQBMrsSxeGhcarerqIlEafgT4XN +p9BMtRyaXE7TTb1BXc35ZYNJLDLJKQxABhrEHtFreQKBgQDpiGwuKAFK8BLPlbAx +zkShhKd9fhlwm2bfRv3cojPQZsxn0BjefmtrISbKCD79Ivyn7TnOyYAoKAxdp2q9 +wtz94aAXV2lfhUw2lhcb/aw4sXuY/s1XnVyoglOO8pYRCUN0o80pKuWFsaDyy/uL +LhINff1oMNCa7vmMdu8Ccz0o/wKBgQDLOqdTQhSFs4f1yhlDDH3pqT6eKvtFNeRJ +usxYDnAyRXHRqwhQ86z1nBZIgwXqq7PfO9V5Y/l6/2HmmA2ufjS8aBTNpCUMuvJk +y98Z4hTjKRdnVlMUjHq9ahCixJVQ8pcCnWRFdeAwSKhHQiJEFLYeYOIrUeCIYJI4 +FiCshSPI3wKBgGU0ErWZ7p18FprRIs8itYlNhIwUxo+POPCPwloIDO5GblSa0Pwy +yvhdIIMzOaDXtahMXN3pYtmEKX+4msBrnvuC+K7E2cxkZtfNCWy+7RCQkaCG45QR +hOMdv3pWVIRDgHEevz0U8uySQs6VaYgySe6A5/1sEiriX1DpBcEJEbsfAoGAKUCb +rGvSbJ1XsM24OQL1IBQJsON6o77fuxOe3RT5M0sjYnL8OipsZmKrp0ZpUgxOc7ba +i0x+3LewMLWWuV/G5qOd7WwvVRkxkMJNZByfLskthf1g2d/2HjLEc7XBtW+4tYAr +VWoq+sIU3noPKJCnsxzpa++vyx8HLzlWoo5YCDMCgYBJvGH2zMgInlQNO/2XY5nl +E53EZMex+RDq8Wzr4tRM3IrCGc2t8WKEQ/9teKNH0tg9xib0vhqqmiGl1xNfqJVo +ePJyfgFabeUx9goG3mgTdV9woSRlBJso62dM0DAC/jsJoHnVzgokysR4/BfW9Da+ +AYTxRZSNbfmsTHawXqG8Fw== +-----END PRIVATE KEY----- From 754f373cb06b9db87547af4c1ca2a5fd4d046e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 18 Jan 2020 23:31:49 +0100 Subject: [PATCH 032/111] add addWorker and deletWorker --- geruecht/__init__.py | 2 + geruecht/baruser/routes.py | 13 ++--- geruecht/controller/__init__.py | 10 +++- geruecht/controller/accesTokenController.py | 23 ++++++-- geruecht/controller/databaseController.py | 64 ++++++++++++++++++++- geruecht/controller/userController.py | 27 ++++++++- geruecht/finanzer/routes.py | 16 +++--- geruecht/routes.py | 8 +-- geruecht/user/routes.py | 4 +- geruecht/vorstand/__init__.py | 0 geruecht/vorstand/routes.py | 24 ++++++++ 11 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 geruecht/vorstand/__init__.py create mode 100644 geruecht/vorstand/routes.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 943228c..012a56e 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -21,8 +21,10 @@ from geruecht import routes from geruecht.baruser.routes import baruser from geruecht.finanzer.routes import finanzer from geruecht.user.routes import user +from geruecht.vorstand.routes import vorstand LOGGER.info("Registrate bluebrints") app.register_blueprint(baruser) app.register_blueprint(finanzer) app.register_blueprint(user) +app.register_blueprint(vorstand) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index bc704a7..88290b8 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -18,7 +18,7 @@ def _bar(): print(request.headers) token = request.headers.get("Token") print(token) - accToken = accesTokenController.validateAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, [BAR]) dic = {} if accToken: @@ -57,7 +57,7 @@ def _baradd(): """ token = request.headers.get("Token") print(token) - accToken = accesTokenController.validateAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, [BAR]) if accToken: data = request.get_json() @@ -94,7 +94,7 @@ def _getUsers(): """ token = request.headers.get("Token") print(token) - accToken = accesTokenController.validateAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, [BAR]) retVal = {} if accToken: @@ -105,7 +105,7 @@ def _getUsers(): @baruser.route("/barGetUser", methods=['POST']) def _getUser(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, [BAR]) if accToken: data = request.get_json() username = data['userId'] @@ -126,10 +126,9 @@ def _getUser(): def _search(): token = request.headers.get("Token") print(token) - accToken = accesTokenController.validateAccessToken(token, BAR) - accToken2 = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [BAR, MONEY]) - if accToken or accToken2: + if accToken: data = request.get_json() searchString = data['searchString'] diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index ed03c81..7b0d1c8 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -36,8 +36,14 @@ mailConfig = config.getMail() db = DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) ldapController = LDAPController(ldapConfig['URL'], ldapConfig['dn']) -accesTokenController = AccesTokenController(accConfig) + from . emailController import EmailController emailController = EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) from . userController import UserController -userController = UserController() \ No newline at end of file +def getUserController(): + if userController is not None: + return userController + else: + return UserController() +userController = UserController() +accesTokenController = AccesTokenController(accConfig) \ No newline at end of file diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 281ad37..7ec0c08 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,9 +1,12 @@ from geruecht.model.accessToken import AccessToken +#import geruecht.controller.userController as userController +from geruecht.model import BAR from geruecht.controller import LOGGER from datetime import datetime, timedelta import hashlib from . import Singleton + class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken @@ -26,6 +29,12 @@ class AccesTokenController(metaclass=Singleton): self.tokenList = [] + #def checkBar(self, user): + # if (userController.checkBarUser(user)): + # user.group.append(BAR) + # elif BAR in user.group: + # user.group.remove(BAR) + def validateAccessToken(self, token, group): """ Verify Accestoken @@ -47,6 +56,7 @@ class AccesTokenController(metaclass=Singleton): now = datetime.now() LOGGER.debug("Check if AccessToken's Endtime {} is bigger then now {}".format(endTime, now)) if now <= endTime: + self.checkBar(accToken.user) LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) if self.isSameGroup(accToken, group): accToken.updateTimestamp() @@ -72,24 +82,27 @@ class AccesTokenController(metaclass=Singleton): LOGGER.info("Create AccessToken") now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() + self.checkBar(user) accToken = AccessToken(user, token, datetime.now()) LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) self.tokenList.append(accToken) LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) return token - def isSameGroup(self, accToken, group): + def isSameGroup(self, accToken, groups): """ Verify group in AccessToken Verify if the User in the AccesToken has the right group. Args: accToken: AccessToken to verify. - group: Group to verify. + groups: Group to verify. Returns: A Bool. If the same then True else False """ - print("controll if", accToken, "hase group", group) - LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, group)) - return True if group in accToken.user.group else False + print("controll if", accToken, "hase groups", groups) + LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, groups)) + for group in groups: + if group in accToken.user.group: return True + return False diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index f58cd6c..fa4f6a9 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -2,7 +2,7 @@ import pymysql from . import Singleton from geruecht.model.user import User from geruecht.model.creditList import CreditList -from datetime import datetime +from datetime import datetime, timedelta class DatabaseController(metaclass=Singleton): ''' @@ -61,6 +61,22 @@ class DatabaseController(metaclass=Singleton): return retVal + def getUserById(self, id): + self.connect() + retVal = None + try: + cursor = self.db.cursor() + cursor.execute("select * from user where id={}".format(id)) + data = cursor.fetchone() + self.db.close() + except Exception as err: + raise err + if data: + retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) + return retVal + def _convertGroupToString(self, groups): retVal = '' for group in groups: @@ -164,6 +180,52 @@ class DatabaseController(metaclass=Singleton): self.db.close() raise err + def getWorker(self, user, date): + self.connect() + try: + cursor = self.db.cursor() + cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + data = cursor.fetchone() + self.db.close() + except Exception as err: + raise err + return {"user": user, "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} + + def getWorkers(self, date): + self.connect() + try: + cursor = self.db.cursor() + cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) + data = cursor.fetchall() + self.db.close() + except Exception as err: + raise err + + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime']} for work in data] + + def setWorker(self, user, date): + self.connect() + try: + cursor = self.db.cursor() + cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) + self.db.commit() + self.db.close() + except Exception as err: + self.db.rollback() + self.db.close() + raise err + + def deleteWorker(self, user, date): + self.connect() + try: + cursor = self.db.cursor() + cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + self.db.commit() + self.db.close() + except Exception as err: + self.db.rollback() + self.db.close() + raise err if __name__ == '__main__': db = DatabaseController() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index f027816..541ff75 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,13 +1,28 @@ from . import LOGGER, Singleton, db, ldapController as ldap, emailController from geruecht.model.user import User from geruecht.exceptions import PermissionDenied -from datetime import datetime +from datetime import datetime, timedelta class UserController(metaclass=Singleton): def __init__(self): pass + def getWorker(self, date, username=None): + if (username): + user = self.getUser(username) + return [db.getWorker(user, date)] + return db.getWorkers(date) + + def addWorker(self, username, date): + user = self.getUser(username) + if (not db.getWorker(user, date)): + db.setWorker(user, date) + + def deleteWorker(self, username, date): + user = self.getUser(username) + db.setWorker(user, date) + def lockUser(self, username, locked): user = self.getUser(username) user.updateData({'locked': locked}) @@ -54,6 +69,16 @@ class UserController(metaclass=Singleton): self.__updateGeruechte(user) return db.getAllUser() + def checkBarUser(self, user): + date = datetime.now() + startdatetime = date.replace(hour=11, minute=0, microsecond=0) + enddatetime = startdatetime + timedelta(days=1) + result = False + if date >= startdatetime and date < enddatetime: + result = db.getWorker(user, startdatetime) + return True if result else False + + def getUser(self, username): user = db.getUser(username) groups = ldap.getGroup(username) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 4f1894d..209f14e 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -20,7 +20,7 @@ def _getFinanzer(): LOGGER.info("Get main for Finanzer") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: LOGGER.debug("Get all Useres") users = userController.getAllUsersfromDB() @@ -50,7 +50,7 @@ def _addAmount(): LOGGER.info("Add Amount") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: data = request.get_json() @@ -93,7 +93,7 @@ def _addCredit(): LOGGER.info("Add Amount") token = request.headers.get("Token") LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: @@ -128,7 +128,7 @@ def _addCredit(): @finanzer.route("/finanzerLock", methods=['POST']) def _finanzerLock(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: data = request.get_json() @@ -141,7 +141,7 @@ def _finanzerLock(): @finanzer.route("/finanzerSetConfig", methods=['POST']) def _finanzerSetConfig(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: data = request.get_json() @@ -155,7 +155,7 @@ def _finanzerSetConfig(): @finanzer.route("/finanzerAddUser", methods=['POST']) def _finanzerAddUser(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: data = request.get_json() @@ -175,7 +175,7 @@ def _finanzerAddUser(): @finanzer.route("/finanzerSendOneMail", methods=['POST']) def _finanzerSendOneMail(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: data = request.get_json() @@ -187,7 +187,7 @@ def _finanzerSendOneMail(): @finanzer.route("/finanzerSendAllMail", methods=['GET']) def _finanzerSendAllMail(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: retVal = userController.sendAllMail() diff --git a/geruecht/routes.py b/geruecht/routes.py index 6db7239..346691e 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -12,16 +12,16 @@ def login(user, password): @app.route("/valid") def _valid(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, MONEY) + accToken = accesTokenController.validateAccessToken(token, [MONEY]) if accToken: return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, BAR) + accToken = accesTokenController.validateAccessToken(token, [BAR]) if accToken: return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, GASTRO) + accToken = accesTokenController.validateAccessToken(token, [GASTRO]) if accToken: return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, USER) + accToken = accesTokenController.validateAccessToken(token, [USER]) if accToken: return jsonify(accToken.user.toJSON()) return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index af4341b..03f3a0f 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -9,7 +9,7 @@ user = Blueprint("user", __name__) def _main(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, USER) + accToken = accesTokenController.validateAccessToken(token, [USER]) if accToken: accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() @@ -21,7 +21,7 @@ def _main(): def _addAmount(): token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, USER) + accToken = accesTokenController.validateAccessToken(token, [USER]) if accToken: data = request.get_json() amount = int(data['amount']) diff --git a/geruecht/vorstand/__init__.py b/geruecht/vorstand/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py new file mode 100644 index 0000000..a0535a8 --- /dev/null +++ b/geruecht/vorstand/routes.py @@ -0,0 +1,24 @@ +from flask import Blueprint, request, jsonify +from datetime import datetime +from geruecht.controller import accesTokenController, userController +from geruecht.model import MONEY, GASTRO + +vorstand = Blueprint("vorstand", __name__) + +@vorstand.route("/sm/addUser", methods=['POST', 'GET']) +def _addUser(): + + if request.method == 'GET': + return "

HEllo World

" + + token = request.headers.get("Token") + accToken = accesTokenController.validateAccessToken(token, [MONEY, GASTRO]) + if accToken: + data = request.get_json() + user = data['user'] + date = datetime.utcfromtimestamp(int(data['date'])) + userController.addWorker(user['username'], date) + + print(data) + return jsonify({"date": date}) + return jsonify({"error": "permission denied"}), 401 \ No newline at end of file From 29f20b23278e3c5e87ef3cb27e12e0299c1bc06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 19 Jan 2020 00:37:40 +0100 Subject: [PATCH 033/111] fixed imports, bugfix that accLifetime will load from config file --- geruecht/baruser/routes.py | 9 ++++++- geruecht/configparser.py | 2 +- geruecht/controller/__init__.py | 28 --------------------- geruecht/controller/accesTokenController.py | 16 ++++++------ geruecht/controller/databaseController.py | 2 +- geruecht/controller/userController.py | 15 +++++++++-- geruecht/finanzer/routes.py | 5 +++- geruecht/routes.py | 7 ++++-- geruecht/user/routes.py | 7 +++++- 9 files changed, 47 insertions(+), 44 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 88290b8..7f6248d 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,10 +1,17 @@ from flask import Blueprint, request, jsonify -from geruecht.controller import ldapController as ldap, accesTokenController, userController +import geruecht.controller as gc +import geruecht.controller.ldapController as lc +import geruecht.controller.accesTokenController as ac +import geruecht.controller.userController as uc from datetime import datetime from geruecht.model import BAR, MONEY baruser = Blueprint("baruser", __name__) +ldap= lc.LDAPController(gc.ldapConfig['URL'], gc.ldapConfig['dn']) +accesTokenController = ac.AccesTokenController() +userController = uc.UserController() + @baruser.route("/bar") def _bar(): """ Main function for Baruser diff --git a/geruecht/configparser.py b/geruecht/configparser.py index 247f23c..fc92b61 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -34,7 +34,7 @@ class ConifgParser(): self.ldap = self.config['LDAP'] LOGGER.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: - self.accessTokenLifeTime = self.config['AccessTokenLifeTime'] + self.accessTokenLifeTime = int(self.config['AccessTokenLifeTime']) LOGGER.info("Set AccessTokenLifeTime: {}".format(self.accessTokenLifeTime)) else: self.accessTokenLifeTime = default['AccessTokenLifeTime'] diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index 7b0d1c8..b659474 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -15,35 +15,7 @@ class Singleton(type): cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] -from .databaseController import DatabaseController -def getDatabesController(): - if db is not None: - return db - else: - return DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) -from .ldapController import LDAPController -def getLDAPController(): - if ldapController is not None: - return ldapController - else: - return LDAPController(ldapConfig['URL'], ldapConfig['dn']) -from .accesTokenController import AccesTokenController - dbConfig = config.getDatabase() ldapConfig = config.getLDAP() accConfig = config.getAccessToken() mailConfig = config.getMail() - -db = DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) -ldapController = LDAPController(ldapConfig['URL'], ldapConfig['dn']) - -from . emailController import EmailController -emailController = EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) -from . userController import UserController -def getUserController(): - if userController is not None: - return userController - else: - return UserController() -userController = UserController() -accesTokenController = AccesTokenController(accConfig) \ No newline at end of file diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 7ec0c08..160459e 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,11 +1,13 @@ from geruecht.model.accessToken import AccessToken -#import geruecht.controller.userController as userController +import geruecht.controller as gc +import geruecht.controller.userController as uc from geruecht.model import BAR from geruecht.controller import LOGGER from datetime import datetime, timedelta import hashlib from . import Singleton +userController = uc.UserController() class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken @@ -25,15 +27,15 @@ class AccesTokenController(metaclass=Singleton): Initialize Thread and set tokenList empty. """ LOGGER.info("Initialize AccessTokenController") - self.lifetime = lifetime + self.lifetime = gc.accConfig self.tokenList = [] - #def checkBar(self, user): - # if (userController.checkBarUser(user)): - # user.group.append(BAR) - # elif BAR in user.group: - # user.group.remove(BAR) + def checkBar(self, user): + if (userController.checkBarUser(user)): + user.group.append(BAR) + elif BAR in user.group: + user.group.remove(BAR) def validateAccessToken(self, token, group): """ Verify Accestoken diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index fa4f6a9..50153c7 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -189,7 +189,7 @@ class DatabaseController(metaclass=Singleton): self.db.close() except Exception as err: raise err - return {"user": user, "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} + return {"user": user, "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None def getWorkers(self, date): self.connect() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 541ff75..151d248 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,8 +1,15 @@ -from . import LOGGER, Singleton, db, ldapController as ldap, emailController +from . import LOGGER, Singleton, ldapConfig, dbConfig, mailConfig +import geruecht.controller.databaseController as dc +import geruecht.controller.ldapController as lc +import geruecht.controller.emailController as ec from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta +db = dc.DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) +ldap = lc.LDAPController(ldapConfig['URL'], ldapConfig['dn']) +emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) + class UserController(metaclass=Singleton): def __init__(self): @@ -71,7 +78,11 @@ class UserController(metaclass=Singleton): def checkBarUser(self, user): date = datetime.now() - startdatetime = date.replace(hour=11, minute=0, microsecond=0) + zero = date.replace(hour=0, minute=0, second=0, microsecond=0) + end = zero + timedelta(hours=11) + startdatetime = date.replace(hour=11, minute=0, second=0, microsecond=0) + if date > zero and end > date: + startdatetime = startdatetime - timedelta(days=1) enddatetime = startdatetime + timedelta(days=1) result = False if date >= startdatetime and date < enddatetime: diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 209f14e..15bfc3b 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,11 +1,14 @@ from flask import Blueprint, request, jsonify from geruecht.finanzer import LOGGER from datetime import datetime -from geruecht.controller import accesTokenController, userController +import geruecht.controller.userController as uc +import geruecht.controller.accesTokenController as ac from geruecht.model import MONEY finanzer = Blueprint("finanzer", __name__) +accesTokenController = ac.AccesTokenController() +userController = uc.UserController() @finanzer.route("/getFinanzerMain") def _getFinanzer(): diff --git a/geruecht/routes.py b/geruecht/routes.py index 346691e..daf8d78 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,9 +1,12 @@ from geruecht import app, LOGGER from geruecht.exceptions import PermissionDenied -from geruecht.controller import accesTokenController, userController +import geruecht.controller.accesTokenController as ac +import geruecht.controller.userController as uc from geruecht.model import MONEY, BAR, USER, GASTRO from flask import request, jsonify +accesTokenController = ac.AccesTokenController() +userController = uc.UserController() def login(user, password): return user.login(password) @@ -48,7 +51,7 @@ def _login(): user = userController.loginUser(username, password) user.password = password token = accesTokenController.createAccesToken(user) - dic = user.toJSON() + dic = accesTokenController.validateAccessToken(token, [USER]).user.toJSON() dic["token"] = token dic["accessToken"] = token LOGGER.info("User {} success login.".format(username)) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 03f3a0f..089f3c7 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -1,10 +1,15 @@ from flask import Blueprint, request, jsonify -from geruecht.controller import userController, accesTokenController +import geruecht.controller as gc +import geruecht.controller.userController as uc +import geruecht.controller.accesTokenController as ac from geruecht.model import USER from datetime import datetime user = Blueprint("user", __name__) +accesTokenController = ac.AccesTokenController() +userController = uc.UserController() + @user.route("/user/main") def _main(): From f782be934d0627bf002e34c577e48fec1a5991a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 19 Jan 2020 09:07:45 +0100 Subject: [PATCH 034/111] added decoratos for connected in database and login_requird in routes --- geruecht/__init__.py | 2 +- geruecht/baruser/routes.py | 167 +++++++-------- geruecht/controller/databaseController.py | 107 ++++------ geruecht/decorator.py | 21 ++ geruecht/finanzer/routes.py | 243 +++++++++------------- geruecht/user/routes.py | 27 ++- geruecht/vorstand/routes.py | 23 +- 7 files changed, 266 insertions(+), 324 deletions(-) create mode 100644 geruecht/decorator.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 012a56e..a303078 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -15,7 +15,7 @@ from flask_cors import CORS LOGGER.info("Build APP") app = Flask(__name__) CORS(app) -# app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' +app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' from geruecht import routes from geruecht.baruser.routes import baruser diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 7f6248d..92ac2bb 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,19 +1,20 @@ from flask import Blueprint, request, jsonify import geruecht.controller as gc import geruecht.controller.ldapController as lc -import geruecht.controller.accesTokenController as ac import geruecht.controller.userController as uc from datetime import datetime from geruecht.model import BAR, MONEY +from geruecht.decorator import login_required baruser = Blueprint("baruser", __name__) ldap= lc.LDAPController(gc.ldapConfig['URL'], gc.ldapConfig['dn']) -accesTokenController = ac.AccesTokenController() userController = uc.UserController() + @baruser.route("/bar") -def _bar(): +@login_required(groups=[BAR]) +def _bar(**kwargs): """ Main function for Baruser Returns JSON-file with all Users, who hast amounts in this month. @@ -22,38 +23,33 @@ def _bar(): JSON-File with Users, who has amounts in this month or ERROR 401 Permission Denied """ - print(request.headers) - token = request.headers.get("Token") - print(token) - accToken = accesTokenController.validateAccessToken(token, [BAR]) - dic = {} - if accToken: - users = userController.getAllUsersfromDB() - for user in users: - geruecht = None - geruecht = user.getGeruecht(datetime.now().year) - if geruecht is not None: - month = geruecht.getMonth(datetime.now().month) - amount = month[0] - month[1] - all = geruecht.getSchulden() - if all != 0: - if all >= 0: - type = 'credit' - else: - type = 'amount' - dic[user.uid] = {"username": user.uid, - "firstname": user.firstname, - "lastname": user.lastname, - "amount": abs(all), - "locked": user.locked, - "type": type - } - return jsonify(dic) - return jsonify({"error": "permission denied"}), 401 + users = userController.getAllUsersfromDB() + for user in users: + geruecht = None + geruecht = user.getGeruecht(datetime.now().year) + if geruecht is not None: + month = geruecht.getMonth(datetime.now().month) + amount = month[0] - month[1] + all = geruecht.getSchulden() + if all != 0: + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic[user.uid] = {"username": user.uid, + "firstname": user.firstname, + "lastname": user.lastname, + "amount": abs(all), + "locked": user.locked, + "type": type + } + return jsonify(dic) + @baruser.route("/baradd", methods=['POST']) -def _baradd(): +@login_required(groups=[BAR]) +def _baradd(**kwargs): """ Function for Baruser to add amount This function added to the user with the posted userID the posted amount. @@ -62,35 +58,31 @@ def _baradd(): JSON-File with userID and the amount or ERROR 401 Permission Denied """ - token = request.headers.get("Token") - print(token) - accToken = accesTokenController.validateAccessToken(token, [BAR]) + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) - if accToken: - data = request.get_json() - userID = data['userId'] - amount = int(data['amount']) + date = datetime.now() + userController.addAmount(userID, amount, year=date.year, month=date.month) + user = userController.getUser(userID) + geruecht = user.getGeruecht(year=date.year) + month = geruecht.getMonth(month=date.month) + amount = abs(month[0] - month[1]) + all = geruecht.getSchulden() + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic = user.toJSON() + dic['amount'] = abs(all) + dic['type'] = type - date = datetime.now() - userController.addAmount(userID, amount, year=date.year, month=date.month) - user = userController.getUser(userID) - geruecht = user.getGeruecht(year=date.year) - month = geruecht.getMonth(month=date.month) - amount = abs(month[0] - month[1]) - all = geruecht.getSchulden() - if all >= 0: - type = 'credit' - else: - type = 'amount' - dic = user.toJSON() - dic['amount'] = abs(all) - dic['type'] = type + return jsonify(dic) - return jsonify(dic) - return jsonify({"error", "permission denied"}), 401 @baruser.route("/barGetUsers") -def _getUsers(): +@login_required(groups=[BAR, MONEY]) +def _getUsers(**kwargs): """ Get Users without amount This Function returns all Users, who hasn't an amount in this month. @@ -99,48 +91,33 @@ def _getUsers(): JSON-File with Users or ERROR 401 Permission Denied """ - token = request.headers.get("Token") - print(token) - accToken = accesTokenController.validateAccessToken(token, [BAR]) - retVal = {} - if accToken: - retVal = ldap.getAllUser() - return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 + retVal = ldap.getAllUser() + return jsonify(retVal) + @baruser.route("/barGetUser", methods=['POST']) -def _getUser(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [BAR]) - if accToken: - data = request.get_json() - username = data['userId'] - user = userController.getUser(username) - amount = user.getGeruecht(datetime.now().year).getSchulden() - if amount >= 0: - type = 'credit' - else: - type = 'amount' +@login_required(groups=[BAR]) +def _getUser(**kwargs): + data = request.get_json() + username = data['userId'] + user = userController.getUser(username) + amount = user.getGeruecht(datetime.now().year).getSchulden() + if amount >= 0: + type = 'credit' + else: + type = 'amount' + + retVal = user.toJSON() + retVal['amount'] = amount + retVal['type'] = type + return jsonify(retVal) - retVal = user.toJSON() - retVal['amount'] = amount - retVal['type'] = type - return jsonify(retVal) - return jsonify("error", "permission denied"), 401 @baruser.route("/search", methods=['POST']) -def _search(): - token = request.headers.get("Token") - print(token) - accToken = accesTokenController.validateAccessToken(token, [BAR, MONEY]) - - if accToken: - data = request.get_json() - - searchString = data['searchString'] - - retVal = ldap.searchUser(searchString) - - return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 +@login_required(groups=[BAR, MONEY]) +def _search(**kwargs): + data = request.get_json() + searchString = data['searchString'] + retVal = ldap.searchUser(searchString) + return jsonify(retVal) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 50153c7..a1df38e 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -4,6 +4,14 @@ from geruecht.model.user import User from geruecht.model.creditList import CreditList from datetime import datetime, timedelta +def connected(func): + def wrapper(*args, **kwargs): + self = args[0] + if not self.db.open: + self.connect() + return func(*args,**kwargs) + return wrapper + class DatabaseController(metaclass=Singleton): ''' DatabaesController @@ -24,16 +32,12 @@ class DatabaseController(metaclass=Singleton): self.db = pymysql.connect(self.url, self.user, self.password, self.database, cursorclass=pymysql.cursors.DictCursor) except Exception as err: raise err - + @connected def getAllUser(self): - self.connect() cursor = self.db.cursor() - try: - cursor.execute("select * from user") - data = cursor.fetchall() - self.db.close() - except Exception as err: - raise err + cursor.execute("select * from user") + data = cursor.fetchall() + self.db.close() if data: retVal = [] @@ -43,34 +47,26 @@ class DatabaseController(metaclass=Singleton): user.initGeruechte(creditLists) retVal.append(user) return retVal - + @connected def getUser(self, username): - self.connect() retVal = None cursor = self.db.cursor() - try: - cursor.execute("select * from user where uid='{}'".format(username)) - data = cursor.fetchone() - self.db.close() - except Exception as err: - raise err + cursor.execute("select * from user where uid='{}'".format(username)) + data = cursor.fetchone() + self.db.close() if data: retVal = User(data) creditLists = self.getCreditListFromUser(retVal) retVal.initGeruechte(creditLists) return retVal - + @connected def getUserById(self, id): - self.connect() retVal = None - try: - cursor = self.db.cursor() - cursor.execute("select * from user where id={}".format(id)) - data = cursor.fetchone() - self.db.close() - except Exception as err: - raise err + cursor = self.db.cursor() + cursor.execute("select * from user where id={}".format(id)) + data = cursor.fetchone() + self.db.close() if data: retVal = User(data) creditLists = self.getCreditListFromUser(retVal) @@ -85,8 +81,8 @@ class DatabaseController(metaclass=Singleton): retVal += group return retVal + @connected def insertUser(self, user): - self.connect() cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: @@ -99,8 +95,8 @@ class DatabaseController(metaclass=Singleton): raise err self.db.close() + @connected def updateUser(self, user): - self.connect() cursor = self.db.cursor() groups = self._convertGroupToString(user.group) try: @@ -117,38 +113,35 @@ class DatabaseController(metaclass=Singleton): self.db.close() + @connected def getCreditListFromUser(self, user, **kwargs): - self.connect() cursor = self.db.cursor() - try: - if 'year' in kwargs: - sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) - else: - sql = "select * from creditList where user_id={}".format(user.id) - cursor.execute(sql) - data = cursor.fetchall() - self.db.close() - except Exception as err: - self.db.close() - raise err + if 'year' in kwargs: + sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) + else: + sql = "select * from creditList where user_id={}".format(user.id) + cursor.execute(sql) + data = cursor.fetchall() + self.db.close() if len(data) == 1: return [CreditList(data[0])] else: return [CreditList(value) for value in data] + @connected def createCreditList(self, user_id, year=datetime.now().year): - self.connect() cursor = self.db.cursor() try: cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) self.db.commit() self.db.close() except Exception as err: + self.db.rollback() self.db.close() raise err + @connected def updateCreditList(self, creditlist): - self.connect() cursor = self.db.cursor() try: cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) @@ -179,32 +172,24 @@ class DatabaseController(metaclass=Singleton): self.db.rollback() self.db.close() raise err - + @connected def getWorker(self, user, date): - self.connect() - try: - cursor = self.db.cursor() - cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - data = cursor.fetchone() - self.db.close() - except Exception as err: - raise err + cursor = self.db.cursor() + cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + data = cursor.fetchone() + self.db.close() return {"user": user, "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None + @connected def getWorkers(self, date): - self.connect() - try: - cursor = self.db.cursor() - cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) - data = cursor.fetchall() - self.db.close() - except Exception as err: - raise err - + cursor = self.db.cursor() + cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) + data = cursor.fetchall() + self.db.close() return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime']} for work in data] + @connected def setWorker(self, user, date): - self.connect() try: cursor = self.db.cursor() cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) @@ -215,8 +200,8 @@ class DatabaseController(metaclass=Singleton): self.db.close() raise err + @connected def deleteWorker(self, user, date): - self.connect() try: cursor = self.db.cursor() cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) diff --git a/geruecht/decorator.py b/geruecht/decorator.py new file mode 100644 index 0000000..4addb6a --- /dev/null +++ b/geruecht/decorator.py @@ -0,0 +1,21 @@ +from functools import wraps +def login_required(**kwargs): + import geruecht.controller.accesTokenController as ac + from geruecht.model import BAR, USER, MONEY, GASTRO + from flask import request, jsonify + accessController = ac.AccesTokenController() + groups = [USER, BAR, GASTRO, MONEY] + if "groups" in kwargs: + groups = kwargs["groups"] + def real_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + token = request.headers.get('Token') + accToken = accessController.validateAccessToken(token, groups) + kwargs['accToken'] = accToken + if accToken: + return func(*args, **kwargs) + else: + return jsonify({"error": "error", "message": "permission denied"}), 401 + return wrapper + return real_decorator \ No newline at end of file diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 15bfc3b..92d7be1 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -2,16 +2,17 @@ from flask import Blueprint, request, jsonify from geruecht.finanzer import LOGGER from datetime import datetime import geruecht.controller.userController as uc -import geruecht.controller.accesTokenController as ac from geruecht.model import MONEY +from geruecht.decorator import login_required finanzer = Blueprint("finanzer", __name__) -accesTokenController = ac.AccesTokenController() userController = uc.UserController() + @finanzer.route("/getFinanzerMain") -def _getFinanzer(): +@login_required(groups=[MONEY]) +def _getFinanzer(**kwargs): """ Function for /getFinanzerMain Retrieves all User for the groupe 'moneymaster' @@ -20,26 +21,20 @@ def _getFinanzer(): A JSON-File with Users or ERROR 401 Permission Denied. """ - LOGGER.info("Get main for Finanzer") - token = request.headers.get("Token") - LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - if accToken: - LOGGER.debug("Get all Useres") - users = userController.getAllUsersfromDB() - dic = {} - for user in users: - LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - LOGGER.debug("ReturnValue is {}".format(dic)) - LOGGER.info("Send main for Finanzer") - return jsonify(dic) - LOGGER.info("Permission Denied") - return jsonify({"error": "permission denied"}), 401 + LOGGER.debug("Get all Useres") + users = userController.getAllUsersfromDB() + dic = {} + for user in users: + LOGGER.debug("Add User {} to ReturnValue".format(user)) + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} + LOGGER.debug("ReturnValue is {}".format(dic)) + LOGGER.info("Send main for Finanzer") + return jsonify(dic) @finanzer.route("/finanzerAddAmount", methods=['POST']) -def _addAmount(): +@login_required(groups=[MONEY]) +def _addAmount(**kwargs): """ Add Amount to User This Function add an amount to the user with posted userID. @@ -50,39 +45,32 @@ def _addAmount(): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - LOGGER.info("Add Amount") - token = request.headers.get("Token") - LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - - if accToken: - data = request.get_json() - LOGGER.debug("Get data {}".format(data)) - userID = data['userId'] - amount = int(data['amount']) - LOGGER.debug("UserID is {} and amount is {}".format(userID, amount)) - try: - year = int(data['year']) - except KeyError as er: - LOGGER.error("KeyError in year. Year is set to default.") - year = datetime.now().year - try: - month = int(data['month']) - except KeyError as er: - LOGGER.error("KeyError in month. Month is set to default.") - month = datetime.now().month - LOGGER.debug("Year is {} and Month is {}".format(year, month)) - userController.addAmount(userID, amount, year=year, month=month, finanzer=True) - user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} - retVal['locked'] = user.locked - LOGGER.info("Send updated Geruecht") - return jsonify(retVal) - LOGGER.info("Permission Denied") - return jsonify({"error": "permission denied"}), 401 + data = request.get_json() + LOGGER.debug("Get data {}".format(data)) + userID = data['userId'] + amount = int(data['amount']) + LOGGER.debug("UserID is {} and amount is {}".format(userID, amount)) + try: + year = int(data['year']) + except KeyError as er: + LOGGER.error("KeyError in year. Year is set to default.") + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + LOGGER.error("KeyError in month. Month is set to default.") + month = datetime.now().month + LOGGER.debug("Year is {} and Month is {}".format(year, month)) + userController.addAmount(userID, amount, year=year, month=month, finanzer=True) + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked + LOGGER.info("Send updated Geruecht") + return jsonify(retVal) @finanzer.route("/finanzerAddCredit", methods=['POST']) -def _addCredit(): +@login_required(groups=[MONEY]) +def _addCredit(**kwargs): """ Add Credit to User This Function add an credit to the user with posted userID. @@ -93,106 +81,79 @@ def _addCredit(): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - LOGGER.info("Add Amount") - token = request.headers.get("Token") - LOGGER.debug("Verify AccessToken with Token {}".format(token)) - accToken = accesTokenController.validateAccessToken(token, [MONEY]) + data = request.get_json() + print(data) + LOGGER.debug("Get data {}".format(data)) + userID = data['userId'] + credit = int(data['credit']) + LOGGER.debug("UserID is {} and credit is {}".format(userID, credit)) - if accToken: + try: + year = int(data['year']) + except KeyError as er: + LOGGER.error("KeyError in year. Year is set to default.") + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + LOGGER.error("KeyError in month. Month is set to default.") + month = datetime.now().month - data = request.get_json() - print(data) - LOGGER.debug("Get data {}".format(data)) - userID = data['userId'] - credit = int(data['credit']) - LOGGER.debug("UserID is {} and credit is {}".format(userID, credit)) + LOGGER.debug("Year is {} and Month is {}".format(year, month)) + userController.addCredit(userID, credit, year=year, month=month).toJSON() + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked + LOGGER.info("Send updated Geruecht") + return jsonify(retVal) - try: - year = int(data['year']) - except KeyError as er: - LOGGER.error("KeyError in year. Year is set to default.") - year = datetime.now().year - try: - month = int(data['month']) - except KeyError as er: - LOGGER.error("KeyError in month. Month is set to default.") - month = datetime.now().month - - LOGGER.debug("Year is {} and Month is {}".format(year, month)) - userController.addCredit(userID, credit, year=year, month=month).toJSON() - user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} - retVal['locked'] = user.locked - LOGGER.info("Send updated Geruecht") - return jsonify(retVal) - LOGGER.info("Permission Denied") - return jsonify({"error": "permission denied"}), 401 @finanzer.route("/finanzerLock", methods=['POST']) -def _finanzerLock(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) +@login_required(groups=[MONEY]) +def _finanzerLock(**kwargs): + data = request.get_json() + username = data['userId'] + locked = bool(data['locked']) + retVal = userController.lockUser(username, locked).toJSON() + return jsonify(retVal) - if accToken: - data = request.get_json() - username = data['userId'] - locked = bool(data['locked']) - retVal = userController.lockUser(username, locked).toJSON() - return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 @finanzer.route("/finanzerSetConfig", methods=['POST']) -def _finanzerSetConfig(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - - if accToken: - data = request.get_json() - username = data['userId'] - autoLock = bool(data['autoLock']) - limit = int(data['limit']) - retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() - return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 +@login_required(groups=[MONEY]) +def _finanzerSetConfig(**kwargs): + data = request.get_json() + username = data['userId'] + autoLock = bool(data['autoLock']) + limit = int(data['limit']) + retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() + return jsonify(retVal) @finanzer.route("/finanzerAddUser", methods=['POST']) -def _finanzerAddUser(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - - if accToken: - data = request.get_json() - username = data['userId'] - userController.getUser(username) - LOGGER.debug("Get all Useres") - users = userController.getAllUsersfromDB() - dic = {} - for user in users: - LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - LOGGER.debug("ReturnValue is {}".format(dic)) - return jsonify(dic), 200 - return jsonify({"error": "permission denied"}), 401 +@login_required(groups=[MONEY]) +def _finanzerAddUser(**kwargs): + data = request.get_json() + username = data['userId'] + userController.getUser(username) + LOGGER.debug("Get all Useres") + users = userController.getAllUsersfromDB() + dic = {} + for user in users: + LOGGER.debug("Add User {} to ReturnValue".format(user)) + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} + LOGGER.debug("ReturnValue is {}".format(dic)) + return jsonify(dic), 200 @finanzer.route("/finanzerSendOneMail", methods=['POST']) -def _finanzerSendOneMail(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - - if accToken: - data = request.get_json() - username = data['userId'] - retVal = userController.sendMail(username) - return jsonify(retVal) - return jsonify({"error:", "permission denied"}), 401 +@login_required(groups=[MONEY]) +def _finanzerSendOneMail(**kwargs): + data = request.get_json() + username = data['userId'] + retVal = userController.sendMail(username) + return jsonify(retVal) @finanzer.route("/finanzerSendAllMail", methods=['GET']) -def _finanzerSendAllMail(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - - if accToken: - retVal = userController.sendAllMail() - return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 \ No newline at end of file +@login_required(groups=[MONEY]) +def _finanzerSendAllMail(**kwargs): + retVal = userController.sendAllMail() + return jsonify(retVal) \ No newline at end of file diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 089f3c7..5b30297 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -1,33 +1,30 @@ from flask import Blueprint, request, jsonify -import geruecht.controller as gc +from geruecht.decorator import login_required import geruecht.controller.userController as uc -import geruecht.controller.accesTokenController as ac from geruecht.model import USER from datetime import datetime user = Blueprint("user", __name__) -accesTokenController = ac.AccesTokenController() userController = uc.UserController() -@user.route("/user/main") -def _main(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [USER]) - if accToken: +@user.route("/user/main") +@login_required(groups=[USER]) +def _main(**kwargs): + if 'accToken' in kwargs: + accToken = kwargs['accToken'] accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 + return jsonify("error", "something went wrong"), 500 @user.route("/user/addAmount", methods=['POST']) -def _addAmount(): - - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [USER]) - if accToken: +@login_required(groups=[USER]) +def _addAmount(**kwargs): + if 'accToken' in kwargs: + accToken = kwargs['accToken'] data = request.get_json() amount = int(data['amount']) date = datetime.now() @@ -36,4 +33,4 @@ def _addAmount(): retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} return jsonify(retVal) - return jsonify({"error": "permission denied"}), 401 \ No newline at end of file + return jsonify({"error": "something went wrong"}), 500 \ No newline at end of file diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index a0535a8..3d69f90 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -1,24 +1,25 @@ from flask import Blueprint, request, jsonify from datetime import datetime -from geruecht.controller import accesTokenController, userController +import geruecht.controller.userController as uc +from geruecht.decorator import login_required from geruecht.model import MONEY, GASTRO vorstand = Blueprint("vorstand", __name__) +userController = uc.UserController() + @vorstand.route("/sm/addUser", methods=['POST', 'GET']) + +@login_required(groups=[MONEY, GASTRO]) def _addUser(): if request.method == 'GET': return "

HEllo World

" - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY, GASTRO]) - if accToken: - data = request.get_json() - user = data['user'] - date = datetime.utcfromtimestamp(int(data['date'])) - userController.addWorker(user['username'], date) + data = request.get_json() + user = data['user'] + date = datetime.utcfromtimestamp(int(data['date'])) + userController.addWorker(user['username'], date) - print(data) - return jsonify({"date": date}) - return jsonify({"error": "permission denied"}), 401 \ No newline at end of file + print(data) + return jsonify({"date": date}) \ No newline at end of file From 635051d615d2667434dba219f65bc36180b5665a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 19 Jan 2020 21:32:58 +0100 Subject: [PATCH 035/111] change database controller without multithreading --- geruecht/__init__.py | 8 + geruecht/configparser.py | 3 +- geruecht/controller/databaseController.py | 190 +++++++--------------- geruecht/controller/userController.py | 5 +- geruecht/model/priceList.py | 17 -- geruecht/vorstand/routes.py | 26 ++- 6 files changed, 97 insertions(+), 152 deletions(-) delete mode 100644 geruecht/model/priceList.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index a303078..af71892 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -5,6 +5,8 @@ """ from .logger import getLogger +from geruecht.controller import dbConfig +from flask_mysqldb import MySQL LOGGER = getLogger(__name__) LOGGER.info("Initialize App") @@ -16,6 +18,12 @@ LOGGER.info("Build APP") app = Flask(__name__) CORS(app) app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' +app.config['MYSQL_HOST'] = dbConfig['URL'] +app.config['MYSQL_USER'] = dbConfig['user'] +app.config['MYSQL_PASSWORD'] = dbConfig['passwd'] +app.config['MYSQL_DB'] = dbConfig['database'] +app.config['MYSQL_CURSORCLASS'] = 'DictCursor' +db = MySQL(app) from geruecht import routes from geruecht.baruser.routes import baruser diff --git a/geruecht/configparser.py b/geruecht/configparser.py index fc92b61..e1ab855 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -1,6 +1,7 @@ import yaml import sys -from . import LOGGER +from .logger import getLogger +LOGGER = getLogger(__name__) default = { 'AccessTokenLifeTime': 1800, diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index a1df38e..da45192 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -1,17 +1,10 @@ import pymysql from . import Singleton +from geruecht import db from geruecht.model.user import User from geruecht.model.creditList import CreditList from datetime import datetime, timedelta -def connected(func): - def wrapper(*args, **kwargs): - self = args[0] - if not self.db.open: - self.connect() - return func(*args,**kwargs) - return wrapper - class DatabaseController(metaclass=Singleton): ''' DatabaesController @@ -19,25 +12,13 @@ class DatabaseController(metaclass=Singleton): Connect to the Database and execute sql-executions ''' - def __init__(self, url='192.168.5.108', user='wu5', password='E1n$tein', database='geruecht'): - self.url = url - self.user = user - self.password = password - self.database = database - self.connect() + def __init__(self): + self.db = db - - def connect(self): - try: - self.db = pymysql.connect(self.url, self.user, self.password, self.database, cursorclass=pymysql.cursors.DictCursor) - except Exception as err: - raise err - @connected def getAllUser(self): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() cursor.execute("select * from user") data = cursor.fetchall() - self.db.close() if data: retVal = [] @@ -47,26 +28,24 @@ class DatabaseController(metaclass=Singleton): user.initGeruechte(creditLists) retVal.append(user) return retVal - @connected + def getUser(self, username): retVal = None - cursor = self.db.cursor() + cursor = self.db.connection.cursor() cursor.execute("select * from user where uid='{}'".format(username)) data = cursor.fetchone() - self.db.close() if data: retVal = User(data) creditLists = self.getCreditListFromUser(retVal) retVal.initGeruechte(creditLists) return retVal - @connected + def getUserById(self, id): retVal = None - cursor = self.db.cursor() + cursor = self.db.connection.cursor() cursor.execute("select * from user where id={}".format(id)) data = cursor.fetchone() - self.db.close() if data: retVal = User(data) creditLists = self.getCreditListFromUser(retVal) @@ -81,136 +60,93 @@ class DatabaseController(metaclass=Singleton): retVal += group return retVal - @connected + def insertUser(self, user): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() groups = self._convertGroupToString(user.group) - try: - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( - user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) - self.db.commit() - except Exception as err: - self.db.rollback() - self.db.close() - raise err - self.db.close() + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( + user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) + self.db.connection.commit() + - @connected def updateUser(self, user): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() groups = self._convertGroupToString(user.group) - try: - sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( - user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) - print(sql) - cursor.execute(sql) - self.db.commit() - except Exception as err: - self.db.rollback() - self.db.close() - print(err.__traceback__) - raise err + sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) + print(sql) + cursor.execute(sql) + self.db.connection.commit() - self.db.close() - @connected def getCreditListFromUser(self, user, **kwargs): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() if 'year' in kwargs: sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) else: sql = "select * from creditList where user_id={}".format(user.id) cursor.execute(sql) data = cursor.fetchall() - self.db.close() if len(data) == 1: return [CreditList(data[0])] else: return [CreditList(value) for value in data] - @connected - def createCreditList(self, user_id, year=datetime.now().year): - cursor = self.db.cursor() - try: - cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) - self.db.commit() - self.db.close() - except Exception as err: - self.db.rollback() - self.db.close() - raise err - @connected + def createCreditList(self, user_id, year=datetime.now().year): + cursor = self.db.connection.cursor() + cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) + self.db.connection.commit() + + def updateCreditList(self, creditlist): - cursor = self.db.cursor() - try: - cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) - data = cursor.fetchall() - self.db.close() - if len(data) == 0: - self.createCreditList(creditlist.user_id, creditlist.year) - sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, - creditlist.feb_guthaben, creditlist.feb_schulden, - creditlist.maer_guthaben, creditlist.maer_schulden, - creditlist.apr_guthaben, creditlist.apr_schulden, - creditlist.mai_guthaben, creditlist.mai_schulden, - creditlist.jun_guthaben, creditlist.jun_schulden, - creditlist.jul_guthaben, creditlist.jul_schulden, - creditlist.aug_guthaben, creditlist.aug_schulden, - creditlist.sep_guthaben, creditlist.sep_schulden, - creditlist.okt_guthaben, creditlist.okt_schulden, - creditlist.nov_guthaben, creditlist.nov_schulden, - creditlist.dez_guthaben, creditlist.dez_schulden, - creditlist.last_schulden, creditlist.year, creditlist.user_id) - print(sql) - self.connect() - cursor = self.db.cursor() - cursor.execute(sql) - self.db.commit() - self.db.close() - except Exception as err: - self.db.rollback() - self.db.close() - raise err - @connected + cursor = self.db.connection.cursor() + cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) + data = cursor.fetchall() + if len(data) == 0: + self.createCreditList(creditlist.user_id, creditlist.year) + sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, + creditlist.feb_guthaben, creditlist.feb_schulden, + creditlist.maer_guthaben, creditlist.maer_schulden, + creditlist.apr_guthaben, creditlist.apr_schulden, + creditlist.mai_guthaben, creditlist.mai_schulden, + creditlist.jun_guthaben, creditlist.jun_schulden, + creditlist.jul_guthaben, creditlist.jul_schulden, + creditlist.aug_guthaben, creditlist.aug_schulden, + creditlist.sep_guthaben, creditlist.sep_schulden, + creditlist.okt_guthaben, creditlist.okt_schulden, + creditlist.nov_guthaben, creditlist.nov_schulden, + creditlist.dez_guthaben, creditlist.dez_schulden, + creditlist.last_schulden, creditlist.year, creditlist.user_id) + print(sql) + cursor = self.db.connection.cursor() + cursor.execute(sql) + self.db.connection.commit() + def getWorker(self, user, date): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) data = cursor.fetchone() - self.db.close() - return {"user": user, "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None + return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None + - @connected def getWorkers(self, date): - cursor = self.db.cursor() + cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) data = cursor.fetchall() - self.db.close() return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime']} for work in data] - @connected - def setWorker(self, user, date): - try: - cursor = self.db.cursor() - cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) - self.db.commit() - self.db.close() - except Exception as err: - self.db.rollback() - self.db.close() - raise err - @connected + def setWorker(self, user, date): + cursor = self.db.connection.cursor() + cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) + self.db.connection.commit() + + def deleteWorker(self, user, date): - try: - cursor = self.db.cursor() - cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - self.db.commit() - self.db.close() - except Exception as err: - self.db.rollback() - self.db.close() - raise err + cursor = self.db.connection.cursor() + cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + self.db.connection.commit() if __name__ == '__main__': db = DatabaseController() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 151d248..1590749 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -6,7 +6,7 @@ from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta -db = dc.DatabaseController(dbConfig['URL'], dbConfig['user'], dbConfig['passwd'], dbConfig['database']) +db = dc.DatabaseController() ldap = lc.LDAPController(ldapConfig['URL'], ldapConfig['dn']) emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) @@ -25,10 +25,11 @@ class UserController(metaclass=Singleton): user = self.getUser(username) if (not db.getWorker(user, date)): db.setWorker(user, date) + return self.getWorker(date, username=username) def deleteWorker(self, username, date): user = self.getUser(username) - db.setWorker(user, date) + db.deleteWorker(user, date) def lockUser(self, username, locked): user = self.getUser(username) diff --git a/geruecht/model/priceList.py b/geruecht/model/priceList.py deleted file mode 100644 index 0f8c6ef..0000000 --- a/geruecht/model/priceList.py +++ /dev/null @@ -1,17 +0,0 @@ -from geruecht.controller import db - -class PriceList(db.Model): - """ Database Model for PriceList - - PriceList has lots of Drinks and safe all Prices (normal, for club, for other clubs, which catagory, etc) - """ - id = db.Column(db.Integer, primary_key=True) - - name = db.Column(db.String, nullable=False, unique=True) - price = db.Column(db.Integer, nullable=False) - price_club = db.Column(db.Integer, nullable=False) - price_ext_club = db.Column(db.Integer, nullable=False) - category = db.Column(db.Integer, nullable=False) - upPrice = db.Column(db.Integer) - upPrice_club = db.Column(db.Integer) - upPrice_ext_club = db.Column(db.Integer) diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 3d69f90..d1184dc 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -9,9 +9,8 @@ userController = uc.UserController() @vorstand.route("/sm/addUser", methods=['POST', 'GET']) - @login_required(groups=[MONEY, GASTRO]) -def _addUser(): +def _addUser(**kwargs): if request.method == 'GET': return "

HEllo World

" @@ -19,7 +18,24 @@ def _addUser(): data = request.get_json() user = data['user'] date = datetime.utcfromtimestamp(int(data['date'])) - userController.addWorker(user['username'], date) + retVal = userController.addWorker(user['username'], date) + print(retVal) + return jsonify(retVal) - print(data) - return jsonify({"date": date}) \ No newline at end of file +@vorstand.route("/sm/getUser", methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _getUser(**kwargs): + data = request.get_json() + date = datetime.utcfromtimestamp(int(data['date'])) + retVal = userController.getWorker(date) + print(retVal) + return jsonify(retVal) + +@vorstand.route("/sm/deleteUser", methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _deletUser(**kwargs): + data = request.get_json() + user = data['user'] + date = datetime.utcfromtimestamp(int(data['date'])) + userController.deleteWorker(user['username'], date) + return jsonify({"ok": "ok"}) \ No newline at end of file From 59a6440c31ac084cc4552a78a46120c39c7f3622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 21 Jan 2020 06:54:35 +0100 Subject: [PATCH 036/111] added flask ldapconn, fixed bug in login --- geruecht/__init__.py | 8 ++ geruecht/controller/ldapController.py | 123 +++++++++++++------------- geruecht/controller/userController.py | 2 +- geruecht/routes.py | 2 + 4 files changed, 72 insertions(+), 63 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index af71892..0963dd8 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -7,6 +7,7 @@ from .logger import getLogger from geruecht.controller import dbConfig from flask_mysqldb import MySQL +from flask_ldapconn import LDAPConn LOGGER = getLogger(__name__) LOGGER.info("Initialize App") @@ -23,6 +24,13 @@ app.config['MYSQL_USER'] = dbConfig['user'] app.config['MYSQL_PASSWORD'] = dbConfig['passwd'] app.config['MYSQL_DB'] = dbConfig['database'] app.config['MYSQL_CURSORCLASS'] = 'DictCursor' +app.config['LDAP_SERVER'] = '192.168.5.128' +app.config['LDAP_PORT'] = 389 +app.config['LDAP_BINDDN'] = 'dc=ldap,dc=example,dc=local' +app.config['LDAP_USE_TLS'] = False +app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] = True + +ldap = LDAPConn(app) db = MySQL(app) from geruecht import routes diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 7002f44..517cdfd 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -1,72 +1,68 @@ -import ldap +from geruecht import ldap +from ldap3 import SUBTREE, Connection from geruecht.model import MONEY, USER, GASTRO, BAR from geruecht.exceptions import PermissionDenied from . import Singleton +import traceback class LDAPController(metaclass=Singleton): ''' Authentification over LDAP. Create Account on-the-fly ''' - def __init__(self, url="ldap://192.168.5.108", dn='dc=ldap,dc=example,dc=local'): - self.url = url + def __init__(self, dn='dc=ldap,dc=example,dc=local'): self.dn = dn - self.connect() + self.ldap = ldap - def connect(self): - try: - self.client = ldap.initialize(self.url, bytes_mode=False) - except Exception as err: - raise err def login(self, username, password): - self.connect() try: - cn = self.client.search_s("ou=user,{}".format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username),['cn'])[0][1]['cn'][0].decode('utf-8') - self.client.bind_s("cn={},ou=user,{}".format(cn, self.dn), password) - self.client.unbind_s() - except: - self.client.unbind_s() - raise PermissionDenied("Invalid Password or Username") + retVal = self.ldap.authenticate(username, password, 'uid', self.dn) + if not retVal: + raise PermissionDenied("Invalid Password or Username") + except Exception as err: + traceback.print_exception(err) + raise PermissionDenied("Wrong username or password.") def getUserData(self, username): try: - self.connect() - search_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['uid', 'givenName', 'sn', 'mail']) - retVal = search_data[0][1] - for k,v in retVal.items(): - retVal[k] = v[0].decode('utf-8') - retVal['dn'] = self.dn - retVal['firstname'] = retVal['givenName'] - retVal['lastname'] = retVal['sn'] + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) + user = self.ldap.connection.response[0]['attributes'] + retVal = { + 'dn': self.ldap.connection.response[0]['dn'], + 'firstname': user['givenName'][0], + 'lastname': user['sn'][0], + 'uid': username + } return retVal except: raise PermissionDenied("No User exists with this uid.") def getGroup(self, username): - retVal = [] - self.connect() - main_group_data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'uid={}'.format(username), ['gidNumber']) - if main_group_data: - main_group_number = main_group_data[0][1]['gidNumber'][0].decode('utf-8') - group_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'gidNumber={}'.format(main_group_number), ['cn']) - if group_data: - group_name = group_data[0][1]['cn'][0].decode('utf-8') + try: + retVal = [] + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['gidNumber']) + main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber'] + if main_group_number: + group_data = self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) + group_name = self.ldap.connection.response[0]['attributes']['cn'][0] if group_name == 'ldap-user': retVal.append(USER) - groups_data = self.client.search_s('ou=group,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'memberUID={}'.format(username), ['cn']) - for data in groups_data: - print(data[1]['cn'][0].decode('utf-8')) - group_name = data[1]['cn'][0].decode('utf-8') - if group_name == 'finanzer': - retVal.append(MONEY) - elif group_name == 'gastro': - retVal.append(GASTRO) - elif group_name == 'bar': - retVal.append(BAR) - return retVal + self.ldap.connection.search('ou=group,{}'.format(self.dn), '(memberUID={})'.format(username), SUBTREE, attributes=['cn']) + groups_data = self.ldap.connection.response + for data in groups_data: + group_name = data['attributes']['cn'][0] + if group_name == 'finanzer': + retVal.append(MONEY) + elif group_name == 'gastro': + retVal.append(GASTRO) + elif group_name == 'bar': + retVal.append(BAR) + return retVal + except Exception as err: + traceback.print_exception(err) def __isUserInList(self, list, username): help_list = [] @@ -77,19 +73,19 @@ class LDAPController(metaclass=Singleton): return False def getAllUser(self): - self.connect() retVal = [] - data = self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, attrlist=['uid', 'givenName', 'sn', 'mail']) + self.ldap.connection.search() + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) + data = self.ldap.connection.response for user in data: - if 'uid' in user[1]: - username = user[1]['uid'][0].decode('utf-8') - firstname = user[1]['givenName'][0].decode('utf-8') - lastname = user[1]['sn'][0].decode('utf-8') + if 'uid' in user['attributes']: + username = user['attributes']['uid'][0] + firstname = user['attributes']['givenName'][0] + lastname = user['attributes']['sn'][0] retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) return retVal def searchUser(self, searchString): - self.connect() name = searchString.split(" ") @@ -103,25 +99,28 @@ class LDAPController(metaclass=Singleton): if len(name) == 1: if name[0] == "**": - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, - attrlist=['uid', 'givenName', 'sn'])) + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE, + attributes=['uid', 'givenName', 'sn']) + name_result.append(self.ldap.connection.response) else: - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'givenName={}'.format(name[0]), ['uid', 'givenName', 'sn', 'mail'])) - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[0]),['uid', 'givenName', 'sn'], 'mail')) + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(givenName={})'.format(name[0]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) + name_result.append(self.ldap.connection.response) + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(sn={})'.format(name[0]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) + name_result.append(self.ldap.connection.response) else: - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, - 'givenName={}'.format(name[1]), ['uid', 'givenName', 'sn'])) - name_result.append(self.client.search_s('ou=user,{}'.format(self.dn), ldap.SCOPE_SUBTREE, 'sn={}'.format(name[1]), - ['uid', 'givenName', 'sn', 'mail'])) + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(givenName={})'.format(name[1]), SUBTREE, attributes=['uid', 'givenName', 'sn']) + name_result.append(self.ldap.connection.response) + self.ldap.connection.search('ou=user,{}'.format(self.dn), '(sn={})'.format(name[1]), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) + name_result.append(self.ldap.connection.response) retVal = [] for names in name_result: for user in names: - if 'uid' in user[1]: - username = user[1]['uid'][0].decode('utf-8') + if 'uid' in user['attributes']: + username = user['attributes']['uid'][0] if not self.__isUserInList(retVal, username): - firstname = user[1]['givenName'][0].decode('utf-8') - lastname = user[1]['sn'][0].decode('utf-8') + firstname = user['attributes']['givenName'][0] + lastname = user['attributes']['sn'][0] retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) return retVal diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 1590749..97a94cb 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -7,7 +7,7 @@ from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta db = dc.DatabaseController() -ldap = lc.LDAPController(ldapConfig['URL'], ldapConfig['dn']) +ldap = lc.LDAPController(ldapConfig['dn']) emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) class UserController(metaclass=Singleton): diff --git a/geruecht/routes.py b/geruecht/routes.py index daf8d78..9b7fa6b 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -58,5 +58,7 @@ def _login(): return jsonify(dic) except PermissionDenied as err: return jsonify({"error": str(err)}), 401 + except Exception: + return jsonify({"error": "permission denied"}), 401 LOGGER.info("User {} does not exist.".format(username)) return jsonify({"error": "wrong username"}), 401 From c76ed6d6da16745184d38a08a8a792aab715f891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 23 Jan 2020 23:27:26 +0100 Subject: [PATCH 037/111] pushup --- geruecht/controller/databaseController.py | 10 +++++++--- geruecht/vorstand/routes.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index da45192..321610b 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -4,6 +4,7 @@ from geruecht import db from geruecht.model.user import User from geruecht.model.creditList import CreditList from datetime import datetime, timedelta +import traceback class DatabaseController(metaclass=Singleton): ''' @@ -144,9 +145,12 @@ class DatabaseController(metaclass=Singleton): def deleteWorker(self, user, date): - cursor = self.db.connection.cursor() - cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() if __name__ == '__main__': db = DatabaseController() diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index d1184dc..35e375b 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -3,6 +3,7 @@ from datetime import datetime import geruecht.controller.userController as uc from geruecht.decorator import login_required from geruecht.model import MONEY, GASTRO +import time vorstand = Blueprint("vorstand", __name__) userController = uc.UserController() From 16521a60c20c075c7bb91df8718de869b09d1dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 26 Jan 2020 23:31:22 +0100 Subject: [PATCH 038/111] added ldap modifying for users --- geruecht/controller/accesTokenController.py | 4 +-- geruecht/controller/databaseController.py | 15 +++++++++ geruecht/controller/ldapController.py | 34 ++++++++++++++++++++- geruecht/controller/userController.py | 24 ++++++++++++++- geruecht/exceptions/__init__.py | 8 +++++ geruecht/finanzer/routes.py | 1 + geruecht/model/accessToken.py | 4 ++- geruecht/model/user.py | 4 ++- geruecht/routes.py | 4 +-- geruecht/user/routes.py | 14 ++++++++- 10 files changed, 103 insertions(+), 9 deletions(-) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 160459e..b23e2d1 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -70,7 +70,7 @@ class AccesTokenController(metaclass=Singleton): LOGGER.info("Found no valid AccessToken with token: {} and group: {}".format(token, group)) return False - def createAccesToken(self, user): + def createAccesToken(self, user, ldap_conn): """ Create an AccessToken Create an AccessToken for an User and add it to the tokenList. @@ -85,7 +85,7 @@ class AccesTokenController(metaclass=Singleton): now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() self.checkBar(user) - accToken = AccessToken(user, token, datetime.now()) + accToken = AccessToken(user, token, ldap_conn, datetime.now()) LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) self.tokenList.append(accToken) LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 321610b..ae62453 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -4,6 +4,7 @@ from geruecht import db from geruecht.model.user import User from geruecht.model.creditList import CreditList from datetime import datetime, timedelta +from geruecht.exceptions import UsernameExistDB, DatabaseExecption import traceback class DatabaseController(metaclass=Singleton): @@ -152,6 +153,20 @@ class DatabaseController(metaclass=Singleton): except Exception as err: traceback.print_exc() + def changeUsername(self, user, newUsername): + try: + cursor= self.db.connection.cursor() + cursor.execute("select * from user where uid='{}'".format(newUsername)) + data = cursor.fetchall() + if data: + raise UsernameExistDB("Username already exists") + else: + cursor.execute("update user set uid='{}' where id={}".format(newUsername, user.id)) + self.db.connection() + except Exception as err: + traceback.print_exc() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 517cdfd..40c95b3 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -1,8 +1,10 @@ from geruecht import ldap -from ldap3 import SUBTREE, Connection +from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5 +from ldap3.utils.hashed import hashed from geruecht.model import MONEY, USER, GASTRO, BAR from geruecht.exceptions import PermissionDenied from . import Singleton +from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion import traceback class LDAPController(metaclass=Singleton): @@ -24,6 +26,10 @@ class LDAPController(metaclass=Singleton): traceback.print_exception(err) raise PermissionDenied("Wrong username or password.") + def bind(self, user, password): + ldap_conn = self.ldap.connect(user.dn, password) + return ldap_conn + def getUserData(self, username): try: self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) @@ -43,6 +49,7 @@ class LDAPController(metaclass=Singleton): try: retVal = [] self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['gidNumber']) + response = self.ldap.connection.response main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber'] if main_group_number: group_data = self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) @@ -125,6 +132,31 @@ class LDAPController(metaclass=Singleton): return retVal + def modifyUser(self, user, conn, attributes): + try: + if 'username' in attributes: + conn.search('ou=user,{}'.format(self.dn), '(uid={})'.format(attributes['username'])) + if conn.entries: + raise UsernameExistLDAP("Username already exists in LDAP") + #create modifyer + mody = {} + if 'username' in attributes: + mody['uid'] = [(MODIFY_REPLACE, [attributes['username']])] + if 'firstname' in attributes: + mody['givenName'] = [(MODIFY_REPLACE, [attributes['firstname']])] + if 'lastname' in attributes: + mody['sn'] = [(MODIFY_REPLACE, [attributes['lastname']])] + if 'mail' in attributes: + mody['mail'] = [(MODIFY_REPLACE, [attributes['mail']])] + if 'password' in attributes: + salted_password = hashed(HASHED_SALTED_MD5, attributes['password']) + mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])] + conn.modify(user.dn, mody) + except Exception as err: + traceback.print_exc() + raise LDAPExcetpion("Something went wrong in LDAP: {}".format(err)) + + if __name__ == '__main__': a = LDAPController() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 97a94cb..6d551c8 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -5,6 +5,7 @@ import geruecht.controller.emailController as ec from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta +from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion db = dc.DatabaseController() ldap = lc.LDAPController(ldapConfig['dn']) @@ -128,10 +129,31 @@ class UserController(metaclass=Singleton): retVal.append(self.sendMail(user)) return retVal + def modifyUser(self, user, ldap_conn, attributes): + try: + if 'username' in attributes: + db.changeUsername(user, attributes['username']) + ldap.modifyUser(user, ldap_conn, attributes) + if 'username' in attributes: + return self.getUser(attributes['username']) + else: + return self.getUser(user.uid) + except UsernameExistLDAP as err: + db.changeUsername(user, user.uid) + raise Exception(err) + except LDAPExcetpion as err: + if 'username' in attributes: + db.changeUsername(user, user.uid) + raise Exception(err) + except Exception as err: + raise Exception(err) + def loginUser(self, username, password): try: user = self.getUser(username) + user.password = password ldap.login(username, password) - return user + ldap_conn = ldap.bind(user, password) + return user, ldap_conn except PermissionDenied as err: raise err diff --git a/geruecht/exceptions/__init__.py b/geruecht/exceptions/__init__.py index 30bba52..6aeaf8e 100644 --- a/geruecht/exceptions/__init__.py +++ b/geruecht/exceptions/__init__.py @@ -1,2 +1,10 @@ class PermissionDenied(Exception): + pass +class UsernameExistDB(Exception): + pass +class UsernameExistLDAP(Exception): + pass +class DatabaseExecption(Exception): + pass +class LDAPExcetpion(Exception): pass \ No newline at end of file diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 92d7be1..d6f1a57 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -4,6 +4,7 @@ from datetime import datetime import geruecht.controller.userController as uc from geruecht.model import MONEY from geruecht.decorator import login_required +import time finanzer = Blueprint("finanzer", __name__) diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 0718dc4..f63db6c 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -15,8 +15,9 @@ class AccessToken(): timestamp = None user = None token = None + ldap_conn = None - def __init__(self, user, token, timestamp=datetime.now()): + def __init__(self, user, token, ldap_conn, timestamp=datetime.now()): """ Initialize Class AccessToken No more to say. @@ -30,6 +31,7 @@ class AccessToken(): self.user = user self.timestamp = timestamp self.token = token + self.ldap_conn = ldap_conn def updateTimestamp(self): """ Update the Timestamp diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 925ca37..23460fb 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -49,6 +49,7 @@ class User(): self.group = data['gruppe'].split(',') if 'creditLists' in data: self.geruechte = data['creditLists'] + self.password = '' def updateData(self, data): if 'dn' in data: @@ -204,7 +205,8 @@ class User(): "username": self.uid, "locked": self.locked, "autoLock": self.autoLock, - "limit": self.limit + "limit": self.limit, + "mail": self.mail } return dic diff --git a/geruecht/routes.py b/geruecht/routes.py index 9b7fa6b..bd71ea5 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -48,9 +48,9 @@ def _login(): password = data['password'] LOGGER.info("search {} in database".format(username)) try: - user = userController.loginUser(username, password) + user, ldap_conn = userController.loginUser(username, password) user.password = password - token = accesTokenController.createAccesToken(user) + token = accesTokenController.createAccesToken(user, ldap_conn) dic = accesTokenController.validateAccessToken(token, [USER]).user.toJSON() dic["token"] = token dic["accessToken"] = token diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 5b30297..1ef4846 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -33,4 +33,16 @@ def _addAmount(**kwargs): retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} return jsonify(retVal) - return jsonify({"error": "something went wrong"}), 500 \ No newline at end of file + return jsonify({"error": "something went wrong"}), 500 + +@user.route("/user/saveConfig", methods=['POST']) +@login_required(groups=[USER]) +def _saveConfig(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + data = request.get_json() + accToken.user = userController.modifyUser(accToken.user, accToken.ldap_conn, data) + return jsonify(data) + except Exception as err: + return jsonify("error", err), 409 \ No newline at end of file From ca1f2259c5c106f6e2f190226a850333f782a738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 26 Jan 2020 23:54:18 +0100 Subject: [PATCH 039/111] better return value when change ldap entrys --- geruecht/user/routes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 1ef4846..23958ce 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -3,6 +3,7 @@ from geruecht.decorator import login_required import geruecht.controller.userController as uc from geruecht.model import USER from datetime import datetime +import time user = Blueprint("user", __name__) @@ -43,6 +44,8 @@ def _saveConfig(**kwargs): accToken = kwargs['accToken'] data = request.get_json() accToken.user = userController.modifyUser(accToken.user, accToken.ldap_conn, data) - return jsonify(data) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + return jsonify(retVal) except Exception as err: - return jsonify("error", err), 409 \ No newline at end of file + return jsonify({"error": err}), 409 \ No newline at end of file From 3dca45f12e95cec8df53adf9fc257bf0eb4af553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 27 Jan 2020 20:16:04 +0100 Subject: [PATCH 040/111] add new route for user, so he can see, when he has jobs --- geruecht/controller/databaseController.py | 11 +++++---- geruecht/controller/ldapController.py | 2 +- geruecht/user/routes.py | 27 ++++++++++++++++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index ae62453..3666374 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -56,10 +56,12 @@ class DatabaseController(metaclass=Singleton): def _convertGroupToString(self, groups): retVal = '' - for group in groups: - if len(retVal) != 0: - retVal += ',' - retVal += group + print('groups: {}'.format(groups)) + if groups: + for group in groups: + if len(retVal) != 0: + retVal += ',' + retVal += group return retVal @@ -73,6 +75,7 @@ class DatabaseController(metaclass=Singleton): def updateUser(self, user): cursor = self.db.connection.cursor() + print('uid: {}; group: {}'.format(user.uid, user.group)) groups = self._convertGroupToString(user.group) sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 40c95b3..1356c45 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -69,7 +69,7 @@ class LDAPController(metaclass=Singleton): retVal.append(BAR) return retVal except Exception as err: - traceback.print_exception(err) + traceback.print_exc() def __isUserInList(self, list, username): help_list = [] diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 23958ce..139a96a 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -4,6 +4,7 @@ import geruecht.controller.userController as uc from geruecht.model import USER from datetime import datetime import time +import traceback user = Blueprint("user", __name__) @@ -48,4 +49,28 @@ def _saveConfig(**kwargs): retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} return jsonify(retVal) except Exception as err: - return jsonify({"error": err}), 409 \ No newline at end of file + return jsonify({"error": err}), 409 + +@user.route("/user/job", methods=['POST']) +@login_required(groups=[USER]) +def _getJob(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + data = request.get_json() + date = datetime.utcfromtimestamp(int(data['date'])) + test = userController.getWorker(date, username=accToken.user.uid) + if test == [None]: + job = False + else: + job = True + if job: + workers = userController.getWorker(date) + for worker in workers: + if worker['user']['uid'] == accToken.user.uid: + workers.remove(worker) + return jsonify({'job': job, 'workers': workers}) + return jsonify({'job': job}) + except Exception as err: + traceback.print_exc() + return jsonify({"error": str(err)}), 409 \ No newline at end of file From 69c4c98192c78db47bfced0d930c5ceba8c122ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 29 Jan 2020 23:24:02 +0100 Subject: [PATCH 041/111] required libs for backend in required.txt --- required.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 required.txt diff --git a/required.txt b/required.txt new file mode 100644 index 0000000..9b2b3bc --- /dev/null +++ b/required.txt @@ -0,0 +1,15 @@ +Click==7.0 +Flask==1.1.1 +Flask-Cors==3.0.8 +Flask-LDAPConn==0.10.1 +Flask-MySQLdb==0.2.0 +itsdangerous==1.1.0 +Jinja2==2.11.0 +ldap3==2.6.1 +MarkupSafe==1.1.1 +mysqlclient==1.4.6 +pyasn1==0.4.8 +PyMySQL==0.9.3 +PyYAML==5.3 +six==1.14.0 +Werkzeug==0.16.1 From ee65f12d7e4e0a896169b49c9ab9d9c51f9c10eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 19 Feb 2020 21:47:44 +0100 Subject: [PATCH 042/111] fixed ##86, no strange symbol in emails --- geruecht/controller/emailController.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index 23882d8..b11eb40 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -30,10 +30,10 @@ class EmailController(): msg = MIMEMultipart() msg['From'] = self.email msg['To'] = user.mail - msg['Subject'] = Header('Gerücht, bezahle deine ￿Schulden!', 'utf-8') + msg['Subject'] = Header('Gerücht, bezahle deine Schulden!', 'utf-8') sum = user.getGeruecht(datetime.now().year).getSchulden() if sum < 0: - type = '￿Schulden' + type = 'Schulden' add = 'Bezahle diese umgehend an den Finanzer.' else: type = 'Guthaben' @@ -46,4 +46,4 @@ class EmailController(): LOGGER.debug("Sended email to {}. {}".format(user.uid, {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} except Exception: - return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} \ No newline at end of file + return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} From 0035a2517da3364ecef1bb787d49f4f503b5a265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 19 Feb 2020 21:47:44 +0100 Subject: [PATCH 043/111] fixed ##178, no strange symbol in emails --- geruecht/controller/emailController.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index 23882d8..b11eb40 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -30,10 +30,10 @@ class EmailController(): msg = MIMEMultipart() msg['From'] = self.email msg['To'] = user.mail - msg['Subject'] = Header('Gerücht, bezahle deine ￿Schulden!', 'utf-8') + msg['Subject'] = Header('Gerücht, bezahle deine Schulden!', 'utf-8') sum = user.getGeruecht(datetime.now().year).getSchulden() if sum < 0: - type = '￿Schulden' + type = 'Schulden' add = 'Bezahle diese umgehend an den Finanzer.' else: type = 'Guthaben' @@ -46,4 +46,4 @@ class EmailController(): LOGGER.debug("Sended email to {}. {}".format(user.uid, {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} except Exception: - return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} \ No newline at end of file + return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} From 7038928e49a6ec5713b2fce86e81ada9916da883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 19 Feb 2020 23:11:24 +0100 Subject: [PATCH 044/111] route for baruser to storno, ##164 --- geruecht/baruser/routes.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 92ac2bb..a8c47a4 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -95,6 +95,37 @@ def _getUsers(**kwargs): retVal = ldap.getAllUser() return jsonify(retVal) +@baruser.route("/bar/storno", methods=['POST']) +@login_required(groups=[BAR]) +def _storno(**kwargs): + """ Function for Baruser to storno amount + + This function added to the user with the posted userID the posted amount. + + Returns: + JSON-File with userID and the amount + or ERROR 401 Permission Denied + """ + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) + + date = datetime.now() + userController.addCredit(userID, amount, year=date.year, month=date.month) + user = userController.getUser(userID) + geruecht = user.getGeruecht(year=date.year) + month = geruecht.getMonth(month=date.month) + amount = abs(month[0] - month[1]) + all = geruecht.getSchulden() + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic = user.toJSON() + dic['amount'] = abs(all) + dic['type'] = type + + return jsonify(dic) @baruser.route("/barGetUser", methods=['POST']) @login_required(groups=[BAR]) From e3bf18a92762c08c7d0d3f68cf14e18dd059fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 22 Feb 2020 11:15:20 +0100 Subject: [PATCH 045/111] finished ##168 --- geruecht/user/routes.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 139a96a..11cb669 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -53,24 +53,9 @@ def _saveConfig(**kwargs): @user.route("/user/job", methods=['POST']) @login_required(groups=[USER]) -def _getJob(**kwargs): - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - data = request.get_json() - date = datetime.utcfromtimestamp(int(data['date'])) - test = userController.getWorker(date, username=accToken.user.uid) - if test == [None]: - job = False - else: - job = True - if job: - workers = userController.getWorker(date) - for worker in workers: - if worker['user']['uid'] == accToken.user.uid: - workers.remove(worker) - return jsonify({'job': job, 'workers': workers}) - return jsonify({'job': job}) - except Exception as err: - traceback.print_exc() - return jsonify({"error": str(err)}), 409 \ No newline at end of file +def _getUser(**kwargs): + data = request.get_json() + date = datetime.utcfromtimestamp(int(data['date'])) + retVal = userController.getWorker(date) + print(retVal) + return jsonify(retVal) \ No newline at end of file From 77931a48c6fe5683d191c35d5f9035a16fb40794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 23 Feb 2020 11:18:54 +0100 Subject: [PATCH 046/111] finished ##188 for sm only send year, month, day. But you have to decrement month to send, because month starts with 0. Also you have to increment month to get. --- geruecht/controller/databaseController.py | 4 ++-- geruecht/user/routes.py | 5 ++++- geruecht/vorstand/routes.py | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 3666374..fe84080 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -132,14 +132,14 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) data = cursor.fetchone() - return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None + return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}} if data else None def getWorkers(self, date): cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) data = cursor.fetchall() - return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime']} for work in data] + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}} for work in data] def setWorker(self, user, date): diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 11cb669..b1af021 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -55,7 +55,10 @@ def _saveConfig(**kwargs): @login_required(groups=[USER]) def _getUser(**kwargs): data = request.get_json() - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) retVal = userController.getWorker(date) print(retVal) return jsonify(retVal) \ No newline at end of file diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 35e375b..a420330 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -18,7 +18,10 @@ def _addUser(**kwargs): data = request.get_json() user = data['user'] - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year,month,day,12) retVal = userController.addWorker(user['username'], date) print(retVal) return jsonify(retVal) @@ -27,7 +30,10 @@ def _addUser(**kwargs): @login_required(groups=[MONEY, GASTRO]) def _getUser(**kwargs): data = request.get_json() - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) retVal = userController.getWorker(date) print(retVal) return jsonify(retVal) @@ -37,6 +43,9 @@ def _getUser(**kwargs): def _deletUser(**kwargs): data = request.get_json() user = data['user'] - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) userController.deleteWorker(user['username'], date) return jsonify({"ok": "ok"}) \ No newline at end of file From 9a5c7e83669a146cdf4112297feb4cb54b925ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 23 Feb 2020 11:18:54 +0100 Subject: [PATCH 047/111] finished ##188 update routes in vorstand and user. You have to Parse day, month, and year. Datetime ist set to 12 o'clock --- geruecht/controller/databaseController.py | 4 ++-- geruecht/user/routes.py | 5 ++++- geruecht/vorstand/routes.py | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 3666374..fe84080 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -132,14 +132,14 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) data = cursor.fetchone() - return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime']} if data else None + return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}} if data else None def getWorkers(self, date): cursor = self.db.connection.cursor() cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) data = cursor.fetchall() - return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime']} for work in data] + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}} for work in data] def setWorker(self, user, date): diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 11cb669..b1af021 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -55,7 +55,10 @@ def _saveConfig(**kwargs): @login_required(groups=[USER]) def _getUser(**kwargs): data = request.get_json() - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) retVal = userController.getWorker(date) print(retVal) return jsonify(retVal) \ No newline at end of file diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 35e375b..a420330 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -18,7 +18,10 @@ def _addUser(**kwargs): data = request.get_json() user = data['user'] - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year,month,day,12) retVal = userController.addWorker(user['username'], date) print(retVal) return jsonify(retVal) @@ -27,7 +30,10 @@ def _addUser(**kwargs): @login_required(groups=[MONEY, GASTRO]) def _getUser(**kwargs): data = request.get_json() - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) retVal = userController.getWorker(date) print(retVal) return jsonify(retVal) @@ -37,6 +43,9 @@ def _getUser(**kwargs): def _deletUser(**kwargs): data = request.get_json() user = data['user'] - date = datetime.utcfromtimestamp(int(data['date'])) + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) userController.deleteWorker(user['username'], date) return jsonify({"ok": "ok"}) \ No newline at end of file From 576002a95cf31ebb1adb38e61b5c9dc620843199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 23 Feb 2020 11:46:38 +0100 Subject: [PATCH 048/111] fixed ##190 --- geruecht/controller/userController.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 6d551c8..23ca96d 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -81,8 +81,8 @@ class UserController(metaclass=Singleton): def checkBarUser(self, user): date = datetime.now() zero = date.replace(hour=0, minute=0, second=0, microsecond=0) - end = zero + timedelta(hours=11) - startdatetime = date.replace(hour=11, minute=0, second=0, microsecond=0) + end = zero + timedelta(hours=12) + startdatetime = date.replace(hour=12, minute=0, second=0, microsecond=0) if date > zero and end > date: startdatetime = startdatetime - timedelta(days=1) enddatetime = startdatetime + timedelta(days=1) From 65d09225b1b8112bf24a2e0adaca616e5973b0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 23 Feb 2020 21:27:03 +0100 Subject: [PATCH 049/111] finished ##189 it test if the date is lower or equal then date now if true the month will be locked in soft mode if one day exists in database the status of locked will not change --- geruecht/controller/databaseController.py | 281 +++++++++++++++------- geruecht/controller/userController.py | 9 + geruecht/user/routes.py | 5 +- geruecht/vorstand/routes.py | 5 +- 4 files changed, 205 insertions(+), 95 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index fe84080..c88c977 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -6,6 +6,7 @@ from geruecht.model.creditList import CreditList from datetime import datetime, timedelta from geruecht.exceptions import UsernameExistDB, DatabaseExecption import traceback +from MySQLdb._exceptions import IntegrityError class DatabaseController(metaclass=Singleton): ''' @@ -18,41 +19,55 @@ class DatabaseController(metaclass=Singleton): self.db = db def getAllUser(self): - cursor = self.db.connection.cursor() - cursor.execute("select * from user") - data = cursor.fetchall() + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user") + data = cursor.fetchall() - if data: - retVal = [] - for value in data: - user = User(value) - creditLists = self.getCreditListFromUser(user) - user.initGeruechte(creditLists) - retVal.append(user) - return retVal + if data: + retVal = [] + for value in data: + user = User(value) + creditLists = self.getCreditListFromUser(user) + user.initGeruechte(creditLists) + retVal.append(user) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def getUser(self, username): - retVal = None - cursor = self.db.connection.cursor() - cursor.execute("select * from user where uid='{}'".format(username)) - data = cursor.fetchone() - if data: - retVal = User(data) - creditLists = self.getCreditListFromUser(retVal) - retVal.initGeruechte(creditLists) - - return retVal + try: + retVal = None + cursor = self.db.connection.cursor() + cursor.execute("select * from user where uid='{}'".format(username)) + data = cursor.fetchone() + if data: + retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def getUserById(self, id): - retVal = None - cursor = self.db.connection.cursor() - cursor.execute("select * from user where id={}".format(id)) - data = cursor.fetchone() - if data: - retVal = User(data) - creditLists = self.getCreditListFromUser(retVal) - retVal.initGeruechte(creditLists) - return retVal + try: + retVal = None + cursor = self.db.connection.cursor() + cursor.execute("select * from user where id={}".format(id)) + data = cursor.fetchone() + if data: + retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def _convertGroupToString(self, groups): retVal = '' @@ -66,86 +81,124 @@ class DatabaseController(metaclass=Singleton): def insertUser(self, user): - cursor = self.db.connection.cursor() - groups = self._convertGroupToString(user.group) - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( - user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + groups = self._convertGroupToString(user.group) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( + user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def updateUser(self, user): - cursor = self.db.connection.cursor() - print('uid: {}; group: {}'.format(user.uid, user.group)) - groups = self._convertGroupToString(user.group) - sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( - user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) - print(sql) - cursor.execute(sql) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + print('uid: {}; group: {}'.format(user.uid, user.group)) + groups = self._convertGroupToString(user.group) + sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) + print(sql) + cursor.execute(sql) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def getCreditListFromUser(self, user, **kwargs): - cursor = self.db.connection.cursor() - if 'year' in kwargs: - sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) - else: - sql = "select * from creditList where user_id={}".format(user.id) - cursor.execute(sql) - data = cursor.fetchall() - if len(data) == 1: - return [CreditList(data[0])] - else: - return [CreditList(value) for value in data] + try: + cursor = self.db.connection.cursor() + if 'year' in kwargs: + sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) + else: + sql = "select * from creditList where user_id={}".format(user.id) + cursor.execute(sql) + data = cursor.fetchall() + if len(data) == 1: + return [CreditList(data[0])] + else: + return [CreditList(value) for value in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def createCreditList(self, user_id, year=datetime.now().year): - cursor = self.db.connection.cursor() - cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def updateCreditList(self, creditlist): - cursor = self.db.connection.cursor() - cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) - data = cursor.fetchall() - if len(data) == 0: - self.createCreditList(creditlist.user_id, creditlist.year) - sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, - creditlist.feb_guthaben, creditlist.feb_schulden, - creditlist.maer_guthaben, creditlist.maer_schulden, - creditlist.apr_guthaben, creditlist.apr_schulden, - creditlist.mai_guthaben, creditlist.mai_schulden, - creditlist.jun_guthaben, creditlist.jun_schulden, - creditlist.jul_guthaben, creditlist.jul_schulden, - creditlist.aug_guthaben, creditlist.aug_schulden, - creditlist.sep_guthaben, creditlist.sep_schulden, - creditlist.okt_guthaben, creditlist.okt_schulden, - creditlist.nov_guthaben, creditlist.nov_schulden, - creditlist.dez_guthaben, creditlist.dez_schulden, - creditlist.last_schulden, creditlist.year, creditlist.user_id) - print(sql) - cursor = self.db.connection.cursor() - cursor.execute(sql) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) + data = cursor.fetchall() + if len(data) == 0: + self.createCreditList(creditlist.user_id, creditlist.year) + sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, + creditlist.feb_guthaben, creditlist.feb_schulden, + creditlist.maer_guthaben, creditlist.maer_schulden, + creditlist.apr_guthaben, creditlist.apr_schulden, + creditlist.mai_guthaben, creditlist.mai_schulden, + creditlist.jun_guthaben, creditlist.jun_schulden, + creditlist.jul_guthaben, creditlist.jul_schulden, + creditlist.aug_guthaben, creditlist.aug_schulden, + creditlist.sep_guthaben, creditlist.sep_schulden, + creditlist.okt_guthaben, creditlist.okt_schulden, + creditlist.nov_guthaben, creditlist.nov_schulden, + creditlist.dez_guthaben, creditlist.dez_schulden, + creditlist.last_schulden, creditlist.year, creditlist.user_id) + print(sql) + cursor = self.db.connection.cursor() + cursor.execute(sql) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def getWorker(self, user, date): - cursor = self.db.connection.cursor() - cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - data = cursor.fetchone() - return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}} if data else None - + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + data = cursor.fetchone() + return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}} if data else None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def getWorkers(self, date): - cursor = self.db.connection.cursor() - cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) - data = cursor.fetchall() - return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}} for work in data] - + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) + data = cursor.fetchall() + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}} for work in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def setWorker(self, user, date): - cursor = self.db.connection.cursor() - cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) - self.db.connection.commit() + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def deleteWorker(self, user, date): @@ -155,6 +208,8 @@ class DatabaseController(metaclass=Singleton): self.db.connection.commit() except Exception as err: traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) def changeUsername(self, user, newUsername): try: @@ -165,9 +220,49 @@ class DatabaseController(metaclass=Singleton): raise UsernameExistDB("Username already exists") else: cursor.execute("update user set uid='{}' where id={}".format(newUsername, user.id)) - self.db.connection() + self.db.connection.commit() except Exception as err: traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getLockedDay(self, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from locked_days where daydate='{}'".format(date)) + data = cursor.fetchone() + return data + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def setLockedDay(self, date, locked, hard=False): + try: + cursor = self.db.connection.cursor() + sql = "insert into locked_days (daydate, locked) VALUES ('{}', {})".format(date, locked) + cursor.execute(sql) + self.db.connection.commit() + return self.getLockedDay(date) + except IntegrityError as err: + self.db.connection.rollback() + try: + exists = self.getLockedDay(date) + if hard: + sql = "update locked_days set locked={} where id={}".format(locked, exists['id']) + else: + sql = False + if sql: + cursor.execute(sql) + self.db.connection.commit() + return self.getLockedDay(date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseController("Something went wrong with Database: {}".format(err)) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) if __name__ == '__main__': diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 23ca96d..8171995 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -2,6 +2,7 @@ from . import LOGGER, Singleton, ldapConfig, dbConfig, mailConfig import geruecht.controller.databaseController as dc import geruecht.controller.ldapController as lc import geruecht.controller.emailController as ec +import calendar from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta @@ -16,6 +17,14 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def getLockedDay(self, date): + now = datetime.now() + daysInMonth = calendar.monthrange(date.year, date.month)[1] + if date.year <= now.year and date.month <= now.month: + for i in range(1, daysInMonth + 1): + db.setLockedDay(datetime(date.year, date.month, i).date(), True) + return db.getLockedDay(date.date()) + def getWorker(self, date, username=None): if (username): user = self.getUser(username) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index b1af021..469a66e 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -59,6 +59,9 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - retVal = userController.getWorker(date) + retVal = { + 'worker': userController.getWorker(date), + 'day': userController.getLockedDay(date) + } print(retVal) return jsonify(retVal) \ No newline at end of file diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index a420330..57680ac 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -34,7 +34,10 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - retVal = userController.getWorker(date) + retVal = { + 'worker': userController.getWorker(date), + 'day': userController.getLockedDay(date) + } print(retVal) return jsonify(retVal) From 12ca655d027fce235c1563c9b943f7eb359ba805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 23 Feb 2020 22:31:22 +0100 Subject: [PATCH 050/111] finished ##185 --- geruecht/controller/userController.py | 9 ++++-- geruecht/exceptions/__init__.py | 2 ++ geruecht/user/routes.py | 46 ++++++++++++++++++++++++--- geruecht/vorstand/routes.py | 21 +++++++++++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 8171995..cabf70b 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -6,7 +6,7 @@ import calendar from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta -from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion +from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion, DayLocked db = dc.DatabaseController() ldap = lc.LDAPController(ldapConfig['dn']) @@ -31,7 +31,12 @@ class UserController(metaclass=Singleton): return [db.getWorker(user, date)] return db.getWorkers(date) - def addWorker(self, username, date): + def addWorker(self, username, date, userExc=False): + if (userExc): + lockedDay = self.getLockedDay(date) + if lockedDay: + if lockedDay['locked']: + raise DayLocked("Day is locked. You can't get the Job") user = self.getUser(username) if (not db.getWorker(user, date)): db.setWorker(user, date) diff --git a/geruecht/exceptions/__init__.py b/geruecht/exceptions/__init__.py index 6aeaf8e..4ef2dbf 100644 --- a/geruecht/exceptions/__init__.py +++ b/geruecht/exceptions/__init__.py @@ -7,4 +7,6 @@ class UsernameExistLDAP(Exception): class DatabaseExecption(Exception): pass class LDAPExcetpion(Exception): + pass +class DayLocked(Exception): pass \ No newline at end of file diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 469a66e..e98be40 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -3,8 +3,7 @@ from geruecht.decorator import login_required import geruecht.controller.userController as uc from geruecht.model import USER from datetime import datetime -import time -import traceback +from geruecht.exceptions import DayLocked user = Blueprint("user", __name__) @@ -59,9 +58,48 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) + lockedDay = userController.getLockedDay(date) + if not lockedDay: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': False + } + else: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } retVal = { 'worker': userController.getWorker(date), - 'day': userController.getLockedDay(date) + 'day': lockedDay } print(retVal) - return jsonify(retVal) \ No newline at end of file + return jsonify(retVal) + +@user.route("/user/addJob", methods=['POST']) +@login_required(groups=[USER]) +def _addUser(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year,month,day,12) + retVal = userController.addWorker(user.uid, date, userExc=True) + print(retVal) + return jsonify(retVal) + except DayLocked as err: + return jsonify({'error': err}), 403 + except Exception as err: + return jsonify({'error': err}), 409 \ No newline at end of file diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 57680ac..40c6234 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -34,9 +34,28 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) + lockedDay = userController.getLockedDay(date) + if not lockedDay: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': False + } + else: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } retVal = { 'worker': userController.getWorker(date), - 'day': userController.getLockedDay(date) + 'day': lockedDay } print(retVal) return jsonify(retVal) From 61b9f7001a7ad608e1abc362f60c334705705cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 24 Feb 2020 12:19:52 +0100 Subject: [PATCH 051/111] finished ##187 --- geruecht/controller/userController.py | 10 +++++++- geruecht/vorstand/routes.py | 36 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index cabf70b..0f7c293 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -17,12 +17,20 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def setLockedDay(self, date, locked,hard=False): + return db.setLockedDay(date.date(), locked, hard) + def getLockedDay(self, date): now = datetime.now() daysInMonth = calendar.monthrange(date.year, date.month)[1] if date.year <= now.year and date.month <= now.month: for i in range(1, daysInMonth + 1): - db.setLockedDay(datetime(date.year, date.month, i).date(), True) + self.setLockedDay(datetime(date.year, date.month, i), True) + for i in range(1, 8): + nextMonth = datetime(date.year, date.month + 1, i) + if nextMonth.weekday() == 2: + break + self.setLockedDay(nextMonth, True) return db.getLockedDay(date.date()) def getWorker(self, date, username=None): diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 40c6234..8e6ff0d 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -70,4 +70,38 @@ def _deletUser(**kwargs): year = data['year'] date = datetime(year, month, day, 12) userController.deleteWorker(user['username'], date) - return jsonify({"ok": "ok"}) \ No newline at end of file + return jsonify({"ok": "ok"}) + +@vorstand.route("/sm/lockDay", methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _lockDay(**kwargs): + try: + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + locked = data['locked'] + date = datetime(year, month, day, 12) + lockedDay = userController.setLockedDay(date, locked, True) + if not lockedDay: + retVal = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': False + } + else: + retVal = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({'error': err}), 409 \ No newline at end of file From 4e91587731dd660045c3c27c307273eaac4cf978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 24 Feb 2020 18:55:57 +0100 Subject: [PATCH 052/111] finished ##193 route for user to delete job, but only if day not locked fixed a bug, that the month start on the first wednesday in month and ended in first tuesday in nextmonth --- geruecht/controller/userController.py | 14 ++++++++++++-- geruecht/user/routes.py | 21 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 0f7c293..293820e 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -23,8 +23,13 @@ class UserController(metaclass=Singleton): def getLockedDay(self, date): now = datetime.now() daysInMonth = calendar.monthrange(date.year, date.month)[1] + startMonth = 1 + for i in range(1, 8): + if datetime(date.year, date.month, i).weekday() == 2: + startMonth = i + break if date.year <= now.year and date.month <= now.month: - for i in range(1, daysInMonth + 1): + for i in range(startMonth, daysInMonth + 1): self.setLockedDay(datetime(date.year, date.month, i), True) for i in range(1, 8): nextMonth = datetime(date.year, date.month + 1, i) @@ -50,7 +55,12 @@ class UserController(metaclass=Singleton): db.setWorker(user, date) return self.getWorker(date, username=username) - def deleteWorker(self, username, date): + def deleteWorker(self, username, date, userExc=False): + if userExc: + lockedDay = self.getLockedDay(date) + if lockedDay: + if lockedDay['locked']: + raise DayLocked("Day is locked. You can't delete the Job") user = self.getUser(username) db.deleteWorker(user, date) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index e98be40..4f4ed33 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -102,4 +102,23 @@ def _addUser(**kwargs): except DayLocked as err: return jsonify({'error': err}), 403 except Exception as err: - return jsonify({'error': err}), 409 \ No newline at end of file + return jsonify({'error': err}), 409 + +@user.route("/user/deleteJob", methods=['POST']) +@login_required(groups=[USER]) +def _deletJob(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year,month,day,12) + userController.deleteWorker(user.uid, date, True) + return jsonify({"ok": "ok"}) + except DayLocked as err: + return jsonify({"error": err}), 403 + except Exception as err: + return jsonify({"error": err}), 409 \ No newline at end of file From 90d503f6aa7952a9f4a9ac59e4ec934470a53284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 25 Feb 2020 22:50:32 +0100 Subject: [PATCH 053/111] finished ##169 and ##170 (without config.yml) --- geruecht/baruser/routes.py | 4 +- geruecht/controller/databaseController.py | 106 ++++++++++++++++++- geruecht/controller/userController.py | 42 +++++++- geruecht/exceptions/__init__.py | 2 + geruecht/user/routes.py | 122 +++++++++++++++++++++- 5 files changed, 264 insertions(+), 12 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index a8c47a4..58dbc52 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -3,7 +3,7 @@ import geruecht.controller as gc import geruecht.controller.ldapController as lc import geruecht.controller.userController as uc from datetime import datetime -from geruecht.model import BAR, MONEY +from geruecht.model import BAR, MONEY, USER from geruecht.decorator import login_required baruser = Blueprint("baruser", __name__) @@ -146,7 +146,7 @@ def _getUser(**kwargs): @baruser.route("/search", methods=['POST']) -@login_required(groups=[BAR, MONEY]) +@login_required(groups=[BAR, MONEY, USER]) def _search(**kwargs): data = request.get_json() searchString = data['searchString'] diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index c88c977..b9c2ba3 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -259,12 +259,116 @@ class DatabaseController(metaclass=Singleton): except Exception as err: traceback.print_exc() self.db.connection.rollback() - raise DatabaseController("Something went wrong with Database: {}".format(err)) + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) except Exception as err: traceback.print_exc() self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + def setTransactJob(self, from_user, to_user, date): + try: + exists = self.getTransactJob(from_user, to_user, date) + if exists: + raise IntegrityError("job_transact already exists") + cursor = self.db.connection.cursor() + cursor.execute("insert into job_transact (jobdate, from_user_id, to_user_id) VALUES ('{}', {}, {})".format(date, from_user.id, to_user.id)) + self.db.connection.commit() + return self.getTransactJob(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getTransactJob(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) + data = cursor.fetchone() + if data: + return {"from_user": from_user, "to_user": to_user, "date": data['jobdate'], "answerd": data['answerd'], "accepted": data['accepted']} + return None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def getAllTransactJobFromUser(self, from_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={}".format(from_user.id)) + data = cursor.fetchall() + retVal = [] + for transact in data: + if date <= transact['jobdate']: + retVal.append({"from_user": from_user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getAllTransactJobToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where to_user_id={}".format(to_user.id)) + data = cursor.fetchall() + retVal = [] + for transact in data: + if date <= transact['jobdate']: + retVal.append({"to_user": to_user, "from_user": self.getUserById(transact['from_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getTransactJobToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where to_user_id={} and jobdate='{}'".format(to_user.id, date)) + data = cursor.fetchone() + if data: + return {"from_user": self.getUserById(data['from_user_id']), "to_user": to_user, "date": data['jobdate'], "accepted": data['accepted'], "answerd": data['answerd']} + else: + return None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def updateTransactJob(self, from_user, to_user, date, accepted): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_transact set accepted={}, answerd=true where to_user_id={} and jobdate='{}'".format(accepted, to_user.id, date)) + self.db.connection.commit() + return self.getTransactJob(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getTransactJobFromUser(self, user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={} and jobdate='{}'".format(user.id, date)) + data = cursor.fetchall() + return [{"from_user": user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']} for transact in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def deleteTransactJob(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 293820e..b7210ae 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -6,7 +6,7 @@ import calendar from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta -from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion, DayLocked +from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion, DayLocked, TansactJobIsAnswerdException db = dc.DatabaseController() ldap = lc.LDAPController(ldapConfig['dn']) @@ -17,7 +17,34 @@ class UserController(metaclass=Singleton): def __init__(self): pass - def setLockedDay(self, date, locked,hard=False): + def setTransactJob(self, from_user, to_user, date): + return db.setTransactJob(from_user, to_user, date.date()) + + def getTransactJobFromUser(self, user, date): + return db.getTransactJobFromUser(user, date.date()) + + def getAllTransactJobFromUser(self, user, date): + return db.getAllTransactJobFromUser(user, date.date()) + + def getAllTransactJobToUser(self, user, date): + return db.getAllTransactJobToUser(user, date.date()) + + def getTransactJob(self, from_user, to_user, date): + return db.getTransactJob(from_user, to_user, date.date()) + + def deleteTransactJob(self, from_user, to_user, date): + transactJob = self.getTransactJob(from_user, to_user, date) + if transactJob['answerd']: + raise TansactJobIsAnswerdException("TransactJob is already answerd") + db.deleteTransactJob(from_user, to_user, date.date()) + + def answerdTransactJob(self, from_user, to_user, date, answer): + transactJob = db.updateTransactJob(from_user, to_user, date.date(), answer) + if answer: + self.addWorker(to_user.uid, date) + return transactJob + + def setLockedDay(self, date, locked, hard=False): return db.setLockedDay(date.date(), locked, hard) def getLockedDay(self, date): @@ -56,12 +83,19 @@ class UserController(metaclass=Singleton): return self.getWorker(date, username=username) def deleteWorker(self, username, date, userExc=False): + user = self.getUser(username) if userExc: lockedDay = self.getLockedDay(date) if lockedDay: if lockedDay['locked']: - raise DayLocked("Day is locked. You can't delete the Job") - user = self.getUser(username) + transactJobs = self.getTransactJobFromUser(user, date) + found = False + for job in transactJobs: + if job['accepted'] and job['answerd']: + found = True + break + if not found: + raise DayLocked("Day is locked. You can't delete the Job") db.deleteWorker(user, date) def lockUser(self, username, locked): diff --git a/geruecht/exceptions/__init__.py b/geruecht/exceptions/__init__.py index 4ef2dbf..307c48e 100644 --- a/geruecht/exceptions/__init__.py +++ b/geruecht/exceptions/__init__.py @@ -9,4 +9,6 @@ class DatabaseExecption(Exception): class LDAPExcetpion(Exception): pass class DayLocked(Exception): + pass +class TansactJobIsAnswerdException(Exception): pass \ No newline at end of file diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 4f4ed33..beaade9 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -48,7 +48,7 @@ def _saveConfig(**kwargs): retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} return jsonify(retVal) except Exception as err: - return jsonify({"error": err}), 409 + return jsonify({"error": str(err)}), 409 @user.route("/user/job", methods=['POST']) @login_required(groups=[USER]) @@ -100,9 +100,9 @@ def _addUser(**kwargs): print(retVal) return jsonify(retVal) except DayLocked as err: - return jsonify({'error': err}), 403 + return jsonify({'error': str(err)}), 403 except Exception as err: - return jsonify({'error': err}), 409 + return jsonify({'error': str(err)}), 409 @user.route("/user/deleteJob", methods=['POST']) @login_required(groups=[USER]) @@ -119,6 +119,118 @@ def _deletJob(**kwargs): userController.deleteWorker(user.uid, date, True) return jsonify({"ok": "ok"}) except DayLocked as err: - return jsonify({"error": err}), 403 + return jsonify({"error": str(err)}), 403 except Exception as err: - return jsonify({"error": err}), 409 \ No newline at end of file + return jsonify({"error": str(err)}), 409 + +@user.route("/user/transactJob", methods=['POST']) +@login_required(groups=[USER]) +def _transactJob(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + username = data['user'] + date = datetime(year, month, day, 12) + to_user = userController.getUser(username) + retVal = userController.setTransactJob(user, to_user, date) + retVal['from_user'] = retVal['from_user'].toJSON() + retVal['to_user'] = retVal['to_user'].toJSON() + retVal['date'] = {'year': year, 'month': month, 'day': day} + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 409 + +@user.route("/user/answerTransactJob", methods=['POST']) +@login_required(groups=[USER]) +def _answer(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + answer = data['answer'] + username = data['username'] + date = datetime(year, month, day, 12) + from_user = userController.getUser(username) + retVal = userController.answerdTransactJob(from_user, user, date, answer) + retVal['from_user'] = retVal['from_user'].toJSON() + retVal['to_user'] = retVal['to_user'].toJSON() + retVal['date'] = {'year': year, 'month': month, 'day': day} + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 409 + +@user.route("/user/jobRequests", methods=['POST']) +@login_required(groups=[USER]) +def _requests(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + date = datetime(year, month, day, 12) + retVal = userController.getAllTransactJobToUser(user, date) + for data in retVal: + data['from_user'] = data['from_user'].toJSON() + data['to_user'] = data['to_user'].toJSON() + data_date = data['date'] + data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 409 + +@user.route("/user/getTransactJobs", methods=['POST']) +@login_required(groups=[USER]) +def _getTransactJobs(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + date = datetime(year, month, day, 12) + retVal = userController.getAllTransactJobFromUser(user, date) + for data in retVal: + data['from_user'] = data['from_user'].toJSON() + data['to_user'] = data['to_user'].toJSON() + data_date = data['date'] + data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 409 + +@user.route("/user/deleteTransactJob", methods=['POST']) +@login_required(groups=[USER]) +def _deleteTransactJob(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + from_user = accToken.user + data = request.get_json() + year = data['year'] + month = data['month'] + day = data['day'] + username = data['username'] + date = datetime(year, month, day, 12) + to_user = userController.getUser(username) + userController.deleteTransactJob(from_user, to_user, date) + return jsonify({"ok": "ok"}) + except Exception as err: + return jsonify({"error": str(err)}), 409 \ No newline at end of file From 80fbe2b7592b6b24d5db632c1cae8b316e4fe53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 26 Feb 2020 22:13:44 +0100 Subject: [PATCH 054/111] finished ##177 --- geruecht/configparser.py | 6 +++++- geruecht/controller/emailController.py | 24 +++++++++++++++++++----- geruecht/controller/userController.py | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/geruecht/configparser.py b/geruecht/configparser.py index e1ab855..dc519a0 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -10,7 +10,8 @@ default = { 'port': 0, 'user': '', 'passwd': '', - 'email': '' + 'email': '', + 'crypt': 'STARTTLS' } } @@ -61,6 +62,9 @@ class ConifgParser(): if 'email' not in self.config['Mail']: self.config['Mail']['email'] = default['Mail']['email'] LOGGER.info("No Config for email in Mail found. Set it to default") + if 'crypt' not in self.config['Mail']: + self.config['Mail']['crypt'] = default['Mail']['crypt'] + LOGGER.info("No Config for crypt in Mail found. Set it to default") self.mail = self.config['Mail'] LOGGER.info('Set Mailconfig: {}'.format(self.mail)) diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index b11eb40..ebad0da 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -3,24 +3,38 @@ from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header -from . import LOGGER +from geruecht import getLogger + +LOGGER = getLogger('E-MailController') class EmailController(): - def __init__(self, smtpServer, user, passwd, port = 587, email = ""): + def __init__(self, smtpServer, user, passwd, crypt, port=587, email=""): self.smtpServer = smtpServer self.port = port self.user = user self.passwd = passwd + self.crypt = crypt if email: self.email = email else: self.email = user + LOGGER.debug('Init EmailController with smtpServer={}, port={}, user={}, crypt={}, email={}'.format(smtpServer, user, port, crypt, self.email)) def __connect__(self): - self.smtp = smtplib.SMTP(self.smtpServer, self.port) - self.smtp.starttls() - self.smtp.login(self.user, self.passwd) + LOGGER.info('Connect to E-Mail-Server') + if self.crypt == 'SSL': + self.smtp = smtplib.SMTP_SSL(self.smtpServer, self.port) + log = self.smtp.ehlo() + LOGGER.debug(log) + if self.crypt == 'STARTTLS': + self.smtp = smtplib.SMTP(self.smtpServer, self.port) + log = self.smtp.ehlo() + LOGGER.debug(log) + log = self.smtp.starttls() + LOGGER.debug(log) + log = self.smtp.login(self.user, self.passwd) + LOGGER.debug(log) def sendMail(self, user): try: diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index b7210ae..ea562ca 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -10,7 +10,7 @@ from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExec db = dc.DatabaseController() ldap = lc.LDAPController(ldapConfig['dn']) -emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['port'], mailConfig['email']) +emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) class UserController(metaclass=Singleton): From 358826e8c406639411cbe4da2ce11115433e9d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 26 Feb 2020 22:49:23 +0100 Subject: [PATCH 055/111] try to update userdata in database from ldap when getAllUsersFromDB is executed --- geruecht/controller/ldapController.py | 4 +++- geruecht/controller/userController.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 1356c45..9fa3d5b 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -38,8 +38,10 @@ class LDAPController(metaclass=Singleton): 'dn': self.ldap.connection.response[0]['dn'], 'firstname': user['givenName'][0], 'lastname': user['sn'][0], - 'uid': username + 'uid': username, } + if user['mail']: + retVal['mail'] = user['mail'][0] return retVal except: raise PermissionDenied("No User exists with this uid.") diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index ea562ca..a7a542c 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -110,6 +110,14 @@ class UserController(metaclass=Singleton): db.updateUser(user) return self.getUser(username) + def __updateDataFromLDAP(self, user): + groups = ldap.getGroup(user.uid) + user_data = ldap.getUserData(user.uid) + user_data['gruppe'] = groups + user_data['group'] = groups + user.updateData(user_data) + db.updateUser(user) + def autoLock(self, user): if user.autoLock: if user.getGeruecht(year=datetime.now().year).getSchulden() <= (-1*user.limit): @@ -141,6 +149,10 @@ class UserController(metaclass=Singleton): def getAllUsersfromDB(self): users = db.getAllUser() for user in users: + try: + self.__updateDataFromLDAP(user) + except: + pass self.__updateGeruechte(user) return db.getAllUser() From ff2df817a3bbe5274c4998955aa27310a5816da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 26 Feb 2020 23:03:02 +0100 Subject: [PATCH 056/111] finished ##179 --- geruecht/__init__.py | 8 ++++---- geruecht/configparser.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 0963dd8..ed985cc 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -5,7 +5,7 @@ """ from .logger import getLogger -from geruecht.controller import dbConfig +from geruecht.controller import dbConfig, ldapConfig from flask_mysqldb import MySQL from flask_ldapconn import LDAPConn @@ -24,9 +24,9 @@ app.config['MYSQL_USER'] = dbConfig['user'] app.config['MYSQL_PASSWORD'] = dbConfig['passwd'] app.config['MYSQL_DB'] = dbConfig['database'] app.config['MYSQL_CURSORCLASS'] = 'DictCursor' -app.config['LDAP_SERVER'] = '192.168.5.128' -app.config['LDAP_PORT'] = 389 -app.config['LDAP_BINDDN'] = 'dc=ldap,dc=example,dc=local' +app.config['LDAP_SERVER'] = ldapConfig['URL'] +app.config['LDAP_PORT'] = ldapConfig['port'] +app.config['LDAP_BINDDN'] = ldapConfig['dn'] app.config['LDAP_USE_TLS'] = False app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] = True diff --git a/geruecht/configparser.py b/geruecht/configparser.py index dc519a0..5e98271 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -33,6 +33,9 @@ class ConifgParser(): self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') if 'URL' not in self.config['LDAP'] or 'dn' not in self.config['LDAP']: self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') + if 'port' not in self.config['LDAP']: + LOGGER.info('No Config for port in LDAP found. Set it to default: {}'.format(389)) + self.config['LDAP']['port'] = 389 self.ldap = self.config['LDAP'] LOGGER.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: From f3e2ef2515aa3528342cca44ab73acd65835677a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 27 Feb 2020 15:01:41 +0100 Subject: [PATCH 057/111] finished ##171 --- geruecht/controller/emailController.py | 44 ++++++++++++++++++++------ geruecht/controller/userController.py | 4 ++- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index ebad0da..314342c 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -19,7 +19,7 @@ class EmailController(): self.email = email else: self.email = user - LOGGER.debug('Init EmailController with smtpServer={}, port={}, user={}, crypt={}, email={}'.format(smtpServer, user, port, crypt, self.email)) + LOGGER.debug('Init EmailController with smtpServer={}, port={}, user={}, crypt={}, email={}'.format(smtpServer, port, user, crypt, self.email)) def __connect__(self): LOGGER.info('Connect to E-Mail-Server') @@ -36,7 +36,30 @@ class EmailController(): log = self.smtp.login(self.user, self.passwd) LOGGER.debug(log) - def sendMail(self, user): + def jobTransact(self, user, jobtransact): + date = '{}.{}.{}'.format(jobtransact['date'].day, jobtransact['date'].month, jobtransact['date'].year) + from_user = '{} {}'.format(jobtransact['from_user'].firstname, jobtransact['from_user'].lastname) + subject = 'Bardienstanfrage am {}'.format(date) + text = MIMEText( + "Hallo {} {},\n" + "{} fragt, ob du am {} zum Bardienst teilnehmen willst. Beantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'utf-8') + return (subject, text) + + def credit(self, user): + subject = Header('Gerücht, bezahle deine Schulden!', 'utf-8') + sum = user.getGeruecht(datetime.now().year).getSchulden() + if sum < 0: + type = 'Schulden' + add = 'Bezahle diese umgehend an den Finanzer.' + else: + type = 'Guthaben' + add = '' + text = MIMEText( + "Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format( + user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain', 'utf-8') + return (subject, text) + + def sendMail(self, user, type='credit', jobtransact=None): try: if user.mail == 'None' or not user.mail: LOGGER.debug("cant send email to {}. Has no email-address. {}".format(user.uid, {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) @@ -44,16 +67,17 @@ class EmailController(): msg = MIMEMultipart() msg['From'] = self.email msg['To'] = user.mail - msg['Subject'] = Header('Gerücht, bezahle deine Schulden!', 'utf-8') - sum = user.getGeruecht(datetime.now().year).getSchulden() - if sum < 0: - type = 'Schulden' - add = 'Bezahle diese umgehend an den Finanzer.' + + if type == 'credit': + subject, text = self.credit(user) + elif type == 'jobtransact': + subject, text = self.jobTransact(user, jobtransact) else: - type = 'Guthaben' - add = '' - text = MIMEText("Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format(user.firstname, user.lastname, type, abs(sum)/100, add), 'plain', 'utf-8') + raise Exception("Fail to send Email. No type is set. user={}, type={} , jobtransact={}".format(user, type, jobtransact)) + + msg['Subject'] = subject msg.attach(text) + LOGGER.debug("Send email to {}: '{}'".format(user.uid, msg.as_string())) self.__connect__() self.smtp.sendmail(self.email, user.mail, msg.as_string()) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index a7a542c..852662f 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -18,7 +18,9 @@ class UserController(metaclass=Singleton): pass def setTransactJob(self, from_user, to_user, date): - return db.setTransactJob(from_user, to_user, date.date()) + jobtransact = db.setTransactJob(from_user, to_user, date.date()) + emailController.sendMail(jobtransact['to_user'], 'jobtransact', jobtransact) + return jobtransact def getTransactJobFromUser(self, user, date): return db.getTransactJobFromUser(user, date.date()) From f296986924d23f429906ffba4447daf01d5ceae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 27 Feb 2020 15:53:49 +0100 Subject: [PATCH 058/111] finished ##201 api that user can storno transaction --- geruecht/user/routes.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index beaade9..ab041de 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -233,4 +233,31 @@ def _deleteTransactJob(**kwargs): userController.deleteTransactJob(from_user, to_user, date) return jsonify({"ok": "ok"}) except Exception as err: - return jsonify({"error": str(err)}), 409 \ No newline at end of file + return jsonify({"error": str(err)}), 409 + +@user.route("/user/storno", methods=['POST']) +@login_required(groups=[USER]) +def _storno(**kwargs): + """ Function for Baruser to storno amount + + This function added to the user with the posted userID the posted amount. + + Returns: + JSON-File with userID and the amount + or ERROR 401 Permission Denied + """ + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + user = accToken.user + data = request.get_json() + amount = int(data['amount']) + + date = datetime.now() + userController.addCredit(user.uid, amount, year=date.year, month=date.month) + accToken.user = userController.getUser(accToken.user.uid) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 409 From 3e61893baf0bf2e906f0c9d7aa8ee12efbbe2f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 27 Feb 2020 21:55:00 +0100 Subject: [PATCH 059/111] finished ##203 --- geruecht/controller/databaseController.py | 25 +++++++++++++++++++++++ geruecht/controller/userController.py | 7 +++++++ geruecht/routes.py | 10 +++++++++ 3 files changed, 42 insertions(+) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index b9c2ba3..124acbd 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -368,6 +368,31 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + def getPriceList(self): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from pricelist") + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getDrinkType(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = 'select * from drink_type where name={}'.format(name) + if type(name) == int: + sql = 'select * from drink_type where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) if __name__ == '__main__': db = DatabaseController() diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 852662f..f0a0d0e 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -17,6 +17,13 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def getPricelist(self): + list = db.getPriceList() + for element in list: + type = db.getDrinkType(element['type']) + element['type'] = type['name'] + return list + def setTransactJob(self, from_user, to_user, date): jobtransact = db.setTransactJob(from_user, to_user, date.date()) emailController.sendMail(jobtransact['to_user'], 'jobtransact', jobtransact) diff --git a/geruecht/routes.py b/geruecht/routes.py index bd71ea5..2cc7c4d 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -29,6 +29,16 @@ def _valid(): return jsonify(accToken.user.toJSON()) return jsonify({"error": "permission denied"}), 401 +@app.route("/pricelist", methods=['GET']) +def _getPricelist(): + try: + retVal = userController.getPricelist() + print(retVal) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}) + + @app.route("/login", methods=['POST']) def _login(): From 71c850c8c63f71f64978b96631fa3613a5633558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 1 Mar 2020 19:20:47 +0100 Subject: [PATCH 060/111] finished ##172 add, edit, delete drinks in pricelist add, edit, delete drinkTypes --- geruecht/__init__.py | 2 + geruecht/baruser/routes.py | 3 +- geruecht/controller/databaseController.py | 101 +++++++++++++++++++++- geruecht/controller/ldapController.py | 5 +- geruecht/controller/userController.py | 28 +++++- geruecht/gastro/__init__.py | 0 geruecht/gastro/routes.py | 71 +++++++++++++++ geruecht/routes.py | 8 ++ 8 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 geruecht/gastro/__init__.py create mode 100644 geruecht/gastro/routes.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index ed985cc..2815956 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -38,9 +38,11 @@ from geruecht.baruser.routes import baruser from geruecht.finanzer.routes import finanzer from geruecht.user.routes import user from geruecht.vorstand.routes import vorstand +from geruecht.gastro.routes import gastrouser LOGGER.info("Registrate bluebrints") app.register_blueprint(baruser) app.register_blueprint(finanzer) app.register_blueprint(user) app.register_blueprint(vorstand) +app.register_blueprint(gastrouser) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 58dbc52..0e2c85f 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,5 +1,4 @@ from flask import Blueprint, request, jsonify -import geruecht.controller as gc import geruecht.controller.ldapController as lc import geruecht.controller.userController as uc from datetime import datetime @@ -8,7 +7,7 @@ from geruecht.decorator import login_required baruser = Blueprint("baruser", __name__) -ldap= lc.LDAPController(gc.ldapConfig['URL'], gc.ldapConfig['dn']) +ldap= lc.LDAPController() userController = uc.UserController() diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 124acbd..3976ece 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -378,12 +378,67 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + def getDrinkPrice(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from pricelist where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from pricelist where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setDrinkPrice(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute( + "insert into pricelist (name, price, price_big, price_club, price_club_big, premium, premium_club, price_extern_club, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", + ( + drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], + drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'])) + self.db.connection.commit() + return self.getDrinkPrice(str(drink['name'])) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def updateDrinkPrice(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute("update pricelist set name=%s, price=%s, price_big=%s, price_club=%s, price_club_big=%s, premium=%s, premium_club=%s, price_extern_club=%s, type=%s where id=%s", + ( + drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'], drink['id'] + )) + self.db.connection.commit() + return self.getDrinkPrice(drink['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def deleteDrink(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from pricelist where id={}".format(drink['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + def getDrinkType(self, name): try: cursor = self.db.connection.cursor() if type(name) == str: - sql = 'select * from drink_type where name={}'.format(name) - if type(name) == int: + sql = "select * from drink_type where name='{}'".format(name) + elif type(name) == int: sql = 'select * from drink_type where id={}'.format(name) else: raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) @@ -394,6 +449,48 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + def setDrinkType(self, name): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into drink_type (name) values ('{}')".format(name)) + self.db.connection.commit() + return self.getDrinkType(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def updateDrinkType(self, type): + try: + cursor = self.db.connection.cursor() + cursor.execute("update drink_type set name='{}' where id={}".format(type['name'], type['id'])) + self.db.connection.commit() + return self.getDrinkType(type['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def deleteDrinkType(self, type): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from drink_type where id={}".format(type['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getAllDrinkTypes(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from drink_type') + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 9fa3d5b..2e09d9b 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -5,6 +5,7 @@ from geruecht.model import MONEY, USER, GASTRO, BAR from geruecht.exceptions import PermissionDenied from . import Singleton from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion +from geruecht import ldapConfig import traceback class LDAPController(metaclass=Singleton): @@ -12,8 +13,8 @@ class LDAPController(metaclass=Singleton): Authentification over LDAP. Create Account on-the-fly ''' - def __init__(self, dn='dc=ldap,dc=example,dc=local'): - self.dn = dn + def __init__(self): + self.dn = ldapConfig['dn'] self.ldap = ldap diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index f0a0d0e..5d912c4 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion, DayLocked, TansactJobIsAnswerdException db = dc.DatabaseController() -ldap = lc.LDAPController(ldapConfig['dn']) +ldap = lc.LDAPController() emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) class UserController(metaclass=Singleton): @@ -17,11 +17,31 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def deleteDrinkType(self, type): + db.deleteDrinkType(type) + + def updateDrinkType(self, type): + return db.updateDrinkType(type) + + def setDrinkType(self, type): + return db.setDrinkType(type) + + def deletDrinkPrice(self, drink): + db.deleteDrink(drink) + + def setDrinkPrice(self, drink): + retVal = db.setDrinkPrice(drink) + return retVal + + def updateDrinkPrice(self, drink): + retVal = db.updateDrinkPrice(drink) + return retVal + + def getAllDrinkTypes(self): + return db.getAllDrinkTypes() + def getPricelist(self): list = db.getPriceList() - for element in list: - type = db.getDrinkType(element['type']) - element['type'] = type['name'] return list def setTransactJob(self, from_user, to_user, date): diff --git a/geruecht/gastro/__init__.py b/geruecht/gastro/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geruecht/gastro/routes.py b/geruecht/gastro/routes.py new file mode 100644 index 0000000..9deca0a --- /dev/null +++ b/geruecht/gastro/routes.py @@ -0,0 +1,71 @@ +from flask import request, jsonify, Blueprint +from geruecht.decorator import login_required +import geruecht.controller.userController as uc +from geruecht.model import GASTRO + +gastrouser = Blueprint('gastrouser', __name__) + +userController = uc.UserController() + +@gastrouser.route('/gastro/setDrink', methods=['POST']) +@login_required(groups=[GASTRO]) +def setDrink(**kwargs): + try: + data = request.get_json() + retVal = userController.setDrinkPrice(data) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@gastrouser.route('/gastro/updateDrink', methods=['POST']) +@login_required(groups=[GASTRO]) +def updateDrink(**kwargs): + try: + data = request.get_json() + retVal = userController.updateDrinkPrice(data) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@gastrouser.route('/gastro/deleteDrink', methods=['POST']) +@login_required(groups=[GASTRO]) +def deleteDrink(**kwargs): + try: + data = request.get_json() + id = data['id'] + retVal = userController.deletDrinkPrice({"id": id}) + return jsonify({"ok": "ok"}) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@gastrouser.route('/gastro/setDrinkType', methods=['POST']) +@login_required(groups=[GASTRO]) +def setType(**kwark): + try: + data = request.get_json() + name = data['name'] + retVal = userController.setDrinkType(name) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@gastrouser.route('/gastro/updateDrinkType', methods=['POST']) +@login_required(groups=[GASTRO]) +def updateType(**kwargs): + try: + data = request.get_json() + retVal = userController.updateDrinkType(data) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@gastrouser.route('/gastro/deleteDrinkType', methods=['POST']) +@login_required(groups=[GASTRO]) +def deleteType(**kwargs): + try: + data = request.get_json() + userController.deleteDrinkType(data) + return jsonify({"ok": "ok"}) + except Exception as err: + return jsonify({"error": str(err)}), 500 + diff --git a/geruecht/routes.py b/geruecht/routes.py index 2cc7c4d..6801fb4 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -38,6 +38,14 @@ def _getPricelist(): except Exception as err: return jsonify({"error": str(err)}) +@app.route('/drinkTypes', methods=['GET']) +def getTypes(): + try: + retVal = userController.getAllDrinkTypes() + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + @app.route("/login", methods=['POST']) From 708ecb1aa6c8dedade2bccbc92e42bacfa204e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 1 Mar 2020 21:44:38 +0100 Subject: [PATCH 061/111] fixed bug ##206 bug fix that the --- geruecht/controller/userController.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 5d912c4..8578da7 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -78,17 +78,25 @@ class UserController(metaclass=Singleton): def getLockedDay(self, date): now = datetime.now() - daysInMonth = calendar.monthrange(date.year, date.month)[1] - startMonth = 1 + oldMonth = False for i in range(1, 8): - if datetime(date.year, date.month, i).weekday() == 2: - startMonth = i + if datetime(now.year, now.month, i).weekday() == 2: + if now.day < i: + oldMonth = True + break + lockedYear = date.year + lockedMonth = date.month if date.month < now.month else now.month - 1 if oldMonth else now.month + daysInMonth = calendar.monthrange(lockedYear, lockedMonth)[1] + startDay = 1 + for i in range(1, 8): + if datetime(lockedYear, lockedMonth, i).weekday() == 2: + startDay = i break - if date.year <= now.year and date.month <= now.month: - for i in range(startMonth, daysInMonth + 1): - self.setLockedDay(datetime(date.year, date.month, i), True) + if lockedYear <= now.year and lockedMonth <= now.month: + for i in range(startDay, daysInMonth + 1): + self.setLockedDay(datetime(lockedYear, lockedMonth, i), True) for i in range(1, 8): - nextMonth = datetime(date.year, date.month + 1, i) + nextMonth = datetime(lockedYear, lockedMonth + 1, i) if nextMonth.weekday() == 2: break self.setLockedDay(nextMonth, True) From 068da1e57b066e4d70c14f27269ced250101e864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 3 Mar 2020 22:33:47 +0100 Subject: [PATCH 062/111] finished ##208 --- geruecht/baruser/routes.py | 2 +- geruecht/controller/databaseController.py | 80 +++++++++++++++++++++++ geruecht/controller/userController.py | 21 ++++++ geruecht/model/creditList.py | 2 +- geruecht/model/user.py | 17 ++++- geruecht/routes.py | 29 ++++++++ geruecht/vorstand/routes.py | 54 +++++++++++++++ 7 files changed, 202 insertions(+), 3 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 0e2c85f..706b938 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -39,7 +39,7 @@ def _bar(**kwargs): dic[user.uid] = {"username": user.uid, "firstname": user.firstname, "lastname": user.lastname, - "amount": abs(all), + "amount": all, "locked": user.locked, "type": type } diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 3976ece..b54f846 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -491,6 +491,86 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went worng with Database: {}".format(err)) + def getAllStatus(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from statusgroup') + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getStatus(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from statusgroup where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from statusgroup where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setStatus(self, name): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into statusgroup (name) values ('{}')".format(name)) + self.db.connection.commit() + return self.getStatus(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateStatus(self, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("update statusgroup set name='{}' where id={}".format(status['name'], status['id'])) + self.db.connection.commit() + return self.getStatus(status['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteStatus(self, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from statusgroup where id={}".format(status['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateStatusOfUser(self, username, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("update user set statusgroup={} where uid='{}'".format(status['id'], username)) + self.db.connection.commit() + return self.getUser(username) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateVotingOfUser(self, username, voting): + try: + cursor = self.db.connection.cursor() + cursor.execute("update user set voting={} where uid='{}'".format(voting, username)) + self.db.connection.commit() + return self.getUser(username) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 8578da7..497b2bb 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -17,6 +17,27 @@ class UserController(metaclass=Singleton): def __init__(self): pass + def getAllStatus(self): + return db.getAllStatus() + + def getStatus(self, name): + return db.getStatus(name) + + def setStatus(self, name): + return db.setStatus(name) + + def deleteStatus(self, status): + db.deleteStatus(status) + + def updateStatus(self, status): + return db.updateStatus(status) + + def updateStatusOfUser(self, username, status): + return db.updateStatusOfUser(username, status) + + def updateVotingOfUser(self, username, voting): + return db.updateVotingOfUser(username, voting) + def deleteDrinkType(self, type): db.deleteDrinkType(type) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 5bb8688..cf1a605 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -142,7 +142,7 @@ class CreditList(): elif month == 2: retValue = (self.feb_guthaben, self.feb_schulden) elif month == 3: - retValue = (self.mear_guthaben, self.maer_schulden) + retValue = (self.maer_guthaben, self.maer_schulden) elif month == 4: retValue = (self.apr_guthaben, self.apr_schulden) elif month == 5: diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 23460fb..a1a0e8e 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -27,6 +27,14 @@ class User(): self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] + if 'statusgroup' in data: + self.statusgroup = data['statusgroup'] + else: + self.statusgroup = None + if 'voting' in data: + self.voting = data['voting'] + else: + self.voting = None if 'mail' in data: self.mail = data['mail'] else: @@ -68,6 +76,10 @@ class User(): self.autoLock = bool(data['autoLock']) if 'mail' in data: self.mail = data['mail'] + if 'statusgorup' in data: + self.statusgroup = data['statusgroup'] + if 'voting' in data: + self.voting = data['voting'] def initGeruechte(self, creditLists): if type(creditLists) == list: @@ -196,6 +208,7 @@ class User(): A Dic with static Attributes. """ dic = { + "id": self.id, "userId": self.uid, "uid": self.uid, "dn": self.dn, @@ -206,7 +219,9 @@ class User(): "locked": self.locked, "autoLock": self.autoLock, "limit": self.limit, - "mail": self.mail + "mail": self.mail, + "statusgroup": self.statusgroup, + "voting": self.voting } return dic diff --git a/geruecht/routes.py b/geruecht/routes.py index 6801fb4..785327e 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,4 +1,5 @@ from geruecht import app, LOGGER +from geruecht.decorator import login_required from geruecht.exceptions import PermissionDenied import geruecht.controller.accesTokenController as ac import geruecht.controller.userController as uc @@ -46,7 +47,35 @@ def getTypes(): except Exception as err: return jsonify({"error": str(err)}), 500 +@app.route('/getAllStatus', methods=['GET']) +@login_required(groups=[USER, MONEY, GASTRO, BAR]) +def _getAllStatus(**kwargs): + try: + retVal = userController.getAllStatus() + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 +@app.route('/getStatus', methods=['POST']) +@login_required(groups=[USER, MONEY, GASTRO, BAR]) +def _getStatus(**kwargs): + try: + data = request.get_json() + name = data['name'] + retVal = userController.getStatus(name) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@app.route('/getUsers', methods=['GET']) +@login_required(groups=[MONEY, GASTRO]) +def _getUsers(**kwargs): + try: + users = userController.getAllUsersfromDB() + retVal = [user.toJSON() for user in users] + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 @app.route("/login", methods=['POST']) def _login(): diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 8e6ff0d..2dd9c29 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -8,6 +8,60 @@ import time vorstand = Blueprint("vorstand", __name__) userController = uc.UserController() +@vorstand.route('/um/setStatus', methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _setStatus(**kwargs): + try: + data = request.get_json() + name = data['name'] + retVal = userController.setStatus(name) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@vorstand.route('/um/updateStatus', methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _updateStatus(**kwargs): + try: + data = request.get_json() + retVal = userController.updateStatus(data) + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@vorstand.route('/um/deleteStatus', methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _deleteStatus(**kwargs): + try: + data = request.get_json() + userController.deleteStatus(data) + return jsonify({"ok": "ok"}) + except Exception as err: + return jsonify({"error": str(err)}), 409 + +@vorstand.route('/um/updateStatusUser', methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _updateStatusUser(**kwargs): + try: + data = request.get_json() + username = data['username'] + status = data['status'] + retVal = userController.updateStatusOfUser(username, status).toJSON() + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@vorstand.route('/um/updateVoting', methods=['POST']) +@login_required(groups=[MONEY, GASTRO]) +def _updateVoting(**kwargs): + try: + data = request.get_json() + username = data['username'] + voting = data['voting'] + retVal = userController.updateVotingOfUser(username, voting).toJSON() + return jsonify(retVal) + except Exception as err: + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/addUser", methods=['POST', 'GET']) @login_required(groups=[MONEY, GASTRO]) From abe081c589ab27edb68ef319e8006f42a8a144c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 4 Mar 2020 21:11:41 +0100 Subject: [PATCH 063/111] finished ##213 --- geruecht/baruser/routes.py | 15 ++++++------ geruecht/controller/databaseController.py | 4 +++- geruecht/controller/ldapController.py | 7 ++++-- geruecht/controller/userController.py | 6 ++++- geruecht/model/__init__.py | 2 ++ geruecht/routes.py | 12 +++++----- geruecht/vorstand/routes.py | 28 +++++++++++++++-------- 7 files changed, 47 insertions(+), 27 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 706b938..8653305 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify import geruecht.controller.ldapController as lc import geruecht.controller.userController as uc from datetime import datetime -from geruecht.model import BAR, MONEY, USER +from geruecht.model import BAR, MONEY, USER, VORSTAND from geruecht.decorator import login_required baruser = Blueprint("baruser", __name__) @@ -143,11 +143,12 @@ def _getUser(**kwargs): retVal['type'] = type return jsonify(retVal) - -@baruser.route("/search", methods=['POST']) -@login_required(groups=[BAR, MONEY, USER]) +@baruser.route("/search", methods=['GET']) +@login_required(groups=[BAR, MONEY, USER,VORSTAND]) def _search(**kwargs): - data = request.get_json() - searchString = data['searchString'] - retVal = ldap.searchUser(searchString) + retVal = ldap.getAllUser() + for user in retVal: + if user['username'] == 'extern': + retVal.remove(user) + break return jsonify(retVal) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index b54f846..d2d1fdb 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -18,7 +18,7 @@ class DatabaseController(metaclass=Singleton): def __init__(self): self.db = db - def getAllUser(self): + def getAllUser(self, extern=False): try: cursor = self.db.connection.cursor() cursor.execute("select * from user") @@ -27,6 +27,8 @@ class DatabaseController(metaclass=Singleton): if data: retVal = [] for value in data: + if extern and value['uid'] == 'extern': + continue user = User(value) creditLists = self.getCreditListFromUser(user) user.initGeruechte(creditLists) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 2e09d9b..b79ea56 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -1,7 +1,7 @@ from geruecht import ldap from ldap3 import SUBTREE, MODIFY_REPLACE, HASHED_SALTED_MD5 from ldap3.utils.hashed import hashed -from geruecht.model import MONEY, USER, GASTRO, BAR +from geruecht.model import MONEY, USER, GASTRO, BAR, VORSTAND, EXTERN from geruecht.exceptions import PermissionDenied from . import Singleton from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion @@ -59,6 +59,8 @@ class LDAPController(metaclass=Singleton): group_name = self.ldap.connection.response[0]['attributes']['cn'][0] if group_name == 'ldap-user': retVal.append(USER) + if group_name == 'extern': + retVal.append(EXTERN) self.ldap.connection.search('ou=group,{}'.format(self.dn), '(memberUID={})'.format(username), SUBTREE, attributes=['cn']) groups_data = self.ldap.connection.response @@ -70,6 +72,8 @@ class LDAPController(metaclass=Singleton): retVal.append(GASTRO) elif group_name == 'bar': retVal.append(BAR) + elif group_name == 'vorstand': + retVal.append(VORSTAND) return retVal except Exception as err: traceback.print_exc() @@ -84,7 +88,6 @@ class LDAPController(metaclass=Singleton): def getAllUser(self): retVal = [] - self.ldap.connection.search() self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) data = self.ldap.connection.response for user in data: diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 497b2bb..8f9ae5c 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -187,6 +187,8 @@ class UserController(metaclass=Singleton): def addAmount(self, username, amount, year, month, finanzer=False): user = self.getUser(username) + if user.uid == 'extern': + return if not user.locked or finanzer: user.addAmount(amount, year=year, month=month) creditLists = user.updateGeruecht() @@ -197,6 +199,8 @@ class UserController(metaclass=Singleton): def addCredit(self, username, credit, year, month): user = self.getUser(username) + if user.uid == 'extern': + return user.addCredit(credit, year=year, month=month) creditLists = user.updateGeruecht() for creditList in creditLists: @@ -212,7 +216,7 @@ class UserController(metaclass=Singleton): except: pass self.__updateGeruechte(user) - return db.getAllUser() + return db.getAllUser(extern=True) def checkBarUser(self, user): date = datetime.now() diff --git a/geruecht/model/__init__.py b/geruecht/model/__init__.py index a0d2bbb..065f441 100644 --- a/geruecht/model/__init__.py +++ b/geruecht/model/__init__.py @@ -1,4 +1,6 @@ MONEY = "moneymaster" +VORSTAND = "vorstand" +EXTERN = "extern" GASTRO = "gastro" USER = "user" BAR = "bar" \ No newline at end of file diff --git a/geruecht/routes.py b/geruecht/routes.py index 785327e..4fedfcd 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -3,7 +3,7 @@ from geruecht.decorator import login_required from geruecht.exceptions import PermissionDenied import geruecht.controller.accesTokenController as ac import geruecht.controller.userController as uc -from geruecht.model import MONEY, BAR, USER, GASTRO +from geruecht.model import MONEY, BAR, USER, GASTRO, VORSTAND, EXTERN from flask import request, jsonify accesTokenController = ac.AccesTokenController() @@ -48,7 +48,7 @@ def getTypes(): return jsonify({"error": str(err)}), 500 @app.route('/getAllStatus', methods=['GET']) -@login_required(groups=[USER, MONEY, GASTRO, BAR]) +@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getAllStatus(**kwargs): try: retVal = userController.getAllStatus() @@ -57,7 +57,7 @@ def _getAllStatus(**kwargs): return jsonify({"error": str(err)}), 500 @app.route('/getStatus', methods=['POST']) -@login_required(groups=[USER, MONEY, GASTRO, BAR]) +@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getStatus(**kwargs): try: data = request.get_json() @@ -68,7 +68,7 @@ def _getStatus(**kwargs): return jsonify({"error": str(err)}), 500 @app.route('/getUsers', methods=['GET']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUsers(**kwargs): try: users = userController.getAllUsersfromDB() @@ -98,14 +98,14 @@ def _login(): user, ldap_conn = userController.loginUser(username, password) user.password = password token = accesTokenController.createAccesToken(user, ldap_conn) - dic = accesTokenController.validateAccessToken(token, [USER]).user.toJSON() + dic = accesTokenController.validateAccessToken(token, [USER, EXTERN]).user.toJSON() dic["token"] = token dic["accessToken"] = token LOGGER.info("User {} success login.".format(username)) return jsonify(dic) except PermissionDenied as err: return jsonify({"error": str(err)}), 401 - except Exception: + except Exception as err: return jsonify({"error": "permission denied"}), 401 LOGGER.info("User {} does not exist.".format(username)) return jsonify({"error": "wrong username"}), 401 diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 2dd9c29..c83bff3 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -1,15 +1,17 @@ from flask import Blueprint, request, jsonify from datetime import datetime import geruecht.controller.userController as uc +import geruecht.controller.ldapController as lc from geruecht.decorator import login_required -from geruecht.model import MONEY, GASTRO +from geruecht.model import MONEY, GASTRO, VORSTAND import time vorstand = Blueprint("vorstand", __name__) userController = uc.UserController() +ldap= lc.LDAPController() @vorstand.route('/um/setStatus', methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _setStatus(**kwargs): try: data = request.get_json() @@ -20,7 +22,7 @@ def _setStatus(**kwargs): return jsonify({"error": str(err)}), 500 @vorstand.route('/um/updateStatus', methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatus(**kwargs): try: data = request.get_json() @@ -30,7 +32,7 @@ def _updateStatus(**kwargs): return jsonify({"error": str(err)}), 500 @vorstand.route('/um/deleteStatus', methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deleteStatus(**kwargs): try: data = request.get_json() @@ -40,7 +42,7 @@ def _deleteStatus(**kwargs): return jsonify({"error": str(err)}), 409 @vorstand.route('/um/updateStatusUser', methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatusUser(**kwargs): try: data = request.get_json() @@ -52,7 +54,7 @@ def _updateStatusUser(**kwargs): return jsonify({"error": str(err)}), 500 @vorstand.route('/um/updateVoting', methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateVoting(**kwargs): try: data = request.get_json() @@ -64,7 +66,7 @@ def _updateVoting(**kwargs): return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/addUser", methods=['POST', 'GET']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _addUser(**kwargs): if request.method == 'GET': @@ -81,7 +83,7 @@ def _addUser(**kwargs): return jsonify(retVal) @vorstand.route("/sm/getUser", methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUser(**kwargs): data = request.get_json() day = data['day'] @@ -127,7 +129,7 @@ def _deletUser(**kwargs): return jsonify({"ok": "ok"}) @vorstand.route("/sm/lockDay", methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _lockDay(**kwargs): try: data = request.get_json() @@ -158,4 +160,10 @@ def _lockDay(**kwargs): print(retVal) return jsonify(retVal) except Exception as err: - return jsonify({'error': err}), 409 \ No newline at end of file + return jsonify({'error': err}), 409 + +@vorstand.route("/sm/searchWithExtern", methods=['GET']) +@login_required(groups=[VORSTAND]) +def _search(**kwargs): + retVal = ldap.getAllUser() + return jsonify(retVal) \ No newline at end of file From e0c3581a4c88c082818738401f164d4a88510467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 4 Mar 2020 21:38:21 +0100 Subject: [PATCH 064/111] fixed ##193 --- geruecht/controller/accesTokenController.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index b23e2d1..dd7a4ae 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -33,9 +33,11 @@ class AccesTokenController(metaclass=Singleton): def checkBar(self, user): if (userController.checkBarUser(user)): - user.group.append(BAR) - elif BAR in user.group: - user.group.remove(BAR) + if BAR not in user.group: + user.group.append(BAR) + else: + while BAR in user.group: + user.group.remove(BAR) def validateAccessToken(self, token, group): """ Verify Accestoken From 0d1c116da75478e2fa970f2ca16be1775e130f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 5 Mar 2020 21:24:32 +0100 Subject: [PATCH 065/111] fixed bug that deleteWorker has access from `vorstand` --- geruecht/vorstand/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index c83bff3..26185b8 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -117,7 +117,7 @@ def _getUser(**kwargs): return jsonify(retVal) @vorstand.route("/sm/deleteUser", methods=['POST']) -@login_required(groups=[MONEY, GASTRO]) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deletUser(**kwargs): data = request.get_json() user = data['user'] From 39095af89121a8c55e43b409ea08f6c24ecc1e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 7 Mar 2020 14:56:44 +0100 Subject: [PATCH 066/111] finished ##218 --- geruecht/controller/accesTokenController.py | 25 ++++++++------ geruecht/model/accessToken.py | 3 +- geruecht/routes.py | 37 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index dd7a4ae..19d3951 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -53,25 +53,28 @@ class AccesTokenController(metaclass=Singleton): """ LOGGER.info("Verify AccessToken with token: {} and group: {}".format(token, group)) for accToken in self.tokenList: - LOGGER.debug("Check is token {} same as in AccessToken {}".format(token, accToken)) - if accToken == token: - LOGGER.debug("AccessToken is {}".format(accToken)) - endTime = accToken.timestamp + timedelta(seconds=self.lifetime) - now = datetime.now() - LOGGER.debug("Check if AccessToken's Endtime {} is bigger then now {}".format(endTime, now)) - if now <= endTime: + LOGGER.debug("AccessToken is {}".format(accToken)) + endTime = accToken.timestamp + timedelta(seconds=accToken.lifetime) + now = datetime.now() + LOGGER.debug("Check if AccessToken's Endtime {} is bigger then now {}".format(endTime, now)) + if now <= endTime: + LOGGER.debug("Check is token {} same as in AccessToken {}".format(token, accToken)) + if accToken == token: self.checkBar(accToken.user) LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) if self.isSameGroup(accToken, group): accToken.updateTimestamp() LOGGER.info("Found AccessToken {} with token: {} and group: {}".format(accToken, token, group)) return accToken - else: - LOGGER.debug("AccessToken {} is no longer valid and will removed".format(accToken)) - self.tokenList.remove(accToken) + else: + self.deleteAccessToken(accToken) LOGGER.info("Found no valid AccessToken with token: {} and group: {}".format(token, group)) return False + def deleteAccessToken(self, accToken): + LOGGER.debug("AccessToken {} is no longer valid and will removed".format(accToken)) + self.tokenList.remove(accToken) + def createAccesToken(self, user, ldap_conn): """ Create an AccessToken @@ -87,7 +90,7 @@ class AccesTokenController(metaclass=Singleton): now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() self.checkBar(user) - accToken = AccessToken(user, token, ldap_conn, datetime.now()) + accToken = AccessToken(user, token, ldap_conn, self.lifetime, datetime.now()) LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) self.tokenList.append(accToken) LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index f63db6c..577010c 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -17,7 +17,7 @@ class AccessToken(): token = None ldap_conn = None - def __init__(self, user, token, ldap_conn, timestamp=datetime.now()): + def __init__(self, user, token, ldap_conn, lifetime, timestamp=datetime.now()): """ Initialize Class AccessToken No more to say. @@ -30,6 +30,7 @@ class AccessToken(): LOGGER.debug("Initialize AccessToken") self.user = user self.timestamp = timestamp + self.lifetime = lifetime self.token = token self.ldap_conn = ldap_conn diff --git a/geruecht/routes.py b/geruecht/routes.py index 4fedfcd..05dced8 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -77,6 +77,43 @@ def _getUsers(**kwargs): except Exception as err: return jsonify({"error": str(err)}), 500 +@app.route("/getLifeTime", methods=['GET']) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +def _getLifeTime(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + return jsonify({"value": accToken.lifetime}) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@app.route("/saveLifeTime", methods=['POST']) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +def _saveLifeTime(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + + data = request.get_json() + lifetime = data['value'] + accToken.lifetime = lifetime + accToken.updateTimestamp() + + return jsonify({"value": accToken.lifetime}) + except Exception as err: + return jsonify({"error": str(err)}), 500 + +@app.route("/logout", methods=['GET']) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +def _logout(**kwargs): + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + accesTokenController.deleteAccessToken(accToken) + return jsonify({"ok": "ok"}) + except Exception as err: + return jsonify({"error": str(err)}), 500 + @app.route("/login", methods=['POST']) def _login(): """ Login User From 57f0f17a903ee3a591959d2f5d42864b76d2f631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 9 Mar 2020 19:54:51 +0100 Subject: [PATCH 067/111] add new logger --- geruecht/__init__.py | 10 ++-- geruecht/configparser.py | 33 +++++++------ geruecht/controller/emailController.py | 2 +- geruecht/decorator.py | 5 ++ geruecht/finanzer/__init__.py | 2 +- geruecht/logger.py | 22 +++++++-- geruecht/model/accessToken.py | 6 +-- geruecht/model/creditList.py | 2 +- geruecht/routes.py | 67 ++++++++++++++++++++------ 9 files changed, 104 insertions(+), 45 deletions(-) diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 2815956..8cb368a 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -4,18 +4,18 @@ Initialize also a singelton for the AccesTokenControler and start the Thread. """ -from .logger import getLogger +from .logger import getDebugLogger, getInfoLogger from geruecht.controller import dbConfig, ldapConfig from flask_mysqldb import MySQL from flask_ldapconn import LDAPConn -LOGGER = getLogger(__name__) -LOGGER.info("Initialize App") +DEBUG = getDebugLogger('INIT', True) +DEBUG.info("Initialize App") from flask import Flask from flask_cors import CORS -LOGGER.info("Build APP") +DEBUG.info("Build APP") app = Flask(__name__) CORS(app) app.config['SECRET_KEY'] = '0a657b97ef546da90b2db91862ad4e29' @@ -40,7 +40,7 @@ from geruecht.user.routes import user from geruecht.vorstand.routes import vorstand from geruecht.gastro.routes import gastrouser -LOGGER.info("Registrate bluebrints") +DEBUG.info("Registrate bluebrints") app.register_blueprint(baruser) app.register_blueprint(finanzer) app.register_blueprint(user) diff --git a/geruecht/configparser.py b/geruecht/configparser.py index 5e98271..308ba78 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -1,7 +1,7 @@ import yaml import sys -from .logger import getLogger -LOGGER = getLogger(__name__) +from .logger import getDebugLogger, getInfoLogger +DEBUG = getDebugLogger("CONFIG", True) default = { 'AccessTokenLifeTime': 1800, @@ -27,49 +27,50 @@ class ConifgParser(): self.__error__('Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') self.db = self.config['Database'] - LOGGER.debug("Set Databaseconfig: {}".format(self.db)) + DEBUG.debug("Set Databaseconfig: {}".format(self.db)) if 'LDAP' not in self.config: self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') if 'URL' not in self.config['LDAP'] or 'dn' not in self.config['LDAP']: self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') if 'port' not in self.config['LDAP']: - LOGGER.info('No Config for port in LDAP found. Set it to default: {}'.format(389)) + DEBUG.info('No Config for port in LDAP found. Set it to default: {}'.format(389)) self.config['LDAP']['port'] = 389 self.ldap = self.config['LDAP'] - LOGGER.info("Set LDAPconfig: {}".format(self.ldap)) + DEBUG.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: self.accessTokenLifeTime = int(self.config['AccessTokenLifeTime']) - LOGGER.info("Set AccessTokenLifeTime: {}".format(self.accessTokenLifeTime)) + DEBUG.info("Set AccessTokenLifeTime: {}".format(self.accessTokenLifeTime)) else: self.accessTokenLifeTime = default['AccessTokenLifeTime'] - LOGGER.info("No Config for AccessTokenLifetime found. Set it to default: {}".format(self.accessTokenLifeTime)) + DEBUG.info("No Config for AccessTokenLifetime found. Set it to default: {}".format(self.accessTokenLifeTime)) if 'Mail' not in self.config: self.config['Mail'] = default['Mail'] - LOGGER.info('No Conifg for Mail found. Set it to defaul: {}'.format(self.config['Mail'])) + DEBUG.info('No Conifg for Mail found. Set it to defaul: {}'.format(self.config['Mail'])) if 'URL' not in self.config['Mail']: self.config['Mail']['URL'] = default['Mail']['URL'] - LOGGER.info("No Config for URL in Mail found. Set it to default") + DEBUG.info("No Config for URL in Mail found. Set it to default") if 'port' not in self.config['Mail']: self.config['Mail']['port'] = default['Mail']['port'] - LOGGER.info("No Config for port in Mail found. Set it to default") + DEBUG.info("No Config for port in Mail found. Set it to default") else: self.config['Mail']['port'] = int(self.config['Mail']['port']) + DEBUG.info("No Conifg for port in Mail found. Set it to default") if 'user' not in self.config['Mail']: self.config['Mail']['user'] = default['Mail']['user'] - LOGGER.info("No Config for user in Mail found. Set it to default") + DEBUG.info("No Config for user in Mail found. Set it to default") if 'passwd' not in self.config['Mail']: self.config['Mail']['passwd'] = default['Mail']['passwd'] - LOGGER.info("No Config for passwd in Mail found. Set it to default") + DEBUG.info("No Config for passwd in Mail found. Set it to default") if 'email' not in self.config['Mail']: self.config['Mail']['email'] = default['Mail']['email'] - LOGGER.info("No Config for email in Mail found. Set it to default") + DEBUG.info("No Config for email in Mail found. Set it to default") if 'crypt' not in self.config['Mail']: self.config['Mail']['crypt'] = default['Mail']['crypt'] - LOGGER.info("No Config for crypt in Mail found. Set it to default") + DEBUG.info("No Config for crypt in Mail found. Set it to default") self.mail = self.config['Mail'] - LOGGER.info('Set Mailconfig: {}'.format(self.mail)) + DEBUG.info('Set Mailconfig: {}'.format(self.mail)) def getLDAP(self): @@ -85,7 +86,7 @@ class ConifgParser(): return self.mail def __error__(self, msg): - LOGGER.error(msg) + DEBUG.error(msg) sys.exit(-1) if __name__ == '__main__': diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index 314342c..2c6d1b6 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -3,7 +3,7 @@ from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header -from geruecht import getLogger +from geruecht.logger import getLogger LOGGER = getLogger('E-MailController') diff --git a/geruecht/decorator.py b/geruecht/decorator.py index 4addb6a..8d2851d 100644 --- a/geruecht/decorator.py +++ b/geruecht/decorator.py @@ -1,4 +1,6 @@ from functools import wraps +from .logger import getInfoLogger, getDebugLogger +DEBUG = getDebugLogger("login-decorator", True) def login_required(**kwargs): import geruecht.controller.accesTokenController as ac from geruecht.model import BAR, USER, MONEY, GASTRO @@ -11,11 +13,14 @@ def login_required(**kwargs): @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get('Token') + DEBUG.info("get token {{}}".format(token)) accToken = accessController.validateAccessToken(token, groups) kwargs['accToken'] = accToken if accToken: + DEBUG.info("token {{}} is valid".format(token)) return func(*args, **kwargs) else: + DEBUG.warning("token {{}} is not valid".format(token)) return jsonify({"error": "error", "message": "permission denied"}), 401 return wrapper return real_decorator \ No newline at end of file diff --git a/geruecht/finanzer/__init__.py b/geruecht/finanzer/__init__.py index 6464ff6..57a1836 100644 --- a/geruecht/finanzer/__init__.py +++ b/geruecht/finanzer/__init__.py @@ -1,3 +1,3 @@ -from geruecht import getLogger +from geruecht.logger import getLogger LOGGER = getLogger(__name__) diff --git a/geruecht/logger.py b/geruecht/logger.py index 5fbe2dd..496266f 100644 --- a/geruecht/logger.py +++ b/geruecht/logger.py @@ -4,18 +4,32 @@ import sys FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") -logFileHandler = WatchedFileHandler("testlog.log") +logFileHandler = WatchedFileHandler("geruecht/log/debug.log") logFileHandler.setFormatter(FORMATTER) logStreamHandler = logging.StreamHandler(stream=sys.stdout) logStreamHandler.setFormatter(FORMATTER) def getLogger(logger_name): + return getDebugLogger(logger_name) + +def getInfoLogger(logger_name): logger = logging.getLogger(logger_name) + logger.setLevel(logging.INFO) + logger.addHandler(logStreamHandler) + logFileHandler = WatchedFileHandler("geruecht/log/info/{}.log".format(logger_name)) + logFileHandler.setFormatter(FORMATTER) + logger.addHandler(logFileHandler) + return logger + +def getDebugLogger(logger_name, path=False): + + logger = logging.getLogger(logger_name) + if path: + logSecondFileHandler = WatchedFileHandler("geruecht/log/debug/{}.log".format(logger_name)) + logSecondFileHandler.setFormatter(FORMATTER) + logger.addHandler(logSecondFileHandler) logger.setLevel(logging.DEBUG) logger.addHandler(logFileHandler) - logger.addHandler(logStreamHandler) - logger.propagate = False - return logger \ No newline at end of file diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 577010c..7e37ddb 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -1,5 +1,5 @@ from datetime import datetime -from geruecht import getLogger +from geruecht.logger import getLogger LOGGER = getLogger(__name__) @@ -49,7 +49,7 @@ class AccessToken(): return other - self.timestamp def __str__(self): - return "AccessToken({}, {}, {}".format(self.user, self.token, self.timestamp) + return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(self.user, self.token, self.timestamp, self.lifetime) def __repr__(self): - return "AccessToken({}, {}, {}".format(self.user, self.token, self.timestamp) + return "AccessToken(user={}, token={}, timestamp={}, lifetime={}".format(self.user, self.token, self.timestamp, self.lifetime) diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index cf1a605..4dd2f5e 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,5 +1,5 @@ from datetime import datetime -from geruecht import getLogger +from geruecht.logger import getLogger LOGGER = getLogger(__name__) def create_empty_data(): diff --git a/geruecht/routes.py b/geruecht/routes.py index 05dced8..1070229 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,4 +1,5 @@ -from geruecht import app, LOGGER +from geruecht import app +from geruecht.logger import getDebugLogger, getInfoLogger from geruecht.decorator import login_required from geruecht.exceptions import PermissionDenied import geruecht.controller.accesTokenController as ac @@ -9,6 +10,9 @@ from flask import request, jsonify accesTokenController = ac.AccesTokenController() userController = uc.UserController() +debug = getDebugLogger("MAIN-ROUTE", True) +info = getInfoLogger("MAIN-ROUTE") + def login(user, password): return user.login(password) @@ -33,85 +37,117 @@ def _valid(): @app.route("/pricelist", methods=['GET']) def _getPricelist(): try: + debug.info("get pricelist") retVal = userController.getPricelist() - print(retVal) + debug.info("return pricelist {{}}".format(retVal)) return jsonify(retVal) except Exception as err: - return jsonify({"error": str(err)}) + debug.warning("exception in get pricelist.", exc_info=True) + return jsonify({"error": str(err)}), 500 @app.route('/drinkTypes', methods=['GET']) def getTypes(): try: + debug.info("get drinktypes") retVal = userController.getAllDrinkTypes() + debug.info("return drinktypes {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.warning("exception in get drinktypes.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route('/getAllStatus', methods=['GET']) @login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getAllStatus(**kwargs): try: + debug.info("get all status for users") retVal = userController.getAllStatus() + debug.info("return all status for users {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.warning("exception in get all status for users.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route('/getStatus', methods=['POST']) @login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getStatus(**kwargs): try: + debug.info("get status from user") data = request.get_json() name = data['name'] + debug.info("get status from user {{}}".format(name)) retVal = userController.getStatus(name) + debug.info("return status from user {{}} : {{}}".format(name, retVal)) return jsonify(retVal) except Exception as err: + debug.warning("exception in get status from user.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route('/getUsers', methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUsers(**kwargs): try: + debug.info("get all users from database") users = userController.getAllUsersfromDB() + debug.debug("users are {{}}".format(users)) retVal = [user.toJSON() for user in users] + debug.info("return all users from database {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.warning("exception in get all users from database.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route("/getLifeTime", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _getLifeTime(**kwargs): try: + debug.info("get lifetime of accesstoken") if 'accToken' in kwargs: accToken = kwargs['accToken'] - return jsonify({"value": accToken.lifetime}) + debug.debug("accessToken is {{}}".format(accToken)) + retVal = {"value": accToken.lifetime} + debug.info("return get lifetime from accesstoken {{}}".format(retVal)) + return jsonify(retVal) except Exception as err: + debug.info("exception in get lifetime of accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route("/saveLifeTime", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _saveLifeTime(**kwargs): try: + debug.info("save lifetime for accessToken") if 'accToken' in kwargs: accToken = kwargs['accToken'] - + debug.debug("accessToken is {{}}".format(accToken)) data = request.get_json() lifetime = data['value'] + debug.debug("lifetime is {{}}".format(lifetime)) + debug.info("set lifetime {{}} to accesstoken {{}}".format(lifetime, accToken)) accToken.lifetime = lifetime + debug.info("update accesstoken timestamp") accToken.updateTimestamp() - - return jsonify({"value": accToken.lifetime}) + retVal = {"value": accToken.lifetime} + debug.info("return save lifetime for accessToken {{}}".format(retVal)) + return jsonify(retVal) except Exception as err: + debug.warning("exception in save lifetime for accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route("/logout", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _logout(**kwargs): try: + debug.info("logout user") if 'accToken' in kwargs: accToken = kwargs['accToken'] + debug.debug("accesstoken is {{}}".format(accToken)) + debug.info("delete accesstoken") accesTokenController.deleteAccessToken(accToken) + debug.info("return ok logout user") return jsonify({"ok": "ok"}) except Exception as err: + debug.warning("exception in logout user.", exc_info=True) return jsonify({"error": str(err)}), 500 @app.route("/login", methods=['POST']) @@ -124,25 +160,28 @@ def _login(): Returns: A JSON-File with createt Token or Errors """ - LOGGER.info("Start log in.") + debug.info("Start log in.") data = request.get_json() - print(data) - LOGGER.debug("JSON from request: {}".format(data)) + debug.debug("JSON from request: {}".format(data)) username = data['username'] password = data['password'] - LOGGER.info("search {} in database".format(username)) try: + debug.info("search {{}} in database".format(username)) user, ldap_conn = userController.loginUser(username, password) + debug.debug("user is {{}}".format(user)) user.password = password token = accesTokenController.createAccesToken(user, ldap_conn) + debug.debug("accesstoken is {{}}".format(token)) + debug.info("validate accesstoken") dic = accesTokenController.validateAccessToken(token, [USER, EXTERN]).user.toJSON() dic["token"] = token dic["accessToken"] = token - LOGGER.info("User {} success login.".format(username)) + debug.info("User {{}} success login.".format(username)) + debug.info("return login {{}}".format(dic)) return jsonify(dic) except PermissionDenied as err: + debug.warning("permission denied exception in logout", exc_info=True) return jsonify({"error": str(err)}), 401 except Exception as err: + debug.warning("exception in logout.", exc_info=True) return jsonify({"error": "permission denied"}), 401 - LOGGER.info("User {} does not exist.".format(username)) - return jsonify({"error": "wrong username"}), 401 From ba5f0339812eba9a4169519e3609affc4d7a9517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 10 Mar 2020 09:19:11 +0100 Subject: [PATCH 068/111] new logger implementet -- not ready logger dont create folder to log model and controller (except userController) not ready --- geruecht/__init__.py | 4 +- geruecht/baruser/routes.py | 202 +++++++++++++--------- geruecht/configparser.py | 6 +- geruecht/controller/__init__.py | 4 +- geruecht/controller/emailController.py | 4 +- geruecht/controller/userController.py | 226 +++++++++++++++++++++---- geruecht/decorator.py | 10 +- geruecht/finanzer/__init__.py | 2 - geruecht/finanzer/routes.py | 205 ++++++++++++---------- geruecht/gastro/routes.py | 21 +++ geruecht/logger.py | 43 ++--- geruecht/logging.yml | 57 +++++++ geruecht/model/accessToken.py | 4 +- geruecht/model/creditList.py | 4 +- geruecht/model/user.py | 4 +- geruecht/routes.py | 28 +-- geruecht/user/routes.py | 159 +++++++++++------ geruecht/vorstand/routes.py | 150 ++++++++++------ 18 files changed, 759 insertions(+), 374 deletions(-) create mode 100644 geruecht/logging.yml diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 8cb368a..0b4d1da 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -4,12 +4,12 @@ Initialize also a singelton for the AccesTokenControler and start the Thread. """ -from .logger import getDebugLogger, getInfoLogger +from .logger import getDebugLogger from geruecht.controller import dbConfig, ldapConfig from flask_mysqldb import MySQL from flask_ldapconn import LDAPConn -DEBUG = getDebugLogger('INIT', True) +DEBUG = getDebugLogger() DEBUG.info("Initialize App") from flask import Flask diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 8653305..3d756db 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -4,6 +4,10 @@ import geruecht.controller.userController as uc from datetime import datetime from geruecht.model import BAR, MONEY, USER, VORSTAND from geruecht.decorator import login_required +from geruecht.logger import getDebugLogger, getCreditLogger + +debug = getDebugLogger() +creditL = getCreditLogger() baruser = Blueprint("baruser", __name__) @@ -22,28 +26,34 @@ def _bar(**kwargs): JSON-File with Users, who has amounts in this month or ERROR 401 Permission Denied """ - dic = {} - users = userController.getAllUsersfromDB() - for user in users: - geruecht = None - geruecht = user.getGeruecht(datetime.now().year) - if geruecht is not None: - month = geruecht.getMonth(datetime.now().month) - amount = month[0] - month[1] - all = geruecht.getSchulden() - if all != 0: - if all >= 0: - type = 'credit' - else: - type = 'amount' - dic[user.uid] = {"username": user.uid, - "firstname": user.firstname, - "lastname": user.lastname, - "amount": all, - "locked": user.locked, - "type": type - } - return jsonify(dic) + debug.info("/bar") + try: + dic = {} + users = userController.getAllUsersfromDB() + for user in users: + geruecht = None + geruecht = user.getGeruecht(datetime.now().year) + if geruecht is not None: + month = geruecht.getMonth(datetime.now().month) + amount = month[0] - month[1] + all = geruecht.getSchulden() + if all != 0: + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic[user.uid] = {"username": user.uid, + "firstname": user.firstname, + "lastname": user.lastname, + "amount": all, + "locked": user.locked, + "type": type + } + debug.debug("return {{}}".format(dic)) + return jsonify(dic) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @baruser.route("/baradd", methods=['POST']) @@ -57,26 +67,32 @@ def _baradd(**kwargs): JSON-File with userID and the amount or ERROR 401 Permission Denied """ - data = request.get_json() - userID = data['userId'] - amount = int(data['amount']) + debug.info("/baradd") + try: + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) - date = datetime.now() - userController.addAmount(userID, amount, year=date.year, month=date.month) - user = userController.getUser(userID) - geruecht = user.getGeruecht(year=date.year) - month = geruecht.getMonth(month=date.month) - amount = abs(month[0] - month[1]) - all = geruecht.getSchulden() - if all >= 0: - type = 'credit' - else: - type = 'amount' - dic = user.toJSON() - dic['amount'] = abs(all) - dic['type'] = type - - return jsonify(dic) + date = datetime.now() + userController.addAmount(userID, amount, year=date.year, month=date.month) + user = userController.getUser(userID) + geruecht = user.getGeruecht(year=date.year) + month = geruecht.getMonth(month=date.month) + amount = abs(month[0] - month[1]) + all = geruecht.getSchulden() + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic = user.toJSON() + dic['amount'] = abs(all) + dic['type'] = type + debug.debug("return {{}}".format(dic)) + creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format(date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amount/100)) + return jsonify(dic) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @baruser.route("/barGetUsers") @@ -90,9 +106,15 @@ def _getUsers(**kwargs): JSON-File with Users or ERROR 401 Permission Denied """ - retVal = {} - retVal = ldap.getAllUser() - return jsonify(retVal) + debug.info("/barGetUsers") + try: + retVal = {} + retVal = ldap.getAllUser() + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @baruser.route("/bar/storno", methods=['POST']) @login_required(groups=[BAR]) @@ -105,50 +127,68 @@ def _storno(**kwargs): JSON-File with userID and the amount or ERROR 401 Permission Denied """ - data = request.get_json() - userID = data['userId'] - amount = int(data['amount']) + debug.info("/bar/storno") + try: + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) - date = datetime.now() - userController.addCredit(userID, amount, year=date.year, month=date.month) - user = userController.getUser(userID) - geruecht = user.getGeruecht(year=date.year) - month = geruecht.getMonth(month=date.month) - amount = abs(month[0] - month[1]) - all = geruecht.getSchulden() - if all >= 0: - type = 'credit' - else: - type = 'amount' - dic = user.toJSON() - dic['amount'] = abs(all) - dic['type'] = type - - return jsonify(dic) + date = datetime.now() + userController.addCredit(userID, amount, year=date.year, month=date.month) + user = userController.getUser(userID) + geruecht = user.getGeruecht(year=date.year) + month = geruecht.getMonth(month=date.month) + amount = abs(month[0] - month[1]) + all = geruecht.getSchulden() + if all >= 0: + type = 'credit' + else: + type = 'amount' + dic = user.toJSON() + dic['amount'] = abs(all) + dic['type'] = type + debug.debug("return {{}}".format(dic)) + creditL.info("{} Baruser {} {} storniert {} € von {} {}".format(date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100, user.firstname, user.lastname)) + return jsonify(dic) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @baruser.route("/barGetUser", methods=['POST']) @login_required(groups=[BAR]) def _getUser(**kwargs): - data = request.get_json() - username = data['userId'] - user = userController.getUser(username) - amount = user.getGeruecht(datetime.now().year).getSchulden() - if amount >= 0: - type = 'credit' - else: - type = 'amount' + debug.info("/barGetUser") + try: + data = request.get_json() + username = data['userId'] + user = userController.getUser(username) + amount = user.getGeruecht(datetime.now().year).getSchulden() + if amount >= 0: + type = 'credit' + else: + type = 'amount' - retVal = user.toJSON() - retVal['amount'] = amount - retVal['type'] = type - return jsonify(retVal) + retVal = user.toJSON() + retVal['amount'] = amount + retVal['type'] = type + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @baruser.route("/search", methods=['GET']) @login_required(groups=[BAR, MONEY, USER,VORSTAND]) def _search(**kwargs): - retVal = ldap.getAllUser() - for user in retVal: - if user['username'] == 'extern': - retVal.remove(user) - break - return jsonify(retVal) + debug.info("/search") + try: + retVal = ldap.getAllUser() + for user in retVal: + if user['username'] == 'extern': + retVal.remove(user) + break + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 diff --git a/geruecht/configparser.py b/geruecht/configparser.py index 308ba78..96dfadd 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -1,7 +1,7 @@ import yaml import sys -from .logger import getDebugLogger, getInfoLogger -DEBUG = getDebugLogger("CONFIG", True) +from .logger import getDebugLogger +DEBUG = getDebugLogger() default = { 'AccessTokenLifeTime': 1800, @@ -86,7 +86,7 @@ class ConifgParser(): return self.mail def __error__(self, msg): - DEBUG.error(msg) + DEBUG.error(msg, exc_info=True) sys.exit(-1) if __name__ == '__main__': diff --git a/geruecht/controller/__init__.py b/geruecht/controller/__init__.py index b659474..b7f1734 100644 --- a/geruecht/controller/__init__.py +++ b/geruecht/controller/__init__.py @@ -1,4 +1,4 @@ -from geruecht.logger import getLogger +from geruecht.logger import getDebugLogger from geruecht.configparser import ConifgParser import os @@ -6,7 +6,7 @@ print(os.getcwd()) config = ConifgParser('geruecht/config.yml') -LOGGER = getLogger(__name__) +LOGGER = getDebugLogger() class Singleton(type): _instances = {} diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index 2c6d1b6..f6ccdee 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -3,9 +3,9 @@ from datetime import datetime from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header -from geruecht.logger import getLogger +from geruecht.logger import getDebugLogger -LOGGER = getLogger('E-MailController') +LOGGER = getDebugLogger() class EmailController(): diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 8f9ae5c..585145e 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -1,4 +1,4 @@ -from . import LOGGER, Singleton, ldapConfig, dbConfig, mailConfig +from . import Singleton, mailConfig import geruecht.controller.databaseController as dc import geruecht.controller.ldapController as lc import geruecht.controller.emailController as ec @@ -6,219 +6,359 @@ import calendar from geruecht.model.user import User from geruecht.exceptions import PermissionDenied from datetime import datetime, timedelta -from geruecht.exceptions import UsernameExistLDAP, UsernameExistDB, DatabaseExecption, LDAPExcetpion, DayLocked, TansactJobIsAnswerdException +from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, DayLocked, TansactJobIsAnswerdException +from geruecht.logger import getDebugLogger db = dc.DatabaseController() ldap = lc.LDAPController() emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) +debug = getDebugLogger() + class UserController(metaclass=Singleton): def __init__(self): + debug.debug("init UserController") pass def getAllStatus(self): - return db.getAllStatus() + debug.info("get all status for user") + retVal = db.getAllStatus() + debug.debug("status are {{}}".format(retVal)) + return retVal def getStatus(self, name): - return db.getStatus(name) + debug.info("get status of user {{}}".format(name)) + retVal = db.getStatus(name) + debug.debug("status of user {{}} is {{}}".format(name, retVal)) + return retVal def setStatus(self, name): - return db.setStatus(name) + debug.info("set status of user {{}}".format(name)) + retVal = db.setStatus(name) + debug.debug("settet status of user {{}} is {{}}".format(name, retVal)) + return retVal def deleteStatus(self, status): + debug.info("delete status {{}}".format(status)) db.deleteStatus(status) def updateStatus(self, status): - return db.updateStatus(status) + debug.info("update status {{}}".format(status)) + retVal = db.updateStatus(status) + debug.debug("updated status is {{}}".format(retVal)) + return retVal def updateStatusOfUser(self, username, status): - return db.updateStatusOfUser(username, status) + debug.info("update status {{}} of user {{}}".format(status, username)) + retVal = db.updateStatusOfUser(username, status) + debug.debug("updatet status of user {{}} is {{}}".format(username, retVal)) + return retVal def updateVotingOfUser(self, username, voting): - return db.updateVotingOfUser(username, voting) + debug.info("update voting {{}} of user {{}}".format(voting, username)) + retVal = db.updateVotingOfUser(username, voting) + debug.debug("updatet voting of user {{}} is {{}}".format(username, retVal)) + return retVal def deleteDrinkType(self, type): + debug.info("delete drink type {{}}".format(type)) db.deleteDrinkType(type) def updateDrinkType(self, type): - return db.updateDrinkType(type) + debug.info("update drink type {{}}".format(type)) + retVal = db.updateDrinkType(type) + debug.debug("updated drink type is {{}}".format(retVal)) + return retVal def setDrinkType(self, type): - return db.setDrinkType(type) + debug.info("set drink type {{}}".format(type)) + retVal = db.setDrinkType(type) + debug.debug("seted drink type is {{}}".format(retVal)) + return retVal def deletDrinkPrice(self, drink): + debug.info("delete drink {{}}".format(drink)) db.deleteDrink(drink) def setDrinkPrice(self, drink): + debug.info("set drink {{}}".format(drink)) retVal = db.setDrinkPrice(drink) + debug.debug("seted drink is {{}}".format(retVal)) return retVal def updateDrinkPrice(self, drink): + debug.info("update drink {{}}".format(drink)) retVal = db.updateDrinkPrice(drink) + debug.debug("updated drink is {{}}".format(retVal)) return retVal def getAllDrinkTypes(self): - return db.getAllDrinkTypes() + debug.info("get all drink types") + retVal = db.getAllDrinkTypes() + debug.debug("all drink types are {{}}".format(retVal)) + return retVal def getPricelist(self): + debug.info("get all drinks") list = db.getPriceList() + debug.debug("all drinks are {{}}".format(list)) return list def setTransactJob(self, from_user, to_user, date): + debug.info("set transact job from {{}} to {{}} on {{}}".format(from_user, to_user, date)) jobtransact = db.setTransactJob(from_user, to_user, date.date()) + debug.debug("transact job is {{}}".format(jobtransact)) + debug.info("send mail with transact job to user") emailController.sendMail(jobtransact['to_user'], 'jobtransact', jobtransact) return jobtransact def getTransactJobFromUser(self, user, date): - return db.getTransactJobFromUser(user, date.date()) + debug.info("get transact job from user {{}} on {{}}".format(user, date)) + retVal = db.getTransactJobFromUser(user, date.date()) + debug.debug("transact job from user {{}} is {{}}".format(user, retVal)) + return retVal def getAllTransactJobFromUser(self, user, date): - return db.getAllTransactJobFromUser(user, date.date()) + debug.info("get all transact job from user {{}} start on {{}}".format(user, date)) + retVal = db.getAllTransactJobFromUser(user, date,date()) + debug.debug("all transact job are {{}}".format(retVal)) + return retVal def getAllTransactJobToUser(self, user, date): - return db.getAllTransactJobToUser(user, date.date()) + debug.info("get all transact job from to_user {{}} start on {{}}".format(user, date)) + retVal = db.getAllTransactJobToUser(user, date.date()) + debug.debug("all transact job are {{}}".format(retVal)) + return retVal def getTransactJob(self, from_user, to_user, date): - return db.getTransactJob(from_user, to_user, date.date()) + debug.info("get transact job from user {{}} to user {{}} on {{}}".format(from_user, to_user, date)) + retVal = db.getTransactJob(from_user, to_user, date.date()) + debug.debug("transact job is {{}}".format(retVal)) + return retVal def deleteTransactJob(self, from_user, to_user, date): + debug.info("delete transact job from user {{}} to user {{}} on {{}}".format(from_user, to_user, date)) transactJob = self.getTransactJob(from_user, to_user, date) + debug.debug("transact job is {{}}".format(transactJob)) if transactJob['answerd']: + debug.warning("transactjob {{}} can not delete because is answerd") raise TansactJobIsAnswerdException("TransactJob is already answerd") db.deleteTransactJob(from_user, to_user, date.date()) def answerdTransactJob(self, from_user, to_user, date, answer): + debug.info("answer transact job from user {{}} to user {{}} on {{}} with answer {{}}".format(from_user, to_user, date, answer)) transactJob = db.updateTransactJob(from_user, to_user, date.date(), answer) + debug.debug("transactjob is {{}}".format(transactJob)) if answer: + debug.info("add worker on date {{}}".format(date)) self.addWorker(to_user.uid, date) return transactJob def setLockedDay(self, date, locked, hard=False): - return db.setLockedDay(date.date(), locked, hard) + debug.info("set day locked on {{}} with state {{}}".format(date, locked)) + retVal = db.setLockedDay(date.date(), locked, hard) + debug.debug("seted day locked is {{}}".format(retVal)) + return retVal def getLockedDay(self, date): + debug.info("get locked day on {{}}".format(date)) now = datetime.now() + debug.debug("now is {{}}".format(now)) oldMonth = False + debug.debug("check if date old month or current month") for i in range(1, 8): if datetime(now.year, now.month, i).weekday() == 2: if now.day < i: oldMonth = True break + debug.debug("oldMonth is {{}}".format(oldMonth)) lockedYear = date.year lockedMonth = date.month if date.month < now.month else now.month - 1 if oldMonth else now.month daysInMonth = calendar.monthrange(lockedYear, lockedMonth)[1] startDay = 1 + debug.debug("calculate start day of month") for i in range(1, 8): if datetime(lockedYear, lockedMonth, i).weekday() == 2: startDay = i break + debug.debug("start day of month is {{}}".format(startDay)) + debug.debug("check if date should be locked") if lockedYear <= now.year and lockedMonth <= now.month: for i in range(startDay, daysInMonth + 1): + debug.debug("lock day {{}}".format(datetime(lockedYear, lockedMonth, i))) self.setLockedDay(datetime(lockedYear, lockedMonth, i), True) for i in range(1, 8): nextMonth = datetime(lockedYear, lockedMonth + 1, i) if nextMonth.weekday() == 2: break + debug.debug("lock day {{}}".format(datetime(lockedYear, lockedMonth, i))) self.setLockedDay(nextMonth, True) - return db.getLockedDay(date.date()) + retVal = db.getLockedDay(date.date()) + debug.debug("locked day is {{}}".format(retVal)) + return def getWorker(self, date, username=None): + debug.info("get worker on {{}}".format(username, date)) if (username): user = self.getUser(username) - return [db.getWorker(user, date)] - return db.getWorkers(date) + debug.debug("user is {{}}".format(user)) + retVal = [db.getWorker(user, date)] + debug.debug("worker is {{}}".format(retVal)) + return retVal + retVal = db.getWorkers(date) + debug.debug("workers are {{}}".format(retVal)) + return retVal def addWorker(self, username, date, userExc=False): + debug.info("add job user {{}} on {{}}".format(username, date)) if (userExc): + debug.debug("this is a user execution, check if day is locked") lockedDay = self.getLockedDay(date) if lockedDay: if lockedDay['locked']: + debug.debug("day is lockey. user cant get job") raise DayLocked("Day is locked. You can't get the Job") user = self.getUser(username) + debug.debug("user is {{}}".format(user)) + debug.debug("check if user has job on date") if (not db.getWorker(user, date)): + debug.debug("set job to user") db.setWorker(user, date) - return self.getWorker(date, username=username) + retVal = self.getWorker(date, username=username) + debug.debug("worker on date is {{}}".format(retVal)) + return retVal def deleteWorker(self, username, date, userExc=False): + debug.info("delete worker {{}} on date {{}}".format(username, date)) user = self.getUser(username) + debug.debug("user is {{}}".format(user)) if userExc: + debug.debug("is user execution, check if day locked") lockedDay = self.getLockedDay(date) if lockedDay: if lockedDay['locked']: + debug.debug("day is locked, check if accepted transact job exists") transactJobs = self.getTransactJobFromUser(user, date) + debug.debug("transact job is {{}}".format(transactJobs)) found = False for job in transactJobs: if job['accepted'] and job['answerd']: + debug.debug("accepted transact job exists") found = True break if not found: + debug.debug("no accepted transact job found") raise DayLocked("Day is locked. You can't delete the Job") db.deleteWorker(user, date) def lockUser(self, username, locked): + debug.info("lock user {{}} for credit with status {{}}".format(username, locked)) user = self.getUser(username) + debug.debug("user is {{}}".format(user)) user.updateData({'locked': locked}) db.updateUser(user) - return self.getUser(username) + retVal = self.getUser(username) + debug.debug("locked user is {{}}".format(retVal)) + return retVal def updateConfig(self, username, data): + debug.info("update config of user {{}} with config {{}}".format(username, data)) user = self.getUser(username) + debug.debug("user is {{}}".format(user)) user.updateData(data) db.updateUser(user) - return self.getUser(username) + retVal = self.getUser(username) + debug.debug("updated config of user is {{}}".format(retVal)) + return retVal def __updateDataFromLDAP(self, user): + debug.info("update data from ldap for user {{}}".format(user)) groups = ldap.getGroup(user.uid) + debug.debug("ldap gorups are {{}}".format(groups)) user_data = ldap.getUserData(user.uid) + debug.debug("ldap data is {{}}".format(user_data)) user_data['gruppe'] = groups user_data['group'] = groups user.updateData(user_data) db.updateUser(user) def autoLock(self, user): + debug.info("start autolock of user {{}}".format(user)) if user.autoLock: - if user.getGeruecht(year=datetime.now().year).getSchulden() <= (-1*user.limit): + debug.debug("autolock is active") + credit = user.getGeruecht(year=datetime.now().year).getSchulden() + limit = -1*user.limit + if credit <= limit: + debug.debug("credit {{}} is more than user limit {{}}".format(credit, limit)) + debug.debug("lock user") user.updateData({'locked': True}) + debug.debug("send mail to user") emailController.sendMail(user) else: + debug.debug("cretid {{}} is less than user limit {{}}".format(credit, limit)) + debug.debug("unlock user") user.updateData({'locked': False}) db.updateUser(user) def addAmount(self, username, amount, year, month, finanzer=False): + debug.info("add amount {{}} to user {{}} no month {{}}, year {{}}".format(amount, username, month, year)) user = self.getUser(username) + debug.debug("user is {{}}".format(user)) if user.uid == 'extern': + debug.debug("user is extern user, so exit add amount") return if not user.locked or finanzer: + debug.debug("user is not locked {{}} or is finanzer execution {{}}".format(user.locked, finanzer)) user.addAmount(amount, year=year, month=month) creditLists = user.updateGeruecht() + debug.debug("creditList is {{}}".format(creditLists)) for creditList in creditLists: + debug.debug("update creditlist {{}}".format(creditList)) db.updateCreditList(creditList) + debug.debug("do autolock") self.autoLock(user) - return user.getGeruecht(year) + retVal = user.getGeruecht(year) + debug.debug("updated creditlists is {{}}".format(retVal)) + return retVal def addCredit(self, username, credit, year, month): + debug.info("add credit {{}} to user {{}} on month {{}}, year {{}}".format(credit, username, month, year)) user = self.getUser(username) + debug.debug("user is {{}}".format(user)) if user.uid == 'extern': + debug.debug("user is extern user, so exit add credit") return user.addCredit(credit, year=year, month=month) creditLists = user.updateGeruecht() + debug.debug("creditlists are {{}}".format(creditLists)) for creditList in creditLists: + debug.debug("update creditlist {{}}".format(creditList)) db.updateCreditList(creditList) + debug.debug("do autolock") self.autoLock(user) - return user.getGeruecht(year) + retVal = user.getGeruecht(year) + debug.debug("updated creditlists are {{}}".format(retVal)) + return retVal def getAllUsersfromDB(self): + debug.info("get all users from database") users = db.getAllUser() + debug.debug("users are {{}}".format(users)) for user in users: try: + debug.debug("update data from ldap") self.__updateDataFromLDAP(user) except: pass + debug.debug("update creditlists") self.__updateGeruechte(user) - return db.getAllUser(extern=True) + retVal = db.getAllUser(extern=True) + debug.debug("all users are {{}}".format(retVal)) + return retVal def checkBarUser(self, user): + debug.info("check if user {{}} is baruser") date = datetime.now() zero = date.replace(hour=0, minute=0, second=0, microsecond=0) end = zero + timedelta(hours=12) @@ -226,59 +366,84 @@ class UserController(metaclass=Singleton): if date > zero and end > date: startdatetime = startdatetime - timedelta(days=1) enddatetime = startdatetime + timedelta(days=1) + debug.debug("startdatetime is {{}} and enddatetime is {{}}".format(startdatetime, end)) result = False if date >= startdatetime and date < enddatetime: result = db.getWorker(user, startdatetime) + debug.debug("worker is {{}}".format(result)) return True if result else False def getUser(self, username): + debug.info("get user {{}}".format(username)) user = db.getUser(username) + debug.debug("user is {{}}".format(user)) groups = ldap.getGroup(username) + debug.debug("groups are {{}}".format(groups)) user_data = ldap.getUserData(username) + debug.debug("user data from ldap is {{}}".format(user_data)) user_data['gruppe'] = groups user_data['group'] = groups if user is None: + debug.debug("user not exists in database -> insert into database") user = User(user_data) db.insertUser(user) else: + debug.debug("update database with user") user.updateData(user_data) db.updateUser(user) user = db.getUser(username) self.__updateGeruechte(user) + debug.debug("user is {{}}".format(user)) return user def __updateGeruechte(self, user): + debug.debug("update creditlists") user.getGeruecht(datetime.now().year) creditLists = user.updateGeruecht() + debug.debug("creditlists are {{}}".format(creditLists)) if user.getGeruecht(datetime.now().year).getSchulden() != 0: for creditList in creditLists: + debug.debug("update creditlist {{}}".format(creditList)) db.updateCreditList(creditList) def sendMail(self, username): + debug.info("send mail to user {{}}".format(username)) if type(username) == User: user = username if type(username) == str: user = db.getUser(username) - return emailController.sendMail(user) + retVal = emailController.sendMail(user) + debug.debug("send mail is {{}}".format(retVal)) + return retVal def sendAllMail(self): + debug.info("send mail to all user") retVal = [] users = db.getAllUser() + debug.debug("users are {{}}".format(users)) for user in users: retVal.append(self.sendMail(user)) + debug.debug("send mails are {{}}".format(retVal)) return retVal def modifyUser(self, user, ldap_conn, attributes): + debug.info("modify user {{}} with attributes {{}} with ldap_conn {{}}".format(user, attributes, ldap_conn)) try: if 'username' in attributes: + debug.debug("change username, so change first in database") db.changeUsername(user, attributes['username']) ldap.modifyUser(user, ldap_conn, attributes) if 'username' in attributes: - return self.getUser(attributes['username']) + retVal = self.getUser(attributes['username']) + debug.debug("user is {{}}".format(retVal)) + return retVal else: - return self.getUser(user.uid) + retVal = self.getUser(user.uid) + debug.debug("user is {{}}".format(retVal)) + return retVal except UsernameExistLDAP as err: + debug.debug("username exists on ldap, rechange username on database", exc_info=True) db.changeUsername(user, user.uid) raise Exception(err) except LDAPExcetpion as err: @@ -289,11 +454,14 @@ class UserController(metaclass=Singleton): raise Exception(err) def loginUser(self, username, password): + debug.info("login user {{}}".format(username)) try: user = self.getUser(username) + debug.debug("user is {{}}".format(user)) user.password = password ldap.login(username, password) ldap_conn = ldap.bind(user, password) return user, ldap_conn except PermissionDenied as err: + debug.debug("permission is denied", exc_info=True) raise err diff --git a/geruecht/decorator.py b/geruecht/decorator.py index 8d2851d..582eb08 100644 --- a/geruecht/decorator.py +++ b/geruecht/decorator.py @@ -1,6 +1,6 @@ from functools import wraps -from .logger import getInfoLogger, getDebugLogger -DEBUG = getDebugLogger("login-decorator", True) +from .logger import getDebugLogger +DEBUG = getDebugLogger() def login_required(**kwargs): import geruecht.controller.accesTokenController as ac from geruecht.model import BAR, USER, MONEY, GASTRO @@ -9,15 +9,17 @@ def login_required(**kwargs): groups = [USER, BAR, GASTRO, MONEY] if "groups" in kwargs: groups = kwargs["groups"] + DEBUG.debug("groups are {{}}".format(groups)) def real_decorator(func): @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get('Token') - DEBUG.info("get token {{}}".format(token)) + DEBUG.debug("token is {{}}".format(token)) accToken = accessController.validateAccessToken(token, groups) + DEBUG.debug("accToken is {{}}".format(accToken)) kwargs['accToken'] = accToken if accToken: - DEBUG.info("token {{}} is valid".format(token)) + DEBUG.debug("token {{}} is valid".format(token)) return func(*args, **kwargs) else: DEBUG.warning("token {{}} is not valid".format(token)) diff --git a/geruecht/finanzer/__init__.py b/geruecht/finanzer/__init__.py index 57a1836..8b13789 100644 --- a/geruecht/finanzer/__init__.py +++ b/geruecht/finanzer/__init__.py @@ -1,3 +1 @@ -from geruecht.logger import getLogger -LOGGER = getLogger(__name__) diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index d6f1a57..93c7969 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,10 +1,12 @@ from flask import Blueprint, request, jsonify -from geruecht.finanzer import LOGGER from datetime import datetime import geruecht.controller.userController as uc from geruecht.model import MONEY from geruecht.decorator import login_required -import time +from geruecht.logger import getDebugLogger, getCreditLogger + +debug = getDebugLogger() +creditL = getCreditLogger() finanzer = Blueprint("finanzer", __name__) @@ -22,16 +24,18 @@ def _getFinanzer(**kwargs): A JSON-File with Users or ERROR 401 Permission Denied. """ - LOGGER.debug("Get all Useres") - users = userController.getAllUsersfromDB() - dic = {} - for user in users: - LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - LOGGER.debug("ReturnValue is {}".format(dic)) - LOGGER.info("Send main for Finanzer") - return jsonify(dic) + debug.info("/getFinanzerMain") + try: + users = userController.getAllUsersfromDB() + dic = {} + for user in users: + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} + debug.debug("return {{}}".format(dic)) + return jsonify(dic) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerAddAmount", methods=['POST']) @login_required(groups=[MONEY]) @@ -46,28 +50,29 @@ def _addAmount(**kwargs): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - data = request.get_json() - LOGGER.debug("Get data {}".format(data)) - userID = data['userId'] - amount = int(data['amount']) - LOGGER.debug("UserID is {} and amount is {}".format(userID, amount)) + debug.info("/finanzerAddAmount") try: - year = int(data['year']) - except KeyError as er: - LOGGER.error("KeyError in year. Year is set to default.") - year = datetime.now().year - try: - month = int(data['month']) - except KeyError as er: - LOGGER.error("KeyError in month. Month is set to default.") - month = datetime.now().month - LOGGER.debug("Year is {} and Month is {}".format(year, month)) - userController.addAmount(userID, amount, year=year, month=month, finanzer=True) - user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} - retVal['locked'] = user.locked - LOGGER.info("Send updated Geruecht") - return jsonify(retVal) + data = request.get_json() + userID = data['userId'] + amount = int(data['amount']) + try: + year = int(data['year']) + except KeyError as er: + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + month = datetime.now().month + userController.addAmount(userID, amount, year=year, month=month, finanzer=True) + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked + debug.debug("return {{}}".format(retVal)) + creditL.info("{} Finanzer {} {} fügt {} {} {} € Schulden hinzu.".format(datetime(year,month).date(), kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerAddCredit", methods=['POST']) @login_required(groups=[MONEY]) @@ -82,79 +87,109 @@ def _addCredit(**kwargs): JSON-File with geruecht of year or ERROR 401 Permission Denied """ - data = request.get_json() - print(data) - LOGGER.debug("Get data {}".format(data)) - userID = data['userId'] - credit = int(data['credit']) - LOGGER.debug("UserID is {} and credit is {}".format(userID, credit)) - + debug.info("/finanzerAddCredit") try: - year = int(data['year']) - except KeyError as er: - LOGGER.error("KeyError in year. Year is set to default.") - year = datetime.now().year - try: - month = int(data['month']) - except KeyError as er: - LOGGER.error("KeyError in month. Month is set to default.") - month = datetime.now().month + data = request.get_json() + userID = data['userId'] + credit = int(data['credit']) - LOGGER.debug("Year is {} and Month is {}".format(year, month)) - userController.addCredit(userID, credit, year=year, month=month).toJSON() - user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} - retVal['locked'] = user.locked - LOGGER.info("Send updated Geruecht") - return jsonify(retVal) + try: + year = int(data['year']) + except KeyError as er: + year = datetime.now().year + try: + month = int(data['month']) + except KeyError as er: + month = datetime.now().month + + userController.addCredit(userID, credit, year=year, month=month).toJSON() + user = userController.getUser(userID) + retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal['locked'] = user.locked + debug.debug("return {{}}".format(retVal)) + creditL.info("{} Finanzer {} {} fügt {} {} {} € Guthaben hinzu.".format(datetime(year, month).date(), + kwargs['accToken'].user.firstname, + kwargs['accToken'].user.lastname, + credit / 100)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerLock", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerLock(**kwargs): - data = request.get_json() - username = data['userId'] - locked = bool(data['locked']) - retVal = userController.lockUser(username, locked).toJSON() - return jsonify(retVal) + debug.info("/finanzerLock") + try: + data = request.get_json() + username = data['userId'] + locked = bool(data['locked']) + retVal = userController.lockUser(username, locked).toJSON() + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerSetConfig", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerSetConfig(**kwargs): - data = request.get_json() - username = data['userId'] - autoLock = bool(data['autoLock']) - limit = int(data['limit']) - retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() - return jsonify(retVal) + debug.info("/finanzerSetConfig") + try: + data = request.get_json() + username = data['userId'] + autoLock = bool(data['autoLock']) + limit = int(data['limit']) + retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerAddUser", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerAddUser(**kwargs): - data = request.get_json() - username = data['userId'] - userController.getUser(username) - LOGGER.debug("Get all Useres") - users = userController.getAllUsersfromDB() - dic = {} - for user in users: - LOGGER.debug("Add User {} to ReturnValue".format(user)) - dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - LOGGER.debug("ReturnValue is {}".format(dic)) - return jsonify(dic), 200 + debug.info("/finanzerAddUser") + try: + data = request.get_json() + username = data['userId'] + userController.getUser(username) + users = userController.getAllUsersfromDB() + dic = {} + for user in users: + dic[user.uid] = user.toJSON() + dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} + debug.debug("return {{}}".format(dic)) + return jsonify(dic), 200 + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerSendOneMail", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerSendOneMail(**kwargs): - data = request.get_json() - username = data['userId'] - retVal = userController.sendMail(username) - return jsonify(retVal) + debug.info("/finanzerSendOneMail") + try: + data = request.get_json() + username = data['userId'] + retVal = userController.sendMail(username) + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @finanzer.route("/finanzerSendAllMail", methods=['GET']) @login_required(groups=[MONEY]) def _finanzerSendAllMail(**kwargs): - retVal = userController.sendAllMail() - return jsonify(retVal) \ No newline at end of file + debug.info("/finanzerSendAllMail") + try: + retVal = userController.sendAllMail() + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 \ No newline at end of file diff --git a/geruecht/gastro/routes.py b/geruecht/gastro/routes.py index 9deca0a..787faf8 100644 --- a/geruecht/gastro/routes.py +++ b/geruecht/gastro/routes.py @@ -2,6 +2,9 @@ from flask import request, jsonify, Blueprint from geruecht.decorator import login_required import geruecht.controller.userController as uc from geruecht.model import GASTRO +from geruecht.logger import getCreditLogger, getDebugLogger + +debug = getDebugLogger() gastrouser = Blueprint('gastrouser', __name__) @@ -10,62 +13,80 @@ userController = uc.UserController() @gastrouser.route('/gastro/setDrink', methods=['POST']) @login_required(groups=[GASTRO]) def setDrink(**kwargs): + debug.info("/gastro/setDrink") try: data = request.get_json() retVal = userController.setDrinkPrice(data) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @gastrouser.route('/gastro/updateDrink', methods=['POST']) @login_required(groups=[GASTRO]) def updateDrink(**kwargs): + debug.info("/gastro/updateDrink") try: data = request.get_json() retVal = userController.updateDrinkPrice(data) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @gastrouser.route('/gastro/deleteDrink', methods=['POST']) @login_required(groups=[GASTRO]) def deleteDrink(**kwargs): + debug.info("/gastro/dleteDrink") try: data = request.get_json() id = data['id'] retVal = userController.deletDrinkPrice({"id": id}) + debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @gastrouser.route('/gastro/setDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def setType(**kwark): + debug.info("/gastro/setDrinkType") try: data = request.get_json() name = data['name'] retVal = userController.setDrinkType(name) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @gastrouser.route('/gastro/updateDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def updateType(**kwargs): + debug.info("/gastro/updateDrinkType") try: data = request.get_json() retVal = userController.updateDrinkType(data) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @gastrouser.route('/gastro/deleteDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def deleteType(**kwargs): + debug.info("/gastro/deleteDrinkType") try: data = request.get_json() userController.deleteDrinkType(data) + debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 diff --git a/geruecht/logger.py b/geruecht/logger.py index 496266f..1d9b9a8 100644 --- a/geruecht/logger.py +++ b/geruecht/logger.py @@ -1,35 +1,24 @@ import logging -from logging.handlers import WatchedFileHandler -import sys +import logging.config +import yaml +from os import path -FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") +if not path.exists("geruecht/log/debug"): + a = path.join(path.curdir ,"geruecht", "log", "debug") -logFileHandler = WatchedFileHandler("geruecht/log/debug.log") -logFileHandler.setFormatter(FORMATTER) +if not path.exists("geruecht/log/info"): + b = path.join(path.curdir, "geruecht", "log", "info") -logStreamHandler = logging.StreamHandler(stream=sys.stdout) -logStreamHandler.setFormatter(FORMATTER) -def getLogger(logger_name): - return getDebugLogger(logger_name) +with open("geruecht/logging.yml", 'rt') as file: + config = yaml.safe_load(file.read()) +logging.config.dictConfig(config) -def getInfoLogger(logger_name): - logger = logging.getLogger(logger_name) - logger.setLevel(logging.INFO) - logger.addHandler(logStreamHandler) - logFileHandler = WatchedFileHandler("geruecht/log/info/{}.log".format(logger_name)) - logFileHandler.setFormatter(FORMATTER) - logger.addHandler(logFileHandler) - return logger +def getDebugLogger(): + return logging.getLogger("debug_logger") -def getDebugLogger(logger_name, path=False): +def getCreditLogger(): + return logging.getLogger("credit_logger") - logger = logging.getLogger(logger_name) - if path: - logSecondFileHandler = WatchedFileHandler("geruecht/log/debug/{}.log".format(logger_name)) - logSecondFileHandler.setFormatter(FORMATTER) - logger.addHandler(logSecondFileHandler) - logger.setLevel(logging.DEBUG) - logger.addHandler(logFileHandler) - logger.propagate = False - return logger \ No newline at end of file +def getJobsLogger(): + return logging.getLogger("jobs_logger") \ No newline at end of file diff --git a/geruecht/logging.yml b/geruecht/logging.yml new file mode 100644 index 0000000..6ca9241 --- /dev/null +++ b/geruecht/logging.yml @@ -0,0 +1,57 @@ +version: 1 +disable_existing_loggers: True + +formatters: + debug: + format: "%(asctime)s — %(filename)s - %(funcName)s - %(lineno)d - %(threadName)s - %(name)s — %(levelname)s — %(message)s" + + simple: + format: "%(asctime)s - %(name)s - %(message)s" + +handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: debug + stream: ext://sys.stdout + + debug: + class: logging.handlers.WatchedFileHandler + level: DEBUG + formatter: debug + filename: geruecht/log/debug/debug.log + encoding: utf8 + + credit: + class: logging.handlers.WatchedFileHandler + level: INFO + formatter: simple + filename: geruecht/log/info/geruecht.log + encoding: utf8 + + jobs: + class: logging.handlers.WatchedFileHandler + level: INFO + formatter: simple + filename: geruecht/log/info/jobs.log + encoding: utf8 + +loggers: + debug_logger: + level: DEBUG + handlers: [console, debug] + propagate: no + + credit_logger: + level: INFO + handlers: [credit] + propagate: no + + jobs_logger: + level: INFO + handlers: [jobs] + propagate: no + +root: + level: INFO + handlers: [console, debug] \ No newline at end of file diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 7e37ddb..2daa007 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -1,7 +1,7 @@ from datetime import datetime -from geruecht.logger import getLogger +from geruecht.logger import getDebugLogger -LOGGER = getLogger(__name__) +LOGGER = getDebugLogger() class AccessToken(): """ Model for an AccessToken diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 4dd2f5e..44f8e1a 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,7 +1,7 @@ from datetime import datetime -from geruecht.logger import getLogger +from geruecht.logger import getDebugLogger -LOGGER = getLogger(__name__) +LOGGER = getDebugLogger() def create_empty_data(): empty_data = {'id': 0, 'jan_guthaben': 0, diff --git a/geruecht/model/user.py b/geruecht/model/user.py index a1a0e8e..680d834 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -1,8 +1,8 @@ -from geruecht.logger import getLogger +from geruecht.logger import getDebugLogger from geruecht.model.creditList import CreditList, create_empty_data from datetime import datetime -LOGGER = getLogger(__name__) +LOGGER = getDebugLogger() class User(): diff --git a/geruecht/routes.py b/geruecht/routes.py index 1070229..cd5792f 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -1,5 +1,5 @@ from geruecht import app -from geruecht.logger import getDebugLogger, getInfoLogger +from geruecht.logger import getDebugLogger from geruecht.decorator import login_required from geruecht.exceptions import PermissionDenied import geruecht.controller.accesTokenController as ac @@ -10,29 +10,7 @@ from flask import request, jsonify accesTokenController = ac.AccesTokenController() userController = uc.UserController() -debug = getDebugLogger("MAIN-ROUTE", True) -info = getInfoLogger("MAIN-ROUTE") - -def login(user, password): - return user.login(password) - - -@app.route("/valid") -def _valid(): - token = request.headers.get("Token") - accToken = accesTokenController.validateAccessToken(token, [MONEY]) - if accToken: - return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, [BAR]) - if accToken: - return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, [GASTRO]) - if accToken: - return jsonify(accToken.user.toJSON()) - accToken = accesTokenController.validateAccessToken(token, [USER]) - if accToken: - return jsonify(accToken.user.toJSON()) - return jsonify({"error": "permission denied"}), 401 +debug = getDebugLogger() @app.route("/pricelist", methods=['GET']) def _getPricelist(): @@ -162,9 +140,9 @@ def _login(): """ debug.info("Start log in.") data = request.get_json() - debug.debug("JSON from request: {}".format(data)) username = data['username'] password = data['password'] + debug.debug("username is {{}}".format(username)) try: debug.info("search {{}} in database".format(username)) user, ldap_conn = userController.loginUser(username, password) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index ab041de..9e0f225 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -4,41 +4,57 @@ import geruecht.controller.userController as uc from geruecht.model import USER from datetime import datetime from geruecht.exceptions import DayLocked +from geruecht.logger import getDebugLogger, getCreditLogger, getJobsLogger user = Blueprint("user", __name__) userController = uc.UserController() +debug = getDebugLogger() +creditL = getCreditLogger() +jobL = getJobsLogger() @user.route("/user/main") @login_required(groups=[USER]) def _main(**kwargs): - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - accToken.user = userController.getUser(accToken.user.uid) - retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - return jsonify(retVal) - return jsonify("error", "something went wrong"), 500 + debug.info("/user/main") + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + accToken.user = userController.getUser(accToken.user.uid) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify("error", "something went wrong"), 500 @user.route("/user/addAmount", methods=['POST']) @login_required(groups=[USER]) def _addAmount(**kwargs): - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - data = request.get_json() - amount = int(data['amount']) - date = datetime.now() - userController.addAmount(accToken.user.uid, amount, year=date.year, month=date.month) - accToken.user = userController.getUser(accToken.user.uid) - retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - return jsonify(retVal) - return jsonify({"error": "something went wrong"}), 500 + debug.info("/user/addAmount") + try: + if 'accToken' in kwargs: + accToken = kwargs['accToken'] + data = request.get_json() + amount = int(data['amount']) + date = datetime.now() + userController.addAmount(accToken.user.uid, amount, year=date.year, month=date.month) + accToken.user = userController.getUser(accToken.user.uid) + retVal = accToken.user.toJSON() + retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + debug.debug("return {{}}".format(retVal)) + creditL.info("{} {} {} fügt sich selbst {} € Schulden hinzu".format(date, accToken.user.firstname, accToken.user.lastname, amount/100)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": "something went wrong"}), 500 @user.route("/user/saveConfig", methods=['POST']) @login_required(groups=[USER]) def _saveConfig(**kwargs): + debug.info("/user/saveConfig") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -46,47 +62,55 @@ def _saveConfig(**kwargs): accToken.user = userController.modifyUser(accToken.user, accToken.ldap_conn, data) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/job", methods=['POST']) @login_required(groups=[USER]) def _getUser(**kwargs): - data = request.get_json() - day = data['day'] - month = data['month'] - year = data['year'] - date = datetime(year, month, day, 12) - lockedDay = userController.getLockedDay(date) - if not lockedDay: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': False + debug.info("/user/job") + try: + data = request.get_json() + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) + lockedDay = userController.getLockedDay(date) + if not lockedDay: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': False + } + else: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } + retVal = { + 'worker': userController.getWorker(date), + 'day': lockedDay } - else: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': lockedDay['locked'] - } - retVal = { - 'worker': userController.getWorker(date), - 'day': lockedDay - } - print(retVal) - return jsonify(retVal) + debug.debug("retrun {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @user.route("/user/addJob", methods=['POST']) @login_required(groups=[USER]) def _addUser(**kwargs): + debug.info("/user/addJob") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -97,16 +121,20 @@ def _addUser(**kwargs): year = data['year'] date = datetime(year,month,day,12) retVal = userController.addWorker(user.uid, date, userExc=True) - print(retVal) + debug.debug("return {{}}".format(retVal)) + jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format(user.firstname, user.lastname, date.date())) return jsonify(retVal) except DayLocked as err: + debug.debug("exception", exc_info=True) return jsonify({'error': str(err)}), 403 except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({'error': str(err)}), 409 @user.route("/user/deleteJob", methods=['POST']) @login_required(groups=[USER]) def _deletJob(**kwargs): + debug.info("/user/deleteJob") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -117,15 +145,20 @@ def _deletJob(**kwargs): year = data['year'] date = datetime(year,month,day,12) userController.deleteWorker(user.uid, date, True) + debug.debug("return ok") + jobL.info("Mitglied {} {} entfernt sich am {} aus dem Dienst".format(user.firstname, user.lastname, date.date())) return jsonify({"ok": "ok"}) except DayLocked as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 403 except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/transactJob", methods=['POST']) @login_required(groups=[USER]) def _transactJob(**kwargs): + debug.info("/user/transactJob") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -138,17 +171,22 @@ def _transactJob(**kwargs): date = datetime(year, month, day, 12) to_user = userController.getUser(username) retVal = userController.setTransactJob(user, to_user, date) + from_userl = retVal['from_user'] + to_userl = retVal['to_user'] retVal['from_user'] = retVal['from_user'].toJSON() retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} - print(retVal) + debug.debug("return {{}}".format(retVal)) + jobL.info("Mitglied {} {} sendet Dienstanfrage an Mitglied {} {} am {}".format(from_userl.firstname, from_userl.lastname, to_userl.firstname, to_userl.lastname, date.date())) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/answerTransactJob", methods=['POST']) @login_required(groups=[USER]) def _answer(**kwargs): + debug.info("/user/answerTransactJob") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -162,17 +200,22 @@ def _answer(**kwargs): date = datetime(year, month, day, 12) from_user = userController.getUser(username) retVal = userController.answerdTransactJob(from_user, user, date, answer) + from_userl = retVal['from_user'] + to_userl = retVal['to_user'] retVal['from_user'] = retVal['from_user'].toJSON() retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} - print(retVal) + debug.debug("return {{}}".format(retVal)) + jobL.info("Mitglied {} {} beantwortet Dienstanfrage von {} {} am {} mit {}".format(to_userl.firstname, to_userl.lastname, from_userl.firstname, from_userl.lastname, date.date(), 'JA' if answer else 'NEIN')) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/jobRequests", methods=['POST']) @login_required(groups=[USER]) def _requests(**kwargs): + debug.info("/user/jobRequests") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -188,14 +231,16 @@ def _requests(**kwargs): data['to_user'] = data['to_user'].toJSON() data_date = data['date'] data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} - print(retVal) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/getTransactJobs", methods=['POST']) @login_required(groups=[USER]) def _getTransactJobs(**kwargs): + debug.info("/user/getTransactJobs") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -211,14 +256,16 @@ def _getTransactJobs(**kwargs): data['to_user'] = data['to_user'].toJSON() data_date = data['date'] data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} - print(retVal) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/deleteTransactJob", methods=['POST']) @login_required(groups=[USER]) def _deleteTransactJob(**kwargs): + debug.info("/user/deleteTransactJob") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -231,8 +278,11 @@ def _deleteTransactJob(**kwargs): date = datetime(year, month, day, 12) to_user = userController.getUser(username) userController.deleteTransactJob(from_user, to_user, date) + debug.debug("return ok") + jobL.info("Mitglied {} {} entfernt Dienstanfrage an {} {} am {}".format(from_user.firstname, from_user.lastname, to_user.firstname, to_user.lastname, date.date())) return jsonify({"ok": "ok"}) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @user.route("/user/storno", methods=['POST']) @@ -246,6 +296,8 @@ def _storno(**kwargs): JSON-File with userID and the amount or ERROR 401 Permission Denied """ + + debug.info("/user/storno") try: if 'accToken' in kwargs: accToken = kwargs['accToken'] @@ -258,6 +310,9 @@ def _storno(**kwargs): accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + debug.debug("return {{}}".format(retVal)) + creditL.info("{} {} {} storniert {} €".format(date, user.firstname, user.lastname, amount/100)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 26185b8..dd0d81c 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -4,7 +4,10 @@ import geruecht.controller.userController as uc import geruecht.controller.ldapController as lc from geruecht.decorator import login_required from geruecht.model import MONEY, GASTRO, VORSTAND -import time +from geruecht.logger import getDebugLogger, getJobsLogger + +debug = getDebugLogger() +jobL = getJobsLogger() vorstand = Blueprint("vorstand", __name__) userController = uc.UserController() @@ -13,124 +16,156 @@ ldap= lc.LDAPController() @vorstand.route('/um/setStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _setStatus(**kwargs): + debug.info("/um/setStatus") try: data = request.get_json() name = data['name'] retVal = userController.setStatus(name) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @vorstand.route('/um/updateStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatus(**kwargs): + debug.info("/um/updateStatus") try: data = request.get_json() retVal = userController.updateStatus(data) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @vorstand.route('/um/deleteStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deleteStatus(**kwargs): + debug.info("/um/deleteStatus") try: data = request.get_json() userController.deleteStatus(data) + debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 @vorstand.route('/um/updateStatusUser', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatusUser(**kwargs): + debug.info("/um/updateStatusUser") try: data = request.get_json() username = data['username'] status = data['status'] retVal = userController.updateStatusOfUser(username, status).toJSON() + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @vorstand.route('/um/updateVoting', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateVoting(**kwargs): + debug.info("/um/updateVoting") try: data = request.get_json() username = data['username'] voting = data['voting'] retVal = userController.updateVotingOfUser(username, voting).toJSON() + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/addUser", methods=['POST', 'GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _addUser(**kwargs): - - if request.method == 'GET': - return "

HEllo World

" - - data = request.get_json() - user = data['user'] - day = data['day'] - month = data['month'] - year = data['year'] - date = datetime(year,month,day,12) - retVal = userController.addWorker(user['username'], date) - print(retVal) - return jsonify(retVal) + debug.info("/sm/addUser") + try: + data = request.get_json() + user = data['user'] + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year,month,day,12) + retVal = userController.addWorker(user['username'], date) + debug.debug("retrun {{}}".format(retVal)) + userl = userController.getUser(user) + jobL.info("Vorstand {} {} schreibt Mitglied {} {} am {} zum Dienst ein".format(kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, userl.firstname, userl.lastname, date.date())) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/getUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUser(**kwargs): - data = request.get_json() - day = data['day'] - month = data['month'] - year = data['year'] - date = datetime(year, month, day, 12) - lockedDay = userController.getLockedDay(date) - if not lockedDay: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': False + debug.info("/sm/getUser") + try: + data = request.get_json() + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) + lockedDay = userController.getLockedDay(date) + if not lockedDay: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': False + } + else: + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } + retVal = { + 'worker': userController.getWorker(date), + 'day': lockedDay } - else: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': lockedDay['locked'] - } - retVal = { - 'worker': userController.getWorker(date), - 'day': lockedDay - } - print(retVal) - return jsonify(retVal) + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/deleteUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deletUser(**kwargs): - data = request.get_json() - user = data['user'] - day = data['day'] - month = data['month'] - year = data['year'] - date = datetime(year, month, day, 12) - userController.deleteWorker(user['username'], date) - return jsonify({"ok": "ok"}) + debug.info("/sm/deletUser") + try: + data = request.get_json() + user = data['user'] + day = data['day'] + month = data['month'] + year = data['year'] + date = datetime(year, month, day, 12) + userController.deleteWorker(user['username'], date) + debug.debug("return ok") + user = userController.getUser(user) + jobL.info("Vorstand {} {} entfernt Mitglied {} {} am {} vom Dienst".format(kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, date.date())) + return jsonify({"ok": "ok"}) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/lockDay", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _lockDay(**kwargs): + debug.info("/sm/lockDay") try: data = request.get_json() year = data['year'] @@ -157,13 +192,20 @@ def _lockDay(**kwargs): }, 'locked': lockedDay['locked'] } - print(retVal) + debug.debug("return {{}}".format(retVal)) return jsonify(retVal) except Exception as err: + debug.debug("exception", exc_info=True) return jsonify({'error': err}), 409 @vorstand.route("/sm/searchWithExtern", methods=['GET']) @login_required(groups=[VORSTAND]) def _search(**kwargs): - retVal = ldap.getAllUser() - return jsonify(retVal) \ No newline at end of file + debug.info("/sm/searchWithExtern") + try: + retVal = ldap.getAllUser() + debug.debug("return {{}}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 \ No newline at end of file From f029aa6096c54e27ecb22916698ad6094d150f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=B6ger?= Date: Tue, 10 Mar 2020 11:08:24 +0100 Subject: [PATCH 069/111] replace string with '{{}}' to '{{ {} }}' for logging fixed missing values (bugs that crash) --- geruecht/baruser/routes.py | 45 ++++++++++++++------------ geruecht/configparser.py | 29 +++++++++++------ geruecht/decorator.py | 18 +++++++---- geruecht/finanzer/routes.py | 61 ++++++++++++++++++++--------------- geruecht/gastro/routes.py | 17 ++++++---- geruecht/logger.py | 7 ++-- geruecht/routes.py | 64 +++++++++++++++++++++++-------------- geruecht/user/routes.py | 24 +++++++------- geruecht/vorstand/routes.py | 40 +++++++++++++++-------- 9 files changed, 185 insertions(+), 120 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 3d756db..d22d87c 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -11,7 +11,7 @@ creditL = getCreditLogger() baruser = Blueprint("baruser", __name__) -ldap= lc.LDAPController() +ldap = lc.LDAPController() userController = uc.UserController() @@ -34,8 +34,6 @@ def _bar(**kwargs): geruecht = None geruecht = user.getGeruecht(datetime.now().year) if geruecht is not None: - month = geruecht.getMonth(datetime.now().month) - amount = month[0] - month[1] all = geruecht.getSchulden() if all != 0: if all >= 0: @@ -43,13 +41,13 @@ def _bar(**kwargs): else: type = 'amount' dic[user.uid] = {"username": user.uid, - "firstname": user.firstname, - "lastname": user.lastname, - "amount": all, - "locked": user.locked, - "type": type - } - debug.debug("return {{}}".format(dic)) + "firstname": user.firstname, + "lastname": user.lastname, + "amount": all, + "locked": user.locked, + "type": type + } + debug.debug("return {{ {} }}".format(dic)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) @@ -74,7 +72,8 @@ def _baradd(**kwargs): amount = int(data['amount']) date = datetime.now() - userController.addAmount(userID, amount, year=date.year, month=date.month) + userController.addAmount( + userID, amount, year=date.year, month=date.month) user = userController.getUser(userID) geruecht = user.getGeruecht(year=date.year) month = geruecht.getMonth(month=date.month) @@ -87,8 +86,9 @@ def _baradd(**kwargs): dic = user.toJSON() dic['amount'] = abs(all) dic['type'] = type - debug.debug("return {{}}".format(dic)) - creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format(date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amount/100)) + debug.debug("return {{ {} }}".format(dic)) + creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format( + date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amount/100)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) @@ -110,12 +110,13 @@ def _getUsers(**kwargs): try: retVal = {} retVal = ldap.getAllUser() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @baruser.route("/bar/storno", methods=['POST']) @login_required(groups=[BAR]) def _storno(**kwargs): @@ -134,7 +135,8 @@ def _storno(**kwargs): amount = int(data['amount']) date = datetime.now() - userController.addCredit(userID, amount, year=date.year, month=date.month) + userController.addCredit( + userID, amount, year=date.year, month=date.month) user = userController.getUser(userID) geruecht = user.getGeruecht(year=date.year) month = geruecht.getMonth(month=date.month) @@ -147,13 +149,15 @@ def _storno(**kwargs): dic = user.toJSON() dic['amount'] = abs(all) dic['type'] = type - debug.debug("return {{}}".format(dic)) - creditL.info("{} Baruser {} {} storniert {} € von {} {}".format(date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100, user.firstname, user.lastname)) + debug.debug("return {{ {} }}".format(dic)) + creditL.info("{} Baruser {} {} storniert {} € von {} {}".format( + date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100, user.firstname, user.lastname)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @baruser.route("/barGetUser", methods=['POST']) @login_required(groups=[BAR]) def _getUser(**kwargs): @@ -171,14 +175,15 @@ def _getUser(**kwargs): retVal = user.toJSON() retVal['amount'] = amount retVal['type'] = type - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @baruser.route("/search", methods=['GET']) -@login_required(groups=[BAR, MONEY, USER,VORSTAND]) +@login_required(groups=[BAR, MONEY, USER, VORSTAND]) def _search(**kwargs): debug.info("/search") try: @@ -187,7 +192,7 @@ def _search(**kwargs): if user['username'] == 'extern': retVal.remove(user) break - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) diff --git a/geruecht/configparser.py b/geruecht/configparser.py index 96dfadd..2106e93 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -15,6 +15,7 @@ default = { } } + class ConifgParser(): def __init__(self, file='config.yml'): self.file = file @@ -22,32 +23,40 @@ class ConifgParser(): self.config = yaml.safe_load(f) if 'Database' not in self.config: - self.__error__('Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') + self.__error__( + 'Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') if 'URL' not in self.config['Database'] or 'user' not in self.config['Database'] or 'passwd' not in self.config['Database'] or 'database' not in self.config['Database']: - self.__error__('Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') + self.__error__( + 'Wrong Configuration for Database. You should configure databaseconfig with "URL", "user", "passwd", "database"') self.db = self.config['Database'] DEBUG.debug("Set Databaseconfig: {}".format(self.db)) if 'LDAP' not in self.config: - self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') + self.__error__( + 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') if 'URL' not in self.config['LDAP'] or 'dn' not in self.config['LDAP']: - self.__error__('Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') + self.__error__( + 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') if 'port' not in self.config['LDAP']: - DEBUG.info('No Config for port in LDAP found. Set it to default: {}'.format(389)) + DEBUG.info( + 'No Config for port in LDAP found. Set it to default: {}'.format(389)) self.config['LDAP']['port'] = 389 self.ldap = self.config['LDAP'] DEBUG.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: self.accessTokenLifeTime = int(self.config['AccessTokenLifeTime']) - DEBUG.info("Set AccessTokenLifeTime: {}".format(self.accessTokenLifeTime)) + DEBUG.info("Set AccessTokenLifeTime: {}".format( + self.accessTokenLifeTime)) else: self.accessTokenLifeTime = default['AccessTokenLifeTime'] - DEBUG.info("No Config for AccessTokenLifetime found. Set it to default: {}".format(self.accessTokenLifeTime)) + DEBUG.info("No Config for AccessTokenLifetime found. Set it to default: {}".format( + self.accessTokenLifeTime)) if 'Mail' not in self.config: self.config['Mail'] = default['Mail'] - DEBUG.info('No Conifg for Mail found. Set it to defaul: {}'.format(self.config['Mail'])) + DEBUG.info('No Conifg for Mail found. Set it to defaul: {}'.format( + self.config['Mail'])) if 'URL' not in self.config['Mail']: self.config['Mail']['URL'] = default['Mail']['URL'] DEBUG.info("No Config for URL in Mail found. Set it to default") @@ -72,7 +81,6 @@ class ConifgParser(): self.mail = self.config['Mail'] DEBUG.info('Set Mailconfig: {}'.format(self.mail)) - def getLDAP(self): return self.ldap @@ -89,5 +97,6 @@ class ConifgParser(): DEBUG.error(msg, exc_info=True) sys.exit(-1) + if __name__ == '__main__': - ConifgParser() \ No newline at end of file + ConifgParser() diff --git a/geruecht/decorator.py b/geruecht/decorator.py index 582eb08..c01bd77 100644 --- a/geruecht/decorator.py +++ b/geruecht/decorator.py @@ -1,6 +1,8 @@ from functools import wraps from .logger import getDebugLogger DEBUG = getDebugLogger() + + def login_required(**kwargs): import geruecht.controller.accesTokenController as ac from geruecht.model import BAR, USER, MONEY, GASTRO @@ -9,20 +11,22 @@ def login_required(**kwargs): groups = [USER, BAR, GASTRO, MONEY] if "groups" in kwargs: groups = kwargs["groups"] - DEBUG.debug("groups are {{}}".format(groups)) + DEBUG.debug("groups are {{ {} }}".format(groups)) + def real_decorator(func): @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get('Token') - DEBUG.debug("token is {{}}".format(token)) + DEBUG.debug("token is {{ {} }}".format(token)) accToken = accessController.validateAccessToken(token, groups) - DEBUG.debug("accToken is {{}}".format(accToken)) + DEBUG.debug("accToken is {{ {} }}".format(accToken)) kwargs['accToken'] = accToken if accToken: - DEBUG.debug("token {{}} is valid".format(token)) + DEBUG.debug("token {{ {} }} is valid".format(token)) return func(*args, **kwargs) else: - DEBUG.warning("token {{}} is not valid".format(token)) - return jsonify({"error": "error", "message": "permission denied"}), 401 + DEBUG.warning("token {{ {} }} is not valid".format(token)) + return jsonify({"error": "error", + "message": "permission denied"}), 401 return wrapper - return real_decorator \ No newline at end of file + return real_decorator diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 93c7969..96b72d1 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -30,13 +30,15 @@ def _getFinanzer(**kwargs): dic = {} for user in users: dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - debug.debug("return {{}}".format(dic)) + dic[user.uid]['creditList'] = { + credit.year: credit.toJSON() for credit in user.geruechte} + debug.debug("return {{ {} }}".format(dic)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @finanzer.route("/finanzerAddAmount", methods=['POST']) @login_required(groups=[MONEY]) def _addAmount(**kwargs): @@ -57,23 +59,27 @@ def _addAmount(**kwargs): amount = int(data['amount']) try: year = int(data['year']) - except KeyError as er: + except KeyError: year = datetime.now().year try: month = int(data['month']) - except KeyError as er: + except KeyError: month = datetime.now().month - userController.addAmount(userID, amount, year=year, month=month, finanzer=True) + userController.addAmount( + userID, amount, year=year, month=month, finanzer=True) user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal = {str(geruecht.year): geruecht.toJSON() + for geruecht in user.geruechte} retVal['locked'] = user.locked - debug.debug("return {{}}".format(retVal)) - creditL.info("{} Finanzer {} {} fügt {} {} {} € Schulden hinzu.".format(datetime(year,month).date(), kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100)) + debug.debug("return {{ {} }}".format(retVal)) + creditL.info("{} Finanzer {} {} fügt {} {} {} € Schulden hinzu.".format(datetime(year, month, 1).date( + ), kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amount/100)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @finanzer.route("/finanzerAddCredit", methods=['POST']) @login_required(groups=[MONEY]) def _addCredit(**kwargs): @@ -95,22 +101,22 @@ def _addCredit(**kwargs): try: year = int(data['year']) - except KeyError as er: + except KeyError: year = datetime.now().year try: month = int(data['month']) - except KeyError as er: + except KeyError: month = datetime.now().month - userController.addCredit(userID, credit, year=year, month=month).toJSON() + userController.addCredit( + userID, credit, year=year, month=month).toJSON() user = userController.getUser(userID) - retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} + retVal = {str(geruecht.year): geruecht.toJSON() + for geruecht in user.geruechte} retVal['locked'] = user.locked - debug.debug("return {{}}".format(retVal)) - creditL.info("{} Finanzer {} {} fügt {} {} {} € Guthaben hinzu.".format(datetime(year, month).date(), - kwargs['accToken'].user.firstname, - kwargs['accToken'].user.lastname, - credit / 100)) + debug.debug("return {{ {} }}".format(retVal)) + creditL.info("{} Finanzer {} {} fügt {} {} {} € Guthaben hinzu.".format(datetime(year, month, 1).date( + ), kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, credit / 100)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -126,7 +132,7 @@ def _finanzerLock(**kwargs): username = data['userId'] locked = bool(data['locked']) retVal = userController.lockUser(username, locked).toJSON() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -142,13 +148,15 @@ def _finanzerSetConfig(**kwargs): username = data['userId'] autoLock = bool(data['autoLock']) limit = int(data['limit']) - retVal = userController.updateConfig(username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() - debug.debug("return {{}}".format(retVal)) + retVal = userController.updateConfig( + username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @finanzer.route("/finanzerAddUser", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerAddUser(**kwargs): @@ -161,13 +169,15 @@ def _finanzerAddUser(**kwargs): dic = {} for user in users: dic[user.uid] = user.toJSON() - dic[user.uid]['creditList'] = {credit.year: credit.toJSON() for credit in user.geruechte} - debug.debug("return {{}}".format(dic)) + dic[user.uid]['creditList'] = { + credit.year: credit.toJSON() for credit in user.geruechte} + debug.debug("return {{ {} }}".format(dic)) return jsonify(dic), 200 except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @finanzer.route("/finanzerSendOneMail", methods=['POST']) @login_required(groups=[MONEY]) def _finanzerSendOneMail(**kwargs): @@ -176,20 +186,21 @@ def _finanzerSendOneMail(**kwargs): data = request.get_json() username = data['userId'] retVal = userController.sendMail(username) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @finanzer.route("/finanzerSendAllMail", methods=['GET']) @login_required(groups=[MONEY]) def _finanzerSendAllMail(**kwargs): debug.info("/finanzerSendAllMail") try: retVal = userController.sendAllMail() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 500 \ No newline at end of file + return jsonify({"error": str(err)}), 500 diff --git a/geruecht/gastro/routes.py b/geruecht/gastro/routes.py index 787faf8..5ddb3c1 100644 --- a/geruecht/gastro/routes.py +++ b/geruecht/gastro/routes.py @@ -10,6 +10,7 @@ gastrouser = Blueprint('gastrouser', __name__) userController = uc.UserController() + @gastrouser.route('/gastro/setDrink', methods=['POST']) @login_required(groups=[GASTRO]) def setDrink(**kwargs): @@ -17,12 +18,13 @@ def setDrink(**kwargs): try: data = request.get_json() retVal = userController.setDrinkPrice(data) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @gastrouser.route('/gastro/updateDrink', methods=['POST']) @login_required(groups=[GASTRO]) def updateDrink(**kwargs): @@ -30,12 +32,13 @@ def updateDrink(**kwargs): try: data = request.get_json() retVal = userController.updateDrinkPrice(data) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @gastrouser.route('/gastro/deleteDrink', methods=['POST']) @login_required(groups=[GASTRO]) def deleteDrink(**kwargs): @@ -43,13 +46,14 @@ def deleteDrink(**kwargs): try: data = request.get_json() id = data['id'] - retVal = userController.deletDrinkPrice({"id": id}) + userController.deletDrinkPrice({"id": id}) debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @gastrouser.route('/gastro/setDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def setType(**kwark): @@ -58,12 +62,13 @@ def setType(**kwark): data = request.get_json() name = data['name'] retVal = userController.setDrinkType(name) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @gastrouser.route('/gastro/updateDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def updateType(**kwargs): @@ -71,12 +76,13 @@ def updateType(**kwargs): try: data = request.get_json() retVal = userController.updateDrinkType(data) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @gastrouser.route('/gastro/deleteDrinkType', methods=['POST']) @login_required(groups=[GASTRO]) def deleteType(**kwargs): @@ -89,4 +95,3 @@ def deleteType(**kwargs): except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 - diff --git a/geruecht/logger.py b/geruecht/logger.py index 1d9b9a8..f63ef04 100644 --- a/geruecht/logger.py +++ b/geruecht/logger.py @@ -4,7 +4,7 @@ import yaml from os import path if not path.exists("geruecht/log/debug"): - a = path.join(path.curdir ,"geruecht", "log", "debug") + a = path.join(path.curdir, "geruecht", "log", "debug") if not path.exists("geruecht/log/info"): b = path.join(path.curdir, "geruecht", "log", "info") @@ -14,11 +14,14 @@ with open("geruecht/logging.yml", 'rt') as file: config = yaml.safe_load(file.read()) logging.config.dictConfig(config) + def getDebugLogger(): return logging.getLogger("debug_logger") + def getCreditLogger(): return logging.getLogger("credit_logger") + def getJobsLogger(): - return logging.getLogger("jobs_logger") \ No newline at end of file + return logging.getLogger("jobs_logger") diff --git a/geruecht/routes.py b/geruecht/routes.py index cd5792f..ce92b09 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -12,40 +12,44 @@ userController = uc.UserController() debug = getDebugLogger() + @app.route("/pricelist", methods=['GET']) def _getPricelist(): try: debug.info("get pricelist") retVal = userController.getPricelist() - debug.info("return pricelist {{}}".format(retVal)) + debug.info("return pricelist {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.warning("exception in get pricelist.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route('/drinkTypes', methods=['GET']) def getTypes(): try: debug.info("get drinktypes") retVal = userController.getAllDrinkTypes() - debug.info("return drinktypes {{}}".format(retVal)) + debug.info("return drinktypes {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.warning("exception in get drinktypes.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route('/getAllStatus', methods=['GET']) @login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getAllStatus(**kwargs): try: debug.info("get all status for users") retVal = userController.getAllStatus() - debug.info("return all status for users {{}}".format(retVal)) + debug.info("return all status for users {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.warning("exception in get all status for users.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route('/getStatus', methods=['POST']) @login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) def _getStatus(**kwargs): @@ -53,28 +57,32 @@ def _getStatus(**kwargs): debug.info("get status from user") data = request.get_json() name = data['name'] - debug.info("get status from user {{}}".format(name)) + debug.info("get status from user {{ {} }}".format(name)) retVal = userController.getStatus(name) - debug.info("return status from user {{}} : {{}}".format(name, retVal)) + debug.info( + "return status from user {{ {} }} : {{ {} }}".format(name, retVal)) return jsonify(retVal) except Exception as err: debug.warning("exception in get status from user.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route('/getUsers', methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUsers(**kwargs): try: debug.info("get all users from database") users = userController.getAllUsersfromDB() - debug.debug("users are {{}}".format(users)) + debug.debug("users are {{ {} }}".format(users)) retVal = [user.toJSON() for user in users] - debug.info("return all users from database {{}}".format(retVal)) + debug.info("return all users from database {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: - debug.warning("exception in get all users from database.", exc_info=True) + debug.warning( + "exception in get all users from database.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route("/getLifeTime", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _getLifeTime(**kwargs): @@ -82,14 +90,16 @@ def _getLifeTime(**kwargs): debug.info("get lifetime of accesstoken") if 'accToken' in kwargs: accToken = kwargs['accToken'] - debug.debug("accessToken is {{}}".format(accToken)) + debug.debug("accessToken is {{ {} }}".format(accToken)) retVal = {"value": accToken.lifetime} - debug.info("return get lifetime from accesstoken {{}}".format(retVal)) + debug.info( + "return get lifetime from accesstoken {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.info("exception in get lifetime of accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route("/saveLifeTime", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _saveLifeTime(**kwargs): @@ -97,21 +107,25 @@ def _saveLifeTime(**kwargs): debug.info("save lifetime for accessToken") if 'accToken' in kwargs: accToken = kwargs['accToken'] - debug.debug("accessToken is {{}}".format(accToken)) + debug.debug("accessToken is {{ {} }}".format(accToken)) data = request.get_json() lifetime = data['value'] - debug.debug("lifetime is {{}}".format(lifetime)) - debug.info("set lifetime {{}} to accesstoken {{}}".format(lifetime, accToken)) + debug.debug("lifetime is {{ {} }}".format(lifetime)) + debug.info("set lifetime {{ {} }} to accesstoken {{ {} }}".format( + lifetime, accToken)) accToken.lifetime = lifetime debug.info("update accesstoken timestamp") accToken.updateTimestamp() retVal = {"value": accToken.lifetime} - debug.info("return save lifetime for accessToken {{}}".format(retVal)) + debug.info( + "return save lifetime for accessToken {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: - debug.warning("exception in save lifetime for accesstoken.", exc_info=True) + debug.warning( + "exception in save lifetime for accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route("/logout", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) def _logout(**kwargs): @@ -119,7 +133,7 @@ def _logout(**kwargs): debug.info("logout user") if 'accToken' in kwargs: accToken = kwargs['accToken'] - debug.debug("accesstoken is {{}}".format(accToken)) + debug.debug("accesstoken is {{ {} }}".format(accToken)) debug.info("delete accesstoken") accesTokenController.deleteAccessToken(accToken) debug.info("return ok logout user") @@ -128,6 +142,7 @@ def _logout(**kwargs): debug.warning("exception in logout user.", exc_info=True) return jsonify({"error": str(err)}), 500 + @app.route("/login", methods=['POST']) def _login(): """ Login User @@ -142,24 +157,25 @@ def _login(): data = request.get_json() username = data['username'] password = data['password'] - debug.debug("username is {{}}".format(username)) + debug.debug("username is {{ {} }}".format(username)) try: - debug.info("search {{}} in database".format(username)) + debug.info("search {{ {} }} in database".format(username)) user, ldap_conn = userController.loginUser(username, password) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) user.password = password token = accesTokenController.createAccesToken(user, ldap_conn) - debug.debug("accesstoken is {{}}".format(token)) + debug.debug("accesstoken is {{ {} }}".format(token)) debug.info("validate accesstoken") - dic = accesTokenController.validateAccessToken(token, [USER, EXTERN]).user.toJSON() + dic = accesTokenController.validateAccessToken( + token, [USER, EXTERN]).user.toJSON() dic["token"] = token dic["accessToken"] = token - debug.info("User {{}} success login.".format(username)) - debug.info("return login {{}}".format(dic)) + debug.info("User {{ {} }} success login.".format(username)) + debug.info("return login {{ {} }}".format(dic)) return jsonify(dic) except PermissionDenied as err: debug.warning("permission denied exception in logout", exc_info=True) return jsonify({"error": str(err)}), 401 - except Exception as err: + except Exception: debug.warning("exception in logout.", exc_info=True) return jsonify({"error": "permission denied"}), 401 diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 9e0f225..357e784 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -24,9 +24,9 @@ def _main(**kwargs): accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) - except Exception as err: + except Exception: debug.debug("exception", exc_info=True) return jsonify("error", "something went wrong"), 500 @@ -44,10 +44,10 @@ def _addAmount(**kwargs): accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) creditL.info("{} {} {} fügt sich selbst {} € Schulden hinzu".format(date, accToken.user.firstname, accToken.user.lastname, amount/100)) return jsonify(retVal) - except Exception as err: + except Exception: debug.debug("exception", exc_info=True) return jsonify({"error": "something went wrong"}), 500 @@ -62,7 +62,7 @@ def _saveConfig(**kwargs): accToken.user = userController.modifyUser(accToken.user, accToken.ldap_conn, data) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -101,7 +101,7 @@ def _getUser(**kwargs): 'worker': userController.getWorker(date), 'day': lockedDay } - debug.debug("retrun {{}}".format(retVal)) + debug.debug("retrun {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -121,7 +121,7 @@ def _addUser(**kwargs): year = data['year'] date = datetime(year,month,day,12) retVal = userController.addWorker(user.uid, date, userExc=True) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format(user.firstname, user.lastname, date.date())) return jsonify(retVal) except DayLocked as err: @@ -176,7 +176,7 @@ def _transactJob(**kwargs): retVal['from_user'] = retVal['from_user'].toJSON() retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) jobL.info("Mitglied {} {} sendet Dienstanfrage an Mitglied {} {} am {}".format(from_userl.firstname, from_userl.lastname, to_userl.firstname, to_userl.lastname, date.date())) return jsonify(retVal) except Exception as err: @@ -205,7 +205,7 @@ def _answer(**kwargs): retVal['from_user'] = retVal['from_user'].toJSON() retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) jobL.info("Mitglied {} {} beantwortet Dienstanfrage von {} {} am {} mit {}".format(to_userl.firstname, to_userl.lastname, from_userl.firstname, from_userl.lastname, date.date(), 'JA' if answer else 'NEIN')) return jsonify(retVal) except Exception as err: @@ -231,7 +231,7 @@ def _requests(**kwargs): data['to_user'] = data['to_user'].toJSON() data_date = data['date'] data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -256,7 +256,7 @@ def _getTransactJobs(**kwargs): data['to_user'] = data['to_user'].toJSON() data_date = data['date'] data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) @@ -310,7 +310,7 @@ def _storno(**kwargs): accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) creditL.info("{} {} {} storniert {} €".format(date, user.firstname, user.lastname, amount/100)) return jsonify(retVal) except Exception as err: diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index dd0d81c..60070d5 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -11,7 +11,8 @@ jobL = getJobsLogger() vorstand = Blueprint("vorstand", __name__) userController = uc.UserController() -ldap= lc.LDAPController() +ldap = lc.LDAPController() + @vorstand.route('/um/setStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) @@ -21,12 +22,13 @@ def _setStatus(**kwargs): data = request.get_json() name = data['name'] retVal = userController.setStatus(name) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route('/um/updateStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatus(**kwargs): @@ -34,12 +36,13 @@ def _updateStatus(**kwargs): try: data = request.get_json() retVal = userController.updateStatus(data) - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route('/um/deleteStatus', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deleteStatus(**kwargs): @@ -53,6 +56,7 @@ def _deleteStatus(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @vorstand.route('/um/updateStatusUser', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateStatusUser(**kwargs): @@ -62,12 +66,13 @@ def _updateStatusUser(**kwargs): username = data['username'] status = data['status'] retVal = userController.updateStatusOfUser(username, status).toJSON() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route('/um/updateVoting', methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _updateVoting(**kwargs): @@ -77,12 +82,13 @@ def _updateVoting(**kwargs): username = data['username'] voting = data['voting'] retVal = userController.updateVotingOfUser(username, voting).toJSON() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/addUser", methods=['POST', 'GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _addUser(**kwargs): @@ -93,16 +99,18 @@ def _addUser(**kwargs): day = data['day'] month = data['month'] year = data['year'] - date = datetime(year,month,day,12) + date = datetime(year, month, day, 12) retVal = userController.addWorker(user['username'], date) - debug.debug("retrun {{}}".format(retVal)) + debug.debug("retrun {{ {} }}".format(retVal)) userl = userController.getUser(user) - jobL.info("Vorstand {} {} schreibt Mitglied {} {} am {} zum Dienst ein".format(kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, userl.firstname, userl.lastname, date.date())) + jobL.info("Vorstand {} {} schreibt Mitglied {} {} am {} zum Dienst ein".format( + kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, userl.firstname, userl.lastname, date.date())) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/getUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUser(**kwargs): @@ -136,12 +144,13 @@ def _getUser(**kwargs): 'worker': userController.getWorker(date), 'day': lockedDay } - debug.debug("return {{}}".format(retVal)) - return jsonify(retVal) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/deleteUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _deletUser(**kwargs): @@ -156,12 +165,14 @@ def _deletUser(**kwargs): userController.deleteWorker(user['username'], date) debug.debug("return ok") user = userController.getUser(user) - jobL.info("Vorstand {} {} entfernt Mitglied {} {} am {} vom Dienst".format(kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, date.date())) + jobL.info("Vorstand {} {} entfernt Mitglied {} {} am {} vom Dienst".format( + kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, date.date())) return jsonify({"ok": "ok"}) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/lockDay", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _lockDay(**kwargs): @@ -192,20 +203,21 @@ def _lockDay(**kwargs): }, 'locked': lockedDay['locked'] } - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({'error': err}), 409 + @vorstand.route("/sm/searchWithExtern", methods=['GET']) @login_required(groups=[VORSTAND]) def _search(**kwargs): debug.info("/sm/searchWithExtern") try: retVal = ldap.getAllUser() - debug.debug("return {{}}".format(retVal)) + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 500 \ No newline at end of file + return jsonify({"error": str(err)}), 500 From 804eba1abdbc0568639ebb27adcbc0742f2a9100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 10 Mar 2020 19:23:52 +0100 Subject: [PATCH 070/111] finished ##215 only database controlle is left for debugging --- geruecht/baruser/routes.py | 8 +- geruecht/controller/accesTokenController.py | 36 ++-- geruecht/controller/emailController.py | 26 ++- geruecht/controller/ldapController.py | 39 +++- geruecht/controller/userController.py | 210 ++++++++++---------- geruecht/model/accessToken.py | 7 +- geruecht/model/creditList.py | 23 ++- geruecht/model/user.py | 22 +- geruecht/vorstand/routes.py | 4 +- 9 files changed, 210 insertions(+), 165 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index d22d87c..f80dcc1 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -70,7 +70,7 @@ def _baradd(**kwargs): data = request.get_json() userID = data['userId'] amount = int(data['amount']) - + amountl = amount date = datetime.now() userController.addAmount( userID, amount, year=date.year, month=date.month) @@ -88,7 +88,7 @@ def _baradd(**kwargs): dic['type'] = type debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format( - date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amount/100)) + date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amountl/100)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) @@ -133,7 +133,7 @@ def _storno(**kwargs): data = request.get_json() userID = data['userId'] amount = int(data['amount']) - + amountl = amount date = datetime.now() userController.addCredit( userID, amount, year=date.year, month=date.month) @@ -151,7 +151,7 @@ def _storno(**kwargs): dic['type'] = type debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} storniert {} € von {} {}".format( - date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amount/100, user.firstname, user.lastname)) + date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amountl/100, user.firstname, user.lastname)) return jsonify(dic) except Exception as err: debug.debug("exception", exc_info=True) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 19d3951..bd1970f 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -2,10 +2,12 @@ from geruecht.model.accessToken import AccessToken import geruecht.controller as gc import geruecht.controller.userController as uc from geruecht.model import BAR -from geruecht.controller import LOGGER from datetime import datetime, timedelta import hashlib from . import Singleton +from geruecht.logger import getDebugLogger + +debug = getDebugLogger() userController = uc.UserController() @@ -26,18 +28,21 @@ class AccesTokenController(metaclass=Singleton): Initialize Thread and set tokenList empty. """ - LOGGER.info("Initialize AccessTokenController") + debug.info("init accesstoken controller") self.lifetime = gc.accConfig - self.tokenList = [] def checkBar(self, user): + debug.info("check if user {{ {} }} is baruser".format(user)) if (userController.checkBarUser(user)): if BAR not in user.group: + debug.debug("append bar to user {{ {} }}".format(user)) user.group.append(BAR) else: while BAR in user.group: + debug.debug("delete bar from user {{ {} }}".format(user)) user.group.remove(BAR) + debug.debug("user {{ {} }} groups are {{ {} }}".format(user, user.group)) def validateAccessToken(self, token, group): """ Verify Accestoken @@ -51,28 +56,29 @@ class AccesTokenController(metaclass=Singleton): Returns: An the AccesToken for this given Token or False. """ - LOGGER.info("Verify AccessToken with token: {} and group: {}".format(token, group)) + debug.info("check token {{ {} }} is valid") for accToken in self.tokenList: - LOGGER.debug("AccessToken is {}".format(accToken)) + debug.debug("accesstoken is {}".format(accToken)) endTime = accToken.timestamp + timedelta(seconds=accToken.lifetime) now = datetime.now() - LOGGER.debug("Check if AccessToken's Endtime {} is bigger then now {}".format(endTime, now)) + debug.debug("now is {{ {} }}, endtime is {{ {} }}".format(now, endTime)) if now <= endTime: - LOGGER.debug("Check is token {} same as in AccessToken {}".format(token, accToken)) + debug.debug("check if token {{ {} }} is same as {{ {} }}".format(token, accToken)) if accToken == token: self.checkBar(accToken.user) - LOGGER.debug("Check if AccesToken {} has same group {}".format(accToken, group)) + debug.debug("check if accestoken {{ {} }} has group {{ {} }}".format(accToken, group)) if self.isSameGroup(accToken, group): accToken.updateTimestamp() - LOGGER.info("Found AccessToken {} with token: {} and group: {}".format(accToken, token, group)) + debug.debug("found accesstoken {{ {} }} with token: {{ {} }} and group: {{ {} }}".format(accToken, token, group)) return accToken else: + debug.debug("accesstoken is {{ {} }} out of date".format(accToken)) self.deleteAccessToken(accToken) - LOGGER.info("Found no valid AccessToken with token: {} and group: {}".format(token, group)) + debug.debug("no valid accesstoken with token: {{ {} }} and group: {{ {} }}".format(token, group)) return False def deleteAccessToken(self, accToken): - LOGGER.debug("AccessToken {} is no longer valid and will removed".format(accToken)) + debug.info("delete accesstoken {{ {} }}".format(accToken)) self.tokenList.remove(accToken) def createAccesToken(self, user, ldap_conn): @@ -86,14 +92,13 @@ class AccesTokenController(metaclass=Singleton): Returns: A created Token for User """ - LOGGER.info("Create AccessToken") + debug.info("creat accesstoken") now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() self.checkBar(user) accToken = AccessToken(user, token, ldap_conn, self.lifetime, datetime.now()) - LOGGER.debug("Add AccessToken {} to current Tokens".format(accToken)) + debug.debug("accesstoken is {{ {} }}".format(accToken)) self.tokenList.append(accToken) - LOGGER.info("Finished create AccessToken {} with Token {}".format(accToken, token)) return token def isSameGroup(self, accToken, groups): @@ -108,8 +113,7 @@ class AccesTokenController(metaclass=Singleton): Returns: A Bool. If the same then True else False """ - print("controll if", accToken, "hase groups", groups) - LOGGER.debug("Check if AccessToken {} has group {}".format(accToken, groups)) + debug.info("check accesstoken {{ {} }} has group {{ {} }}".format(accToken, groups)) for group in groups: if group in accToken.user.group: return True return False diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index f6ccdee..e7f269a 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -5,11 +5,12 @@ from email.mime.text import MIMEText from email.header import Header from geruecht.logger import getDebugLogger -LOGGER = getDebugLogger() +debug = getDebugLogger() class EmailController(): def __init__(self, smtpServer, user, passwd, crypt, port=587, email=""): + debug.info("init email controller") self.smtpServer = smtpServer self.port = port self.user = user @@ -19,33 +20,36 @@ class EmailController(): self.email = email else: self.email = user - LOGGER.debug('Init EmailController with smtpServer={}, port={}, user={}, crypt={}, email={}'.format(smtpServer, port, user, crypt, self.email)) + debug.debug("smtpServer is {{ {} }}, port is {{ {} }}, user is {{ {} }}, crypt is {{ {} }}, email is {{ {} }}".format(smtpServer, port, user, crypt, self.email)) def __connect__(self): - LOGGER.info('Connect to E-Mail-Server') + debug.info('connect to email server') if self.crypt == 'SSL': self.smtp = smtplib.SMTP_SSL(self.smtpServer, self.port) log = self.smtp.ehlo() - LOGGER.debug(log) + debug.debug("ehlo is {{ {} }}".format(log)) if self.crypt == 'STARTTLS': self.smtp = smtplib.SMTP(self.smtpServer, self.port) log = self.smtp.ehlo() - LOGGER.debug(log) + debug.debug("ehlo is {{ {} }}".format(log)) log = self.smtp.starttls() - LOGGER.debug(log) + debug.debug("starttles is {{ {} }}".format(log)) log = self.smtp.login(self.user, self.passwd) - LOGGER.debug(log) + debug.debug("login is {{ {} }}".format(log)) def jobTransact(self, user, jobtransact): + debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user)) date = '{}.{}.{}'.format(jobtransact['date'].day, jobtransact['date'].month, jobtransact['date'].year) from_user = '{} {}'.format(jobtransact['from_user'].firstname, jobtransact['from_user'].lastname) subject = 'Bardienstanfrage am {}'.format(date) text = MIMEText( "Hallo {} {},\n" "{} fragt, ob du am {} zum Bardienst teilnehmen willst. Beantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'utf-8') + debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) def credit(self, user): + debug.info("create email credit for user {{ {} }}".format(user)) subject = Header('Gerücht, bezahle deine Schulden!', 'utf-8') sum = user.getGeruecht(datetime.now().year).getSchulden() if sum < 0: @@ -57,12 +61,14 @@ class EmailController(): text = MIMEText( "Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format( user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain', 'utf-8') + debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) def sendMail(self, user, type='credit', jobtransact=None): + debug.info("send email to user {{ {} }}".format(user)) try: if user.mail == 'None' or not user.mail: - LOGGER.debug("cant send email to {}. Has no email-address. {}".format(user.uid, {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) + debug.warning("user {{ {} }} has no email-address".format(user)) raise Exception("no valid Email") msg = MIMEMultipart() msg['From'] = self.email @@ -78,10 +84,10 @@ class EmailController(): msg['Subject'] = subject msg.attach(text) - LOGGER.debug("Send email to {}: '{}'".format(user.uid, msg.as_string())) + debug.debug("send email {{ {} }} to user {{ {} }}".format(msg.as_string(), user)) self.__connect__() self.smtp.sendmail(self.email, user.mail, msg.as_string()) - LOGGER.debug("Sended email to {}. {}".format(user.uid, {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}})) return {'error': False, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} except Exception: + debug.warning("exception in send email", exc_info=True) return {'error': True, 'user': {'userId': user.uid, 'firstname': user.firstname, 'lastname': user.lastname}} diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index b79ea56..e40eb23 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -6,7 +6,9 @@ from geruecht.exceptions import PermissionDenied from . import Singleton from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion from geruecht import ldapConfig -import traceback +from geruecht.logger import getDebugLogger + +debug = getDebugLogger() class LDAPController(metaclass=Singleton): ''' @@ -14,27 +16,38 @@ class LDAPController(metaclass=Singleton): ''' def __init__(self): + debug.info("init ldap controller") self.dn = ldapConfig['dn'] self.ldap = ldap + debug.debug("base dn is {{ {} }}".format(self.dn)) + debug.debug("ldap is {{ {} }}".format(self.ldap)) def login(self, username, password): + debug.info("login user {{ {} }} in ldap") try: retVal = self.ldap.authenticate(username, password, 'uid', self.dn) + debug.debug("authentification to ldap is {{ {} }}".format(retVal)) if not retVal: + debug.debug("authenification is incorrect") raise PermissionDenied("Invalid Password or Username") except Exception as err: - traceback.print_exception(err) - raise PermissionDenied("Wrong username or password.") + debug.warning("exception while login into ldap", exc_info=True) + raise PermissionDenied("Invalid Password or Username. {}".format(err)) def bind(self, user, password): + debug.info("bind user {{ {} }} to ldap") ldap_conn = self.ldap.connect(user.dn, password) + debug.debug("ldap_conn is {{ {} }}".format(ldap_conn)) return ldap_conn def getUserData(self, username): + debug.info("get user data from ldap of user {{ {} }}".format(username)) try: + debug.debug("search user in ldap") self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) user = self.ldap.connection.response[0]['attributes'] + debug.debug("user is {{ {} }}".format(user)) retVal = { 'dn': self.ldap.connection.response[0]['dn'], 'firstname': user['givenName'][0], @@ -43,20 +56,25 @@ class LDAPController(metaclass=Singleton): } if user['mail']: retVal['mail'] = user['mail'][0] + debug.debug("user is {{ {} }}".format(retVal)) return retVal except: + debug.warning("exception in get user data from ldap", exc_info=True) raise PermissionDenied("No User exists with this uid.") def getGroup(self, username): + debug.info("get group from user {{ {} }}".format(username)) try: retVal = [] self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['gidNumber']) response = self.ldap.connection.response main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber'] + debug.debug("main group number is {{ {} }}".format(main_group_number)) if main_group_number: group_data = self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) group_name = self.ldap.connection.response[0]['attributes']['cn'][0] + debug.debug("group name is {{ {} }}".format(group_name)) if group_name == 'ldap-user': retVal.append(USER) if group_name == 'extern': @@ -64,8 +82,10 @@ class LDAPController(metaclass=Singleton): self.ldap.connection.search('ou=group,{}'.format(self.dn), '(memberUID={})'.format(username), SUBTREE, attributes=['cn']) groups_data = self.ldap.connection.response + debug.debug("groups number is {{ {} }}".format(groups_data)) for data in groups_data: group_name = data['attributes']['cn'][0] + debug.debug("group name is {{ {} }}".format(group_name)) if group_name == 'finanzer': retVal.append(MONEY) elif group_name == 'gastro': @@ -74,9 +94,11 @@ class LDAPController(metaclass=Singleton): retVal.append(BAR) elif group_name == 'vorstand': retVal.append(VORSTAND) + debug.debug("groups are {{ {} }}".format(retVal)) return retVal except Exception as err: - traceback.print_exc() + debug.warning("exception in get groups from ldap", exc_info=True) + raise LDAPExcetpion(str(err)) def __isUserInList(self, list, username): help_list = [] @@ -87,15 +109,18 @@ class LDAPController(metaclass=Singleton): return False def getAllUser(self): + debug.info("get all users from ldap") retVal = [] self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid=*)', SUBTREE, attributes=['uid', 'givenName', 'sn', 'mail']) data = self.ldap.connection.response + debug.debug("data is {{ {} }}".format(data)) for user in data: if 'uid' in user['attributes']: username = user['attributes']['uid'][0] firstname = user['attributes']['givenName'][0] lastname = user['attributes']['sn'][0] retVal.append({'username': username, 'firstname': firstname, 'lastname': lastname}) + debug.debug("users are {{ {} }}".format(retVal)) return retVal def searchUser(self, searchString): @@ -139,10 +164,13 @@ class LDAPController(metaclass=Singleton): return retVal def modifyUser(self, user, conn, attributes): + debug.info("modify ldap data from user {{ {} }} with attributes {{ {} }}".format(user, attributes)) try: if 'username' in attributes: + debug.debug("change username") conn.search('ou=user,{}'.format(self.dn), '(uid={})'.format(attributes['username'])) if conn.entries: + debug.warning("username already exists", exc_info=True) raise UsernameExistLDAP("Username already exists in LDAP") #create modifyer mody = {} @@ -157,9 +185,10 @@ class LDAPController(metaclass=Singleton): if 'password' in attributes: salted_password = hashed(HASHED_SALTED_MD5, attributes['password']) mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])] + debug.debug("modyfier are {{ {} }}".format(mody)) conn.modify(user.dn, mody) except Exception as err: - traceback.print_exc() + debug.warning("exception in modify user data from ldap", exc_info=True) raise LDAPExcetpion("Something went wrong in LDAP: {}".format(err)) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 585145e..ead76db 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -24,147 +24,147 @@ class UserController(metaclass=Singleton): def getAllStatus(self): debug.info("get all status for user") retVal = db.getAllStatus() - debug.debug("status are {{}}".format(retVal)) + debug.debug("status are {{ {} }}".format(retVal)) return retVal def getStatus(self, name): - debug.info("get status of user {{}}".format(name)) + debug.info("get status of user {{ {} }}".format(name)) retVal = db.getStatus(name) - debug.debug("status of user {{}} is {{}}".format(name, retVal)) + debug.debug("status of user {{ {} }} is {{ {} }}".format(name, retVal)) return retVal def setStatus(self, name): - debug.info("set status of user {{}}".format(name)) + debug.info("set status of user {{ {} }}".format(name)) retVal = db.setStatus(name) - debug.debug("settet status of user {{}} is {{}}".format(name, retVal)) + debug.debug("settet status of user {{ {} }} is {{ {} }}".format(name, retVal)) return retVal def deleteStatus(self, status): - debug.info("delete status {{}}".format(status)) + debug.info("delete status {{ {} }}".format(status)) db.deleteStatus(status) def updateStatus(self, status): - debug.info("update status {{}}".format(status)) + debug.info("update status {{ {} }}".format(status)) retVal = db.updateStatus(status) - debug.debug("updated status is {{}}".format(retVal)) + debug.debug("updated status is {{ {} }}".format(retVal)) return retVal def updateStatusOfUser(self, username, status): - debug.info("update status {{}} of user {{}}".format(status, username)) + debug.info("update status {{ {} }} of user {{ {} }}".format(status, username)) retVal = db.updateStatusOfUser(username, status) - debug.debug("updatet status of user {{}} is {{}}".format(username, retVal)) + debug.debug("updatet status of user {{ {} }} is {{ {} }}".format(username, retVal)) return retVal def updateVotingOfUser(self, username, voting): - debug.info("update voting {{}} of user {{}}".format(voting, username)) + debug.info("update voting {{ {} }} of user {{ {} }}".format(voting, username)) retVal = db.updateVotingOfUser(username, voting) - debug.debug("updatet voting of user {{}} is {{}}".format(username, retVal)) + debug.debug("updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal)) return retVal def deleteDrinkType(self, type): - debug.info("delete drink type {{}}".format(type)) + debug.info("delete drink type {{ {} }}".format(type)) db.deleteDrinkType(type) def updateDrinkType(self, type): - debug.info("update drink type {{}}".format(type)) + debug.info("update drink type {{ {} }}".format(type)) retVal = db.updateDrinkType(type) - debug.debug("updated drink type is {{}}".format(retVal)) + debug.debug("updated drink type is {{ {} }}".format(retVal)) return retVal def setDrinkType(self, type): - debug.info("set drink type {{}}".format(type)) + debug.info("set drink type {{ {} }}".format(type)) retVal = db.setDrinkType(type) - debug.debug("seted drink type is {{}}".format(retVal)) + debug.debug("seted drink type is {{ {} }}".format(retVal)) return retVal def deletDrinkPrice(self, drink): - debug.info("delete drink {{}}".format(drink)) + debug.info("delete drink {{ {} }}".format(drink)) db.deleteDrink(drink) def setDrinkPrice(self, drink): - debug.info("set drink {{}}".format(drink)) + debug.info("set drink {{ {} }}".format(drink)) retVal = db.setDrinkPrice(drink) - debug.debug("seted drink is {{}}".format(retVal)) + debug.debug("seted drink is {{ {} }}".format(retVal)) return retVal def updateDrinkPrice(self, drink): - debug.info("update drink {{}}".format(drink)) + debug.info("update drink {{ {} }}".format(drink)) retVal = db.updateDrinkPrice(drink) - debug.debug("updated drink is {{}}".format(retVal)) + debug.debug("updated drink is {{ {} }}".format(retVal)) return retVal def getAllDrinkTypes(self): debug.info("get all drink types") retVal = db.getAllDrinkTypes() - debug.debug("all drink types are {{}}".format(retVal)) + debug.debug("all drink types are {{ {} }}".format(retVal)) return retVal def getPricelist(self): debug.info("get all drinks") list = db.getPriceList() - debug.debug("all drinks are {{}}".format(list)) + debug.debug("all drinks are {{ {} }}".format(list)) return list def setTransactJob(self, from_user, to_user, date): - debug.info("set transact job from {{}} to {{}} on {{}}".format(from_user, to_user, date)) + debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format(from_user, to_user, date)) jobtransact = db.setTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{}}".format(jobtransact)) + debug.debug("transact job is {{ {} }}".format(jobtransact)) debug.info("send mail with transact job to user") emailController.sendMail(jobtransact['to_user'], 'jobtransact', jobtransact) return jobtransact def getTransactJobFromUser(self, user, date): - debug.info("get transact job from user {{}} on {{}}".format(user, date)) + debug.info("get transact job from user {{ {} }} on {{ {} }}".format(user, date)) retVal = db.getTransactJobFromUser(user, date.date()) - debug.debug("transact job from user {{}} is {{}}".format(user, retVal)) + debug.debug("transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) return retVal def getAllTransactJobFromUser(self, user, date): - debug.info("get all transact job from user {{}} start on {{}}".format(user, date)) - retVal = db.getAllTransactJobFromUser(user, date,date()) - debug.debug("all transact job are {{}}".format(retVal)) + debug.info("get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) + retVal = db.getAllTransactJobFromUser(user, date.date()) + debug.debug("all transact job are {{ {} }}".format(retVal)) return retVal def getAllTransactJobToUser(self, user, date): - debug.info("get all transact job from to_user {{}} start on {{}}".format(user, date)) + debug.info("get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) retVal = db.getAllTransactJobToUser(user, date.date()) - debug.debug("all transact job are {{}}".format(retVal)) + debug.debug("all transact job are {{ {} }}".format(retVal)) return retVal def getTransactJob(self, from_user, to_user, date): - debug.info("get transact job from user {{}} to user {{}} on {{}}".format(from_user, to_user, date)) + debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format(from_user, to_user, date)) retVal = db.getTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{}}".format(retVal)) + debug.debug("transact job is {{ {} }}".format(retVal)) return retVal def deleteTransactJob(self, from_user, to_user, date): - debug.info("delete transact job from user {{}} to user {{}} on {{}}".format(from_user, to_user, date)) + debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format(from_user, to_user, date)) transactJob = self.getTransactJob(from_user, to_user, date) - debug.debug("transact job is {{}}".format(transactJob)) + debug.debug("transact job is {{ {} }}".format(transactJob)) if transactJob['answerd']: - debug.warning("transactjob {{}} can not delete because is answerd") + debug.warning("transactjob {{ {} }} can not delete because is answerd") raise TansactJobIsAnswerdException("TransactJob is already answerd") db.deleteTransactJob(from_user, to_user, date.date()) def answerdTransactJob(self, from_user, to_user, date, answer): - debug.info("answer transact job from user {{}} to user {{}} on {{}} with answer {{}}".format(from_user, to_user, date, answer)) + debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format(from_user, to_user, date, answer)) transactJob = db.updateTransactJob(from_user, to_user, date.date(), answer) - debug.debug("transactjob is {{}}".format(transactJob)) + debug.debug("transactjob is {{ {} }}".format(transactJob)) if answer: - debug.info("add worker on date {{}}".format(date)) + debug.info("add worker on date {{ {} }}".format(date)) self.addWorker(to_user.uid, date) return transactJob def setLockedDay(self, date, locked, hard=False): - debug.info("set day locked on {{}} with state {{}}".format(date, locked)) + debug.info("set day locked on {{ {} }} with state {{ {} }}".format(date, locked)) retVal = db.setLockedDay(date.date(), locked, hard) - debug.debug("seted day locked is {{}}".format(retVal)) + debug.debug("seted day locked is {{ {} }}".format(retVal)) return retVal def getLockedDay(self, date): - debug.info("get locked day on {{}}".format(date)) + debug.info("get locked day on {{ {} }}".format(date)) now = datetime.now() - debug.debug("now is {{}}".format(now)) + debug.debug("now is {{ {} }}".format(now)) oldMonth = False debug.debug("check if date old month or current month") for i in range(1, 8): @@ -172,7 +172,7 @@ class UserController(metaclass=Singleton): if now.day < i: oldMonth = True break - debug.debug("oldMonth is {{}}".format(oldMonth)) + debug.debug("oldMonth is {{ {} }}".format(oldMonth)) lockedYear = date.year lockedMonth = date.month if date.month < now.month else now.month - 1 if oldMonth else now.month daysInMonth = calendar.monthrange(lockedYear, lockedMonth)[1] @@ -182,36 +182,36 @@ class UserController(metaclass=Singleton): if datetime(lockedYear, lockedMonth, i).weekday() == 2: startDay = i break - debug.debug("start day of month is {{}}".format(startDay)) + debug.debug("start day of month is {{ {} }}".format(startDay)) debug.debug("check if date should be locked") if lockedYear <= now.year and lockedMonth <= now.month: for i in range(startDay, daysInMonth + 1): - debug.debug("lock day {{}}".format(datetime(lockedYear, lockedMonth, i))) + debug.debug("lock day {{ {} }}".format(datetime(lockedYear, lockedMonth, i))) self.setLockedDay(datetime(lockedYear, lockedMonth, i), True) for i in range(1, 8): nextMonth = datetime(lockedYear, lockedMonth + 1, i) if nextMonth.weekday() == 2: break - debug.debug("lock day {{}}".format(datetime(lockedYear, lockedMonth, i))) + debug.debug("lock day {{ {} }}".format(datetime(lockedYear, lockedMonth, i))) self.setLockedDay(nextMonth, True) retVal = db.getLockedDay(date.date()) - debug.debug("locked day is {{}}".format(retVal)) - return + debug.debug("locked day is {{ {} }}".format(retVal)) + return retVal def getWorker(self, date, username=None): - debug.info("get worker on {{}}".format(username, date)) + debug.info("get worker on {{ {} }}".format(username, date)) if (username): user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) retVal = [db.getWorker(user, date)] - debug.debug("worker is {{}}".format(retVal)) + debug.debug("worker is {{ {} }}".format(retVal)) return retVal retVal = db.getWorkers(date) - debug.debug("workers are {{}}".format(retVal)) + debug.debug("workers are {{ {} }}".format(retVal)) return retVal def addWorker(self, username, date, userExc=False): - debug.info("add job user {{}} on {{}}".format(username, date)) + debug.info("add job user {{ {} }} on {{ {} }}".format(username, date)) if (userExc): debug.debug("this is a user execution, check if day is locked") lockedDay = self.getLockedDay(date) @@ -220,19 +220,19 @@ class UserController(metaclass=Singleton): debug.debug("day is lockey. user cant get job") raise DayLocked("Day is locked. You can't get the Job") user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) debug.debug("check if user has job on date") if (not db.getWorker(user, date)): debug.debug("set job to user") db.setWorker(user, date) retVal = self.getWorker(date, username=username) - debug.debug("worker on date is {{}}".format(retVal)) + debug.debug("worker on date is {{ {} }}".format(retVal)) return retVal def deleteWorker(self, username, date, userExc=False): - debug.info("delete worker {{}} on date {{}}".format(username, date)) + debug.info("delete worker {{ {} }} on date {{ {} }}".format(username, date)) user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) if userExc: debug.debug("is user execution, check if day locked") lockedDay = self.getLockedDay(date) @@ -240,7 +240,7 @@ class UserController(metaclass=Singleton): if lockedDay['locked']: debug.debug("day is locked, check if accepted transact job exists") transactJobs = self.getTransactJobFromUser(user, date) - debug.debug("transact job is {{}}".format(transactJobs)) + debug.debug("transact job is {{ {} }}".format(transactJobs)) found = False for job in transactJobs: if job['accepted'] and job['answerd']: @@ -253,98 +253,98 @@ class UserController(metaclass=Singleton): db.deleteWorker(user, date) def lockUser(self, username, locked): - debug.info("lock user {{}} for credit with status {{}}".format(username, locked)) + debug.info("lock user {{ {} }} for credit with status {{ {} }}".format(username, locked)) user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) user.updateData({'locked': locked}) db.updateUser(user) retVal = self.getUser(username) - debug.debug("locked user is {{}}".format(retVal)) + debug.debug("locked user is {{ {} }}".format(retVal)) return retVal def updateConfig(self, username, data): - debug.info("update config of user {{}} with config {{}}".format(username, data)) + debug.info("update config of user {{ {} }} with config {{ {} }}".format(username, data)) user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) user.updateData(data) db.updateUser(user) retVal = self.getUser(username) - debug.debug("updated config of user is {{}}".format(retVal)) + debug.debug("updated config of user is {{ {} }}".format(retVal)) return retVal def __updateDataFromLDAP(self, user): - debug.info("update data from ldap for user {{}}".format(user)) + debug.info("update data from ldap for user {{ {} }}".format(user)) groups = ldap.getGroup(user.uid) - debug.debug("ldap gorups are {{}}".format(groups)) + debug.debug("ldap gorups are {{ {} }}".format(groups)) user_data = ldap.getUserData(user.uid) - debug.debug("ldap data is {{}}".format(user_data)) + debug.debug("ldap data is {{ {} }}".format(user_data)) user_data['gruppe'] = groups user_data['group'] = groups user.updateData(user_data) db.updateUser(user) def autoLock(self, user): - debug.info("start autolock of user {{}}".format(user)) + debug.info("start autolock of user {{ {} }}".format(user)) if user.autoLock: debug.debug("autolock is active") credit = user.getGeruecht(year=datetime.now().year).getSchulden() limit = -1*user.limit if credit <= limit: - debug.debug("credit {{}} is more than user limit {{}}".format(credit, limit)) + debug.debug("credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit)) debug.debug("lock user") user.updateData({'locked': True}) debug.debug("send mail to user") emailController.sendMail(user) else: - debug.debug("cretid {{}} is less than user limit {{}}".format(credit, limit)) + debug.debug("cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit)) debug.debug("unlock user") user.updateData({'locked': False}) db.updateUser(user) def addAmount(self, username, amount, year, month, finanzer=False): - debug.info("add amount {{}} to user {{}} no month {{}}, year {{}}".format(amount, username, month, year)) + debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format(amount, username, month, year)) user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) if user.uid == 'extern': debug.debug("user is extern user, so exit add amount") return if not user.locked or finanzer: - debug.debug("user is not locked {{}} or is finanzer execution {{}}".format(user.locked, finanzer)) + debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format(user.locked, finanzer)) user.addAmount(amount, year=year, month=month) creditLists = user.updateGeruecht() - debug.debug("creditList is {{}}".format(creditLists)) + debug.debug("creditList is {{ {} }}".format(creditLists)) for creditList in creditLists: - debug.debug("update creditlist {{}}".format(creditList)) + debug.debug("update creditlist {{ {} }}".format(creditList)) db.updateCreditList(creditList) debug.debug("do autolock") self.autoLock(user) retVal = user.getGeruecht(year) - debug.debug("updated creditlists is {{}}".format(retVal)) + debug.debug("updated creditlists is {{ {} }}".format(retVal)) return retVal def addCredit(self, username, credit, year, month): - debug.info("add credit {{}} to user {{}} on month {{}}, year {{}}".format(credit, username, month, year)) + debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format(credit, username, month, year)) user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) if user.uid == 'extern': debug.debug("user is extern user, so exit add credit") return user.addCredit(credit, year=year, month=month) creditLists = user.updateGeruecht() - debug.debug("creditlists are {{}}".format(creditLists)) + debug.debug("creditlists are {{ {} }}".format(creditLists)) for creditList in creditLists: - debug.debug("update creditlist {{}}".format(creditList)) + debug.debug("update creditlist {{ {} }}".format(creditList)) db.updateCreditList(creditList) debug.debug("do autolock") self.autoLock(user) retVal = user.getGeruecht(year) - debug.debug("updated creditlists are {{}}".format(retVal)) + debug.debug("updated creditlists are {{ {} }}".format(retVal)) return retVal def getAllUsersfromDB(self): debug.info("get all users from database") users = db.getAllUser() - debug.debug("users are {{}}".format(users)) + debug.debug("users are {{ {} }}".format(users)) for user in users: try: debug.debug("update data from ldap") @@ -354,11 +354,11 @@ class UserController(metaclass=Singleton): debug.debug("update creditlists") self.__updateGeruechte(user) retVal = db.getAllUser(extern=True) - debug.debug("all users are {{}}".format(retVal)) + debug.debug("all users are {{ {} }}".format(retVal)) return retVal def checkBarUser(self, user): - debug.info("check if user {{}} is baruser") + debug.info("check if user {{ {} }} is baruser") date = datetime.now() zero = date.replace(hour=0, minute=0, second=0, microsecond=0) end = zero + timedelta(hours=12) @@ -366,22 +366,22 @@ class UserController(metaclass=Singleton): if date > zero and end > date: startdatetime = startdatetime - timedelta(days=1) enddatetime = startdatetime + timedelta(days=1) - debug.debug("startdatetime is {{}} and enddatetime is {{}}".format(startdatetime, end)) + debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format(startdatetime, end)) result = False if date >= startdatetime and date < enddatetime: result = db.getWorker(user, startdatetime) - debug.debug("worker is {{}}".format(result)) + debug.debug("worker is {{ {} }}".format(result)) return True if result else False def getUser(self, username): - debug.info("get user {{}}".format(username)) + debug.info("get user {{ {} }}".format(username)) user = db.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) groups = ldap.getGroup(username) - debug.debug("groups are {{}}".format(groups)) + debug.debug("groups are {{ {} }}".format(groups)) user_data = ldap.getUserData(username) - debug.debug("user data from ldap is {{}}".format(user_data)) + debug.debug("user data from ldap is {{ {} }}".format(user_data)) user_data['gruppe'] = groups user_data['group'] = groups if user is None: @@ -394,41 +394,41 @@ class UserController(metaclass=Singleton): db.updateUser(user) user = db.getUser(username) self.__updateGeruechte(user) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) return user def __updateGeruechte(self, user): debug.debug("update creditlists") user.getGeruecht(datetime.now().year) creditLists = user.updateGeruecht() - debug.debug("creditlists are {{}}".format(creditLists)) + debug.debug("creditlists are {{ {} }}".format(creditLists)) if user.getGeruecht(datetime.now().year).getSchulden() != 0: for creditList in creditLists: - debug.debug("update creditlist {{}}".format(creditList)) + debug.debug("update creditlist {{ {} }}".format(creditList)) db.updateCreditList(creditList) def sendMail(self, username): - debug.info("send mail to user {{}}".format(username)) + debug.info("send mail to user {{ {} }}".format(username)) if type(username) == User: user = username if type(username) == str: user = db.getUser(username) retVal = emailController.sendMail(user) - debug.debug("send mail is {{}}".format(retVal)) + debug.debug("send mail is {{ {} }}".format(retVal)) return retVal def sendAllMail(self): debug.info("send mail to all user") retVal = [] users = db.getAllUser() - debug.debug("users are {{}}".format(users)) + debug.debug("users are {{ {} }}".format(users)) for user in users: retVal.append(self.sendMail(user)) - debug.debug("send mails are {{}}".format(retVal)) + debug.debug("send mails are {{ {} }}".format(retVal)) return retVal def modifyUser(self, user, ldap_conn, attributes): - debug.info("modify user {{}} with attributes {{}} with ldap_conn {{}}".format(user, attributes, ldap_conn)) + debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format(user, attributes, ldap_conn)) try: if 'username' in attributes: debug.debug("change username, so change first in database") @@ -436,11 +436,11 @@ class UserController(metaclass=Singleton): ldap.modifyUser(user, ldap_conn, attributes) if 'username' in attributes: retVal = self.getUser(attributes['username']) - debug.debug("user is {{}}".format(retVal)) + debug.debug("user is {{ {} }}".format(retVal)) return retVal else: retVal = self.getUser(user.uid) - debug.debug("user is {{}}".format(retVal)) + debug.debug("user is {{ {} }}".format(retVal)) return retVal except UsernameExistLDAP as err: debug.debug("username exists on ldap, rechange username on database", exc_info=True) @@ -454,10 +454,10 @@ class UserController(metaclass=Singleton): raise Exception(err) def loginUser(self, username, password): - debug.info("login user {{}}".format(username)) + debug.info("login user {{ {} }}".format(username)) try: user = self.getUser(username) - debug.debug("user is {{}}".format(user)) + debug.debug("user is {{ {} }}".format(user)) user.password = password ldap.login(username, password) ldap_conn = ldap.bind(user, password) diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 2daa007..542c190 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -1,7 +1,7 @@ from datetime import datetime from geruecht.logger import getDebugLogger -LOGGER = getDebugLogger() +debug = getDebugLogger() class AccessToken(): """ Model for an AccessToken @@ -27,19 +27,20 @@ class AccessToken(): token: Is a String to verify later timestamp: Default current time, but can set to an other datetime-Object. """ - LOGGER.debug("Initialize AccessToken") + debug.debug("init accesstoken") self.user = user self.timestamp = timestamp self.lifetime = lifetime self.token = token self.ldap_conn = ldap_conn + debug.debug("accesstoken is {{ {} }}".format(self)) def updateTimestamp(self): """ Update the Timestamp Update the Timestamp to the current Time. """ - LOGGER.debug("Update Timestamp") + debug.debug("update timestamp from accesstoken {{ {} }}".format(self)) self.timestamp = datetime.now() def __eq__(self, token): diff --git a/geruecht/model/creditList.py b/geruecht/model/creditList.py index 44f8e1a..9c909f8 100644 --- a/geruecht/model/creditList.py +++ b/geruecht/model/creditList.py @@ -1,7 +1,8 @@ from datetime import datetime from geruecht.logger import getDebugLogger -LOGGER = getDebugLogger() +debug = getDebugLogger() + def create_empty_data(): empty_data = {'id': 0, 'jan_guthaben': 0, @@ -46,7 +47,7 @@ class CreditList(): user_id: id from the User. """ def __init__(self, data): - LOGGER.debug("Initialize Geruecht") + debug.debug("init creditlist") self.id = int(data['id']) self.jan_guthaben = int(data['jan_guthaben']) @@ -91,6 +92,8 @@ class CreditList(): self.user_id = int(data['user_id']) + debug.debug("credit list is {{ {} }}".format(self)) + def getSchulden(self): """ Get Schulden @@ -104,7 +107,7 @@ class CreditList(): Returns: double of the calculated amount """ - LOGGER.debug("Calculate amount") + debug.info("calculate amount") jan = self.jan_guthaben - self.jan_schulden feb = self.feb_guthaben - self.feb_schulden maer = self.maer_guthaben - self.maer_schulden @@ -119,7 +122,7 @@ class CreditList(): dez = self.dez_guthaben - self.dez_schulden sum = jan + feb + maer + apr + mai + jun + jul + aug + sep + okt + nov + dez - self.last_schulden - LOGGER.debug("Calculated amount is {}".format(sum)) + debug.debug("amount is {{ {} }}".format(sum)) return sum def getMonth(self, month=datetime.now().month): @@ -134,7 +137,7 @@ class CreditList(): Returns: double (credit, amount) """ - LOGGER.debug("Get Credit and Amount from Month {}".format(month)) + debug.info("get credit and amount from month {{ {} }}".format(month)) retValue = None if month == 1: @@ -161,7 +164,7 @@ class CreditList(): retValue = (self.nov_guthaben, self.nov_schulden) elif month == 12: retValue = (self.dez_guthaben, self.dez_schulden) - LOGGER.debug("Credit and Amount is {}".format(retValue)) + debug.debug("credit and amount is {{ {} }}".format(retValue)) return retValue def addAmount(self, amount, month=datetime.now().month): @@ -177,7 +180,7 @@ class CreditList(): Returns: double (credit, amount) """ - LOGGER.debug("Add Amount in Month {}".format(month)) + debug.info("add amount in month {{ {} }}".format(month)) if month == 1: self.jan_schulden += amount retValue = (self.jan_guthaben, self.jan_schulden) @@ -214,7 +217,7 @@ class CreditList(): elif month == 12: self.dez_schulden += amount retValue = (self.dez_guthaben, self.dez_schulden) - LOGGER.debug("Credit and Amount is {}".format(retValue)) + debug.debug("credit and amount is {{ {} }}".format(retValue)) return retValue def addCredit(self, credit, month=datetime.now().month): @@ -230,7 +233,7 @@ class CreditList(): Returns: double (credit, amount) """ - LOGGER.debug("Add Credit in Month {}".format(month)) + debug.info("add credit in month {{ {} }}".format(month)) if month == 1: self.jan_guthaben += credit retValue = (self.jan_guthaben, self.jan_schulden) @@ -267,7 +270,7 @@ class CreditList(): elif month == 12: self.dez_guthaben += credit retValue = (self.dez_guthaben, self.dez_schulden) - LOGGER.debug("Credit and Amount is {}".format(retValue)) + debug.debug("credit and amount is {{ {} }}".format(retValue)) return retValue def toJSON(self): diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 680d834..d7d5d46 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -2,7 +2,7 @@ from geruecht.logger import getDebugLogger from geruecht.model.creditList import CreditList, create_empty_data from datetime import datetime -LOGGER = getDebugLogger() +debug = getDebugLogger() class User(): @@ -20,6 +20,7 @@ class User(): password: salted hashed password for the User. """ def __init__(self, data): + debug.info("init user") if 'id' in data: self.id = int(data['id']) self.uid = data['uid'] @@ -58,8 +59,10 @@ class User(): if 'creditLists' in data: self.geruechte = data['creditLists'] self.password = '' + debug.debug("user is {{ {} }}".format(self)) def updateData(self, data): + debug.info("update data of user") if 'dn' in data: self.dn = data['dn'] if 'firstname' in data: @@ -98,14 +101,14 @@ class User(): Returns: the created geruecht """ - LOGGER.debug("Create Geruecht for user {} in year {}".format(self, year)) + debug.info("create creditlist for user {{ {} }} in year {{ {} }}".format(self, year)) data = create_empty_data() data['user_id'] = self.id data['last_schulden'] = amount data['year_date'] = year credit = CreditList(data) self.geruechte.append(credit) - LOGGER.debug("Created Geruecht {}".format(credit)) + debug.debug("creditlist is {{ {} }}".format(credit)) return credit def getGeruecht(self, year=datetime.now().year): @@ -120,13 +123,12 @@ class User(): Returns: the geruecht of the year """ - LOGGER.debug("Iterate through Geruechte of User {}".format(self)) + debug.info("get creditlist from user on year {{ {} }}".format(year)) for geruecht in self.geruechte: - LOGGER.debug("Check if Geruecht {} has year {}".format(geruecht, year)) if geruecht.year == year: - LOGGER.debug("Find Geruecht {} for User {}".format(geruecht, self)) + debug.debug("creditlist is {{ {} }} for user {{ {} }}".format(geruecht, self)) return geruecht - LOGGER.debug("No Geruecht found for User {}. Will create one".format(self)) + debug.debug("no creditlist found for user {{ {} }}".format(self)) geruecht = self.createGeruecht(year=year) return self.getGeruecht(year=year) @@ -145,7 +147,7 @@ class User(): Returns: double (credit, amount) """ - LOGGER.debug("Add amount to User {} in year {} and month {}".format(self, year, month)) + debug.info("add amount to user {{ {} }} in year {{ {} }} and month {{ {} }}".format(self, year, month)) geruecht = self.getGeruecht(year=year) retVal = geruecht.addAmount(amount, month=month) @@ -165,7 +167,7 @@ class User(): Returns: double (credit, amount) """ - LOGGER.debug("Add credit to User {} in year {} and month {}".format(self, year, month)) + debug.info("add credit to user {{ {} }} in year {{ {} }} and month {{ {} }}".format(self, year, month)) geruecht = self.getGeruecht(year=year) retVal = geruecht.addCredit(credit, month=month) @@ -176,7 +178,7 @@ class User(): This function iterate through the geruechte, which sorted by year and update the last_schulden of the geruecht. """ - LOGGER.debug("Update all Geruechte ") + debug.info("update all creditlists ") self.geruechte.sort(key=self.sortYear) for index, geruecht in enumerate(self.geruechte): diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 60070d5..d470483 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -102,7 +102,7 @@ def _addUser(**kwargs): date = datetime(year, month, day, 12) retVal = userController.addWorker(user['username'], date) debug.debug("retrun {{ {} }}".format(retVal)) - userl = userController.getUser(user) + userl = userController.getUser(user['username']) jobL.info("Vorstand {} {} schreibt Mitglied {} {} am {} zum Dienst ein".format( kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, userl.firstname, userl.lastname, date.date())) return jsonify(retVal) @@ -164,7 +164,7 @@ def _deletUser(**kwargs): date = datetime(year, month, day, 12) userController.deleteWorker(user['username'], date) debug.debug("return ok") - user = userController.getUser(user) + user = userController.getUser(user['username']) jobL.info("Vorstand {} {} entfernt Mitglied {} {} am {} vom Dienst".format( kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, date.date())) return jsonify({"ok": "ok"}) From cc692bb82c94fe42ba1c17ca323529077e699101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 11 Mar 2020 18:32:57 +0100 Subject: [PATCH 071/111] add route /sm/getUsers updatet usercontroller for getUsers updatet getLockedDay -> only the day what requestet will be locked (if nessecerie) --- geruecht/controller/userController.py | 161 +++++++++++++++++--------- geruecht/vorstand/routes.py | 63 +++++++--- 2 files changed, 149 insertions(+), 75 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index ead76db..1a07598 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -11,10 +11,12 @@ from geruecht.logger import getDebugLogger db = dc.DatabaseController() ldap = lc.LDAPController() -emailController = ec.EmailController(mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) +emailController = ec.EmailController( + mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) debug = getDebugLogger() + class UserController(metaclass=Singleton): def __init__(self): @@ -36,7 +38,8 @@ class UserController(metaclass=Singleton): def setStatus(self, name): debug.info("set status of user {{ {} }}".format(name)) retVal = db.setStatus(name) - debug.debug("settet status of user {{ {} }} is {{ {} }}".format(name, retVal)) + debug.debug( + "settet status of user {{ {} }} is {{ {} }}".format(name, retVal)) return retVal def deleteStatus(self, status): @@ -50,15 +53,19 @@ class UserController(metaclass=Singleton): return retVal def updateStatusOfUser(self, username, status): - debug.info("update status {{ {} }} of user {{ {} }}".format(status, username)) + debug.info("update status {{ {} }} of user {{ {} }}".format( + status, username)) retVal = db.updateStatusOfUser(username, status) - debug.debug("updatet status of user {{ {} }} is {{ {} }}".format(username, retVal)) + debug.debug( + "updatet status of user {{ {} }} is {{ {} }}".format(username, retVal)) return retVal def updateVotingOfUser(self, username, voting): - debug.info("update voting {{ {} }} of user {{ {} }}".format(voting, username)) + debug.info("update voting {{ {} }} of user {{ {} }}".format( + voting, username)) retVal = db.updateVotingOfUser(username, voting) - debug.debug("updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal)) + debug.debug( + "updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal)) return retVal def deleteDrinkType(self, type): @@ -106,49 +113,61 @@ class UserController(metaclass=Singleton): return list def setTransactJob(self, from_user, to_user, date): - debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format(from_user, to_user, date)) + debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) jobtransact = db.setTransactJob(from_user, to_user, date.date()) debug.debug("transact job is {{ {} }}".format(jobtransact)) debug.info("send mail with transact job to user") - emailController.sendMail(jobtransact['to_user'], 'jobtransact', jobtransact) + emailController.sendMail( + jobtransact['to_user'], 'jobtransact', jobtransact) return jobtransact def getTransactJobFromUser(self, user, date): - debug.info("get transact job from user {{ {} }} on {{ {} }}".format(user, date)) + debug.info( + "get transact job from user {{ {} }} on {{ {} }}".format(user, date)) retVal = db.getTransactJobFromUser(user, date.date()) - debug.debug("transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) + debug.debug( + "transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) return retVal def getAllTransactJobFromUser(self, user, date): - debug.info("get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) + debug.info( + "get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) retVal = db.getAllTransactJobFromUser(user, date.date()) debug.debug("all transact job are {{ {} }}".format(retVal)) return retVal def getAllTransactJobToUser(self, user, date): - debug.info("get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) + debug.info( + "get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) retVal = db.getAllTransactJobToUser(user, date.date()) debug.debug("all transact job are {{ {} }}".format(retVal)) return retVal def getTransactJob(self, from_user, to_user, date): - debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format(from_user, to_user, date)) + debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) retVal = db.getTransactJob(from_user, to_user, date.date()) debug.debug("transact job is {{ {} }}".format(retVal)) return retVal def deleteTransactJob(self, from_user, to_user, date): - debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format(from_user, to_user, date)) + debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) transactJob = self.getTransactJob(from_user, to_user, date) debug.debug("transact job is {{ {} }}".format(transactJob)) if transactJob['answerd']: - debug.warning("transactjob {{ {} }} can not delete because is answerd") - raise TansactJobIsAnswerdException("TransactJob is already answerd") + debug.warning( + "transactjob {{ {} }} can not delete because is answerd") + raise TansactJobIsAnswerdException( + "TransactJob is already answerd") db.deleteTransactJob(from_user, to_user, date.date()) def answerdTransactJob(self, from_user, to_user, date, answer): - debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format(from_user, to_user, date, answer)) - transactJob = db.updateTransactJob(from_user, to_user, date.date(), answer) + debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format( + from_user, to_user, date, answer)) + transactJob = db.updateTransactJob( + from_user, to_user, date.date(), answer) debug.debug("transactjob is {{ {} }}".format(transactJob)) if answer: debug.info("add worker on date {{ {} }}".format(date)) @@ -156,11 +175,26 @@ class UserController(metaclass=Singleton): return transactJob def setLockedDay(self, date, locked, hard=False): - debug.info("set day locked on {{ {} }} with state {{ {} }}".format(date, locked)) + debug.info( + "set day locked on {{ {} }} with state {{ {} }}".format(date, locked)) retVal = db.setLockedDay(date.date(), locked, hard) debug.debug("seted day locked is {{ {} }}".format(retVal)) return retVal + def getLockedDays(self, from_date, to_date): + debug.info("get locked days from {{ {} }} to {{ {} }}".format( + from_date.date(), to_date.date())) + oneDay = timedelta(1) + delta = to_date.date() - from_date.date() + retVal = [] + startdate = from_date - oneDay + for _ in range(delta.days + 1): + startdate += oneDay + lockday = self.getLockedDay(startdate) + retVal.append(lockday) + debug.debug("lock days are {{ {} }}".format(retVal)) + return retVal + def getLockedDay(self, date): debug.info("get locked day on {{ {} }}".format(date)) now = datetime.now() @@ -173,33 +207,34 @@ class UserController(metaclass=Singleton): oldMonth = True break debug.debug("oldMonth is {{ {} }}".format(oldMonth)) - lockedYear = date.year - lockedMonth = date.month if date.month < now.month else now.month - 1 if oldMonth else now.month - daysInMonth = calendar.monthrange(lockedYear, lockedMonth)[1] - startDay = 1 - debug.debug("calculate start day of month") + lockedYear = now.year + lockedMonth = now.month if now.month < now.month else now.month - \ + 1 if oldMonth else now.month + endDay = 1 + debug.debug("calculate end day of month") for i in range(1, 8): - if datetime(lockedYear, lockedMonth, i).weekday() == 2: - startDay = i + nextMonth = datetime(lockedYear, lockedMonth + 1, i) + if nextMonth.weekday() == 2: + endDay = i break - debug.debug("start day of month is {{ {} }}".format(startDay)) - debug.debug("check if date should be locked") - if lockedYear <= now.year and lockedMonth <= now.month: - for i in range(startDay, daysInMonth + 1): - debug.debug("lock day {{ {} }}".format(datetime(lockedYear, lockedMonth, i))) - self.setLockedDay(datetime(lockedYear, lockedMonth, i), True) - for i in range(1, 8): - nextMonth = datetime(lockedYear, lockedMonth + 1, i) - if nextMonth.weekday() == 2: - break - debug.debug("lock day {{ {} }}".format(datetime(lockedYear, lockedMonth, i))) - self.setLockedDay(nextMonth, True) + + monthLockedEndDate = datetime(lockedYear, lockedMonth + 1, endDay) + debug.debug("get lock day from database") retVal = db.getLockedDay(date.date()) + if not retVal: + debug.debug( + "lock day not exists, retVal is {{ {} }}".format(retVal)) + if date.date() <= monthLockedEndDate.date(): + debug.debug("lock day {{ {} }}".format(date.date())) + self.setLockedDay(date, True) + retVal = db.getLockedDay(date.date()) + else: + retVal = {"daydate": date.date(), "locked": False} debug.debug("locked day is {{ {} }}".format(retVal)) return retVal def getWorker(self, date, username=None): - debug.info("get worker on {{ {} }}".format(username, date)) + debug.info("get worker {{ {} }} on {{ {} }}".format(username, date)) if (username): user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) @@ -230,7 +265,8 @@ class UserController(metaclass=Singleton): return retVal def deleteWorker(self, username, date, userExc=False): - debug.info("delete worker {{ {} }} on date {{ {} }}".format(username, date)) + debug.info( + "delete worker {{ {} }} on date {{ {} }}".format(username, date)) user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) if userExc: @@ -238,9 +274,11 @@ class UserController(metaclass=Singleton): lockedDay = self.getLockedDay(date) if lockedDay: if lockedDay['locked']: - debug.debug("day is locked, check if accepted transact job exists") + debug.debug( + "day is locked, check if accepted transact job exists") transactJobs = self.getTransactJobFromUser(user, date) - debug.debug("transact job is {{ {} }}".format(transactJobs)) + debug.debug( + "transact job is {{ {} }}".format(transactJobs)) found = False for job in transactJobs: if job['accepted'] and job['answerd']: @@ -249,11 +287,13 @@ class UserController(metaclass=Singleton): break if not found: debug.debug("no accepted transact job found") - raise DayLocked("Day is locked. You can't delete the Job") + raise DayLocked( + "Day is locked. You can't delete the Job") db.deleteWorker(user, date) def lockUser(self, username, locked): - debug.info("lock user {{ {} }} for credit with status {{ {} }}".format(username, locked)) + debug.info("lock user {{ {} }} for credit with status {{ {} }}".format( + username, locked)) user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) user.updateData({'locked': locked}) @@ -263,7 +303,8 @@ class UserController(metaclass=Singleton): return retVal def updateConfig(self, username, data): - debug.info("update config of user {{ {} }} with config {{ {} }}".format(username, data)) + debug.info( + "update config of user {{ {} }} with config {{ {} }}".format(username, data)) user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) user.updateData(data) @@ -290,26 +331,30 @@ class UserController(metaclass=Singleton): credit = user.getGeruecht(year=datetime.now().year).getSchulden() limit = -1*user.limit if credit <= limit: - debug.debug("credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit)) + debug.debug( + "credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit)) debug.debug("lock user") user.updateData({'locked': True}) debug.debug("send mail to user") emailController.sendMail(user) else: - debug.debug("cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit)) + debug.debug( + "cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit)) debug.debug("unlock user") user.updateData({'locked': False}) db.updateUser(user) def addAmount(self, username, amount, year, month, finanzer=False): - debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format(amount, username, month, year)) + debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format( + amount, username, month, year)) user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) if user.uid == 'extern': debug.debug("user is extern user, so exit add amount") return if not user.locked or finanzer: - debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format(user.locked, finanzer)) + debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format( + user.locked, finanzer)) user.addAmount(amount, year=year, month=month) creditLists = user.updateGeruecht() debug.debug("creditList is {{ {} }}".format(creditLists)) @@ -323,7 +368,8 @@ class UserController(metaclass=Singleton): return retVal def addCredit(self, username, credit, year, month): - debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format(credit, username, month, year)) + debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format( + credit, username, month, year)) user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) if user.uid == 'extern': @@ -362,18 +408,19 @@ class UserController(metaclass=Singleton): date = datetime.now() zero = date.replace(hour=0, minute=0, second=0, microsecond=0) end = zero + timedelta(hours=12) - startdatetime = date.replace(hour=12, minute=0, second=0, microsecond=0) + startdatetime = date.replace( + hour=12, minute=0, second=0, microsecond=0) if date > zero and end > date: - startdatetime = startdatetime - timedelta(days=1) + startdatetime = startdatetime - timedelta(days=1) enddatetime = startdatetime + timedelta(days=1) - debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format(startdatetime, end)) + debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format( + startdatetime, end)) result = False if date >= startdatetime and date < enddatetime: result = db.getWorker(user, startdatetime) debug.debug("worker is {{ {} }}".format(result)) return True if result else False - def getUser(self, username): debug.info("get user {{ {} }}".format(username)) user = db.getUser(username) @@ -428,7 +475,8 @@ class UserController(metaclass=Singleton): return retVal def modifyUser(self, user, ldap_conn, attributes): - debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format(user, attributes, ldap_conn)) + debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format( + user, attributes, ldap_conn)) try: if 'username' in attributes: debug.debug("change username, so change first in database") @@ -443,7 +491,8 @@ class UserController(metaclass=Singleton): debug.debug("user is {{ {} }}".format(retVal)) return retVal except UsernameExistLDAP as err: - debug.debug("username exists on ldap, rechange username on database", exc_info=True) + debug.debug( + "username exists on ldap, rechange username on database", exc_info=True) db.changeUsername(user, user.uid) raise Exception(err) except LDAPExcetpion as err: diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index d470483..14035f3 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify -from datetime import datetime +from datetime import datetime, time import geruecht.controller.userController as uc import geruecht.controller.ldapController as lc from geruecht.decorator import login_required @@ -111,6 +111,41 @@ def _addUser(**kwargs): return jsonify({"error": str(err)}), 500 +@vorstand.route("/sm/getUsers", methods=['POST']) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) +def _getUsers(**kwrags): + debug.info("/sm/getUsers") + try: + data = request.get_json() + from_date = data['from_date'] + to_date = data['to_date'] + from_date = datetime( + from_date['year'], from_date['month'], from_date['day']) + to_date = datetime(to_date['year'], to_date['month'], to_date['day']) + lockedDays = userController.getLockedDays(from_date, to_date) + retVal = [] + for lockedDay in lockedDays: + day = datetime.combine(lockedDay['daydate'], time(12)) + retDay = { + "worker": userController.getWorker(day), + "day": { + "date": { + "year": day.year, + "month": day.month, + "day": day.day + }, + "locked": lockedDay['locked'] + } + } + retVal.append(retDay) + + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + + @vorstand.route("/sm/getUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUser(**kwargs): @@ -122,24 +157,14 @@ def _getUser(**kwargs): year = data['year'] date = datetime(year, month, day, 12) lockedDay = userController.getLockedDay(date) - if not lockedDay: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': False - } - else: - lockedDay = { - 'date': { - 'year': year, - 'month': month, - 'day': day - }, - 'locked': lockedDay['locked'] - } + lockedDay = { + 'date': { + 'year': year, + 'month': month, + 'day': day + }, + 'locked': lockedDay['locked'] + } retVal = { 'worker': userController.getWorker(date), 'day': lockedDay From b4d648530e5bfb889d5332528b80af207ac535a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 12 Mar 2020 10:16:50 +0100 Subject: [PATCH 072/111] fixed ##225 and ##226 --- geruecht/controller/userController.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 1a07598..63ce79b 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -212,13 +212,16 @@ class UserController(metaclass=Singleton): 1 if oldMonth else now.month endDay = 1 debug.debug("calculate end day of month") + lockedYear = lockedYear if lockedMonth != 12 else (lockedYear + 1) + lockedMonth = (lockedMonth + 1) if lockedMonth != 12 else 1 for i in range(1, 8): - nextMonth = datetime(lockedYear, lockedMonth + 1, i) + nextMonth = datetime(lockedYear, lockedMonth, i) if nextMonth.weekday() == 2: endDay = i break - monthLockedEndDate = datetime(lockedYear, lockedMonth + 1, endDay) + monthLockedEndDate = datetime( + lockedYear, lockedMonth, endDay) - timedelta(1) debug.debug("get lock day from database") retVal = db.getLockedDay(date.date()) if not retVal: From 2f756983c27a69779d01d2ec7b3107088764c0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 13 Mar 2020 19:06:24 +0200 Subject: [PATCH 073/111] add route for user for ##222 --- geruecht/user/routes.py | 104 ++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 357e784..79631f3 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify from geruecht.decorator import login_required import geruecht.controller.userController as uc from geruecht.model import USER -from datetime import datetime +from datetime import datetime, time from geruecht.exceptions import DayLocked from geruecht.logger import getDebugLogger, getCreditLogger, getJobsLogger @@ -14,6 +14,7 @@ debug = getDebugLogger() creditL = getCreditLogger() jobL = getJobsLogger() + @user.route("/user/main") @login_required(groups=[USER]) def _main(**kwargs): @@ -23,13 +24,15 @@ def _main(**kwargs): accToken = kwargs['accToken'] accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + retVal['creditList'] = {credit.year: credit.toJSON() + for credit in accToken.user.geruechte} debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception: debug.debug("exception", exc_info=True) return jsonify("error", "something went wrong"), 500 + @user.route("/user/addAmount", methods=['POST']) @login_required(groups=[USER]) def _addAmount(**kwargs): @@ -40,17 +43,21 @@ def _addAmount(**kwargs): data = request.get_json() amount = int(data['amount']) date = datetime.now() - userController.addAmount(accToken.user.uid, amount, year=date.year, month=date.month) + userController.addAmount( + accToken.user.uid, amount, year=date.year, month=date.month) accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + retVal['creditList'] = {credit.year: credit.toJSON() + for credit in accToken.user.geruechte} debug.debug("return {{ {} }}".format(retVal)) - creditL.info("{} {} {} fügt sich selbst {} € Schulden hinzu".format(date, accToken.user.firstname, accToken.user.lastname, amount/100)) + creditL.info("{} {} {} fügt sich selbst {} € Schulden hinzu".format( + date, accToken.user.firstname, accToken.user.lastname, amount/100)) return jsonify(retVal) except Exception: debug.debug("exception", exc_info=True) return jsonify({"error": "something went wrong"}), 500 + @user.route("/user/saveConfig", methods=['POST']) @login_required(groups=[USER]) def _saveConfig(**kwargs): @@ -59,15 +66,53 @@ def _saveConfig(**kwargs): if 'accToken' in kwargs: accToken = kwargs['accToken'] data = request.get_json() - accToken.user = userController.modifyUser(accToken.user, accToken.ldap_conn, data) + accToken.user = userController.modifyUser( + accToken.user, accToken.ldap_conn, data) retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + retVal['creditList'] = {credit.year: credit.toJSON() + for credit in accToken.user.geruechte} debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + +@user.route("/user/jobs", methods=['POST']) +@login_required(groups=[USER]) +def _getUsers(**kwrags): + debug.info("/user/jobs") + try: + data = request.get_json() + from_date = data['from_date'] + to_date = data['to_date'] + from_date = datetime( + from_date['year'], from_date['month'], from_date['day']) + to_date = datetime(to_date['year'], to_date['month'], to_date['day']) + lockedDays = userController.getLockedDays(from_date, to_date) + retVal = [] + for lockedDay in lockedDays: + day = datetime.combine(lockedDay['daydate'], time(12)) + retDay = { + "worker": userController.getWorker(day), + "day": { + "date": { + "year": day.year, + "month": day.month, + "day": day.day + }, + "locked": lockedDay['locked'] + } + } + retVal.append(retDay) + + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + + @user.route("/user/job", methods=['POST']) @login_required(groups=[USER]) def _getUser(**kwargs): @@ -107,6 +152,7 @@ def _getUser(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + @user.route("/user/addJob", methods=['POST']) @login_required(groups=[USER]) def _addUser(**kwargs): @@ -119,10 +165,11 @@ def _addUser(**kwargs): day = data['day'] month = data['month'] year = data['year'] - date = datetime(year,month,day,12) + date = datetime(year, month, day, 12) retVal = userController.addWorker(user.uid, date, userExc=True) debug.debug("return {{ {} }}".format(retVal)) - jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format(user.firstname, user.lastname, date.date())) + jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format( + user.firstname, user.lastname, date.date())) return jsonify(retVal) except DayLocked as err: debug.debug("exception", exc_info=True) @@ -131,6 +178,7 @@ def _addUser(**kwargs): debug.debug("exception", exc_info=True) return jsonify({'error': str(err)}), 409 + @user.route("/user/deleteJob", methods=['POST']) @login_required(groups=[USER]) def _deletJob(**kwargs): @@ -143,10 +191,11 @@ def _deletJob(**kwargs): day = data['day'] month = data['month'] year = data['year'] - date = datetime(year,month,day,12) + date = datetime(year, month, day, 12) userController.deleteWorker(user.uid, date, True) debug.debug("return ok") - jobL.info("Mitglied {} {} entfernt sich am {} aus dem Dienst".format(user.firstname, user.lastname, date.date())) + jobL.info("Mitglied {} {} entfernt sich am {} aus dem Dienst".format( + user.firstname, user.lastname, date.date())) return jsonify({"ok": "ok"}) except DayLocked as err: debug.debug("exception", exc_info=True) @@ -155,6 +204,7 @@ def _deletJob(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/transactJob", methods=['POST']) @login_required(groups=[USER]) def _transactJob(**kwargs): @@ -177,12 +227,14 @@ def _transactJob(**kwargs): retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} debug.debug("return {{ {} }}".format(retVal)) - jobL.info("Mitglied {} {} sendet Dienstanfrage an Mitglied {} {} am {}".format(from_userl.firstname, from_userl.lastname, to_userl.firstname, to_userl.lastname, date.date())) + jobL.info("Mitglied {} {} sendet Dienstanfrage an Mitglied {} {} am {}".format( + from_userl.firstname, from_userl.lastname, to_userl.firstname, to_userl.lastname, date.date())) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/answerTransactJob", methods=['POST']) @login_required(groups=[USER]) def _answer(**kwargs): @@ -199,19 +251,22 @@ def _answer(**kwargs): username = data['username'] date = datetime(year, month, day, 12) from_user = userController.getUser(username) - retVal = userController.answerdTransactJob(from_user, user, date, answer) + retVal = userController.answerdTransactJob( + from_user, user, date, answer) from_userl = retVal['from_user'] to_userl = retVal['to_user'] retVal['from_user'] = retVal['from_user'].toJSON() retVal['to_user'] = retVal['to_user'].toJSON() retVal['date'] = {'year': year, 'month': month, 'day': day} debug.debug("return {{ {} }}".format(retVal)) - jobL.info("Mitglied {} {} beantwortet Dienstanfrage von {} {} am {} mit {}".format(to_userl.firstname, to_userl.lastname, from_userl.firstname, from_userl.lastname, date.date(), 'JA' if answer else 'NEIN')) + jobL.info("Mitglied {} {} beantwortet Dienstanfrage von {} {} am {} mit {}".format(to_userl.firstname, + to_userl.lastname, from_userl.firstname, from_userl.lastname, date.date(), 'JA' if answer else 'NEIN')) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/jobRequests", methods=['POST']) @login_required(groups=[USER]) def _requests(**kwargs): @@ -230,13 +285,15 @@ def _requests(**kwargs): data['from_user'] = data['from_user'].toJSON() data['to_user'] = data['to_user'].toJSON() data_date = data['date'] - data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} + data['date'] = {'year': data_date.year, + 'month': data_date.month, 'day': data_date.day} debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/getTransactJobs", methods=['POST']) @login_required(groups=[USER]) def _getTransactJobs(**kwargs): @@ -255,13 +312,15 @@ def _getTransactJobs(**kwargs): data['from_user'] = data['from_user'].toJSON() data['to_user'] = data['to_user'].toJSON() data_date = data['date'] - data['date'] = {'year': data_date.year, 'month': data_date.month, 'day': data_date.day} + data['date'] = {'year': data_date.year, + 'month': data_date.month, 'day': data_date.day} debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/deleteTransactJob", methods=['POST']) @login_required(groups=[USER]) def _deleteTransactJob(**kwargs): @@ -279,12 +338,14 @@ def _deleteTransactJob(**kwargs): to_user = userController.getUser(username) userController.deleteTransactJob(from_user, to_user, date) debug.debug("return ok") - jobL.info("Mitglied {} {} entfernt Dienstanfrage an {} {} am {}".format(from_user.firstname, from_user.lastname, to_user.firstname, to_user.lastname, date.date())) + jobL.info("Mitglied {} {} entfernt Dienstanfrage an {} {} am {}".format( + from_user.firstname, from_user.lastname, to_user.firstname, to_user.lastname, date.date())) return jsonify({"ok": "ok"}) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 + @user.route("/user/storno", methods=['POST']) @login_required(groups=[USER]) def _storno(**kwargs): @@ -306,12 +367,15 @@ def _storno(**kwargs): amount = int(data['amount']) date = datetime.now() - userController.addCredit(user.uid, amount, year=date.year, month=date.month) + userController.addCredit( + user.uid, amount, year=date.year, month=date.month) accToken.user = userController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() - retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} + retVal['creditList'] = {credit.year: credit.toJSON() + for credit in accToken.user.geruechte} debug.debug("return {{ {} }}".format(retVal)) - creditL.info("{} {} {} storniert {} €".format(date, user.firstname, user.lastname, amount/100)) + creditL.info("{} {} {} storniert {} €".format( + date, user.firstname, user.lastname, amount/100)) return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) From 987487d3c4471cfbcef417cd046f8cde9cee8646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 17 Mar 2020 20:37:01 +0100 Subject: [PATCH 074/111] add routes to valid barlock --- geruecht/baruser/routes.py | 20 +++++++++++++++----- geruecht/controller/ldapController.py | 3 +-- geruecht/controller/userController.py | 4 ++++ geruecht/decorator.py | 10 ++++++++-- geruecht/model/accessToken.py | 1 + geruecht/routes.py | 25 +++++++++++++++++++------ 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index f80dcc1..3002613 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -16,7 +16,7 @@ userController = uc.UserController() @baruser.route("/bar") -@login_required(groups=[BAR]) +@login_required(groups=[BAR], bar=True) def _bar(**kwargs): """ Main function for Baruser @@ -55,7 +55,7 @@ def _bar(**kwargs): @baruser.route("/baradd", methods=['POST']) -@login_required(groups=[BAR]) +@login_required(groups=[BAR], bar=True) def _baradd(**kwargs): """ Function for Baruser to add amount @@ -96,7 +96,7 @@ def _baradd(**kwargs): @baruser.route("/barGetUsers") -@login_required(groups=[BAR, MONEY]) +@login_required(groups=[BAR, MONEY], bar=True) def _getUsers(**kwargs): """ Get Users without amount @@ -118,7 +118,7 @@ def _getUsers(**kwargs): @baruser.route("/bar/storno", methods=['POST']) -@login_required(groups=[BAR]) +@login_required(groups=[BAR], bar=True) def _storno(**kwargs): """ Function for Baruser to storno amount @@ -159,7 +159,7 @@ def _storno(**kwargs): @baruser.route("/barGetUser", methods=['POST']) -@login_required(groups=[BAR]) +@login_required(groups=[BAR], bar=True) def _getUser(**kwargs): debug.info("/barGetUser") try: @@ -197,3 +197,13 @@ def _search(**kwargs): except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 + +@baruser.route("/bar/lock", methods=['POST']) +@login_required(groups=[BAR], bar=True) +def _lockbar(**kwargs): + debug.info('/bar/lock') + data = request.get_json() + accToken = kwargs['accToken'] + accToken.lock_bar = [data['value']] + debug.debug('return {{ "value": {} }}'.format(accToken.lock_bar)) + return jsonify({'value': accToken.lock_bar}) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index e40eb23..0692f1c 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -68,11 +68,10 @@ class LDAPController(metaclass=Singleton): try: retVal = [] self.ldap.connection.search('ou=user,{}'.format(self.dn), '(uid={})'.format(username), SUBTREE, attributes=['gidNumber']) - response = self.ldap.connection.response main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber'] debug.debug("main group number is {{ {} }}".format(main_group_number)) if main_group_number: - group_data = self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) + self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) group_name = self.ldap.connection.response[0]['attributes']['cn'][0] debug.debug("group name is {{ {} }}".format(group_name)) if group_name == 'ldap-user': diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 63ce79b..0b86736 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -505,6 +505,10 @@ class UserController(metaclass=Singleton): except Exception as err: raise Exception(err) + def validateUser(self, username, password): + debug.info("validate user {{ {} }}".format(username)) + ldap.login(username, password) + def loginUser(self, username, password): debug.info("login user {{ {} }}".format(username)) try: diff --git a/geruecht/decorator.py b/geruecht/decorator.py index c01bd77..fe9fb58 100644 --- a/geruecht/decorator.py +++ b/geruecht/decorator.py @@ -5,12 +5,15 @@ DEBUG = getDebugLogger() def login_required(**kwargs): import geruecht.controller.accesTokenController as ac - from geruecht.model import BAR, USER, MONEY, GASTRO + from geruecht.model import BAR, USER, MONEY, GASTRO, VORSTAND, EXTERN from flask import request, jsonify accessController = ac.AccesTokenController() - groups = [USER, BAR, GASTRO, MONEY] + groups = [USER, BAR, GASTRO, MONEY, VORSTAND, EXTERN] + bar = False if "groups" in kwargs: groups = kwargs["groups"] + if "bar" in kwargs: + bar = kwargs["bar"] DEBUG.debug("groups are {{ {} }}".format(groups)) def real_decorator(func): @@ -23,6 +26,9 @@ def login_required(**kwargs): kwargs['accToken'] = accToken if accToken: DEBUG.debug("token {{ {} }} is valid".format(token)) + if accToken.lock_bar and not bar: + return jsonify({"error": "error", + "message": "permission forbidden"}), 403 return func(*args, **kwargs) else: DEBUG.warning("token {{ {} }} is not valid".format(token)) diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 542c190..0e7746c 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -33,6 +33,7 @@ class AccessToken(): self.lifetime = lifetime self.token = token self.ldap_conn = ldap_conn + self.lock_bar = False debug.debug("accesstoken is {{ {} }}".format(self)) def updateTimestamp(self): diff --git a/geruecht/routes.py b/geruecht/routes.py index ce92b09..b3b6d88 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -12,6 +12,19 @@ userController = uc.UserController() debug = getDebugLogger() +@app.route("/valid", methods=['POST']) +@login_required(bar=True) +def _valid(**kwargs): + debug.info('/valid') + try: + accToken = kwargs['accToken'] + data = request.get_json() + userController.validateUser(accToken.user.username, data['password']) + debug.debug('return {{ "ok": "ok" }}') + return jsonify({"ok": "ok"}) + except Exception as err: + debug.warning("exception in valide.", exc_info=True) + return jsonify({"error": str(err)}), 500 @app.route("/pricelist", methods=['GET']) def _getPricelist(): @@ -38,7 +51,7 @@ def getTypes(): @app.route('/getAllStatus', methods=['GET']) -@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) +@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND], bar=True) def _getAllStatus(**kwargs): try: debug.info("get all status for users") @@ -51,7 +64,7 @@ def _getAllStatus(**kwargs): @app.route('/getStatus', methods=['POST']) -@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND]) +@login_required(groups=[USER, MONEY, GASTRO, BAR, VORSTAND], bar=True) def _getStatus(**kwargs): try: debug.info("get status from user") @@ -68,7 +81,7 @@ def _getStatus(**kwargs): @app.route('/getUsers', methods=['GET']) -@login_required(groups=[MONEY, GASTRO, VORSTAND]) +@login_required(groups=[MONEY, GASTRO, VORSTAND], bar=True) def _getUsers(**kwargs): try: debug.info("get all users from database") @@ -84,7 +97,7 @@ def _getUsers(**kwargs): @app.route("/getLifeTime", methods=['GET']) -@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER], bar=True) def _getLifeTime(**kwargs): try: debug.info("get lifetime of accesstoken") @@ -101,7 +114,7 @@ def _getLifeTime(**kwargs): @app.route("/saveLifeTime", methods=['POST']) -@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER], bar=True) def _saveLifeTime(**kwargs): try: debug.info("save lifetime for accessToken") @@ -127,7 +140,7 @@ def _saveLifeTime(**kwargs): @app.route("/logout", methods=['GET']) -@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER]) +@login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER], bar=True) def _logout(**kwargs): try: debug.info("logout user") From 3752026e22397bd7228518d3800870e719fb5fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 11 May 2020 23:07:26 +0200 Subject: [PATCH 075/111] fixed bugs send not absolute values in baruser set lockedbar is not an array --- geruecht/baruser/routes.py | 14 ++++++++------ geruecht/routes.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 3002613..98c7df5 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -84,7 +84,7 @@ def _baradd(**kwargs): else: type = 'amount' dic = user.toJSON() - dic['amount'] = abs(all) + dic['amount'] = all dic['type'] = type debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format( @@ -147,7 +147,7 @@ def _storno(**kwargs): else: type = 'amount' dic = user.toJSON() - dic['amount'] = abs(all) + dic['amount'] = all dic['type'] = type debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} storniert {} € von {} {}".format( @@ -183,7 +183,7 @@ def _getUser(**kwargs): @baruser.route("/search", methods=['GET']) -@login_required(groups=[BAR, MONEY, USER, VORSTAND]) +@login_required(groups=[BAR, MONEY, USER, VORSTAND], bar=True) def _search(**kwargs): debug.info("/search") try: @@ -198,12 +198,14 @@ def _search(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 -@baruser.route("/bar/lock", methods=['POST']) +@baruser.route("/bar/lock", methods=['GET', 'POST']) @login_required(groups=[BAR], bar=True) def _lockbar(**kwargs): + debug.info('/bar/lock') - data = request.get_json() accToken = kwargs['accToken'] - accToken.lock_bar = [data['value']] + if request.method == "POST": + data = request.get_json() + accToken.lock_bar = data['value'] debug.debug('return {{ "value": {} }}'.format(accToken.lock_bar)) return jsonify({'value': accToken.lock_bar}) diff --git a/geruecht/routes.py b/geruecht/routes.py index b3b6d88..fec35b2 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -19,7 +19,7 @@ def _valid(**kwargs): try: accToken = kwargs['accToken'] data = request.get_json() - userController.validateUser(accToken.user.username, data['password']) + userController.validateUser(accToken.user.uid, data['password']) debug.debug('return {{ "ok": "ok" }}') return jsonify({"ok": "ok"}) except Exception as err: From 381cc1c028edd76e8e16051baadc86ca29b36d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 15 May 2020 12:56:22 +0200 Subject: [PATCH 076/111] add some gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 037f03d..24662c6 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,8 @@ dmypy.json .idea .vscode/ -*.log \ No newline at end of file +*.log + +# custom +test_pricelist/ +test_project/ From c9fe61b1dc988bddd1fcabc63ba88826246d8a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 16 May 2020 23:25:44 +0200 Subject: [PATCH 077/111] finished ##250 and ##249 --- geruecht/controller/databaseController.py | 64 +++++++++++++++++++++ geruecht/controller/userController.py | 29 ++++++++++ geruecht/vorstand/routes.py | 68 +++++++++++++++++++++++ 3 files changed, 161 insertions(+) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index d2d1fdb..e364e3b 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -573,6 +573,70 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + def getAllWorkgroups(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from workgroup') + list = cursor.fetchall() + for item in list: + if item['boss'] != None: + item['boss']=self.getUserById(item['boss']) + return list + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getWorkgroup(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from workgroup where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from workgroup where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + retVal = cursor.fetchone() + retVal['boss'] = self.getUserById(retVal['boss']) if retVal['boss'] != None else None + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setWorkgroup(self, name, boss): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into workgroup (name, boss) values ('{}', {})".format(name, boss['id'])) + self.db.connection.commit() + return self.getWorkgroup(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateWorkgroup(self, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("update workgroup set name='{}', boss={} where id={}".format(workgroup['name'], workgroup['boss']['id'], workgroup['id'])) + self.db.connection.commit() + return self.getWorkgroup(workgroup['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteWorkgroup(self, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from workgroup where id={}".format(workgroup['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 0b86736..90778d9 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -23,6 +23,35 @@ class UserController(metaclass=Singleton): debug.debug("init UserController") pass + def getAllWorkgroups(self): + debug.info("get all workgroups") + retVal = db.getAllWorkgroups() + debug.debug("workgroups are {{ {} }}".format(retVal)) + return retVal + + def getWorkgroups(self, name): + debug.info("get Workgroup {{ {} }}".format(name)) + retVal = db.getWorkgroup(name) + debug.debug("workgroup is {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def setWorkgroup(self, name, boss): + debug.info("set workgroup {{ {} }} with boss {{ {} }}".format(name, boss)) + retVal = db.setWorkgroup(name, boss) + debug.debug( + "seted workgroup {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def deleteWorkgroup(self, workgroup): + debug.info("delete workgroup {{ {} }}".format(workgroup)) + db.deleteWorkgroup(workgroup) + + def updateWorkgroup(self, workgroup): + debug.info("update workgroup {{ {} }}".format(workgroup)) + retVal = db.updateWorkgroup(workgroup) + debug.debug("updated workgroup is {{ {} }}".format(retVal)) + return retVal + def getAllStatus(self): debug.info("get all status for user") retVal = db.getAllStatus() diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 14035f3..7f35b9a 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -197,6 +197,74 @@ def _deletUser(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 +@vorstand.route("/wgm/getAllWorkgroups", methods=['GET']) +@login_required(bar=True) +def _getAllWorkgroups(**kwargs): + try: + debug.info("get all workgroups") + retVal = userController.getAllWorkgroups() + for item in retVal: + item['boss'] = item['boss'].toJSON() if item['boss'] != None else None + debug.info("return all workgroups {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.warning("exception in get all workgroups.", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/wgm/getWorkgroup", methods=['POST']) +@login_required(bar=True) +def _getWorkgroup(**kwargs): + try: + debug.info("get workgroup") + data = request.get_json() + name = data['name'] + debug.info("get workgroup {{ {} }}".format(name)) + retVal = userController.getWorkgroups(name) + debug.info( + "return workgroup {{ {} }} : {{ {} }}".format(name, retVal)) + retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None + return jsonify(retVal) + except Exception as err: + debug.warning("exception in get workgroup.", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/wgm/workgroup", methods=['POST', 'PUT', 'DELETE']) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) +def _workgroup(**kwargs): + debug.info("/wgm/workgroup") + try: + data = request.get_json() + if request.method == 'PUT': + name = data['name'] + boss = None + if 'boss' in data: + boss = data['boss'] + retVal = userController.setWorkgroup(name, boss) + retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None + debug.debug("return {{ {} }}".format(retVal)) + if request.method == 'POST': + retVal = userController.updateWorkgroup(data) + retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/wgm/deleteWorkgroup", methods=['POST']) +@login_required(groups=[VORSTAND]) +def _deleteWorkgroup(**kwargs): + try: + data = request.get_json() + debug.info("/wgm/deleteWorkgroup") + userController.deleteWorkgroup(data) + retVal = {"ok": "ok"} + debug.debug("return ok") + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/lockDay", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) From 27981efedf9545ab404b4d131c12bfaaa17c0249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 17 May 2020 13:35:33 +0200 Subject: [PATCH 078/111] finished ##247 and ##253 fixed bug that user can't add negative amount. --- geruecht/controller/databaseController.py | 73 +++++++++++++++++++++-- geruecht/controller/userController.py | 7 +++ geruecht/model/user.py | 11 +++- geruecht/vorstand/routes.py | 17 ++++-- 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index e364e3b..82d32b9 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -18,7 +18,7 @@ class DatabaseController(metaclass=Singleton): def __init__(self): self.db = db - def getAllUser(self, extern=False): + def getAllUser(self, extern=False, workgroups=True): try: cursor = self.db.connection.cursor() cursor.execute("select * from user") @@ -32,6 +32,8 @@ class DatabaseController(metaclass=Singleton): user = User(value) creditLists = self.getCreditListFromUser(user) user.initGeruechte(creditLists) + if workgroups: + user.workgroups = self.getWorkgroupsOfUser(user.id) retVal.append(user) return retVal except Exception as err: @@ -39,7 +41,7 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - def getUser(self, username): + def getUser(self, username, workgroups=True): try: retVal = None cursor = self.db.connection.cursor() @@ -49,13 +51,15 @@ class DatabaseController(metaclass=Singleton): retVal = User(data) creditLists = self.getCreditListFromUser(retVal) retVal.initGeruechte(creditLists) + if workgroups: + retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) return retVal except Exception as err: traceback.print_exc() self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - def getUserById(self, id): + def getUserById(self, id, workgroups=True): try: retVal = None cursor = self.db.connection.cursor() @@ -65,6 +69,8 @@ class DatabaseController(metaclass=Singleton): retVal = User(data) creditLists = self.getCreditListFromUser(retVal) retVal.initGeruechte(creditLists) + if workgroups: + retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) return retVal except Exception as err: traceback.print_exc() @@ -580,7 +586,7 @@ class DatabaseController(metaclass=Singleton): list = cursor.fetchall() for item in list: if item['boss'] != None: - item['boss']=self.getUserById(item['boss']) + item['boss']=self.getUserById(item['boss'], workgroups=False).toJSON() return list except Exception as err: traceback.print_exc() @@ -598,7 +604,7 @@ class DatabaseController(metaclass=Singleton): raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) cursor.execute(sql) retVal = cursor.fetchone() - retVal['boss'] = self.getUserById(retVal['boss']) if retVal['boss'] != None else None + retVal['boss'] = self.getUserById(retVal['boss'], workgroups=False).toJSON() if retVal['boss'] != None else None return retVal except Exception as err: traceback.print_exc() @@ -637,6 +643,63 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + def getWorkgroupsOfUser(self, userid): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where user_id={} ".format(userid)) + knots = cursor.fetchall() + retVal = [self.getWorkgroup(knot['workgroup_id']) for knot in knots] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getUsersOfWorkgroups(self, workgroupid): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where workgroup_id={}".format(workgroupid)) + knots = cursor.fetchall() + retVal = [self.getUserById(knot['user_id'], workgroups=False).toJSON() for knot in knots] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getUserWorkgroup(self, user, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where workgroup_id={} and user_id={}".format(workgroup['id'], user['id'])) + knot = cursor.fetchone() + retVal = {"workgroup": self.getWorkgroup(workgroup['id']), "user": self.getUserById(user['id'], workgroups=False).toJSON()} + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setUserWorkgroup(self, user, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into user_workgroup (user_id, workgroup_id) VALUES ({}, {})".format(user['id'], workgroup['id'])) + self.db.connection.commit() + return self.getUserWorkgroup(user, workgroup) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def deleteWorkgroupsOfUser(self, user): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from user_workgroup where user_id={}".format(user['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index 90778d9..ebdffde 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -23,6 +23,13 @@ class UserController(metaclass=Singleton): debug.debug("init UserController") pass + def updateWorkgroupsOfUser(self, user, workgroups): + debug.info("update workgroups {{ {} }} of user {{ {} }}".format(workgroups, user)) + db.deleteWorkgroupsOfUser(user) + for workgroup in workgroups: + db.setUserWorkgroup(user, workgroup) + return db.getWorkgroupsOfUser(user['id']) + def getAllWorkgroups(self): debug.info("get all workgroups") retVal = db.getAllWorkgroups() diff --git a/geruecht/model/user.py b/geruecht/model/user.py index d7d5d46..1af0ffe 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -58,6 +58,10 @@ class User(): self.group = data['gruppe'].split(',') if 'creditLists' in data: self.geruechte = data['creditLists'] + if 'workgroups' in data: + self.workgroups = data['workgroups'] + else: + self.workgroups = None self.password = '' debug.debug("user is {{ {} }}".format(self)) @@ -83,6 +87,10 @@ class User(): self.statusgroup = data['statusgroup'] if 'voting' in data: self.voting = data['voting'] + if 'workgroups' in data: + self.workgroups = data['workgroups'] + else: + self.workgroups = None def initGeruechte(self, creditLists): if type(creditLists) == list: @@ -223,7 +231,8 @@ class User(): "limit": self.limit, "mail": self.mail, "statusgroup": self.statusgroup, - "voting": self.voting + "voting": self.voting, + "workgroups": self.workgroups } return dic diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 7f35b9a..56b8ff6 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -88,6 +88,18 @@ def _updateVoting(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 +@vorstand.route('/um/updateWorkgroups', methods=['POST']) +@login_required(groups=[VORSTAND]) +def _updateWorkgroups(**kwargs): + debug.info("/um/updateWorkgroups") + try: + data = request.get_json() + retVal = userController.updateWorkgroupsOfUser({"id": data['id']}, data['workgroups']) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal), 200 + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/addUser", methods=['POST', 'GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) @@ -203,8 +215,6 @@ def _getAllWorkgroups(**kwargs): try: debug.info("get all workgroups") retVal = userController.getAllWorkgroups() - for item in retVal: - item['boss'] = item['boss'].toJSON() if item['boss'] != None else None debug.info("return all workgroups {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -222,7 +232,6 @@ def _getWorkgroup(**kwargs): retVal = userController.getWorkgroups(name) debug.info( "return workgroup {{ {} }} : {{ {} }}".format(name, retVal)) - retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None return jsonify(retVal) except Exception as err: debug.warning("exception in get workgroup.", exc_info=True) @@ -240,11 +249,9 @@ def _workgroup(**kwargs): if 'boss' in data: boss = data['boss'] retVal = userController.setWorkgroup(name, boss) - retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None debug.debug("return {{ {} }}".format(retVal)) if request.method == 'POST': retVal = userController.updateWorkgroup(data) - retVal['boss'] = retVal['boss'].toJSON() if retVal['boss'] != None else None debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: From 04d6254262a2a10327877bb255f2e58a468a76ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 17 May 2020 20:21:22 +0200 Subject: [PATCH 079/111] vorstand can create, edit or delete job_kinds --- geruecht/controller/databaseController.py | 60 +++++++++++++++++++++++ geruecht/controller/userController.py | 29 +++++++++++ geruecht/vorstand/routes.py | 59 ++++++++++++++++++++++ 3 files changed, 148 insertions(+) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 82d32b9..8e760c2 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -700,6 +700,66 @@ class DatabaseController(metaclass=Singleton): self.db.connection.rollback() raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + def getAllJobKinds(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from job_kind') + list = cursor.fetchall() + return list + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getJobKind(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from job_kind where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from job_kind where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + retVal = cursor.fetchone() + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setJobKind(self, name): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into job_kind (name) values ('{}')".format(name)) + self.db.connection.commit() + return self.getJobKind(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateJobKind(self, jobkind): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_kind set name='{}'where id={}".format(jobkind['name'], jobkind['id'])) + self.db.connection.commit() + return self.getJobKind(jobkind['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteJobKind(self, jobkind): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_kind where id={}".format(jobkind['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + if __name__ == '__main__': db = DatabaseController() user = db.getUser('jhille') diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index ebdffde..b129317 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -23,6 +23,35 @@ class UserController(metaclass=Singleton): debug.debug("init UserController") pass + def getAllJobKinds(self): + debug.info("get all jobkinds") + retVal = db.getAllJobKinds() + debug.debug("jobkinds are {{ {} }}".format(retVal)) + return retVal + + def getJobKind(self, name): + debug.info("get jobkinds {{ {} }}".format(name)) + retVal = db.getJobKind(name) + debug.debug("jobkind is {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def setJobKind(self, name): + debug.info("set jobkind {{ {} }} ".format(name)) + retVal = db.setJobKind(name) + debug.debug( + "seted jobkind {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def deleteJobKind(self, jobkind): + debug.info("delete jobkind {{ {} }}".format(jobkind)) + db.deleteJobKind(jobkind) + + def updateJobKind(self, jobkind): + debug.info("update workgroup {{ {} }}".format(jobkind)) + retVal = db.updateJobKind(jobkind) + debug.debug("updated jobkind is {{ {} }}".format(retVal)) + return retVal + def updateWorkgroupsOfUser(self, user, workgroups): debug.info("update workgroups {{ {} }} of user {{ {} }}".format(workgroups, user)) db.deleteWorkgroupsOfUser(user) diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 56b8ff6..4e99868 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -272,6 +272,65 @@ def _deleteWorkgroup(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 +@vorstand.route("/sm/getAllJobKinds", methods=['GET']) +@login_required(bar=True) +def _getAllJobKinds(**kwargs): + try: + debug.info("get all jobkinds") + retVal = userController.getAllJobKinds() + debug.info("return all jobkinds {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.warning("exception in get all workgroups.", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/sm/getJobKind", methods=['POST']) +@login_required(bar=True) +def _getJobKinds(**kwargs): + try: + debug.info("get jobkind") + data = request.get_json() + name = data['name'] + debug.info("get jobkind {{ {} }}".format(name)) + retVal = userController.getJobKind(name) + debug.info( + "return workgroup {{ {} }} : {{ {} }}".format(name, retVal)) + return jsonify(retVal) + except Exception as err: + debug.warning("exception in get workgroup.", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/sm/JobKind", methods=['POST', 'PUT', 'DELETE']) +@login_required(groups=[MONEY, GASTRO, VORSTAND]) +def _JobKinds(**kwargs): + debug.info("/sm/JobKind") + try: + data = request.get_json() + if request.method == 'PUT': + name = data['name'] + retVal = userController.setJobKind(name) + debug.debug("return {{ {} }}".format(retVal)) + if request.method == 'POST': + retVal = userController.updateJobKind(data) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/sm/deleteJobKind", methods=['POST']) +@login_required(groups=[VORSTAND]) +def _deleteJobKind(**kwargs): + try: + data = request.get_json() + debug.info("/sm/deleteJobKind") + userController.deleteJobKind(data) + retVal = {"ok": "ok"} + debug.debug("return ok") + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @vorstand.route("/sm/lockDay", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) From 7ce8fef278e1f1038c3b1389ee9ebe0bf7a84304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 17 May 2020 21:18:56 +0200 Subject: [PATCH 080/111] vorstand can change the group for jobkind --- geruecht/controller/databaseController.py | 9 ++++++--- geruecht/controller/userController.py | 4 ++-- geruecht/vorstand/routes.py | 5 ++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py index 8e760c2..59c5b1b 100644 --- a/geruecht/controller/databaseController.py +++ b/geruecht/controller/databaseController.py @@ -705,6 +705,8 @@ class DatabaseController(metaclass=Singleton): cursor = self.db.connection.cursor() cursor.execute('select * from job_kind') list = cursor.fetchall() + for item in list: + item['workgroup'] = self.getWorkgroup(item['workgroup']) if item['workgroup'] != None else None return list except Exception as err: traceback.print_exc() @@ -722,16 +724,17 @@ class DatabaseController(metaclass=Singleton): raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) cursor.execute(sql) retVal = cursor.fetchone() + retVal['workgroup'] = self.getWorkgroup(retVal['workgroup']) if retVal['workgroup'] != None else None return retVal except Exception as err: traceback.print_exc() self.db.connection.rollback() raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - def setJobKind(self, name): + def setJobKind(self, name, workgroup_id): try: cursor = self.db.connection.cursor() - cursor.execute("insert into job_kind (name) values ('{}')".format(name)) + cursor.execute("insert into job_kind (name, workgroup) values ('{}', {})".format(name, workgroup_id if workgroup_id != None else 'NULL')) self.db.connection.commit() return self.getJobKind(name) except Exception as err: @@ -742,7 +745,7 @@ class DatabaseController(metaclass=Singleton): def updateJobKind(self, jobkind): try: cursor = self.db.connection.cursor() - cursor.execute("update job_kind set name='{}'where id={}".format(jobkind['name'], jobkind['id'])) + cursor.execute("update job_kind set name='{}', workgroup={} where id={}".format(jobkind['name'], jobkind['workgroup']['id'] if jobkind['workgroup'] != None else 'NULL', jobkind['id'])) self.db.connection.commit() return self.getJobKind(jobkind['id']) except Exception as err: diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py index b129317..3e6f605 100644 --- a/geruecht/controller/userController.py +++ b/geruecht/controller/userController.py @@ -35,9 +35,9 @@ class UserController(metaclass=Singleton): debug.debug("jobkind is {{ {} }} is {{ {} }}".format(name, retVal)) return retVal - def setJobKind(self, name): + def setJobKind(self, name, workgroup=None): debug.info("set jobkind {{ {} }} ".format(name)) - retVal = db.setJobKind(name) + retVal = db.setJobKind(name, workgroup) debug.debug( "seted jobkind {{ {} }} is {{ {} }}".format(name, retVal)) return retVal diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index 4e99868..b098fff 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -308,7 +308,10 @@ def _JobKinds(**kwargs): data = request.get_json() if request.method == 'PUT': name = data['name'] - retVal = userController.setJobKind(name) + workgroup = None + if 'workgroup' in data: + workgroup = data['workgroup'] + retVal = userController.setJobKind(name, workgroup) debug.debug("return {{ {} }}".format(retVal)) if request.method == 'POST': retVal = userController.updateJobKind(data) From 2ef50fbefd50f4e481081219dfcf0fcb87328ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 22 May 2020 21:55:14 +0200 Subject: [PATCH 081/111] vorstand can set job_kinds for day with max persons extern user can be set if job_kind is delete, all jobs are deleted for this date and job_kind --- geruecht/baruser/routes.py | 16 +- geruecht/controller/accesTokenController.py | 6 +- geruecht/controller/databaseController.py | 769 ------------------ .../controller/databaseController/__init__.py | 61 ++ .../dbCreditListController.py | 66 ++ .../databaseController/dbJobKindController.py | 112 +++ .../dbJobTransactController.py | 110 +++ .../dbPricelistController.py | 128 +++ .../databaseController/dbUserController.py | 197 +++++ .../databaseController/dbWorkerController.py | 67 ++ .../dbWorkgroupController.py | 126 +++ geruecht/controller/emailController.py | 21 +- geruecht/controller/ldapController.py | 2 + .../controller/mainController/__init__.py | 141 ++++ .../mainCreditListController.py | 83 ++ .../mainController/mainJobKindController.py | 89 ++ .../mainJobTransactController.py | 71 ++ .../mainController/mainPricelistController.py | 50 ++ .../mainController/mainUserController.py | 160 ++++ .../mainController/mainWorkerController.py | 65 ++ .../mainController/mainWorkgroupController.py | 42 + geruecht/controller/userController.py | 588 ------------- geruecht/finanzer/routes.py | 26 +- geruecht/gastro/routes.py | 16 +- geruecht/routes.py | 21 +- geruecht/user/routes.py | 47 +- geruecht/vorstand/routes.py | 117 ++- 27 files changed, 1704 insertions(+), 1493 deletions(-) delete mode 100644 geruecht/controller/databaseController.py create mode 100644 geruecht/controller/databaseController/__init__.py create mode 100644 geruecht/controller/databaseController/dbCreditListController.py create mode 100644 geruecht/controller/databaseController/dbJobKindController.py create mode 100644 geruecht/controller/databaseController/dbJobTransactController.py create mode 100644 geruecht/controller/databaseController/dbPricelistController.py create mode 100644 geruecht/controller/databaseController/dbUserController.py create mode 100644 geruecht/controller/databaseController/dbWorkerController.py create mode 100644 geruecht/controller/databaseController/dbWorkgroupController.py create mode 100644 geruecht/controller/mainController/__init__.py create mode 100644 geruecht/controller/mainController/mainCreditListController.py create mode 100644 geruecht/controller/mainController/mainJobKindController.py create mode 100644 geruecht/controller/mainController/mainJobTransactController.py create mode 100644 geruecht/controller/mainController/mainPricelistController.py create mode 100644 geruecht/controller/mainController/mainUserController.py create mode 100644 geruecht/controller/mainController/mainWorkerController.py create mode 100644 geruecht/controller/mainController/mainWorkgroupController.py delete mode 100644 geruecht/controller/userController.py diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 98c7df5..76903d1 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, jsonify import geruecht.controller.ldapController as lc -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from datetime import datetime from geruecht.model import BAR, MONEY, USER, VORSTAND from geruecht.decorator import login_required @@ -12,7 +12,7 @@ creditL = getCreditLogger() baruser = Blueprint("baruser", __name__) ldap = lc.LDAPController() -userController = uc.UserController() +mainController = mc.MainController() @baruser.route("/bar") @@ -29,7 +29,7 @@ def _bar(**kwargs): debug.info("/bar") try: dic = {} - users = userController.getAllUsersfromDB() + users = mainController.getAllUsersfromDB() for user in users: geruecht = None geruecht = user.getGeruecht(datetime.now().year) @@ -72,9 +72,9 @@ def _baradd(**kwargs): amount = int(data['amount']) amountl = amount date = datetime.now() - userController.addAmount( + mainController.addAmount( userID, amount, year=date.year, month=date.month) - user = userController.getUser(userID) + user = mainController.getUser(userID) geruecht = user.getGeruecht(year=date.year) month = geruecht.getMonth(month=date.month) amount = abs(month[0] - month[1]) @@ -135,9 +135,9 @@ def _storno(**kwargs): amount = int(data['amount']) amountl = amount date = datetime.now() - userController.addCredit( + mainController.addCredit( userID, amount, year=date.year, month=date.month) - user = userController.getUser(userID) + user = mainController.getUser(userID) geruecht = user.getGeruecht(year=date.year) month = geruecht.getMonth(month=date.month) amount = abs(month[0] - month[1]) @@ -165,7 +165,7 @@ def _getUser(**kwargs): try: data = request.get_json() username = data['userId'] - user = userController.getUser(username) + user = mainController.getUser(username) amount = user.getGeruecht(datetime.now().year).getSchulden() if amount >= 0: type = 'credit' diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index bd1970f..6c683c4 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,6 +1,6 @@ from geruecht.model.accessToken import AccessToken import geruecht.controller as gc -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from geruecht.model import BAR from datetime import datetime, timedelta import hashlib @@ -9,7 +9,7 @@ from geruecht.logger import getDebugLogger debug = getDebugLogger() -userController = uc.UserController() +mainController = mc.MainController() class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken @@ -34,7 +34,7 @@ class AccesTokenController(metaclass=Singleton): def checkBar(self, user): debug.info("check if user {{ {} }} is baruser".format(user)) - if (userController.checkBarUser(user)): + if (mainController.checkBarUser(user)): if BAR not in user.group: debug.debug("append bar to user {{ {} }}".format(user)) user.group.append(BAR) diff --git a/geruecht/controller/databaseController.py b/geruecht/controller/databaseController.py deleted file mode 100644 index 59c5b1b..0000000 --- a/geruecht/controller/databaseController.py +++ /dev/null @@ -1,769 +0,0 @@ -import pymysql -from . import Singleton -from geruecht import db -from geruecht.model.user import User -from geruecht.model.creditList import CreditList -from datetime import datetime, timedelta -from geruecht.exceptions import UsernameExistDB, DatabaseExecption -import traceback -from MySQLdb._exceptions import IntegrityError - -class DatabaseController(metaclass=Singleton): - ''' - DatabaesController - - Connect to the Database and execute sql-executions - ''' - - def __init__(self): - self.db = db - - def getAllUser(self, extern=False, workgroups=True): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from user") - data = cursor.fetchall() - - if data: - retVal = [] - for value in data: - if extern and value['uid'] == 'extern': - continue - user = User(value) - creditLists = self.getCreditListFromUser(user) - user.initGeruechte(creditLists) - if workgroups: - user.workgroups = self.getWorkgroupsOfUser(user.id) - retVal.append(user) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def getUser(self, username, workgroups=True): - try: - retVal = None - cursor = self.db.connection.cursor() - cursor.execute("select * from user where uid='{}'".format(username)) - data = cursor.fetchone() - if data: - retVal = User(data) - creditLists = self.getCreditListFromUser(retVal) - retVal.initGeruechte(creditLists) - if workgroups: - retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def getUserById(self, id, workgroups=True): - try: - retVal = None - cursor = self.db.connection.cursor() - cursor.execute("select * from user where id={}".format(id)) - data = cursor.fetchone() - if data: - retVal = User(data) - creditLists = self.getCreditListFromUser(retVal) - retVal.initGeruechte(creditLists) - if workgroups: - retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def _convertGroupToString(self, groups): - retVal = '' - print('groups: {}'.format(groups)) - if groups: - for group in groups: - if len(retVal) != 0: - retVal += ',' - retVal += group - return retVal - - - def insertUser(self, user): - try: - cursor = self.db.connection.cursor() - groups = self._convertGroupToString(user.group) - cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( - user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - - def updateUser(self, user): - try: - cursor = self.db.connection.cursor() - print('uid: {}; group: {}'.format(user.uid, user.group)) - groups = self._convertGroupToString(user.group) - sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( - user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) - print(sql) - cursor.execute(sql) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - - def getCreditListFromUser(self, user, **kwargs): - try: - cursor = self.db.connection.cursor() - if 'year' in kwargs: - sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) - else: - sql = "select * from creditList where user_id={}".format(user.id) - cursor.execute(sql) - data = cursor.fetchall() - if len(data) == 1: - return [CreditList(data[0])] - else: - return [CreditList(value) for value in data] - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - - def createCreditList(self, user_id, year=datetime.now().year): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - - def updateCreditList(self, creditlist): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) - data = cursor.fetchall() - if len(data) == 0: - self.createCreditList(creditlist.user_id, creditlist.year) - sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, - creditlist.feb_guthaben, creditlist.feb_schulden, - creditlist.maer_guthaben, creditlist.maer_schulden, - creditlist.apr_guthaben, creditlist.apr_schulden, - creditlist.mai_guthaben, creditlist.mai_schulden, - creditlist.jun_guthaben, creditlist.jun_schulden, - creditlist.jul_guthaben, creditlist.jul_schulden, - creditlist.aug_guthaben, creditlist.aug_schulden, - creditlist.sep_guthaben, creditlist.sep_schulden, - creditlist.okt_guthaben, creditlist.okt_schulden, - creditlist.nov_guthaben, creditlist.nov_schulden, - creditlist.dez_guthaben, creditlist.dez_schulden, - creditlist.last_schulden, creditlist.year, creditlist.user_id) - print(sql) - cursor = self.db.connection.cursor() - cursor.execute(sql) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def getWorker(self, user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - data = cursor.fetchone() - return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}} if data else None - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def getWorkers(self, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) - data = cursor.fetchall() - return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}} for work in data] - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def setWorker(self, user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime) values ({},'{}','{}')".format(user.id, date, date + timedelta(days=1))) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - - def deleteWorker(self, user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def changeUsername(self, user, newUsername): - try: - cursor= self.db.connection.cursor() - cursor.execute("select * from user where uid='{}'".format(newUsername)) - data = cursor.fetchall() - if data: - raise UsernameExistDB("Username already exists") - else: - cursor.execute("update user set uid='{}' where id={}".format(newUsername, user.id)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def getLockedDay(self, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from locked_days where daydate='{}'".format(date)) - data = cursor.fetchone() - return data - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def setLockedDay(self, date, locked, hard=False): - try: - cursor = self.db.connection.cursor() - sql = "insert into locked_days (daydate, locked) VALUES ('{}', {})".format(date, locked) - cursor.execute(sql) - self.db.connection.commit() - return self.getLockedDay(date) - except IntegrityError as err: - self.db.connection.rollback() - try: - exists = self.getLockedDay(date) - if hard: - sql = "update locked_days set locked={} where id={}".format(locked, exists['id']) - else: - sql = False - if sql: - cursor.execute(sql) - self.db.connection.commit() - return self.getLockedDay(date) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - - def setTransactJob(self, from_user, to_user, date): - try: - exists = self.getTransactJob(from_user, to_user, date) - if exists: - raise IntegrityError("job_transact already exists") - cursor = self.db.connection.cursor() - cursor.execute("insert into job_transact (jobdate, from_user_id, to_user_id) VALUES ('{}', {}, {})".format(date, from_user.id, to_user.id)) - self.db.connection.commit() - return self.getTransactJob(from_user, to_user, date) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getTransactJob(self, from_user, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) - data = cursor.fetchone() - if data: - return {"from_user": from_user, "to_user": to_user, "date": data['jobdate'], "answerd": data['answerd'], "accepted": data['accepted']} - return None - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def getAllTransactJobFromUser(self, from_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={}".format(from_user.id)) - data = cursor.fetchall() - retVal = [] - for transact in data: - if date <= transact['jobdate']: - retVal.append({"from_user": from_user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getAllTransactJobToUser(self, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where to_user_id={}".format(to_user.id)) - data = cursor.fetchall() - retVal = [] - for transact in data: - if date <= transact['jobdate']: - retVal.append({"to_user": to_user, "from_user": self.getUserById(transact['from_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getTransactJobToUser(self, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where to_user_id={} and jobdate='{}'".format(to_user.id, date)) - data = cursor.fetchone() - if data: - return {"from_user": self.getUserById(data['from_user_id']), "to_user": to_user, "date": data['jobdate'], "accepted": data['accepted'], "answerd": data['answerd']} - else: - return None - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def updateTransactJob(self, from_user, to_user, date, accepted): - try: - cursor = self.db.connection.cursor() - cursor.execute("update job_transact set accepted={}, answerd=true where to_user_id={} and jobdate='{}'".format(accepted, to_user.id, date)) - self.db.connection.commit() - return self.getTransactJob(from_user, to_user, date) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getTransactJobFromUser(self, user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={} and jobdate='{}'".format(user.id, date)) - data = cursor.fetchall() - return [{"from_user": user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']} for transact in data] - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def deleteTransactJob(self, from_user, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getPriceList(self): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from pricelist") - return cursor.fetchall() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getDrinkPrice(self, name): - try: - cursor = self.db.connection.cursor() - if type(name) == str: - sql = "select * from pricelist where name='{}'".format(name) - elif type(name) == int: - sql = 'select * from pricelist where id={}'.format(name) - else: - raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) - cursor.execute(sql) - return cursor.fetchone() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def setDrinkPrice(self, drink): - try: - cursor = self.db.connection.cursor() - cursor.execute( - "insert into pricelist (name, price, price_big, price_club, price_club_big, premium, premium_club, price_extern_club, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", - ( - drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], - drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'])) - self.db.connection.commit() - return self.getDrinkPrice(str(drink['name'])) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def updateDrinkPrice(self, drink): - try: - cursor = self.db.connection.cursor() - cursor.execute("update pricelist set name=%s, price=%s, price_big=%s, price_club=%s, price_club_big=%s, premium=%s, premium_club=%s, price_extern_club=%s, type=%s where id=%s", - ( - drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'], drink['id'] - )) - self.db.connection.commit() - return self.getDrinkPrice(drink['id']) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def deleteDrink(self, drink): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from pricelist where id={}".format(drink['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def getDrinkType(self, name): - try: - cursor = self.db.connection.cursor() - if type(name) == str: - sql = "select * from drink_type where name='{}'".format(name) - elif type(name) == int: - sql = 'select * from drink_type where id={}'.format(name) - else: - raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) - cursor.execute(sql) - return cursor.fetchone() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def setDrinkType(self, name): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into drink_type (name) values ('{}')".format(name)) - self.db.connection.commit() - return self.getDrinkType(name) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def updateDrinkType(self, type): - try: - cursor = self.db.connection.cursor() - cursor.execute("update drink_type set name='{}' where id={}".format(type['name'], type['id'])) - self.db.connection.commit() - return self.getDrinkType(type['id']) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def deleteDrinkType(self, type): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from drink_type where id={}".format(type['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getAllDrinkTypes(self): - try: - cursor = self.db.connection.cursor() - cursor.execute('select * from drink_type') - return cursor.fetchall() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def getAllStatus(self): - try: - cursor = self.db.connection.cursor() - cursor.execute('select * from statusgroup') - return cursor.fetchall() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def getStatus(self, name): - try: - cursor = self.db.connection.cursor() - if type(name) == str: - sql = "select * from statusgroup where name='{}'".format(name) - elif type(name) == int: - sql = 'select * from statusgroup where id={}'.format(name) - else: - raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) - cursor.execute(sql) - return cursor.fetchone() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def setStatus(self, name): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into statusgroup (name) values ('{}')".format(name)) - self.db.connection.commit() - return self.getStatus(name) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def updateStatus(self, status): - try: - cursor = self.db.connection.cursor() - cursor.execute("update statusgroup set name='{}' where id={}".format(status['name'], status['id'])) - self.db.connection.commit() - return self.getStatus(status['id']) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def deleteStatus(self, status): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from statusgroup where id={}".format(status['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def updateStatusOfUser(self, username, status): - try: - cursor = self.db.connection.cursor() - cursor.execute("update user set statusgroup={} where uid='{}'".format(status['id'], username)) - self.db.connection.commit() - return self.getUser(username) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def updateVotingOfUser(self, username, voting): - try: - cursor = self.db.connection.cursor() - cursor.execute("update user set voting={} where uid='{}'".format(voting, username)) - self.db.connection.commit() - return self.getUser(username) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def getAllWorkgroups(self): - try: - cursor = self.db.connection.cursor() - cursor.execute('select * from workgroup') - list = cursor.fetchall() - for item in list: - if item['boss'] != None: - item['boss']=self.getUserById(item['boss'], workgroups=False).toJSON() - return list - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def getWorkgroup(self, name): - try: - cursor = self.db.connection.cursor() - if type(name) == str: - sql = "select * from workgroup where name='{}'".format(name) - elif type(name) == int: - sql = 'select * from workgroup where id={}'.format(name) - else: - raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) - cursor.execute(sql) - retVal = cursor.fetchone() - retVal['boss'] = self.getUserById(retVal['boss'], workgroups=False).toJSON() if retVal['boss'] != None else None - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def setWorkgroup(self, name, boss): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into workgroup (name, boss) values ('{}', {})".format(name, boss['id'])) - self.db.connection.commit() - return self.getWorkgroup(name) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def updateWorkgroup(self, workgroup): - try: - cursor = self.db.connection.cursor() - cursor.execute("update workgroup set name='{}', boss={} where id={}".format(workgroup['name'], workgroup['boss']['id'], workgroup['id'])) - self.db.connection.commit() - return self.getWorkgroup(workgroup['id']) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def deleteWorkgroup(self, workgroup): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from workgroup where id={}".format(workgroup['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def getWorkgroupsOfUser(self, userid): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from user_workgroup where user_id={} ".format(userid)) - knots = cursor.fetchall() - retVal = [self.getWorkgroup(knot['workgroup_id']) for knot in knots] - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getUsersOfWorkgroups(self, workgroupid): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from user_workgroup where workgroup_id={}".format(workgroupid)) - knots = cursor.fetchall() - retVal = [self.getUserById(knot['user_id'], workgroups=False).toJSON() for knot in knots] - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getUserWorkgroup(self, user, workgroup): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from user_workgroup where workgroup_id={} and user_id={}".format(workgroup['id'], user['id'])) - knot = cursor.fetchone() - retVal = {"workgroup": self.getWorkgroup(workgroup['id']), "user": self.getUserById(user['id'], workgroups=False).toJSON()} - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def setUserWorkgroup(self, user, workgroup): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into user_workgroup (user_id, workgroup_id) VALUES ({}, {})".format(user['id'], workgroup['id'])) - self.db.connection.commit() - return self.getUserWorkgroup(user, workgroup) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def deleteWorkgroupsOfUser(self, user): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from user_workgroup where user_id={}".format(user['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def getAllJobKinds(self): - try: - cursor = self.db.connection.cursor() - cursor.execute('select * from job_kind') - list = cursor.fetchall() - for item in list: - item['workgroup'] = self.getWorkgroup(item['workgroup']) if item['workgroup'] != None else None - return list - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def getJobKind(self, name): - try: - cursor = self.db.connection.cursor() - if type(name) == str: - sql = "select * from job_kind where name='{}'".format(name) - elif type(name) == int: - sql = 'select * from job_kind where id={}'.format(name) - else: - raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) - cursor.execute(sql) - retVal = cursor.fetchone() - retVal['workgroup'] = self.getWorkgroup(retVal['workgroup']) if retVal['workgroup'] != None else None - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def setJobKind(self, name, workgroup_id): - try: - cursor = self.db.connection.cursor() - cursor.execute("insert into job_kind (name, workgroup) values ('{}', {})".format(name, workgroup_id if workgroup_id != None else 'NULL')) - self.db.connection.commit() - return self.getJobKind(name) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def updateJobKind(self, jobkind): - try: - cursor = self.db.connection.cursor() - cursor.execute("update job_kind set name='{}', workgroup={} where id={}".format(jobkind['name'], jobkind['workgroup']['id'] if jobkind['workgroup'] != None else 'NULL', jobkind['id'])) - self.db.connection.commit() - return self.getJobKind(jobkind['id']) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - - def deleteJobKind(self, jobkind): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from job_kind where id={}".format(jobkind['id'])) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - -if __name__ == '__main__': - db = DatabaseController() - user = db.getUser('jhille') - db.getCreditListFromUser(user, year=2018) diff --git a/geruecht/controller/databaseController/__init__.py b/geruecht/controller/databaseController/__init__.py new file mode 100644 index 0000000..91f8f8e --- /dev/null +++ b/geruecht/controller/databaseController/__init__.py @@ -0,0 +1,61 @@ +from ..mainController import Singleton +from geruecht import db +from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbJobTransactController, dbPricelistController, dbWorkerController, dbWorkgroupController +from geruecht.exceptions import DatabaseExecption +import traceback +from MySQLdb._exceptions import IntegrityError + +class DatabaseController(dbUserController.Base, dbCreditListController.Base, dbWorkerController.Base, dbWorkgroupController.Base, dbPricelistController.Base, dbJobTransactController.Base, dbJobKindController.Base, metaclass=Singleton): + ''' + DatabaesController + + Connect to the Database and execute sql-executions + ''' + + def __init__(self): + self.db = db + + def getLockedDay(self, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from locked_days where daydate='{}'".format(date)) + data = cursor.fetchone() + return data + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def setLockedDay(self, date, locked, hard=False): + try: + cursor = self.db.connection.cursor() + sql = "insert into locked_days (daydate, locked) VALUES ('{}', {})".format(date, locked) + cursor.execute(sql) + self.db.connection.commit() + return self.getLockedDay(date) + except IntegrityError as err: + self.db.connection.rollback() + try: + exists = self.getLockedDay(date) + if hard: + sql = "update locked_days set locked={} where id={}".format(locked, exists['id']) + else: + sql = False + if sql: + cursor.execute(sql) + self.db.connection.commit() + return self.getLockedDay(date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + +if __name__ == '__main__': + db = DatabaseController() + user = db.getUser('jhille') + db.getCreditListFromUser(user, year=2018) diff --git a/geruecht/controller/databaseController/dbCreditListController.py b/geruecht/controller/databaseController/dbCreditListController.py new file mode 100644 index 0000000..f8098b5 --- /dev/null +++ b/geruecht/controller/databaseController/dbCreditListController.py @@ -0,0 +1,66 @@ +import traceback +from datetime import datetime + +from geruecht.exceptions import DatabaseExecption +from geruecht.model.creditList import CreditList + + +class Base: + def getCreditListFromUser(self, user, **kwargs): + try: + cursor = self.db.connection.cursor() + if 'year' in kwargs: + sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) + else: + sql = "select * from creditList where user_id={}".format(user.id) + cursor.execute(sql) + data = cursor.fetchall() + if len(data) == 1: + return [CreditList(data[0])] + else: + return [CreditList(value) for value in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + + def createCreditList(self, user_id, year=datetime.now().year): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + + def updateCreditList(self, creditlist): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from creditList where user_id={} and year_date={}".format(creditlist.user_id, creditlist.year)) + data = cursor.fetchall() + if len(data) == 0: + self.createCreditList(creditlist.user_id, creditlist.year) + sql = "update creditList set jan_guthaben={}, jan_schulden={},feb_guthaben={}, feb_schulden={}, maer_guthaben={}, maer_schulden={}, apr_guthaben={}, apr_schulden={}, mai_guthaben={}, mai_schulden={}, jun_guthaben={}, jun_schulden={}, jul_guthaben={}, jul_schulden={}, aug_guthaben={}, aug_schulden={},sep_guthaben={}, sep_schulden={},okt_guthaben={}, okt_schulden={}, nov_guthaben={}, nov_schulden={}, dez_guthaben={}, dez_schulden={}, last_schulden={} where year_date={} and user_id={}".format(creditlist.jan_guthaben, creditlist.jan_schulden, + creditlist.feb_guthaben, creditlist.feb_schulden, + creditlist.maer_guthaben, creditlist.maer_schulden, + creditlist.apr_guthaben, creditlist.apr_schulden, + creditlist.mai_guthaben, creditlist.mai_schulden, + creditlist.jun_guthaben, creditlist.jun_schulden, + creditlist.jul_guthaben, creditlist.jul_schulden, + creditlist.aug_guthaben, creditlist.aug_schulden, + creditlist.sep_guthaben, creditlist.sep_schulden, + creditlist.okt_guthaben, creditlist.okt_schulden, + creditlist.nov_guthaben, creditlist.nov_schulden, + creditlist.dez_guthaben, creditlist.dez_schulden, + creditlist.last_schulden, creditlist.year, creditlist.user_id) + print(sql) + cursor = self.db.connection.cursor() + cursor.execute(sql) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbJobKindController.py b/geruecht/controller/databaseController/dbJobKindController.py new file mode 100644 index 0000000..c243f0d --- /dev/null +++ b/geruecht/controller/databaseController/dbJobKindController.py @@ -0,0 +1,112 @@ +import traceback + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getAllJobKinds(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from job_kind') + list = cursor.fetchall() + for item in list: + item['workgroup'] = self.getWorkgroup(item['workgroup']) if item['workgroup'] != None else None + return list + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getJobKind(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from job_kind where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from job_kind where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + retVal = cursor.fetchone() + retVal['workgroup'] = self.getWorkgroup(retVal['workgroup']) if retVal['workgroup'] != None else None + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setJobKind(self, name, workgroup_id): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into job_kind (name, workgroup) values ('{}', {})".format(name, workgroup_id if workgroup_id != None else 'NULL')) + self.db.connection.commit() + return self.getJobKind(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateJobKind(self, jobkind): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_kind set name='{}', workgroup={} where id={}".format(jobkind['name'], jobkind['workgroup']['id'] if jobkind['workgroup'] != None else 'NULL', jobkind['id'])) + self.db.connection.commit() + return self.getJobKind(jobkind['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteJobKind(self, jobkind): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_kind where id={}".format(jobkind['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setJobKindDates(self, date, jobkind, maxpersons): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into job_kind_dates (daydate, job_kind, maxpersons) values ('{}', {}, {})".format(date, jobkind['id'] if jobkind != None else 'NULL', maxpersons)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateJobKindDates(self, jobkindDate): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_kind_dates set job_kind={}, maxpersons='{}' where id={}".format(jobkindDate['job_kind']['id'] if jobkindDate['job_kind'] != None else 'NULL', jobkindDate['maxpersons'], jobkindDate['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getJobKindDates(self, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_kind_dates where daydate='{}'".format(date)) + list = cursor.fetchall() + for item in list: + item['job_kind'] = self.getJobKind(item['job_kind']) if item['job_kind'] != None else None + item['daydate'] = {'year': item['daydate'].year, 'month': item['daydate'].month, 'day': item['daydate'].day} + return list + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteJobKindDates(self, jobkinddates): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_kind_dates where id={}".format(jobkinddates['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbJobTransactController.py b/geruecht/controller/databaseController/dbJobTransactController.py new file mode 100644 index 0000000..cafe463 --- /dev/null +++ b/geruecht/controller/databaseController/dbJobTransactController.py @@ -0,0 +1,110 @@ +import traceback + +from MySQLdb._exceptions import IntegrityError + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getTransactJob(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) + data = cursor.fetchone() + if data: + return {"from_user": from_user, "to_user": to_user, "date": data['jobdate'], "answerd": data['answerd'], "accepted": data['accepted']} + return None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def getAllTransactJobFromUser(self, from_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={}".format(from_user.id)) + data = cursor.fetchall() + retVal = [] + for transact in data: + if date <= transact['jobdate']: + retVal.append({"from_user": from_user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getAllTransactJobToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where to_user_id={}".format(to_user.id)) + data = cursor.fetchall() + retVal = [] + for transact in data: + if date <= transact['jobdate']: + retVal.append({"to_user": to_user, "from_user": self.getUserById(transact['from_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getTransactJobToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where to_user_id={} and jobdate='{}'".format(to_user.id, date)) + data = cursor.fetchone() + if data: + return {"from_user": self.getUserById(data['from_user_id']), "to_user": to_user, "date": data['jobdate'], "accepted": data['accepted'], "answerd": data['answerd']} + else: + return None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def updateTransactJob(self, from_user, to_user, date, accepted): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_transact set accepted={}, answerd=true where to_user_id={} and jobdate='{}'".format(accepted, to_user.id, date)) + self.db.connection.commit() + return self.getTransactJob(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def getTransactJobFromUser(self, user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_transact where from_user_id={} and jobdate='{}'".format(user.id, date)) + data = cursor.fetchall() + return [{"from_user": user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']} for transact in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) + + def deleteTransactJob(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setTransactJob(self, from_user, to_user, date): + try: + exists = self.getTransactJob(from_user, to_user, date) + if exists: + raise IntegrityError("job_transact already exists") + cursor = self.db.connection.cursor() + cursor.execute("insert into job_transact (jobdate, from_user_id, to_user_id) VALUES ('{}', {}, {})".format(date, from_user.id, to_user.id)) + self.db.connection.commit() + return self.getTransactJob(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbPricelistController.py b/geruecht/controller/databaseController/dbPricelistController.py new file mode 100644 index 0000000..2b0c749 --- /dev/null +++ b/geruecht/controller/databaseController/dbPricelistController.py @@ -0,0 +1,128 @@ +import traceback + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getPriceList(self): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from pricelist") + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getDrinkPrice(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from pricelist where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from pricelist where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setDrinkPrice(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute( + "insert into pricelist (name, price, price_big, price_club, price_club_big, premium, premium_club, price_extern_club, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", + ( + drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], + drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'])) + self.db.connection.commit() + return self.getDrinkPrice(str(drink['name'])) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def updateDrinkPrice(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute("update pricelist set name=%s, price=%s, price_big=%s, price_club=%s, price_club_big=%s, premium=%s, premium_club=%s, price_extern_club=%s, type=%s where id=%s", + ( + drink['name'], drink['price'], drink['price_big'], drink['price_club'], drink['price_club_big'], drink['premium'], drink['premium_club'], drink['price_extern_club'], drink['type'], drink['id'] + )) + self.db.connection.commit() + return self.getDrinkPrice(drink['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def deleteDrink(self, drink): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from pricelist where id={}".format(drink['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def getDrinkType(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from drink_type where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from drink_type where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setDrinkType(self, name): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into drink_type (name) values ('{}')".format(name)) + self.db.connection.commit() + return self.getDrinkType(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def updateDrinkType(self, type): + try: + cursor = self.db.connection.cursor() + cursor.execute("update drink_type set name='{}' where id={}".format(type['name'], type['id'])) + self.db.connection.commit() + return self.getDrinkType(type['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) + + def deleteDrinkType(self, type): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from drink_type where id={}".format(type['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getAllDrinkTypes(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from drink_type') + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Database: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbUserController.py b/geruecht/controller/databaseController/dbUserController.py new file mode 100644 index 0000000..08f2d9d --- /dev/null +++ b/geruecht/controller/databaseController/dbUserController.py @@ -0,0 +1,197 @@ +from geruecht.exceptions import DatabaseExecption, UsernameExistDB +from geruecht.model.user import User +import traceback + +class Base: + def getAllUser(self, extern=False, workgroups=True): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user") + data = cursor.fetchall() + + if data: + retVal = [] + for value in data: + if extern and value['uid'] == 'extern': + continue + user = User(value) + creditLists = self.getCreditListFromUser(user) + user.initGeruechte(creditLists) + if workgroups: + user.workgroups = self.getWorkgroupsOfUser(user.id) + retVal.append(user) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getUser(self, username, workgroups=True): + try: + retVal = None + cursor = self.db.connection.cursor() + cursor.execute("select * from user where uid='{}'".format(username)) + data = cursor.fetchone() + if data: + retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) + if workgroups: + retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getUserById(self, id, workgroups=True): + try: + retVal = None + cursor = self.db.connection.cursor() + cursor.execute("select * from user where id={}".format(id)) + data = cursor.fetchone() + if data: + retVal = User(data) + creditLists = self.getCreditListFromUser(retVal) + retVal.initGeruechte(creditLists) + if workgroups: + retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def _convertGroupToString(self, groups): + retVal = '' + print('groups: {}'.format(groups)) + if groups: + for group in groups: + if len(retVal) != 0: + retVal += ',' + retVal += group + return retVal + + + def insertUser(self, user): + try: + cursor = self.db.connection.cursor() + groups = self._convertGroupToString(user.group) + cursor.execute("insert into user (uid, dn, firstname, lastname, gruppe, lockLimit, locked, autoLock, mail) VALUES ('{}','{}','{}','{}','{}',{},{},{},'{}')".format( + user.uid, user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + + def updateUser(self, user): + try: + cursor = self.db.connection.cursor() + print('uid: {}; group: {}'.format(user.uid, user.group)) + groups = self._convertGroupToString(user.group) + sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( + user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) + print(sql) + cursor.execute(sql) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def changeUsername(self, user, newUsername): + try: + cursor= self.db.connection.cursor() + cursor.execute("select * from user where uid='{}'".format(newUsername)) + data = cursor.fetchall() + if data: + raise UsernameExistDB("Username already exists") + else: + cursor.execute("update user set uid='{}' where id={}".format(newUsername, user.id)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getAllStatus(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from statusgroup') + return cursor.fetchall() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getStatus(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from statusgroup where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from statusgroup where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + return cursor.fetchone() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setStatus(self, name): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into statusgroup (name) values ('{}')".format(name)) + self.db.connection.commit() + return self.getStatus(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateStatus(self, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("update statusgroup set name='{}' where id={}".format(status['name'], status['id'])) + self.db.connection.commit() + return self.getStatus(status['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteStatus(self, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from statusgroup where id={}".format(status['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateStatusOfUser(self, username, status): + try: + cursor = self.db.connection.cursor() + cursor.execute("update user set statusgroup={} where uid='{}'".format(status['id'], username)) + self.db.connection.commit() + return self.getUser(username) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateVotingOfUser(self, username, voting): + try: + cursor = self.db.connection.cursor() + cursor.execute("update user set voting={} where uid='{}'".format(voting, username)) + self.db.connection.commit() + return self.getUser(username) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbWorkerController.py b/geruecht/controller/databaseController/dbWorkerController.py new file mode 100644 index 0000000..134625d --- /dev/null +++ b/geruecht/controller/databaseController/dbWorkerController.py @@ -0,0 +1,67 @@ +import traceback +from datetime import timedelta + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getWorker(self, user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + data = cursor.fetchone() + return {"user": user.toJSON(), "startdatetime": data['startdatetime'], "enddatetime": data['enddatetime'], "start": { "year": data['startdatetime'].year, "month": data['startdatetime'].month, "day": data['startdatetime'].day}, "job_kind": self.getJobKind(data['job_kind']) if data['job_kind'] != None else None} if data else None + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getWorkers(self, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) + data = cursor.fetchall() + retVal = [] + # for work in data: + # user = self.getUserById(work['user_id']).toJSON() + # startdatetime = work['startdatetime'] + # enddatetime = work['enddatetime'] + # start = { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day} + # job_kind = self.getJobKind(work['job_kind']) if work['job_kind'] != None else None + # retVal.append({'user': user, 'startdatetime': startdatetime, 'enddatetime': enddatetime, 'start': start, 'job_kind': job_kind}) + # return retVal + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}, "job_kind": self.getJobKind(work['job_kind']) if work['job_kind'] != None else None} for work in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def setWorker(self, user, date, job_kind=None): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into bardienste (user_id, startdatetime, enddatetime, job_kind) values ({},'{}','{}', {})".format(user.id, date, date + timedelta(days=1), job_kind['id'] if job_kind != None else 'NULL')) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def deleteAllWorkerWithJobKind(self, date, job_kind): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from bardienste where startdatetime='{}' and job_kind={}".format(date, job_kind['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def deleteWorker(self, user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from bardienste where user_id={} and startdatetime='{}'".format(user.id, date)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbWorkgroupController.py b/geruecht/controller/databaseController/dbWorkgroupController.py new file mode 100644 index 0000000..c67f5d2 --- /dev/null +++ b/geruecht/controller/databaseController/dbWorkgroupController.py @@ -0,0 +1,126 @@ +import traceback + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getAllWorkgroups(self): + try: + cursor = self.db.connection.cursor() + cursor.execute('select * from workgroup') + list = cursor.fetchall() + for item in list: + if item['boss'] != None: + item['boss']=self.getUserById(item['boss'], workgroups=False).toJSON() + return list + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getWorkgroup(self, name): + try: + cursor = self.db.connection.cursor() + if type(name) == str: + sql = "select * from workgroup where name='{}'".format(name) + elif type(name) == int: + sql = 'select * from workgroup where id={}'.format(name) + else: + raise DatabaseExecption("name as no type int or str. name={}, type={}".format(name, type(name))) + cursor.execute(sql) + retVal = cursor.fetchone() + retVal['boss'] = self.getUserById(retVal['boss'], workgroups=False).toJSON() if retVal['boss'] != None else None + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def setWorkgroup(self, name, boss): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into workgroup (name, boss) values ('{}', {})".format(name, boss['id'])) + self.db.connection.commit() + return self.getWorkgroup(name) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def updateWorkgroup(self, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("update workgroup set name='{}', boss={} where id={}".format(workgroup['name'], workgroup['boss']['id'], workgroup['id'])) + self.db.connection.commit() + return self.getWorkgroup(workgroup['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def deleteWorkgroup(self, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from workgroup where id={}".format(workgroup['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getWorkgroupsOfUser(self, userid): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where user_id={} ".format(userid)) + knots = cursor.fetchall() + retVal = [self.getWorkgroup(knot['workgroup_id']) for knot in knots] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getUsersOfWorkgroups(self, workgroupid): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where workgroup_id={}".format(workgroupid)) + knots = cursor.fetchall() + retVal = [self.getUserById(knot['user_id'], workgroups=False).toJSON() for knot in knots] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def getUserWorkgroup(self, user, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from user_workgroup where workgroup_id={} and user_id={}".format(workgroup['id'], user['id'])) + knot = cursor.fetchone() + retVal = {"workgroup": self.getWorkgroup(workgroup['id']), "user": self.getUserById(user['id'], workgroups=False).toJSON()} + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def setUserWorkgroup(self, user, workgroup): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into user_workgroup (user_id, workgroup_id) VALUES ({}, {})".format(user['id'], workgroup['id'])) + self.db.connection.commit() + return self.getUserWorkgroup(user, workgroup) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) + + def deleteWorkgroupsOfUser(self, user): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from user_workgroup where user_id={}".format(user['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went wrong with Database: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index e7f269a..190ac07 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -4,23 +4,22 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header from geruecht.logger import getDebugLogger +from . import mailConfig debug = getDebugLogger() class EmailController(): - def __init__(self, smtpServer, user, passwd, crypt, port=587, email=""): + def __init__(self): debug.info("init email controller") - self.smtpServer = smtpServer - self.port = port - self.user = user - self.passwd = passwd - self.crypt = crypt - if email: - self.email = email - else: - self.email = user - debug.debug("smtpServer is {{ {} }}, port is {{ {} }}, user is {{ {} }}, crypt is {{ {} }}, email is {{ {} }}".format(smtpServer, port, user, crypt, self.email)) + self.smtpServer = mailConfig['URL'] + self.port = mailConfig['port'] + self.user = mailConfig['user'] + self.passwd = mailConfig['passwd'] + self.crypt = mailConfig['crypt'] + self.email = mailConfig['email'] + + debug.debug("smtpServer is {{ {} }}, port is {{ {} }}, user is {{ {} }}, crypt is {{ {} }}, email is {{ {} }}".format(self.smtpServer, self.port, self.user, self.crypt, self.email)) def __connect__(self): debug.info('connect to email server') diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 0692f1c..ca204b2 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -71,6 +71,8 @@ class LDAPController(metaclass=Singleton): main_group_number = self.ldap.connection.response[0]['attributes']['gidNumber'] debug.debug("main group number is {{ {} }}".format(main_group_number)) if main_group_number: + if type(main_group_number) is list: + main_group_number = main_group_number[0] self.ldap.connection.search('ou=group,{}'.format(self.dn), '(gidNumber={})'.format(main_group_number), attributes=['cn']) group_name = self.ldap.connection.response[0]['attributes']['cn'][0] debug.debug("group name is {{ {} }}".format(group_name)) diff --git a/geruecht/controller/mainController/__init__.py b/geruecht/controller/mainController/__init__.py new file mode 100644 index 0000000..a2c9013 --- /dev/null +++ b/geruecht/controller/mainController/__init__.py @@ -0,0 +1,141 @@ +from .. import Singleton, mailConfig +import geruecht.controller.databaseController as dc +import geruecht.controller.ldapController as lc +import geruecht.controller.emailController as ec +from geruecht.model.user import User +from datetime import datetime, timedelta +from geruecht.logger import getDebugLogger +from ..mainController import mainJobKindController, mainCreditListController, mainJobTransactController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController + +db = dc.DatabaseController() +ldap = lc.LDAPController() +emailController = ec.EmailController() + +debug = getDebugLogger() + + +class MainController(mainJobKindController.Base, + mainCreditListController.Base, + mainJobTransactController.Base, + mainPricelistController.Base, + mainUserController.Base, + mainWorkerController.Base, + mainWorkgroupController.Base, + metaclass=Singleton): + + def __init__(self): + debug.debug("init UserController") + pass + + def setLockedDay(self, date, locked, hard=False): + debug.info( + "set day locked on {{ {} }} with state {{ {} }}".format(date, locked)) + retVal = db.setLockedDay(date.date(), locked, hard) + debug.debug("seted day locked is {{ {} }}".format(retVal)) + return retVal + + def getLockedDays(self, from_date, to_date): + debug.info("get locked days from {{ {} }} to {{ {} }}".format( + from_date.date(), to_date.date())) + oneDay = timedelta(1) + delta = to_date.date() - from_date.date() + retVal = [] + startdate = from_date - oneDay + for _ in range(delta.days + 1): + startdate += oneDay + lockday = self.getLockedDay(startdate) + retVal.append(lockday) + debug.debug("lock days are {{ {} }}".format(retVal)) + return retVal + + def getLockedDay(self, date): + debug.info("get locked day on {{ {} }}".format(date)) + now = datetime.now() + debug.debug("now is {{ {} }}".format(now)) + oldMonth = False + debug.debug("check if date old month or current month") + for i in range(1, 8): + if datetime(now.year, now.month, i).weekday() == 2: + if now.day < i: + oldMonth = True + break + debug.debug("oldMonth is {{ {} }}".format(oldMonth)) + lockedYear = now.year + lockedMonth = now.month if now.month < now.month else now.month - \ + 1 if oldMonth else now.month + endDay = 1 + debug.debug("calculate end day of month") + lockedYear = lockedYear if lockedMonth != 12 else (lockedYear + 1) + lockedMonth = (lockedMonth + 1) if lockedMonth != 12 else 1 + for i in range(1, 8): + nextMonth = datetime(lockedYear, lockedMonth, i) + if nextMonth.weekday() == 2: + endDay = i + break + + monthLockedEndDate = datetime( + lockedYear, lockedMonth, endDay) - timedelta(1) + debug.debug("get lock day from database") + retVal = db.getLockedDay(date.date()) + if not retVal: + debug.debug( + "lock day not exists, retVal is {{ {} }}".format(retVal)) + if date.date() <= monthLockedEndDate.date(): + debug.debug("lock day {{ {} }}".format(date.date())) + self.setLockedDay(date, True) + retVal = db.getLockedDay(date.date()) + else: + retVal = {"daydate": date.date(), "locked": False} + debug.debug("locked day is {{ {} }}".format(retVal)) + return retVal + + def __updateDataFromLDAP(self, user): + debug.info("update data from ldap for user {{ {} }}".format(user)) + groups = ldap.getGroup(user.uid) + debug.debug("ldap gorups are {{ {} }}".format(groups)) + user_data = ldap.getUserData(user.uid) + debug.debug("ldap data is {{ {} }}".format(user_data)) + user_data['gruppe'] = groups + user_data['group'] = groups + user.updateData(user_data) + db.updateUser(user) + + def checkBarUser(self, user): + debug.info("check if user {{ {} }} is baruser") + date = datetime.now() + zero = date.replace(hour=0, minute=0, second=0, microsecond=0) + end = zero + timedelta(hours=12) + startdatetime = date.replace( + hour=12, minute=0, second=0, microsecond=0) + if date > zero and end > date: + startdatetime = startdatetime - timedelta(days=1) + enddatetime = startdatetime + timedelta(days=1) + debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format( + startdatetime, end)) + result = False + if date >= startdatetime and date < enddatetime: + result = db.getWorker(user, startdatetime) + debug.debug("worker is {{ {} }}".format(result)) + return True if result else False + + def sendMail(self, username): + debug.info("send mail to user {{ {} }}".format(username)) + if type(username) == User: + user = username + if type(username) == str: + user = db.getUser(username) + retVal = emailController.sendMail(user) + debug.debug("send mail is {{ {} }}".format(retVal)) + return retVal + + def sendAllMail(self): + debug.info("send mail to all user") + retVal = [] + users = db.getAllUser() + debug.debug("users are {{ {} }}".format(users)) + for user in users: + retVal.append(self.sendMail(user)) + debug.debug("send mails are {{ {} }}".format(retVal)) + return retVal + + diff --git a/geruecht/controller/mainController/mainCreditListController.py b/geruecht/controller/mainController/mainCreditListController.py new file mode 100644 index 0000000..f6e24c2 --- /dev/null +++ b/geruecht/controller/mainController/mainCreditListController.py @@ -0,0 +1,83 @@ +from datetime import datetime + +import geruecht.controller.databaseController as dc +import geruecht.controller.emailController as ec +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +emailController = ec.EmailController() +debug = getDebugLogger() + +class Base: + def autoLock(self, user): + debug.info("start autolock of user {{ {} }}".format(user)) + if user.autoLock: + debug.debug("autolock is active") + credit = user.getGeruecht(year=datetime.now().year).getSchulden() + limit = -1*user.limit + if credit <= limit: + debug.debug( + "credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit)) + debug.debug("lock user") + user.updateData({'locked': True}) + debug.debug("send mail to user") + emailController.sendMail(user) + else: + debug.debug( + "cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit)) + debug.debug("unlock user") + user.updateData({'locked': False}) + db.updateUser(user) + + def addAmount(self, username, amount, year, month, finanzer=False): + debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format( + amount, username, month, year)) + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + if user.uid == 'extern': + debug.debug("user is extern user, so exit add amount") + return + if not user.locked or finanzer: + debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format( + user.locked, finanzer)) + user.addAmount(amount, year=year, month=month) + creditLists = user.updateGeruecht() + debug.debug("creditList is {{ {} }}".format(creditLists)) + for creditList in creditLists: + debug.debug("update creditlist {{ {} }}".format(creditList)) + db.updateCreditList(creditList) + debug.debug("do autolock") + self.autoLock(user) + retVal = user.getGeruecht(year) + debug.debug("updated creditlists is {{ {} }}".format(retVal)) + return retVal + + def addCredit(self, username, credit, year, month): + debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format( + credit, username, month, year)) + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + if user.uid == 'extern': + debug.debug("user is extern user, so exit add credit") + return + user.addCredit(credit, year=year, month=month) + creditLists = user.updateGeruecht() + debug.debug("creditlists are {{ {} }}".format(creditLists)) + for creditList in creditLists: + debug.debug("update creditlist {{ {} }}".format(creditList)) + db.updateCreditList(creditList) + debug.debug("do autolock") + self.autoLock(user) + retVal = user.getGeruecht(year) + debug.debug("updated creditlists are {{ {} }}".format(retVal)) + return retVal + + def __updateGeruechte(self, user): + debug.debug("update creditlists") + user.getGeruecht(datetime.now().year) + creditLists = user.updateGeruecht() + debug.debug("creditlists are {{ {} }}".format(creditLists)) + if user.getGeruecht(datetime.now().year).getSchulden() != 0: + for creditList in creditLists: + debug.debug("update creditlist {{ {} }}".format(creditList)) + db.updateCreditList(creditList) \ No newline at end of file diff --git a/geruecht/controller/mainController/mainJobKindController.py b/geruecht/controller/mainController/mainJobKindController.py new file mode 100644 index 0000000..cc23f6c --- /dev/null +++ b/geruecht/controller/mainController/mainJobKindController.py @@ -0,0 +1,89 @@ +from datetime import date, timedelta, datetime, time +import geruecht.controller.databaseController as dc +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def getAllJobKinds(self): + debug.info("get all jobkinds") + retVal = db.getAllJobKinds() + debug.debug("jobkinds are {{ {} }}".format(retVal)) + return retVal + + def getJobKind(self, name): + debug.info("get jobkinds {{ {} }}".format(name)) + retVal = db.getJobKind(name) + debug.debug("jobkind is {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def setJobKind(self, name, workgroup=None): + debug.info("set jobkind {{ {} }} ".format(name)) + retVal = db.setJobKind(name, workgroup) + debug.debug( + "seted jobkind {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def deleteJobKind(self, jobkind): + debug.info("delete jobkind {{ {} }}".format(jobkind)) + db.deleteJobKind(jobkind) + + def updateJobKind(self, jobkind): + debug.info("update workgroup {{ {} }}".format(jobkind)) + retVal = db.updateJobKind(jobkind) + debug.debug("updated jobkind is {{ {} }}".format(retVal)) + return retVal + + def getJobKindDates(self, date): + debug.info("get jobkinddates on {{ {} }}".format(date)) + retVal = db.getJobKindDates(date) + debug.debug("jobkinddates are {{ {} }}".format(retVal)) + return retVal + + def updateJobKindDates(self, jobkindDate): + debug.info("update jobkinddate {{ {} }}".format(jobkindDate)) + retVal = db.updateJobKindDates(jobkindDate) + debug.debug("updated jobkind is {{ {} }}".format(retVal)) + return retVal + + def deleteJobKindDates(self, jobkinddates): + debug.info("delete jobkinddates {{ {} }}".format(jobkinddates)) + db.deleteJobKindDates(jobkinddates) + + def setJobKindDates(self, datum, jobkind, maxpersons): + debug.info("set jobkinddates with {{ {}, {}, {}, }}".format(datum, jobkind, maxpersons)) + retVal = db.setJobKindDates(datum, jobkind, maxpersons) + debug.debug("seted jobkinddates is {{ {} }}".format(retVal)) + return retVal + + def controllJobKindDates(self, jobkinddates): + debug.info("controll jobkinddates {{ {} }}".format(jobkinddates)) + datum = None + for jobkinddate in jobkinddates: + datum = date(jobkinddate['daydate']['year'], jobkinddate['daydate']['month'], jobkinddate['daydate']['day']) + if jobkinddate['id'] == -1: + self.setJobKindDates(datum, jobkinddate['job_kind'], jobkinddate['maxpersons']) + if jobkinddate['id'] == 0: + jobkinddate['id'] = jobkinddate['backupid'] + db.deleteAllWorkerWithJobKind(datetime.combine(datum, time(12)), jobkinddate['job_kind']) + self.deleteJobKindDates(jobkinddate) + if jobkinddate['id'] >= 1: + self.updateJobKindDates(jobkinddate) + retVal = self.getJobKindDates(datum) if datum != None else [] + debug.debug("controlled jobkinddates {{ {} }}".format(retVal)) + return retVal + + def getJobKindDatesFromTo(self, from_date, to_date): + debug.info("get locked days from {{ {} }} to {{ {} }}".format( + from_date.date(), to_date.date())) + oneDay = timedelta(1) + delta = to_date.date() - from_date.date() + retVal = [] + startdate = from_date - oneDay + for _ in range(delta.days + 1): + startdate += oneDay + jobkinddate = self.getJobKindDates(startdate) + retVal.append(jobkinddate) + debug.debug("lock days are {{ {} }}".format(retVal)) + return retVal \ No newline at end of file diff --git a/geruecht/controller/mainController/mainJobTransactController.py b/geruecht/controller/mainController/mainJobTransactController.py new file mode 100644 index 0000000..87e085f --- /dev/null +++ b/geruecht/controller/mainController/mainJobTransactController.py @@ -0,0 +1,71 @@ +import geruecht.controller.databaseController as dc +import geruecht.controller.emailController as ec +from geruecht.exceptions import TansactJobIsAnswerdException +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +emailController = ec.EmailController() +debug = getDebugLogger() + +class Base: + def setTransactJob(self, from_user, to_user, date): + debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) + jobtransact = db.setTransactJob(from_user, to_user, date.date()) + debug.debug("transact job is {{ {} }}".format(jobtransact)) + debug.info("send mail with transact job to user") + emailController.sendMail( + jobtransact['to_user'], 'jobtransact', jobtransact) + return jobtransact + + def getTransactJobFromUser(self, user, date): + debug.info( + "get transact job from user {{ {} }} on {{ {} }}".format(user, date)) + retVal = db.getTransactJobFromUser(user, date.date()) + debug.debug( + "transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) + return retVal + + def getAllTransactJobFromUser(self, user, date): + debug.info( + "get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) + retVal = db.getAllTransactJobFromUser(user, date.date()) + debug.debug("all transact job are {{ {} }}".format(retVal)) + return retVal + + def getAllTransactJobToUser(self, user, date): + debug.info( + "get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) + retVal = db.getAllTransactJobToUser(user, date.date()) + debug.debug("all transact job are {{ {} }}".format(retVal)) + return retVal + + def getTransactJob(self, from_user, to_user, date): + debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) + retVal = db.getTransactJob(from_user, to_user, date.date()) + debug.debug("transact job is {{ {} }}".format(retVal)) + return retVal + + def deleteTransactJob(self, from_user, to_user, date): + debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( + from_user, to_user, date)) + transactJob = self.getTransactJob(from_user, to_user, date) + debug.debug("transact job is {{ {} }}".format(transactJob)) + if transactJob['answerd']: + debug.warning( + "transactjob {{ {} }} can not delete because is answerd") + raise TansactJobIsAnswerdException( + "TransactJob is already answerd") + db.deleteTransactJob(from_user, to_user, date.date()) + + def answerdTransactJob(self, from_user, to_user, date, answer): + debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format( + from_user, to_user, date, answer)) + transactJob = db.updateTransactJob( + from_user, to_user, date.date(), answer) + debug.debug("transactjob is {{ {} }}".format(transactJob)) + if answer: + debug.info("add worker on date {{ {} }}".format(date)) + self.addWorker(to_user.uid, date) + return transactJob \ No newline at end of file diff --git a/geruecht/controller/mainController/mainPricelistController.py b/geruecht/controller/mainController/mainPricelistController.py new file mode 100644 index 0000000..06cec00 --- /dev/null +++ b/geruecht/controller/mainController/mainPricelistController.py @@ -0,0 +1,50 @@ +import geruecht.controller.databaseController as dc +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def deleteDrinkType(self, type): + debug.info("delete drink type {{ {} }}".format(type)) + db.deleteDrinkType(type) + + def updateDrinkType(self, type): + debug.info("update drink type {{ {} }}".format(type)) + retVal = db.updateDrinkType(type) + debug.debug("updated drink type is {{ {} }}".format(retVal)) + return retVal + + def setDrinkType(self, type): + debug.info("set drink type {{ {} }}".format(type)) + retVal = db.setDrinkType(type) + debug.debug("seted drink type is {{ {} }}".format(retVal)) + return retVal + + def deletDrinkPrice(self, drink): + debug.info("delete drink {{ {} }}".format(drink)) + db.deleteDrink(drink) + + def setDrinkPrice(self, drink): + debug.info("set drink {{ {} }}".format(drink)) + retVal = db.setDrinkPrice(drink) + debug.debug("seted drink is {{ {} }}".format(retVal)) + return retVal + + def updateDrinkPrice(self, drink): + debug.info("update drink {{ {} }}".format(drink)) + retVal = db.updateDrinkPrice(drink) + debug.debug("updated drink is {{ {} }}".format(retVal)) + return retVal + + def getAllDrinkTypes(self): + debug.info("get all drink types") + retVal = db.getAllDrinkTypes() + debug.debug("all drink types are {{ {} }}".format(retVal)) + return retVal + + def getPricelist(self): + debug.info("get all drinks") + list = db.getPriceList() + debug.debug("all drinks are {{ {} }}".format(list)) + return list \ No newline at end of file diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py new file mode 100644 index 0000000..c0c70aa --- /dev/null +++ b/geruecht/controller/mainController/mainUserController.py @@ -0,0 +1,160 @@ +from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, PermissionDenied +import geruecht.controller.databaseController as dc +import geruecht.controller.ldapController as lc +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +ldap = lc.LDAPController() +debug = getDebugLogger() + +class Base: + def getAllStatus(self): + debug.info("get all status for user") + retVal = db.getAllStatus() + debug.debug("status are {{ {} }}".format(retVal)) + return retVal + + def getStatus(self, name): + debug.info("get status of user {{ {} }}".format(name)) + retVal = db.getStatus(name) + debug.debug("status of user {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def setStatus(self, name): + debug.info("set status of user {{ {} }}".format(name)) + retVal = db.setStatus(name) + debug.debug( + "settet status of user {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def deleteStatus(self, status): + debug.info("delete status {{ {} }}".format(status)) + db.deleteStatus(status) + + def updateStatus(self, status): + debug.info("update status {{ {} }}".format(status)) + retVal = db.updateStatus(status) + debug.debug("updated status is {{ {} }}".format(retVal)) + return retVal + + def updateStatusOfUser(self, username, status): + debug.info("update status {{ {} }} of user {{ {} }}".format( + status, username)) + retVal = db.updateStatusOfUser(username, status) + debug.debug( + "updatet status of user {{ {} }} is {{ {} }}".format(username, retVal)) + return retVal + + def updateVotingOfUser(self, username, voting): + debug.info("update voting {{ {} }} of user {{ {} }}".format( + voting, username)) + retVal = db.updateVotingOfUser(username, voting) + debug.debug( + "updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal)) + return retVal + + def lockUser(self, username, locked): + debug.info("lock user {{ {} }} for credit with status {{ {} }}".format( + username, locked)) + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + user.updateData({'locked': locked}) + db.updateUser(user) + retVal = self.getUser(username) + debug.debug("locked user is {{ {} }}".format(retVal)) + return retVal + + def updateConfig(self, username, data): + debug.info( + "update config of user {{ {} }} with config {{ {} }}".format(username, data)) + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + user.updateData(data) + db.updateUser(user) + retVal = self.getUser(username) + debug.debug("updated config of user is {{ {} }}".format(retVal)) + return retVal + + def getAllUsersfromDB(self, extern=True): + debug.info("get all users from database") + users = db.getAllUser() + debug.debug("users are {{ {} }}".format(users)) + for user in users: + try: + debug.debug("update data from ldap") + self.__updateDataFromLDAP(user) + except: + pass + debug.debug("update creditlists") + self.__updateGeruechte(user) + retVal = db.getAllUser(extern=extern) + debug.debug("all users are {{ {} }}".format(retVal)) + return retVal + + def getUser(self, username): + debug.info("get user {{ {} }}".format(username)) + user = db.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + groups = ldap.getGroup(username) + debug.debug("groups are {{ {} }}".format(groups)) + user_data = ldap.getUserData(username) + debug.debug("user data from ldap is {{ {} }}".format(user_data)) + user_data['gruppe'] = groups + user_data['group'] = groups + if user is None: + debug.debug("user not exists in database -> insert into database") + user = User(user_data) + db.insertUser(user) + else: + debug.debug("update database with user") + user.updateData(user_data) + db.updateUser(user) + user = db.getUser(username) + self.__updateGeruechte(user) + debug.debug("user is {{ {} }}".format(user)) + return user + + def modifyUser(self, user, ldap_conn, attributes): + debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format( + user, attributes, ldap_conn)) + try: + if 'username' in attributes: + debug.debug("change username, so change first in database") + db.changeUsername(user, attributes['username']) + ldap.modifyUser(user, ldap_conn, attributes) + if 'username' in attributes: + retVal = self.getUser(attributes['username']) + debug.debug("user is {{ {} }}".format(retVal)) + return retVal + else: + retVal = self.getUser(user.uid) + debug.debug("user is {{ {} }}".format(retVal)) + return retVal + except UsernameExistLDAP as err: + debug.debug( + "username exists on ldap, rechange username on database", exc_info=True) + db.changeUsername(user, user.uid) + raise Exception(err) + except LDAPExcetpion as err: + if 'username' in attributes: + db.changeUsername(user, user.uid) + raise Exception(err) + except Exception as err: + raise Exception(err) + + def validateUser(self, username, password): + debug.info("validate user {{ {} }}".format(username)) + ldap.login(username, password) + + def loginUser(self, username, password): + debug.info("login user {{ {} }}".format(username)) + try: + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + user.password = password + ldap.login(username, password) + ldap_conn = ldap.bind(user, password) + return user, ldap_conn + except PermissionDenied as err: + debug.debug("permission is denied", exc_info=True) + raise err \ No newline at end of file diff --git a/geruecht/controller/mainController/mainWorkerController.py b/geruecht/controller/mainController/mainWorkerController.py new file mode 100644 index 0000000..cbce839 --- /dev/null +++ b/geruecht/controller/mainController/mainWorkerController.py @@ -0,0 +1,65 @@ +import geruecht.controller.databaseController as dc +from geruecht.exceptions import DayLocked +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def getWorker(self, date, username=None): + debug.info("get worker {{ {} }} on {{ {} }}".format(username, date)) + if (username): + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + retVal = [db.getWorker(user, date)] + debug.debug("worker is {{ {} }}".format(retVal)) + return retVal + retVal = db.getWorkers(date) + debug.debug("workers are {{ {} }}".format(retVal)) + return retVal + + def addWorker(self, username, date, job_kind=None, userExc=False): + debug.info("add job user {{ {} }} on {{ {} }} with job_kind {{ {} }}".format(username, date, job_kind)) + if (userExc): + debug.debug("this is a user execution, check if day is locked") + lockedDay = self.getLockedDay(date) + if lockedDay: + if lockedDay['locked']: + debug.debug("day is lockey. user cant get job") + raise DayLocked("Day is locked. You can't get the Job") + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + debug.debug("check if user has job on date") + if (not db.getWorker(user, date)): + debug.debug("set job to user") + db.setWorker(user, date, job_kind=job_kind) + retVal = self.getWorker(date, username=username) + debug.debug("worker on date is {{ {} }}".format(retVal)) + return retVal + + def deleteWorker(self, username, date, userExc=False): + debug.info( + "delete worker {{ {} }} on date {{ {} }}".format(username, date)) + user = self.getUser(username) + debug.debug("user is {{ {} }}".format(user)) + if userExc: + debug.debug("is user execution, check if day locked") + lockedDay = self.getLockedDay(date) + if lockedDay: + if lockedDay['locked']: + debug.debug( + "day is locked, check if accepted transact job exists") + transactJobs = self.getTransactJobFromUser(user, date) + debug.debug( + "transact job is {{ {} }}".format(transactJobs)) + found = False + for job in transactJobs: + if job['accepted'] and job['answerd']: + debug.debug("accepted transact job exists") + found = True + break + if not found: + debug.debug("no accepted transact job found") + raise DayLocked( + "Day is locked. You can't delete the Job") + db.deleteWorker(user, date) \ No newline at end of file diff --git a/geruecht/controller/mainController/mainWorkgroupController.py b/geruecht/controller/mainController/mainWorkgroupController.py new file mode 100644 index 0000000..c32132e --- /dev/null +++ b/geruecht/controller/mainController/mainWorkgroupController.py @@ -0,0 +1,42 @@ +import geruecht.controller.databaseController as dc +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def updateWorkgroupsOfUser(self, user, workgroups): + debug.info("update workgroups {{ {} }} of user {{ {} }}".format(workgroups, user)) + db.deleteWorkgroupsOfUser(user) + for workgroup in workgroups: + db.setUserWorkgroup(user, workgroup) + return db.getWorkgroupsOfUser(user['id']) + + def getAllWorkgroups(self): + debug.info("get all workgroups") + retVal = db.getAllWorkgroups() + debug.debug("workgroups are {{ {} }}".format(retVal)) + return retVal + + def getWorkgroups(self, name): + debug.info("get Workgroup {{ {} }}".format(name)) + retVal = db.getWorkgroup(name) + debug.debug("workgroup is {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def setWorkgroup(self, name, boss): + debug.info("set workgroup {{ {} }} with boss {{ {} }}".format(name, boss)) + retVal = db.setWorkgroup(name, boss) + debug.debug( + "seted workgroup {{ {} }} is {{ {} }}".format(name, retVal)) + return retVal + + def deleteWorkgroup(self, workgroup): + debug.info("delete workgroup {{ {} }}".format(workgroup)) + db.deleteWorkgroup(workgroup) + + def updateWorkgroup(self, workgroup): + debug.info("update workgroup {{ {} }}".format(workgroup)) + retVal = db.updateWorkgroup(workgroup) + debug.debug("updated workgroup is {{ {} }}".format(retVal)) + return retVal \ No newline at end of file diff --git a/geruecht/controller/userController.py b/geruecht/controller/userController.py deleted file mode 100644 index 3e6f605..0000000 --- a/geruecht/controller/userController.py +++ /dev/null @@ -1,588 +0,0 @@ -from . import Singleton, mailConfig -import geruecht.controller.databaseController as dc -import geruecht.controller.ldapController as lc -import geruecht.controller.emailController as ec -import calendar -from geruecht.model.user import User -from geruecht.exceptions import PermissionDenied -from datetime import datetime, timedelta -from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, DayLocked, TansactJobIsAnswerdException -from geruecht.logger import getDebugLogger - -db = dc.DatabaseController() -ldap = lc.LDAPController() -emailController = ec.EmailController( - mailConfig['URL'], mailConfig['user'], mailConfig['passwd'], mailConfig['crypt'], mailConfig['port'], mailConfig['email']) - -debug = getDebugLogger() - - -class UserController(metaclass=Singleton): - - def __init__(self): - debug.debug("init UserController") - pass - - def getAllJobKinds(self): - debug.info("get all jobkinds") - retVal = db.getAllJobKinds() - debug.debug("jobkinds are {{ {} }}".format(retVal)) - return retVal - - def getJobKind(self, name): - debug.info("get jobkinds {{ {} }}".format(name)) - retVal = db.getJobKind(name) - debug.debug("jobkind is {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def setJobKind(self, name, workgroup=None): - debug.info("set jobkind {{ {} }} ".format(name)) - retVal = db.setJobKind(name, workgroup) - debug.debug( - "seted jobkind {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def deleteJobKind(self, jobkind): - debug.info("delete jobkind {{ {} }}".format(jobkind)) - db.deleteJobKind(jobkind) - - def updateJobKind(self, jobkind): - debug.info("update workgroup {{ {} }}".format(jobkind)) - retVal = db.updateJobKind(jobkind) - debug.debug("updated jobkind is {{ {} }}".format(retVal)) - return retVal - - def updateWorkgroupsOfUser(self, user, workgroups): - debug.info("update workgroups {{ {} }} of user {{ {} }}".format(workgroups, user)) - db.deleteWorkgroupsOfUser(user) - for workgroup in workgroups: - db.setUserWorkgroup(user, workgroup) - return db.getWorkgroupsOfUser(user['id']) - - def getAllWorkgroups(self): - debug.info("get all workgroups") - retVal = db.getAllWorkgroups() - debug.debug("workgroups are {{ {} }}".format(retVal)) - return retVal - - def getWorkgroups(self, name): - debug.info("get Workgroup {{ {} }}".format(name)) - retVal = db.getWorkgroup(name) - debug.debug("workgroup is {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def setWorkgroup(self, name, boss): - debug.info("set workgroup {{ {} }} with boss {{ {} }}".format(name, boss)) - retVal = db.setWorkgroup(name, boss) - debug.debug( - "seted workgroup {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def deleteWorkgroup(self, workgroup): - debug.info("delete workgroup {{ {} }}".format(workgroup)) - db.deleteWorkgroup(workgroup) - - def updateWorkgroup(self, workgroup): - debug.info("update workgroup {{ {} }}".format(workgroup)) - retVal = db.updateWorkgroup(workgroup) - debug.debug("updated workgroup is {{ {} }}".format(retVal)) - return retVal - - def getAllStatus(self): - debug.info("get all status for user") - retVal = db.getAllStatus() - debug.debug("status are {{ {} }}".format(retVal)) - return retVal - - def getStatus(self, name): - debug.info("get status of user {{ {} }}".format(name)) - retVal = db.getStatus(name) - debug.debug("status of user {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def setStatus(self, name): - debug.info("set status of user {{ {} }}".format(name)) - retVal = db.setStatus(name) - debug.debug( - "settet status of user {{ {} }} is {{ {} }}".format(name, retVal)) - return retVal - - def deleteStatus(self, status): - debug.info("delete status {{ {} }}".format(status)) - db.deleteStatus(status) - - def updateStatus(self, status): - debug.info("update status {{ {} }}".format(status)) - retVal = db.updateStatus(status) - debug.debug("updated status is {{ {} }}".format(retVal)) - return retVal - - def updateStatusOfUser(self, username, status): - debug.info("update status {{ {} }} of user {{ {} }}".format( - status, username)) - retVal = db.updateStatusOfUser(username, status) - debug.debug( - "updatet status of user {{ {} }} is {{ {} }}".format(username, retVal)) - return retVal - - def updateVotingOfUser(self, username, voting): - debug.info("update voting {{ {} }} of user {{ {} }}".format( - voting, username)) - retVal = db.updateVotingOfUser(username, voting) - debug.debug( - "updatet voting of user {{ {} }} is {{ {} }}".format(username, retVal)) - return retVal - - def deleteDrinkType(self, type): - debug.info("delete drink type {{ {} }}".format(type)) - db.deleteDrinkType(type) - - def updateDrinkType(self, type): - debug.info("update drink type {{ {} }}".format(type)) - retVal = db.updateDrinkType(type) - debug.debug("updated drink type is {{ {} }}".format(retVal)) - return retVal - - def setDrinkType(self, type): - debug.info("set drink type {{ {} }}".format(type)) - retVal = db.setDrinkType(type) - debug.debug("seted drink type is {{ {} }}".format(retVal)) - return retVal - - def deletDrinkPrice(self, drink): - debug.info("delete drink {{ {} }}".format(drink)) - db.deleteDrink(drink) - - def setDrinkPrice(self, drink): - debug.info("set drink {{ {} }}".format(drink)) - retVal = db.setDrinkPrice(drink) - debug.debug("seted drink is {{ {} }}".format(retVal)) - return retVal - - def updateDrinkPrice(self, drink): - debug.info("update drink {{ {} }}".format(drink)) - retVal = db.updateDrinkPrice(drink) - debug.debug("updated drink is {{ {} }}".format(retVal)) - return retVal - - def getAllDrinkTypes(self): - debug.info("get all drink types") - retVal = db.getAllDrinkTypes() - debug.debug("all drink types are {{ {} }}".format(retVal)) - return retVal - - def getPricelist(self): - debug.info("get all drinks") - list = db.getPriceList() - debug.debug("all drinks are {{ {} }}".format(list)) - return list - - def setTransactJob(self, from_user, to_user, date): - debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - jobtransact = db.setTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{ {} }}".format(jobtransact)) - debug.info("send mail with transact job to user") - emailController.sendMail( - jobtransact['to_user'], 'jobtransact', jobtransact) - return jobtransact - - def getTransactJobFromUser(self, user, date): - debug.info( - "get transact job from user {{ {} }} on {{ {} }}".format(user, date)) - retVal = db.getTransactJobFromUser(user, date.date()) - debug.debug( - "transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) - return retVal - - def getAllTransactJobFromUser(self, user, date): - debug.info( - "get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) - retVal = db.getAllTransactJobFromUser(user, date.date()) - debug.debug("all transact job are {{ {} }}".format(retVal)) - return retVal - - def getAllTransactJobToUser(self, user, date): - debug.info( - "get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) - retVal = db.getAllTransactJobToUser(user, date.date()) - debug.debug("all transact job are {{ {} }}".format(retVal)) - return retVal - - def getTransactJob(self, from_user, to_user, date): - debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - retVal = db.getTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{ {} }}".format(retVal)) - return retVal - - def deleteTransactJob(self, from_user, to_user, date): - debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - transactJob = self.getTransactJob(from_user, to_user, date) - debug.debug("transact job is {{ {} }}".format(transactJob)) - if transactJob['answerd']: - debug.warning( - "transactjob {{ {} }} can not delete because is answerd") - raise TansactJobIsAnswerdException( - "TransactJob is already answerd") - db.deleteTransactJob(from_user, to_user, date.date()) - - def answerdTransactJob(self, from_user, to_user, date, answer): - debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format( - from_user, to_user, date, answer)) - transactJob = db.updateTransactJob( - from_user, to_user, date.date(), answer) - debug.debug("transactjob is {{ {} }}".format(transactJob)) - if answer: - debug.info("add worker on date {{ {} }}".format(date)) - self.addWorker(to_user.uid, date) - return transactJob - - def setLockedDay(self, date, locked, hard=False): - debug.info( - "set day locked on {{ {} }} with state {{ {} }}".format(date, locked)) - retVal = db.setLockedDay(date.date(), locked, hard) - debug.debug("seted day locked is {{ {} }}".format(retVal)) - return retVal - - def getLockedDays(self, from_date, to_date): - debug.info("get locked days from {{ {} }} to {{ {} }}".format( - from_date.date(), to_date.date())) - oneDay = timedelta(1) - delta = to_date.date() - from_date.date() - retVal = [] - startdate = from_date - oneDay - for _ in range(delta.days + 1): - startdate += oneDay - lockday = self.getLockedDay(startdate) - retVal.append(lockday) - debug.debug("lock days are {{ {} }}".format(retVal)) - return retVal - - def getLockedDay(self, date): - debug.info("get locked day on {{ {} }}".format(date)) - now = datetime.now() - debug.debug("now is {{ {} }}".format(now)) - oldMonth = False - debug.debug("check if date old month or current month") - for i in range(1, 8): - if datetime(now.year, now.month, i).weekday() == 2: - if now.day < i: - oldMonth = True - break - debug.debug("oldMonth is {{ {} }}".format(oldMonth)) - lockedYear = now.year - lockedMonth = now.month if now.month < now.month else now.month - \ - 1 if oldMonth else now.month - endDay = 1 - debug.debug("calculate end day of month") - lockedYear = lockedYear if lockedMonth != 12 else (lockedYear + 1) - lockedMonth = (lockedMonth + 1) if lockedMonth != 12 else 1 - for i in range(1, 8): - nextMonth = datetime(lockedYear, lockedMonth, i) - if nextMonth.weekday() == 2: - endDay = i - break - - monthLockedEndDate = datetime( - lockedYear, lockedMonth, endDay) - timedelta(1) - debug.debug("get lock day from database") - retVal = db.getLockedDay(date.date()) - if not retVal: - debug.debug( - "lock day not exists, retVal is {{ {} }}".format(retVal)) - if date.date() <= monthLockedEndDate.date(): - debug.debug("lock day {{ {} }}".format(date.date())) - self.setLockedDay(date, True) - retVal = db.getLockedDay(date.date()) - else: - retVal = {"daydate": date.date(), "locked": False} - debug.debug("locked day is {{ {} }}".format(retVal)) - return retVal - - def getWorker(self, date, username=None): - debug.info("get worker {{ {} }} on {{ {} }}".format(username, date)) - if (username): - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - retVal = [db.getWorker(user, date)] - debug.debug("worker is {{ {} }}".format(retVal)) - return retVal - retVal = db.getWorkers(date) - debug.debug("workers are {{ {} }}".format(retVal)) - return retVal - - def addWorker(self, username, date, userExc=False): - debug.info("add job user {{ {} }} on {{ {} }}".format(username, date)) - if (userExc): - debug.debug("this is a user execution, check if day is locked") - lockedDay = self.getLockedDay(date) - if lockedDay: - if lockedDay['locked']: - debug.debug("day is lockey. user cant get job") - raise DayLocked("Day is locked. You can't get the Job") - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - debug.debug("check if user has job on date") - if (not db.getWorker(user, date)): - debug.debug("set job to user") - db.setWorker(user, date) - retVal = self.getWorker(date, username=username) - debug.debug("worker on date is {{ {} }}".format(retVal)) - return retVal - - def deleteWorker(self, username, date, userExc=False): - debug.info( - "delete worker {{ {} }} on date {{ {} }}".format(username, date)) - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - if userExc: - debug.debug("is user execution, check if day locked") - lockedDay = self.getLockedDay(date) - if lockedDay: - if lockedDay['locked']: - debug.debug( - "day is locked, check if accepted transact job exists") - transactJobs = self.getTransactJobFromUser(user, date) - debug.debug( - "transact job is {{ {} }}".format(transactJobs)) - found = False - for job in transactJobs: - if job['accepted'] and job['answerd']: - debug.debug("accepted transact job exists") - found = True - break - if not found: - debug.debug("no accepted transact job found") - raise DayLocked( - "Day is locked. You can't delete the Job") - db.deleteWorker(user, date) - - def lockUser(self, username, locked): - debug.info("lock user {{ {} }} for credit with status {{ {} }}".format( - username, locked)) - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - user.updateData({'locked': locked}) - db.updateUser(user) - retVal = self.getUser(username) - debug.debug("locked user is {{ {} }}".format(retVal)) - return retVal - - def updateConfig(self, username, data): - debug.info( - "update config of user {{ {} }} with config {{ {} }}".format(username, data)) - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - user.updateData(data) - db.updateUser(user) - retVal = self.getUser(username) - debug.debug("updated config of user is {{ {} }}".format(retVal)) - return retVal - - def __updateDataFromLDAP(self, user): - debug.info("update data from ldap for user {{ {} }}".format(user)) - groups = ldap.getGroup(user.uid) - debug.debug("ldap gorups are {{ {} }}".format(groups)) - user_data = ldap.getUserData(user.uid) - debug.debug("ldap data is {{ {} }}".format(user_data)) - user_data['gruppe'] = groups - user_data['group'] = groups - user.updateData(user_data) - db.updateUser(user) - - def autoLock(self, user): - debug.info("start autolock of user {{ {} }}".format(user)) - if user.autoLock: - debug.debug("autolock is active") - credit = user.getGeruecht(year=datetime.now().year).getSchulden() - limit = -1*user.limit - if credit <= limit: - debug.debug( - "credit {{ {} }} is more than user limit {{ {} }}".format(credit, limit)) - debug.debug("lock user") - user.updateData({'locked': True}) - debug.debug("send mail to user") - emailController.sendMail(user) - else: - debug.debug( - "cretid {{ {} }} is less than user limit {{ {} }}".format(credit, limit)) - debug.debug("unlock user") - user.updateData({'locked': False}) - db.updateUser(user) - - def addAmount(self, username, amount, year, month, finanzer=False): - debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format( - amount, username, month, year)) - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - if user.uid == 'extern': - debug.debug("user is extern user, so exit add amount") - return - if not user.locked or finanzer: - debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format( - user.locked, finanzer)) - user.addAmount(amount, year=year, month=month) - creditLists = user.updateGeruecht() - debug.debug("creditList is {{ {} }}".format(creditLists)) - for creditList in creditLists: - debug.debug("update creditlist {{ {} }}".format(creditList)) - db.updateCreditList(creditList) - debug.debug("do autolock") - self.autoLock(user) - retVal = user.getGeruecht(year) - debug.debug("updated creditlists is {{ {} }}".format(retVal)) - return retVal - - def addCredit(self, username, credit, year, month): - debug.info("add credit {{ {} }} to user {{ {} }} on month {{ {} }}, year {{ {} }}".format( - credit, username, month, year)) - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - if user.uid == 'extern': - debug.debug("user is extern user, so exit add credit") - return - user.addCredit(credit, year=year, month=month) - creditLists = user.updateGeruecht() - debug.debug("creditlists are {{ {} }}".format(creditLists)) - for creditList in creditLists: - debug.debug("update creditlist {{ {} }}".format(creditList)) - db.updateCreditList(creditList) - debug.debug("do autolock") - self.autoLock(user) - retVal = user.getGeruecht(year) - debug.debug("updated creditlists are {{ {} }}".format(retVal)) - return retVal - - def getAllUsersfromDB(self): - debug.info("get all users from database") - users = db.getAllUser() - debug.debug("users are {{ {} }}".format(users)) - for user in users: - try: - debug.debug("update data from ldap") - self.__updateDataFromLDAP(user) - except: - pass - debug.debug("update creditlists") - self.__updateGeruechte(user) - retVal = db.getAllUser(extern=True) - debug.debug("all users are {{ {} }}".format(retVal)) - return retVal - - def checkBarUser(self, user): - debug.info("check if user {{ {} }} is baruser") - date = datetime.now() - zero = date.replace(hour=0, minute=0, second=0, microsecond=0) - end = zero + timedelta(hours=12) - startdatetime = date.replace( - hour=12, minute=0, second=0, microsecond=0) - if date > zero and end > date: - startdatetime = startdatetime - timedelta(days=1) - enddatetime = startdatetime + timedelta(days=1) - debug.debug("startdatetime is {{ {} }} and enddatetime is {{ {} }}".format( - startdatetime, end)) - result = False - if date >= startdatetime and date < enddatetime: - result = db.getWorker(user, startdatetime) - debug.debug("worker is {{ {} }}".format(result)) - return True if result else False - - def getUser(self, username): - debug.info("get user {{ {} }}".format(username)) - user = db.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - groups = ldap.getGroup(username) - debug.debug("groups are {{ {} }}".format(groups)) - user_data = ldap.getUserData(username) - debug.debug("user data from ldap is {{ {} }}".format(user_data)) - user_data['gruppe'] = groups - user_data['group'] = groups - if user is None: - debug.debug("user not exists in database -> insert into database") - user = User(user_data) - db.insertUser(user) - else: - debug.debug("update database with user") - user.updateData(user_data) - db.updateUser(user) - user = db.getUser(username) - self.__updateGeruechte(user) - debug.debug("user is {{ {} }}".format(user)) - return user - - def __updateGeruechte(self, user): - debug.debug("update creditlists") - user.getGeruecht(datetime.now().year) - creditLists = user.updateGeruecht() - debug.debug("creditlists are {{ {} }}".format(creditLists)) - if user.getGeruecht(datetime.now().year).getSchulden() != 0: - for creditList in creditLists: - debug.debug("update creditlist {{ {} }}".format(creditList)) - db.updateCreditList(creditList) - - def sendMail(self, username): - debug.info("send mail to user {{ {} }}".format(username)) - if type(username) == User: - user = username - if type(username) == str: - user = db.getUser(username) - retVal = emailController.sendMail(user) - debug.debug("send mail is {{ {} }}".format(retVal)) - return retVal - - def sendAllMail(self): - debug.info("send mail to all user") - retVal = [] - users = db.getAllUser() - debug.debug("users are {{ {} }}".format(users)) - for user in users: - retVal.append(self.sendMail(user)) - debug.debug("send mails are {{ {} }}".format(retVal)) - return retVal - - def modifyUser(self, user, ldap_conn, attributes): - debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format( - user, attributes, ldap_conn)) - try: - if 'username' in attributes: - debug.debug("change username, so change first in database") - db.changeUsername(user, attributes['username']) - ldap.modifyUser(user, ldap_conn, attributes) - if 'username' in attributes: - retVal = self.getUser(attributes['username']) - debug.debug("user is {{ {} }}".format(retVal)) - return retVal - else: - retVal = self.getUser(user.uid) - debug.debug("user is {{ {} }}".format(retVal)) - return retVal - except UsernameExistLDAP as err: - debug.debug( - "username exists on ldap, rechange username on database", exc_info=True) - db.changeUsername(user, user.uid) - raise Exception(err) - except LDAPExcetpion as err: - if 'username' in attributes: - db.changeUsername(user, user.uid) - raise Exception(err) - except Exception as err: - raise Exception(err) - - def validateUser(self, username, password): - debug.info("validate user {{ {} }}".format(username)) - ldap.login(username, password) - - def loginUser(self, username, password): - debug.info("login user {{ {} }}".format(username)) - try: - user = self.getUser(username) - debug.debug("user is {{ {} }}".format(user)) - user.password = password - ldap.login(username, password) - ldap_conn = ldap.bind(user, password) - return user, ldap_conn - except PermissionDenied as err: - debug.debug("permission is denied", exc_info=True) - raise err diff --git a/geruecht/finanzer/routes.py b/geruecht/finanzer/routes.py index 96b72d1..8b2e2fa 100644 --- a/geruecht/finanzer/routes.py +++ b/geruecht/finanzer/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, jsonify from datetime import datetime -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from geruecht.model import MONEY from geruecht.decorator import login_required from geruecht.logger import getDebugLogger, getCreditLogger @@ -10,7 +10,7 @@ creditL = getCreditLogger() finanzer = Blueprint("finanzer", __name__) -userController = uc.UserController() +mainController = mc.MainController() @finanzer.route("/getFinanzerMain") @@ -26,7 +26,7 @@ def _getFinanzer(**kwargs): """ debug.info("/getFinanzerMain") try: - users = userController.getAllUsersfromDB() + users = mainController.getAllUsersfromDB() dic = {} for user in users: dic[user.uid] = user.toJSON() @@ -65,9 +65,9 @@ def _addAmount(**kwargs): month = int(data['month']) except KeyError: month = datetime.now().month - userController.addAmount( + mainController.addAmount( userID, amount, year=year, month=month, finanzer=True) - user = userController.getUser(userID) + user = mainController.getUser(userID) retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} retVal['locked'] = user.locked @@ -108,9 +108,9 @@ def _addCredit(**kwargs): except KeyError: month = datetime.now().month - userController.addCredit( + mainController.addCredit( userID, credit, year=year, month=month).toJSON() - user = userController.getUser(userID) + user = mainController.getUser(userID) retVal = {str(geruecht.year): geruecht.toJSON() for geruecht in user.geruechte} retVal['locked'] = user.locked @@ -131,7 +131,7 @@ def _finanzerLock(**kwargs): data = request.get_json() username = data['userId'] locked = bool(data['locked']) - retVal = userController.lockUser(username, locked).toJSON() + retVal = mainController.lockUser(username, locked).toJSON() debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -148,7 +148,7 @@ def _finanzerSetConfig(**kwargs): username = data['userId'] autoLock = bool(data['autoLock']) limit = int(data['limit']) - retVal = userController.updateConfig( + retVal = mainController.updateConfig( username, {'lockLimit': limit, 'autoLock': autoLock}).toJSON() debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) @@ -164,8 +164,8 @@ def _finanzerAddUser(**kwargs): try: data = request.get_json() username = data['userId'] - userController.getUser(username) - users = userController.getAllUsersfromDB() + mainController.getUser(username) + users = mainController.getAllUsersfromDB() dic = {} for user in users: dic[user.uid] = user.toJSON() @@ -185,7 +185,7 @@ def _finanzerSendOneMail(**kwargs): try: data = request.get_json() username = data['userId'] - retVal = userController.sendMail(username) + retVal = mainController.sendMail(username) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -198,7 +198,7 @@ def _finanzerSendOneMail(**kwargs): def _finanzerSendAllMail(**kwargs): debug.info("/finanzerSendAllMail") try: - retVal = userController.sendAllMail() + retVal = mainController.sendAllMail() debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: diff --git a/geruecht/gastro/routes.py b/geruecht/gastro/routes.py index 5ddb3c1..baca57a 100644 --- a/geruecht/gastro/routes.py +++ b/geruecht/gastro/routes.py @@ -1,6 +1,6 @@ from flask import request, jsonify, Blueprint from geruecht.decorator import login_required -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from geruecht.model import GASTRO from geruecht.logger import getCreditLogger, getDebugLogger @@ -8,7 +8,7 @@ debug = getDebugLogger() gastrouser = Blueprint('gastrouser', __name__) -userController = uc.UserController() +mainController = mc.MainController() @gastrouser.route('/gastro/setDrink', methods=['POST']) @@ -17,7 +17,7 @@ def setDrink(**kwargs): debug.info("/gastro/setDrink") try: data = request.get_json() - retVal = userController.setDrinkPrice(data) + retVal = mainController.setDrinkPrice(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -31,7 +31,7 @@ def updateDrink(**kwargs): debug.info("/gastro/updateDrink") try: data = request.get_json() - retVal = userController.updateDrinkPrice(data) + retVal = mainController.updateDrinkPrice(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -46,7 +46,7 @@ def deleteDrink(**kwargs): try: data = request.get_json() id = data['id'] - userController.deletDrinkPrice({"id": id}) + mainController.deletDrinkPrice({"id": id}) debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: @@ -61,7 +61,7 @@ def setType(**kwark): try: data = request.get_json() name = data['name'] - retVal = userController.setDrinkType(name) + retVal = mainController.setDrinkType(name) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -75,7 +75,7 @@ def updateType(**kwargs): debug.info("/gastro/updateDrinkType") try: data = request.get_json() - retVal = userController.updateDrinkType(data) + retVal = mainController.updateDrinkType(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -89,7 +89,7 @@ def deleteType(**kwargs): debug.info("/gastro/deleteDrinkType") try: data = request.get_json() - userController.deleteDrinkType(data) + mainController.deleteDrinkType(data) debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: diff --git a/geruecht/routes.py b/geruecht/routes.py index fec35b2..ff05912 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -3,12 +3,12 @@ from geruecht.logger import getDebugLogger from geruecht.decorator import login_required from geruecht.exceptions import PermissionDenied import geruecht.controller.accesTokenController as ac -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from geruecht.model import MONEY, BAR, USER, GASTRO, VORSTAND, EXTERN from flask import request, jsonify accesTokenController = ac.AccesTokenController() -userController = uc.UserController() +mainController = mc.MainController() debug = getDebugLogger() @@ -19,7 +19,7 @@ def _valid(**kwargs): try: accToken = kwargs['accToken'] data = request.get_json() - userController.validateUser(accToken.user.uid, data['password']) + mainController.validateUser(accToken.user.uid, data['password']) debug.debug('return {{ "ok": "ok" }}') return jsonify({"ok": "ok"}) except Exception as err: @@ -30,7 +30,7 @@ def _valid(**kwargs): def _getPricelist(): try: debug.info("get pricelist") - retVal = userController.getPricelist() + retVal = mainController.getPricelist() debug.info("return pricelist {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -42,7 +42,7 @@ def _getPricelist(): def getTypes(): try: debug.info("get drinktypes") - retVal = userController.getAllDrinkTypes() + retVal = mainController.getAllDrinkTypes() debug.info("return drinktypes {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -55,7 +55,7 @@ def getTypes(): def _getAllStatus(**kwargs): try: debug.info("get all status for users") - retVal = userController.getAllStatus() + retVal = mainController.getAllStatus() debug.info("return all status for users {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -71,7 +71,7 @@ def _getStatus(**kwargs): data = request.get_json() name = data['name'] debug.info("get status from user {{ {} }}".format(name)) - retVal = userController.getStatus(name) + retVal = mainController.getStatus(name) debug.info( "return status from user {{ {} }} : {{ {} }}".format(name, retVal)) return jsonify(retVal) @@ -84,8 +84,11 @@ def _getStatus(**kwargs): @login_required(groups=[MONEY, GASTRO, VORSTAND], bar=True) def _getUsers(**kwargs): try: + extern = True + if 'extern' in request.args: + extern = not bool(int(request.args['extern'])) debug.info("get all users from database") - users = userController.getAllUsersfromDB() + users = mainController.getAllUsersfromDB(extern=extern) debug.debug("users are {{ {} }}".format(users)) retVal = [user.toJSON() for user in users] debug.info("return all users from database {{ {} }}".format(retVal)) @@ -173,7 +176,7 @@ def _login(): debug.debug("username is {{ {} }}".format(username)) try: debug.info("search {{ {} }} in database".format(username)) - user, ldap_conn = userController.loginUser(username, password) + user, ldap_conn = mainController.loginUser(username, password) debug.debug("user is {{ {} }}".format(user)) user.password = password token = accesTokenController.createAccesToken(user, ldap_conn) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 79631f3..0396106 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, jsonify from geruecht.decorator import login_required -import geruecht.controller.userController as uc +import geruecht.controller.mainController as mc from geruecht.model import USER from datetime import datetime, time from geruecht.exceptions import DayLocked @@ -8,7 +8,7 @@ from geruecht.logger import getDebugLogger, getCreditLogger, getJobsLogger user = Blueprint("user", __name__) -userController = uc.UserController() +mainController = mc.MainController() debug = getDebugLogger() creditL = getCreditLogger() @@ -22,7 +22,7 @@ def _main(**kwargs): try: if 'accToken' in kwargs: accToken = kwargs['accToken'] - accToken.user = userController.getUser(accToken.user.uid) + accToken.user = mainController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} @@ -43,9 +43,9 @@ def _addAmount(**kwargs): data = request.get_json() amount = int(data['amount']) date = datetime.now() - userController.addAmount( + mainController.addAmount( accToken.user.uid, amount, year=date.year, month=date.month) - accToken.user = userController.getUser(accToken.user.uid) + accToken.user = mainController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} @@ -66,7 +66,7 @@ def _saveConfig(**kwargs): if 'accToken' in kwargs: accToken = kwargs['accToken'] data = request.get_json() - accToken.user = userController.modifyUser( + accToken.user = mainController.modifyUser( accToken.user, accToken.ldap_conn, data) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() @@ -89,12 +89,12 @@ def _getUsers(**kwrags): from_date = datetime( from_date['year'], from_date['month'], from_date['day']) to_date = datetime(to_date['year'], to_date['month'], to_date['day']) - lockedDays = userController.getLockedDays(from_date, to_date) + lockedDays = mainController.getLockedDays(from_date, to_date) retVal = [] for lockedDay in lockedDays: day = datetime.combine(lockedDay['daydate'], time(12)) retDay = { - "worker": userController.getWorker(day), + "worker": mainController.getWorker(day), "day": { "date": { "year": day.year, @@ -102,7 +102,8 @@ def _getUsers(**kwrags): "day": day.day }, "locked": lockedDay['locked'] - } + }, + "jobkinddate": mainController.getJobKindDates(day.date()) } retVal.append(retDay) @@ -123,7 +124,7 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - lockedDay = userController.getLockedDay(date) + lockedDay = mainController.getLockedDay(date) if not lockedDay: lockedDay = { 'date': { @@ -143,7 +144,7 @@ def _getUser(**kwargs): 'locked': lockedDay['locked'] } retVal = { - 'worker': userController.getWorker(date), + 'worker': mainController.getWorker(date), 'day': lockedDay } debug.debug("retrun {{ {} }}".format(retVal)) @@ -166,7 +167,7 @@ def _addUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - retVal = userController.addWorker(user.uid, date, userExc=True) + retVal = mainController.addWorker(user.uid, date, userExc=True) debug.debug("return {{ {} }}".format(retVal)) jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format( user.firstname, user.lastname, date.date())) @@ -192,7 +193,7 @@ def _deletJob(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - userController.deleteWorker(user.uid, date, True) + mainController.deleteWorker(user.uid, date, True) debug.debug("return ok") jobL.info("Mitglied {} {} entfernt sich am {} aus dem Dienst".format( user.firstname, user.lastname, date.date())) @@ -219,8 +220,8 @@ def _transactJob(**kwargs): day = data['day'] username = data['user'] date = datetime(year, month, day, 12) - to_user = userController.getUser(username) - retVal = userController.setTransactJob(user, to_user, date) + to_user = mainController.getUser(username) + retVal = mainController.setTransactJob(user, to_user, date) from_userl = retVal['from_user'] to_userl = retVal['to_user'] retVal['from_user'] = retVal['from_user'].toJSON() @@ -250,8 +251,8 @@ def _answer(**kwargs): answer = data['answer'] username = data['username'] date = datetime(year, month, day, 12) - from_user = userController.getUser(username) - retVal = userController.answerdTransactJob( + from_user = mainController.getUser(username) + retVal = mainController.answerdTransactJob( from_user, user, date, answer) from_userl = retVal['from_user'] to_userl = retVal['to_user'] @@ -280,7 +281,7 @@ def _requests(**kwargs): month = data['month'] day = data['day'] date = datetime(year, month, day, 12) - retVal = userController.getAllTransactJobToUser(user, date) + retVal = mainController.getAllTransactJobToUser(user, date) for data in retVal: data['from_user'] = data['from_user'].toJSON() data['to_user'] = data['to_user'].toJSON() @@ -307,7 +308,7 @@ def _getTransactJobs(**kwargs): month = data['month'] day = data['day'] date = datetime(year, month, day, 12) - retVal = userController.getAllTransactJobFromUser(user, date) + retVal = mainController.getAllTransactJobFromUser(user, date) for data in retVal: data['from_user'] = data['from_user'].toJSON() data['to_user'] = data['to_user'].toJSON() @@ -335,8 +336,8 @@ def _deleteTransactJob(**kwargs): day = data['day'] username = data['username'] date = datetime(year, month, day, 12) - to_user = userController.getUser(username) - userController.deleteTransactJob(from_user, to_user, date) + to_user = mainController.getUser(username) + mainController.deleteTransactJob(from_user, to_user, date) debug.debug("return ok") jobL.info("Mitglied {} {} entfernt Dienstanfrage an {} {} am {}".format( from_user.firstname, from_user.lastname, to_user.firstname, to_user.lastname, date.date())) @@ -367,9 +368,9 @@ def _storno(**kwargs): amount = int(data['amount']) date = datetime.now() - userController.addCredit( + mainController.addCredit( user.uid, amount, year=date.year, month=date.month) - accToken.user = userController.getUser(accToken.user.uid) + accToken.user = mainController.getUser(accToken.user.uid) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} diff --git a/geruecht/vorstand/routes.py b/geruecht/vorstand/routes.py index b098fff..143a6c6 100644 --- a/geruecht/vorstand/routes.py +++ b/geruecht/vorstand/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, jsonify -from datetime import datetime, time -import geruecht.controller.userController as uc +from datetime import datetime, time, date +import geruecht.controller.mainController as mc import geruecht.controller.ldapController as lc from geruecht.decorator import login_required from geruecht.model import MONEY, GASTRO, VORSTAND @@ -10,7 +10,7 @@ debug = getDebugLogger() jobL = getJobsLogger() vorstand = Blueprint("vorstand", __name__) -userController = uc.UserController() +mainController = mc.MainController() ldap = lc.LDAPController() @@ -21,7 +21,7 @@ def _setStatus(**kwargs): try: data = request.get_json() name = data['name'] - retVal = userController.setStatus(name) + retVal = mainController.setStatus(name) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -35,7 +35,7 @@ def _updateStatus(**kwargs): debug.info("/um/updateStatus") try: data = request.get_json() - retVal = userController.updateStatus(data) + retVal = mainController.updateStatus(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -49,7 +49,7 @@ def _deleteStatus(**kwargs): debug.info("/um/deleteStatus") try: data = request.get_json() - userController.deleteStatus(data) + mainController.deleteStatus(data) debug.debug("return ok") return jsonify({"ok": "ok"}) except Exception as err: @@ -65,7 +65,7 @@ def _updateStatusUser(**kwargs): data = request.get_json() username = data['username'] status = data['status'] - retVal = userController.updateStatusOfUser(username, status).toJSON() + retVal = mainController.updateStatusOfUser(username, status).toJSON() debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -81,7 +81,7 @@ def _updateVoting(**kwargs): data = request.get_json() username = data['username'] voting = data['voting'] - retVal = userController.updateVotingOfUser(username, voting).toJSON() + retVal = mainController.updateVotingOfUser(username, voting).toJSON() debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -94,7 +94,7 @@ def _updateWorkgroups(**kwargs): debug.info("/um/updateWorkgroups") try: data = request.get_json() - retVal = userController.updateWorkgroupsOfUser({"id": data['id']}, data['workgroups']) + retVal = mainController.updateWorkgroupsOfUser({"id": data['id']}, data['workgroups']) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal), 200 except Exception as err: @@ -112,9 +112,12 @@ def _addUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - retVal = userController.addWorker(user['username'], date) + job_kind = None + if 'job_kind' in data: + job_kind = data['job_kind'] + retVal = mainController.addWorker(user['username'], date, job_kind=job_kind) debug.debug("retrun {{ {} }}".format(retVal)) - userl = userController.getUser(user['username']) + userl = mainController.getUser(user['username']) jobL.info("Vorstand {} {} schreibt Mitglied {} {} am {} zum Dienst ein".format( kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, userl.firstname, userl.lastname, date.date())) return jsonify(retVal) @@ -123,41 +126,6 @@ def _addUser(**kwargs): return jsonify({"error": str(err)}), 500 -@vorstand.route("/sm/getUsers", methods=['POST']) -@login_required(groups=[MONEY, GASTRO, VORSTAND]) -def _getUsers(**kwrags): - debug.info("/sm/getUsers") - try: - data = request.get_json() - from_date = data['from_date'] - to_date = data['to_date'] - from_date = datetime( - from_date['year'], from_date['month'], from_date['day']) - to_date = datetime(to_date['year'], to_date['month'], to_date['day']) - lockedDays = userController.getLockedDays(from_date, to_date) - retVal = [] - for lockedDay in lockedDays: - day = datetime.combine(lockedDay['daydate'], time(12)) - retDay = { - "worker": userController.getWorker(day), - "day": { - "date": { - "year": day.year, - "month": day.month, - "day": day.day - }, - "locked": lockedDay['locked'] - } - } - retVal.append(retDay) - - debug.debug("return {{ {} }}".format(retVal)) - return jsonify(retVal) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 500 - - @vorstand.route("/sm/getUser", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _getUser(**kwargs): @@ -168,7 +136,7 @@ def _getUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - lockedDay = userController.getLockedDay(date) + lockedDay = mainController.getLockedDay(date) lockedDay = { 'date': { 'year': year, @@ -178,7 +146,7 @@ def _getUser(**kwargs): 'locked': lockedDay['locked'] } retVal = { - 'worker': userController.getWorker(date), + 'worker': mainController.getWorker(date), 'day': lockedDay } debug.debug("return {{ {} }}".format(retVal)) @@ -199,9 +167,9 @@ def _deletUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - userController.deleteWorker(user['username'], date) + mainController.deleteWorker(user['username'], date) debug.debug("return ok") - user = userController.getUser(user['username']) + user = mainController.getUser(user['username']) jobL.info("Vorstand {} {} entfernt Mitglied {} {} am {} vom Dienst".format( kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, date.date())) return jsonify({"ok": "ok"}) @@ -214,7 +182,7 @@ def _deletUser(**kwargs): def _getAllWorkgroups(**kwargs): try: debug.info("get all workgroups") - retVal = userController.getAllWorkgroups() + retVal = mainController.getAllWorkgroups() debug.info("return all workgroups {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -229,7 +197,7 @@ def _getWorkgroup(**kwargs): data = request.get_json() name = data['name'] debug.info("get workgroup {{ {} }}".format(name)) - retVal = userController.getWorkgroups(name) + retVal = mainController.getWorkgroups(name) debug.info( "return workgroup {{ {} }} : {{ {} }}".format(name, retVal)) return jsonify(retVal) @@ -248,10 +216,10 @@ def _workgroup(**kwargs): boss = None if 'boss' in data: boss = data['boss'] - retVal = userController.setWorkgroup(name, boss) + retVal = mainController.setWorkgroup(name, boss) debug.debug("return {{ {} }}".format(retVal)) if request.method == 'POST': - retVal = userController.updateWorkgroup(data) + retVal = mainController.updateWorkgroup(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -264,7 +232,7 @@ def _deleteWorkgroup(**kwargs): try: data = request.get_json() debug.info("/wgm/deleteWorkgroup") - userController.deleteWorkgroup(data) + mainController.deleteWorkgroup(data) retVal = {"ok": "ok"} debug.debug("return ok") return jsonify(retVal) @@ -277,7 +245,7 @@ def _deleteWorkgroup(**kwargs): def _getAllJobKinds(**kwargs): try: debug.info("get all jobkinds") - retVal = userController.getAllJobKinds() + retVal = mainController.getAllJobKinds() debug.info("return all jobkinds {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -292,7 +260,7 @@ def _getJobKinds(**kwargs): data = request.get_json() name = data['name'] debug.info("get jobkind {{ {} }}".format(name)) - retVal = userController.getJobKind(name) + retVal = mainController.getJobKind(name) debug.info( "return workgroup {{ {} }} : {{ {} }}".format(name, retVal)) return jsonify(retVal) @@ -311,10 +279,10 @@ def _JobKinds(**kwargs): workgroup = None if 'workgroup' in data: workgroup = data['workgroup'] - retVal = userController.setJobKind(name, workgroup) + retVal = mainController.setJobKind(name, workgroup) debug.debug("return {{ {} }}".format(retVal)) if request.method == 'POST': - retVal = userController.updateJobKind(data) + retVal = mainController.updateJobKind(data) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: @@ -327,7 +295,7 @@ def _deleteJobKind(**kwargs): try: data = request.get_json() debug.info("/sm/deleteJobKind") - userController.deleteJobKind(data) + mainController.deleteJobKind(data) retVal = {"ok": "ok"} debug.debug("return ok") return jsonify(retVal) @@ -335,6 +303,33 @@ def _deleteJobKind(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 +@vorstand.route("/jk/getJobKindDates", methods=['POST']) +@login_required() +def _getJobKindDates(**kwargs): + try: + debug.info("/jk/getJobKindDates") + data = request.get_json() + datum = date(data['year'], data['month'], data['day']) + retVal = mainController.getJobKindDates(datum) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@vorstand.route("/jk/JobKindDate", methods=['POST']) +@login_required(groups=[VORSTAND]) +def _jobKindDates(**kwargs): + try: + debug.info("/jk/JobKindDate") + data = request.get_json() + retVal = mainController.controllJobKindDates(data) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + @vorstand.route("/sm/lockDay", methods=['POST']) @login_required(groups=[MONEY, GASTRO, VORSTAND]) def _lockDay(**kwargs): @@ -346,7 +341,7 @@ def _lockDay(**kwargs): day = data['day'] locked = data['locked'] date = datetime(year, month, day, 12) - lockedDay = userController.setLockedDay(date, locked, True) + lockedDay = mainController.setLockedDay(date, locked, True) if not lockedDay: retVal = { 'date': { From 7bd7acc6bf5f12f8015eeb3e48b88a9743e8b4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 24 May 2020 22:12:46 +0200 Subject: [PATCH 082/111] =?UTF-8?q?routes=20f=C3=BCr=20user=20erweitert,?= =?UTF-8?q?=20sodass=20sich=20der=20user=20in=20einen=20bestimmten=20diens?= =?UTF-8?q?t=20einschreiben=20kann.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/user/routes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 0396106..6ebd0b6 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -167,7 +167,11 @@ def _addUser(**kwargs): month = data['month'] year = data['year'] date = datetime(year, month, day, 12) - retVal = mainController.addWorker(user.uid, date, userExc=True) + job_kind = None + if 'job_kind' in data: + job_kind = data['job_kind'] + mainController.addWorker(user.uid, date, job_kind=job_kind, userExc=True) + retVal = mainController.getWorker(date) debug.debug("return {{ {} }}".format(retVal)) jobL.info("Mitglied {} {} schreib sich am {} zum Dienst ein.".format( user.firstname, user.lastname, date.date())) From efa0257cd05271227952fb289419a1445e4951de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 27 May 2020 12:10:51 +0200 Subject: [PATCH 083/111] =?UTF-8?q?user=20k=C3=B6nnen=20sich=20einladen,?= =?UTF-8?q?=20und=20austragen,=20jobinvites=20k=C3=B6nnen=20geupdated=20we?= =?UTF-8?q?rden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/databaseController/__init__.py | 11 +- .../dbJobInviteController.py | 84 +++++++ .../dbJobTransactController.py | 110 -------- .../controller/mainController/__init__.py | 12 +- .../mainController/mainJobInviteController.py | 38 +++ .../mainJobTransactController.py | 71 ------ .../mainController/mainWorkerController.py | 15 +- geruecht/routes.py | 2 +- geruecht/user/routes.py | 234 +++++++----------- 9 files changed, 233 insertions(+), 344 deletions(-) create mode 100644 geruecht/controller/databaseController/dbJobInviteController.py delete mode 100644 geruecht/controller/databaseController/dbJobTransactController.py create mode 100644 geruecht/controller/mainController/mainJobInviteController.py delete mode 100644 geruecht/controller/mainController/mainJobTransactController.py diff --git a/geruecht/controller/databaseController/__init__.py b/geruecht/controller/databaseController/__init__.py index 91f8f8e..76a47c6 100644 --- a/geruecht/controller/databaseController/__init__.py +++ b/geruecht/controller/databaseController/__init__.py @@ -1,11 +1,18 @@ from ..mainController import Singleton from geruecht import db -from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbJobTransactController, dbPricelistController, dbWorkerController, dbWorkgroupController +from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController from geruecht.exceptions import DatabaseExecption import traceback from MySQLdb._exceptions import IntegrityError -class DatabaseController(dbUserController.Base, dbCreditListController.Base, dbWorkerController.Base, dbWorkgroupController.Base, dbPricelistController.Base, dbJobTransactController.Base, dbJobKindController.Base, metaclass=Singleton): +class DatabaseController(dbUserController.Base, + dbCreditListController.Base, + dbWorkerController.Base, + dbWorkgroupController.Base, + dbPricelistController.Base, + dbJobKindController.Base, + dbJobInviteController.Base, + metaclass=Singleton): ''' DatabaesController diff --git a/geruecht/controller/databaseController/dbJobInviteController.py b/geruecht/controller/databaseController/dbJobInviteController.py new file mode 100644 index 0000000..91d54ad --- /dev/null +++ b/geruecht/controller/databaseController/dbJobInviteController.py @@ -0,0 +1,84 @@ +import traceback + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getJobInvite(self, from_user, to_user, date, id=None): + try: + cursor = self.db.connection.cursor() + if id: + cursor.execute("select * from job_invites where id={}".format(id)) + else: + cursor.execute("select * from job_invites where from_user={} and to_user={} and on_date='{}'".format(from_user['id'], to_user['id'], date)) + retVal = cursor.fetchone() + retVal['to_user'] = self.getUserById(retVal['to_user']).toJSON() + retVal['from_user'] = self.getUserById(retVal['from_user']).toJSON() + retVal['on_date'] = {'year': retVal['on_date'].year, 'month': retVal['on_date'].month, 'day': retVal['on_date'].day} + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getJobInvitesFromUser(self, from_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_invites where from_user={} and on_date>='{}'".format(from_user['id'], date)) + retVal = cursor.fetchall() + for item in retVal: + item['from_user'] = from_user + item['to_user'] = self.getUserById(item['to_user']).toJSON() + item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day} + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getJobInvitesToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_invites where to_user={} and on_date>='{}'".format(to_user['id'], date)) + retVal = cursor.fetchall() + for item in retVal: + item['from_user'] = self.getUserById(item['from_user']).toJSON() + item['to_user'] = to_user + item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day} + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def setJobInvite(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into job_invites (from_user, to_user, on_date) values ({}, {}, '{}')".format(from_user['id'], to_user['id'], date)) + self.db.connection.commit() + return self.getJobInvite(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def updateJobInvite(self, jobinvite): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_invites set watched={} where id={}".format(jobinvite['watched'], jobinvite['id'])) + self.db.connection.commit() + return self.getJobInvite(None, None, None, jobinvite['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def deleteJobInvite(self, jobinvite): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_invites where id={}".format(jobinvite['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbJobTransactController.py b/geruecht/controller/databaseController/dbJobTransactController.py deleted file mode 100644 index cafe463..0000000 --- a/geruecht/controller/databaseController/dbJobTransactController.py +++ /dev/null @@ -1,110 +0,0 @@ -import traceback - -from MySQLdb._exceptions import IntegrityError - -from geruecht.exceptions import DatabaseExecption - - -class Base: - def getTransactJob(self, from_user, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) - data = cursor.fetchone() - if data: - return {"from_user": from_user, "to_user": to_user, "date": data['jobdate'], "answerd": data['answerd'], "accepted": data['accepted']} - return None - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went worng with Database: {}".format(err)) - - def getAllTransactJobFromUser(self, from_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={}".format(from_user.id)) - data = cursor.fetchall() - retVal = [] - for transact in data: - if date <= transact['jobdate']: - retVal.append({"from_user": from_user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getAllTransactJobToUser(self, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where to_user_id={}".format(to_user.id)) - data = cursor.fetchall() - retVal = [] - for transact in data: - if date <= transact['jobdate']: - retVal.append({"to_user": to_user, "from_user": self.getUserById(transact['from_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']}) - return retVal - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getTransactJobToUser(self, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where to_user_id={} and jobdate='{}'".format(to_user.id, date)) - data = cursor.fetchone() - if data: - return {"from_user": self.getUserById(data['from_user_id']), "to_user": to_user, "date": data['jobdate'], "accepted": data['accepted'], "answerd": data['answerd']} - else: - return None - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def updateTransactJob(self, from_user, to_user, date, accepted): - try: - cursor = self.db.connection.cursor() - cursor.execute("update job_transact set accepted={}, answerd=true where to_user_id={} and jobdate='{}'".format(accepted, to_user.id, date)) - self.db.connection.commit() - return self.getTransactJob(from_user, to_user, date) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def getTransactJobFromUser(self, user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("select * from job_transact where from_user_id={} and jobdate='{}'".format(user.id, date)) - data = cursor.fetchall() - return [{"from_user": user, "to_user": self.getUserById(transact['to_user_id']), "date": transact['jobdate'], "accepted": transact['accepted'], "answerd": transact['answerd']} for transact in data] - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) - - def deleteTransactJob(self, from_user, to_user, date): - try: - cursor = self.db.connection.cursor() - cursor.execute("delete from job_transact where from_user_id={} and to_user_id={} and jobdate='{}'".format(from_user.id, to_user.id, date)) - self.db.connection.commit() - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Something went wrong with Database: {}".format(err)) - - def setTransactJob(self, from_user, to_user, date): - try: - exists = self.getTransactJob(from_user, to_user, date) - if exists: - raise IntegrityError("job_transact already exists") - cursor = self.db.connection.cursor() - cursor.execute("insert into job_transact (jobdate, from_user_id, to_user_id) VALUES ('{}', {}, {})".format(date, from_user.id, to_user.id)) - self.db.connection.commit() - return self.getTransactJob(from_user, to_user, date) - except Exception as err: - traceback.print_exc() - self.db.connection.rollback() - raise DatabaseExecption("Somethin went wrong with Database: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/mainController/__init__.py b/geruecht/controller/mainController/__init__.py index a2c9013..3d4c969 100644 --- a/geruecht/controller/mainController/__init__.py +++ b/geruecht/controller/mainController/__init__.py @@ -5,7 +5,7 @@ import geruecht.controller.emailController as ec from geruecht.model.user import User from datetime import datetime, timedelta from geruecht.logger import getDebugLogger -from ..mainController import mainJobKindController, mainCreditListController, mainJobTransactController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController +from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController db = dc.DatabaseController() ldap = lc.LDAPController() @@ -16,11 +16,11 @@ debug = getDebugLogger() class MainController(mainJobKindController.Base, mainCreditListController.Base, - mainJobTransactController.Base, mainPricelistController.Base, mainUserController.Base, mainWorkerController.Base, mainWorkgroupController.Base, + mainJobInviteController.Base, metaclass=Singleton): def __init__(self): @@ -48,6 +48,14 @@ class MainController(mainJobKindController.Base, debug.debug("lock days are {{ {} }}".format(retVal)) return retVal + def getLockedDaysFromList(self, date_list): + debug.info("get locked days from list {{ {} }}".format(date_list)) + retVal = [] + for on_date in date_list: + day = datetime(on_date['on_date']['year'], on_date['on_date']['month'], on_date['on_date']['day'], 12) + retVal.append(self.getLockedDay(day)) + return retVal + def getLockedDay(self, date): debug.info("get locked day on {{ {} }}".format(date)) now = datetime.now() diff --git a/geruecht/controller/mainController/mainJobInviteController.py b/geruecht/controller/mainController/mainJobInviteController.py new file mode 100644 index 0000000..47a1ea0 --- /dev/null +++ b/geruecht/controller/mainController/mainJobInviteController.py @@ -0,0 +1,38 @@ +from datetime import date + +import geruecht.controller.databaseController as dc +from geruecht import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def getJobInvites(self, from_user, to_user, date): + debug.info("get JobInvites from_user {{ {} }} to_user {{ {} }} on date {{ {} }}".format(from_user, to_user, date)) + if from_user is None: + retVal = db.getJobInvitesToUser(to_user, date) + elif to_user is None: + retVal = db.getJobInvitesFromUser(from_user, date) + else: + raise Exception("from_user {{ {} }} and to_user {{ {} }} are None".format(from_user, to_user)) + return retVal + + def setJobInvites(self, data): + debug.info("set new JobInvites data {{ {} }}".format(data)) + retVal = [] + for jobInvite in data: + from_user = jobInvite['from_user'] + to_user = jobInvite['to_user'] + on_date = date(jobInvite['date']['year'], jobInvite['date']['month'], jobInvite['date']['day']) + debug.info("set new JobInvite from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}") + retVal.append(db.setJobInvite(from_user, to_user, on_date)) + debug.debug("seted JobInvites are {{ {} }}".format(retVal)) + return retVal + + def updateJobInvites(self, data): + debug.info("update JobInvites data {{ {} }}".format(data)) + return db.updateJobInvite(data) + + def deleteJobInvite(self, jobInvite): + debug.info("delete JobInvite {{ {} }}".format(jobInvite)) + db.deleteJobInvite(jobInvite) \ No newline at end of file diff --git a/geruecht/controller/mainController/mainJobTransactController.py b/geruecht/controller/mainController/mainJobTransactController.py deleted file mode 100644 index 87e085f..0000000 --- a/geruecht/controller/mainController/mainJobTransactController.py +++ /dev/null @@ -1,71 +0,0 @@ -import geruecht.controller.databaseController as dc -import geruecht.controller.emailController as ec -from geruecht.exceptions import TansactJobIsAnswerdException -from geruecht.logger import getDebugLogger - -db = dc.DatabaseController() -emailController = ec.EmailController() -debug = getDebugLogger() - -class Base: - def setTransactJob(self, from_user, to_user, date): - debug.info("set transact job from {{ {} }} to {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - jobtransact = db.setTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{ {} }}".format(jobtransact)) - debug.info("send mail with transact job to user") - emailController.sendMail( - jobtransact['to_user'], 'jobtransact', jobtransact) - return jobtransact - - def getTransactJobFromUser(self, user, date): - debug.info( - "get transact job from user {{ {} }} on {{ {} }}".format(user, date)) - retVal = db.getTransactJobFromUser(user, date.date()) - debug.debug( - "transact job from user {{ {} }} is {{ {} }}".format(user, retVal)) - return retVal - - def getAllTransactJobFromUser(self, user, date): - debug.info( - "get all transact job from user {{ {} }} start on {{ {} }}".format(user, date)) - retVal = db.getAllTransactJobFromUser(user, date.date()) - debug.debug("all transact job are {{ {} }}".format(retVal)) - return retVal - - def getAllTransactJobToUser(self, user, date): - debug.info( - "get all transact job from to_user {{ {} }} start on {{ {} }}".format(user, date)) - retVal = db.getAllTransactJobToUser(user, date.date()) - debug.debug("all transact job are {{ {} }}".format(retVal)) - return retVal - - def getTransactJob(self, from_user, to_user, date): - debug.info("get transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - retVal = db.getTransactJob(from_user, to_user, date.date()) - debug.debug("transact job is {{ {} }}".format(retVal)) - return retVal - - def deleteTransactJob(self, from_user, to_user, date): - debug.info("delete transact job from user {{ {} }} to user {{ {} }} on {{ {} }}".format( - from_user, to_user, date)) - transactJob = self.getTransactJob(from_user, to_user, date) - debug.debug("transact job is {{ {} }}".format(transactJob)) - if transactJob['answerd']: - debug.warning( - "transactjob {{ {} }} can not delete because is answerd") - raise TansactJobIsAnswerdException( - "TransactJob is already answerd") - db.deleteTransactJob(from_user, to_user, date.date()) - - def answerdTransactJob(self, from_user, to_user, date, answer): - debug.info("answer transact job from user {{ {} }} to user {{ {} }} on {{ {} }} with answer {{ {} }}".format( - from_user, to_user, date, answer)) - transactJob = db.updateTransactJob( - from_user, to_user, date.date(), answer) - debug.debug("transactjob is {{ {} }}".format(transactJob)) - if answer: - debug.info("add worker on date {{ {} }}".format(date)) - self.addWorker(to_user.uid, date) - return transactJob \ No newline at end of file diff --git a/geruecht/controller/mainController/mainWorkerController.py b/geruecht/controller/mainController/mainWorkerController.py index cbce839..95bd2a0 100644 --- a/geruecht/controller/mainController/mainWorkerController.py +++ b/geruecht/controller/mainController/mainWorkerController.py @@ -1,3 +1,5 @@ +from datetime import time, datetime + import geruecht.controller.databaseController as dc from geruecht.exceptions import DayLocked from geruecht.logger import getDebugLogger @@ -47,19 +49,6 @@ class Base: lockedDay = self.getLockedDay(date) if lockedDay: if lockedDay['locked']: - debug.debug( - "day is locked, check if accepted transact job exists") - transactJobs = self.getTransactJobFromUser(user, date) - debug.debug( - "transact job is {{ {} }}".format(transactJobs)) - found = False - for job in transactJobs: - if job['accepted'] and job['answerd']: - debug.debug("accepted transact job exists") - found = True - break - if not found: - debug.debug("no accepted transact job found") raise DayLocked( "Day is locked. You can't delete the Job") db.deleteWorker(user, date) \ No newline at end of file diff --git a/geruecht/routes.py b/geruecht/routes.py index ff05912..96dc57e 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -81,7 +81,7 @@ def _getStatus(**kwargs): @app.route('/getUsers', methods=['GET']) -@login_required(groups=[MONEY, GASTRO, VORSTAND], bar=True) +@login_required(groups=[USER], bar=True) def _getUsers(**kwargs): try: extern = True diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 6ebd0b6..e9b49b0 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify from geruecht.decorator import login_required import geruecht.controller.mainController as mc from geruecht.model import USER -from datetime import datetime, time +from datetime import datetime, time, date from geruecht.exceptions import DayLocked from geruecht.logger import getDebugLogger, getCreditLogger, getJobsLogger @@ -113,6 +113,35 @@ def _getUsers(**kwrags): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 +@user.route("/user/jobsOnDates", methods=['POST']) +@login_required(groups=[USER]) +def _getJobsOnDates(**kwargs): + debug.info("/user/jobsOnDates") + try: + data = request.get_json() + lockedDays = mainController.getLockedDaysFromList(data) + retVal = [] + for lockedDay in lockedDays: + day = datetime.combine(lockedDay['daydate'], time(12)) + retDay = { + "worker": mainController.getWorker(day), + "day": { + "date": { + "year": day.year, + "month": day.month, + "day": day.day + }, + "locked": lockedDay['locked'] + }, + "jobkinddate": mainController.getJobKindDates(day.date()) + } + retVal.append(retDay) + + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 @user.route("/user/job", methods=['POST']) @login_required(groups=[USER]) @@ -198,10 +227,11 @@ def _deletJob(**kwargs): year = data['year'] date = datetime(year, month, day, 12) mainController.deleteWorker(user.uid, date, True) + retVal = mainController.getWorker(date) debug.debug("return ok") jobL.info("Mitglied {} {} entfernt sich am {} aus dem Dienst".format( user.firstname, user.lastname, date.date())) - return jsonify({"ok": "ok"}) + return jsonify(retVal) except DayLocked as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 403 @@ -209,148 +239,6 @@ def _deletJob(**kwargs): debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 409 - -@user.route("/user/transactJob", methods=['POST']) -@login_required(groups=[USER]) -def _transactJob(**kwargs): - debug.info("/user/transactJob") - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - user = accToken.user - data = request.get_json() - year = data['year'] - month = data['month'] - day = data['day'] - username = data['user'] - date = datetime(year, month, day, 12) - to_user = mainController.getUser(username) - retVal = mainController.setTransactJob(user, to_user, date) - from_userl = retVal['from_user'] - to_userl = retVal['to_user'] - retVal['from_user'] = retVal['from_user'].toJSON() - retVal['to_user'] = retVal['to_user'].toJSON() - retVal['date'] = {'year': year, 'month': month, 'day': day} - debug.debug("return {{ {} }}".format(retVal)) - jobL.info("Mitglied {} {} sendet Dienstanfrage an Mitglied {} {} am {}".format( - from_userl.firstname, from_userl.lastname, to_userl.firstname, to_userl.lastname, date.date())) - return jsonify(retVal) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 - - -@user.route("/user/answerTransactJob", methods=['POST']) -@login_required(groups=[USER]) -def _answer(**kwargs): - debug.info("/user/answerTransactJob") - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - user = accToken.user - data = request.get_json() - year = data['year'] - month = data['month'] - day = data['day'] - answer = data['answer'] - username = data['username'] - date = datetime(year, month, day, 12) - from_user = mainController.getUser(username) - retVal = mainController.answerdTransactJob( - from_user, user, date, answer) - from_userl = retVal['from_user'] - to_userl = retVal['to_user'] - retVal['from_user'] = retVal['from_user'].toJSON() - retVal['to_user'] = retVal['to_user'].toJSON() - retVal['date'] = {'year': year, 'month': month, 'day': day} - debug.debug("return {{ {} }}".format(retVal)) - jobL.info("Mitglied {} {} beantwortet Dienstanfrage von {} {} am {} mit {}".format(to_userl.firstname, - to_userl.lastname, from_userl.firstname, from_userl.lastname, date.date(), 'JA' if answer else 'NEIN')) - return jsonify(retVal) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 - - -@user.route("/user/jobRequests", methods=['POST']) -@login_required(groups=[USER]) -def _requests(**kwargs): - debug.info("/user/jobRequests") - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - user = accToken.user - data = request.get_json() - year = data['year'] - month = data['month'] - day = data['day'] - date = datetime(year, month, day, 12) - retVal = mainController.getAllTransactJobToUser(user, date) - for data in retVal: - data['from_user'] = data['from_user'].toJSON() - data['to_user'] = data['to_user'].toJSON() - data_date = data['date'] - data['date'] = {'year': data_date.year, - 'month': data_date.month, 'day': data_date.day} - debug.debug("return {{ {} }}".format(retVal)) - return jsonify(retVal) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 - - -@user.route("/user/getTransactJobs", methods=['POST']) -@login_required(groups=[USER]) -def _getTransactJobs(**kwargs): - debug.info("/user/getTransactJobs") - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - user = accToken.user - data = request.get_json() - year = data['year'] - month = data['month'] - day = data['day'] - date = datetime(year, month, day, 12) - retVal = mainController.getAllTransactJobFromUser(user, date) - for data in retVal: - data['from_user'] = data['from_user'].toJSON() - data['to_user'] = data['to_user'].toJSON() - data_date = data['date'] - data['date'] = {'year': data_date.year, - 'month': data_date.month, 'day': data_date.day} - debug.debug("return {{ {} }}".format(retVal)) - return jsonify(retVal) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 - - -@user.route("/user/deleteTransactJob", methods=['POST']) -@login_required(groups=[USER]) -def _deleteTransactJob(**kwargs): - debug.info("/user/deleteTransactJob") - try: - if 'accToken' in kwargs: - accToken = kwargs['accToken'] - from_user = accToken.user - data = request.get_json() - year = data['year'] - month = data['month'] - day = data['day'] - username = data['username'] - date = datetime(year, month, day, 12) - to_user = mainController.getUser(username) - mainController.deleteTransactJob(from_user, to_user, date) - debug.debug("return ok") - jobL.info("Mitglied {} {} entfernt Dienstanfrage an {} {} am {}".format( - from_user.firstname, from_user.lastname, to_user.firstname, to_user.lastname, date.date())) - return jsonify({"ok": "ok"}) - except Exception as err: - debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 - - @user.route("/user/storno", methods=['POST']) @login_required(groups=[USER]) def _storno(**kwargs): @@ -384,4 +272,60 @@ def _storno(**kwargs): return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 409 + return jsonify({"error": str(err)}), 500 + + +@user.route("/user/getJobInvites", methods=['POST']) +@login_required(groups=[USER]) +def _getJobInvites(**kwargs): + try: + debug.info("/user/getJobInvites") + from_user = None + to_user = None + on_date = None + + data = request.get_json() + + if 'from_user' in data: + from_user = data['from_user'] + if 'to_user' in data: + to_user = data['to_user'] + on_date = date(data['date']['year'], data['date']['month'], data['date']['day']) + retVal = mainController.getJobInvites(from_user, to_user, on_date) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@user.route("/user/JobInvites", methods=['PUT', 'POST']) +@login_required(groups=[USER]) +def _JobInvites(**kwargs): + try: + debug.info("/user/JobInvites") + data = request.get_json() + if request.method == 'PUT': + mainController.setJobInvites(data) + retVal = mainController.getJobInvites(kwargs['accToken'].user.toJSON(), None, datetime.now().date()) + debug.debug("return {{ {} }}".format(retVal)) + if request.method == 'POST': + retVal = mainController.updateJobInvites(data) + + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@user.route("/user/deleteJobInvite", methods=['POST']) +@login_required(groups=[USER]) +def _deleteJobInvite(**kwargs): + try: + debug.info("/user/deleteJobInvite") + data = request.get_json() + mainController.deleteJobInvite(data) + retVal = mainController.getJobInvites(data['from_user'], None, datetime.now().date()) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 \ No newline at end of file From dcc9c5ee1426296ee9588bb241154cd1cc0a17ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 27 May 2020 15:05:19 +0200 Subject: [PATCH 084/111] fix, dass keine job_kinddates gespeichert werden, wo kein job_kind existiert. --- geruecht/controller/mainController/mainJobKindController.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geruecht/controller/mainController/mainJobKindController.py b/geruecht/controller/mainController/mainJobKindController.py index cc23f6c..3eb67f0 100644 --- a/geruecht/controller/mainController/mainJobKindController.py +++ b/geruecht/controller/mainController/mainJobKindController.py @@ -63,7 +63,8 @@ class Base: for jobkinddate in jobkinddates: datum = date(jobkinddate['daydate']['year'], jobkinddate['daydate']['month'], jobkinddate['daydate']['day']) if jobkinddate['id'] == -1: - self.setJobKindDates(datum, jobkinddate['job_kind'], jobkinddate['maxpersons']) + if jobkinddate['job_kind']: + self.setJobKindDates(datum, jobkinddate['job_kind'], jobkinddate['maxpersons']) if jobkinddate['id'] == 0: jobkinddate['id'] = jobkinddate['backupid'] db.deleteAllWorkerWithJobKind(datetime.combine(datum, time(12)), jobkinddate['job_kind']) From b0f09969a5322381b3743965f1761ef348f866a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 2 Jun 2020 23:24:17 +0200 Subject: [PATCH 085/111] =?UTF-8?q?user=20kann=20jobrequest=20updaten,=20e?= =?UTF-8?q?rstellen=20oder=20l=C3=B6schen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/databaseController/__init__.py | 3 +- .../dbJobRequesController.py | 97 +++++++++++++++++++ .../databaseController/dbWorkerController.py | 10 ++ .../controller/mainController/__init__.py | 3 +- .../mainJobRequestController.py | 41 ++++++++ .../mainController/mainWorkerController.py | 4 + geruecht/user/routes.py | 57 +++++++++++ 7 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 geruecht/controller/databaseController/dbJobRequesController.py create mode 100644 geruecht/controller/mainController/mainJobRequestController.py diff --git a/geruecht/controller/databaseController/__init__.py b/geruecht/controller/databaseController/__init__.py index 76a47c6..9ab55bb 100644 --- a/geruecht/controller/databaseController/__init__.py +++ b/geruecht/controller/databaseController/__init__.py @@ -1,6 +1,6 @@ from ..mainController import Singleton from geruecht import db -from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController +from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController from geruecht.exceptions import DatabaseExecption import traceback from MySQLdb._exceptions import IntegrityError @@ -12,6 +12,7 @@ class DatabaseController(dbUserController.Base, dbPricelistController.Base, dbJobKindController.Base, dbJobInviteController.Base, + dbJobRequesController.Base, metaclass=Singleton): ''' DatabaesController diff --git a/geruecht/controller/databaseController/dbJobRequesController.py b/geruecht/controller/databaseController/dbJobRequesController.py new file mode 100644 index 0000000..b65e323 --- /dev/null +++ b/geruecht/controller/databaseController/dbJobRequesController.py @@ -0,0 +1,97 @@ +import traceback + +from geruecht.exceptions import DatabaseExecption + + +class Base: + def getJobRequest(self, from_user, to_user, date, id=None): + try: + cursor = self.db.connection.cursor() + if id: + cursor.execute("select * from job_request where id={}".format(id)) + else: + cursor.execute("select * from job_request where from_user={} and to_user={} and on_date='{}'".format(from_user['id'], to_user['id'], date)) + retVal = cursor.fetchone() + retVal['to_user'] = self.getUserById(retVal['to_user']).toJSON() + retVal['from_user'] = self.getUserById(retVal['from_user']).toJSON() + retVal['on_date'] = {'year': retVal['on_date'].year, 'month': retVal['on_date'].month, 'day': retVal['on_date'].day} + retVal['job_kind'] = self.getJobKind(retVal['job_kind']) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getJobRequestsFromUser(self, from_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_request where from_user={} and on_date>='{}'".format(from_user['id'], date)) + retVal = cursor.fetchall() + for item in retVal: + item['from_user'] = from_user + item['to_user'] = self.getUserById(item['to_user']).toJSON() + item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day} + item['job_kind'] = self.getJobKind(item['job_kind']) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getJobRequestsToUser(self, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_request where to_user={} and on_date>='{}'".format(to_user['id'], date)) + retVal = cursor.fetchall() + for item in retVal: + item['from_user'] = self.getUserById(item['from_user']).toJSON() + item['to_user'] = to_user + item['on_date'] = {'year': item['on_date'].year, 'month': item['on_date'].month, 'day': item['on_date'].day} + item['job_kind'] = self.getJobKind(item['job_kind']) + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def setJobRequest(self, from_user, to_user, date, job_kind): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into job_request (from_user, to_user, on_date, job_kind) values ({}, {}, '{}', {})".format(from_user['id'], to_user['id'], date, job_kind['id'])) + self.db.connection.commit() + return self.getJobRequest(from_user, to_user, date) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def updateJobRequest(self, jobrequest): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_request set watched={}, answered={} where id={}".format(jobrequest['watched'], jobrequest['answered'], jobrequest['id'])) + self.db.connection.commit() + return self.getJobRequest(None, None, None, jobrequest['id']) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def updateAllJobRequest(self, jobrequest): + try: + cursor = self.db.connection.cursor() + cursor.execute("update job_request set answered={}, accepted={} where from_user={} and on_date='{}'".format(jobrequest['answered'], jobrequest['accepted'], jobrequest['from_user']['id'], jobrequest['on_date'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def deleteJobRequest(self, jobrequest): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from job_request where id={}".format(jobrequest['id'])) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/databaseController/dbWorkerController.py b/geruecht/controller/databaseController/dbWorkerController.py index 134625d..7a2e917 100644 --- a/geruecht/controller/databaseController/dbWorkerController.py +++ b/geruecht/controller/databaseController/dbWorkerController.py @@ -46,6 +46,16 @@ class Base: self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + def changeWorker(self, from_user, to_user, date): + try: + cursor = self.db.connection.cursor() + cursor.execute("update bardienste set user_id={} where user_id={} and startdatetime='{}'".format(to_user['id'], from_user['id'], date)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + def deleteAllWorkerWithJobKind(self, date, job_kind): try: cursor = self.db.connection.cursor() diff --git a/geruecht/controller/mainController/__init__.py b/geruecht/controller/mainController/__init__.py index 3d4c969..41f2a7f 100644 --- a/geruecht/controller/mainController/__init__.py +++ b/geruecht/controller/mainController/__init__.py @@ -5,7 +5,7 @@ import geruecht.controller.emailController as ec from geruecht.model.user import User from datetime import datetime, timedelta from geruecht.logger import getDebugLogger -from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController +from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController db = dc.DatabaseController() ldap = lc.LDAPController() @@ -21,6 +21,7 @@ class MainController(mainJobKindController.Base, mainWorkerController.Base, mainWorkgroupController.Base, mainJobInviteController.Base, + mainJobRequestController.Base, metaclass=Singleton): def __init__(self): diff --git a/geruecht/controller/mainController/mainJobRequestController.py b/geruecht/controller/mainController/mainJobRequestController.py new file mode 100644 index 0000000..8c7bc1f --- /dev/null +++ b/geruecht/controller/mainController/mainJobRequestController.py @@ -0,0 +1,41 @@ +from datetime import date, time, datetime + +import geruecht.controller.databaseController as dc +from geruecht import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def getJobRequests(self, from_user, to_user, date): + debug.info("get JobRequests from_user {{ {} }} to_user {{ {} }} on date {{ {} }}".format(from_user, to_user, date)) + if from_user is None: + retVal = db.getJobRequestsToUser(to_user, date) + elif to_user is None: + retVal = db.getJobRequestsFromUser(from_user, date) + else: + raise Exception("from_user {{ {} }} and to_user {{ {} }} are None".format(from_user, to_user)) + return retVal + + def setJobRequests(self, data): + debug.info("set new JobRequests data {{ {} }}".format(data)) + retVal = [] + for jobRequest in data: + from_user = jobRequest['from_user'] + to_user = jobRequest['to_user'] + on_date = date(jobRequest['date']['year'], jobRequest['date']['month'], jobRequest['date']['day']) + debug.info("set new JobRequest from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}") + retVal.append(db.setJobRequest(from_user, to_user, on_date)) + debug.debug("seted JobRequests are {{ {} }}".format(retVal)) + return retVal + + def updateJobRequests(self, data): + debug.info("update JobRequest data {{ {} }}".format(data)) + if data['accepted']: + self.changeWorker(data['from_user'], data['to_user'], datetime.combine(data['on_date'], time(12))) + db.updateAllJobRequest(data) + return db.updateJobRequest(data) + + def deleteJobRequest(self, jobRequest): + debug.info("delete JobRequest {{ {} }}".format(jobRequest)) + db.deleteJobRequest(jobRequest) \ No newline at end of file diff --git a/geruecht/controller/mainController/mainWorkerController.py b/geruecht/controller/mainController/mainWorkerController.py index 95bd2a0..4ae0d16 100644 --- a/geruecht/controller/mainController/mainWorkerController.py +++ b/geruecht/controller/mainController/mainWorkerController.py @@ -39,6 +39,10 @@ class Base: debug.debug("worker on date is {{ {} }}".format(retVal)) return retVal + def changeWorker(self, from_user, to_user, date): + debug.info("change worker from {{ {} }} to {{ {} }} on {{ {} }}".format(from_user, to_user, date)) + db.changeWorker(from_user, to_user, date) + def deleteWorker(self, username, date, userExc=False): debug.info( "delete worker {{ {} }} on date {{ {} }}".format(username, date)) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index e9b49b0..b48e5b3 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -326,6 +326,63 @@ def _deleteJobInvite(**kwargs): retVal = mainController.getJobInvites(data['from_user'], None, datetime.now().date()) debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + + +@user.route("/user/getJobRequests", methods=['POST']) +@login_required(groups=[USER]) +def _getJobRequests(**kwargs): + try: + debug.info("/user/getJobRequests") + from_user = None + to_user = None + on_date = None + + data = request.get_json() + + if 'from_user' in data: + from_user = data['from_user'] + if 'to_user' in data: + to_user = data['to_user'] + on_date = date(data['date']['year'], data['date']['month'], data['date']['day']) + retVal = mainController.getJobRequests(from_user, to_user, on_date) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@user.route("/user/JobRequests", methods=['PUT', 'POST']) +@login_required(groups=[USER]) +def _JobRequests(**kwargs): + try: + debug.info("/user/JobRequests") + data = request.get_json() + data['on_date'] = date(data['on_date']['year'], data['on_date']['month'], data['on_date']['day']) + if request.method == 'PUT': + mainController.setJobRequests(data) + retVal = mainController.getJobRequests(kwargs['accToken'].user.toJSON(), None, datetime.now().date()) + debug.debug("return {{ {} }}".format(retVal)) + if request.method == 'POST': + retVal = mainController.updateJobRequests(data) + + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 + +@user.route("/user/deleteJobRequest", methods=['POST']) +@login_required(groups=[USER]) +def _deleteJobRequest(**kwargs): + try: + debug.info("/user/deleteJobRequest") + data = request.get_json() + mainController.deleteJobRequest(data) + retVal = mainController.getJobRequests(data['from_user'], None, datetime.now().date()) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) return jsonify({"error": str(err)}), 500 \ No newline at end of file From fd2c9a2a7e4924a41d88430eb118c5de36b247e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 4 Jun 2020 13:19:00 +0200 Subject: [PATCH 086/111] =?UTF-8?q?diensteinladung=20und=20=C3=BCbertragun?= =?UTF-8?q?g=20fertig.=20User=20k=C3=B6nnen=20leute=20einladen,=20sich=20a?= =?UTF-8?q?ustragen,=20jobs=20abgeben=20usw.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user können sehen, wenn es neue nachrichten gibt und sehen ob die einladung bzw. anfrage gesehen wurde. --- .../controller/databaseController/dbJobRequesController.py | 4 ++-- .../controller/mainController/mainJobRequestController.py | 3 ++- geruecht/user/routes.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/geruecht/controller/databaseController/dbJobRequesController.py b/geruecht/controller/databaseController/dbJobRequesController.py index b65e323..8f77752 100644 --- a/geruecht/controller/databaseController/dbJobRequesController.py +++ b/geruecht/controller/databaseController/dbJobRequesController.py @@ -68,7 +68,7 @@ class Base: def updateJobRequest(self, jobrequest): try: cursor = self.db.connection.cursor() - cursor.execute("update job_request set watched={}, answered={} where id={}".format(jobrequest['watched'], jobrequest['answered'], jobrequest['id'])) + cursor.execute("update job_request set watched={}, answered={}, accepted={} where id={}".format(jobrequest['watched'], jobrequest['answered'], jobrequest['accepted'], jobrequest['id'])) self.db.connection.commit() return self.getJobRequest(None, None, None, jobrequest['id']) except Exception as err: @@ -79,7 +79,7 @@ class Base: def updateAllJobRequest(self, jobrequest): try: cursor = self.db.connection.cursor() - cursor.execute("update job_request set answered={}, accepted={} where from_user={} and on_date='{}'".format(jobrequest['answered'], jobrequest['accepted'], jobrequest['from_user']['id'], jobrequest['on_date'])) + cursor.execute("update job_request set answered={} where from_user={} and on_date='{}'".format(jobrequest['answered'], jobrequest['from_user']['id'], jobrequest['on_date'])) self.db.connection.commit() except Exception as err: traceback.print_exc() diff --git a/geruecht/controller/mainController/mainJobRequestController.py b/geruecht/controller/mainController/mainJobRequestController.py index 8c7bc1f..05529a6 100644 --- a/geruecht/controller/mainController/mainJobRequestController.py +++ b/geruecht/controller/mainController/mainJobRequestController.py @@ -24,8 +24,9 @@ class Base: from_user = jobRequest['from_user'] to_user = jobRequest['to_user'] on_date = date(jobRequest['date']['year'], jobRequest['date']['month'], jobRequest['date']['day']) + job_kind = jobRequest['job_kind'] debug.info("set new JobRequest from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}") - retVal.append(db.setJobRequest(from_user, to_user, on_date)) + retVal.append(db.setJobRequest(from_user, to_user, on_date, job_kind)) debug.debug("seted JobRequests are {{ {} }}".format(retVal)) return retVal diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index b48e5b3..e433798 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -360,12 +360,12 @@ def _JobRequests(**kwargs): try: debug.info("/user/JobRequests") data = request.get_json() - data['on_date'] = date(data['on_date']['year'], data['on_date']['month'], data['on_date']['day']) if request.method == 'PUT': mainController.setJobRequests(data) retVal = mainController.getJobRequests(kwargs['accToken'].user.toJSON(), None, datetime.now().date()) debug.debug("return {{ {} }}".format(retVal)) if request.method == 'POST': + data['on_date'] = date(data['on_date']['year'], data['on_date']['month'], data['on_date']['day']) retVal = mainController.updateJobRequests(data) return jsonify(retVal) From 7b67d564b0d857f8e77604f9811b684a7f5cfe85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 4 Jun 2020 13:56:00 +0200 Subject: [PATCH 087/111] =?UTF-8?q?groups=20wird=20bei=20getLifeTime=20mit?= =?UTF-8?q?geschickt,=20damit=20kein=20ein=20und=20ausloggen=20beim=20wech?= =?UTF-8?q?sel=20des=20bardienstes=20n=C3=B6tig=20ist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/routes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/geruecht/routes.py b/geruecht/routes.py index 96dc57e..a79e402 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -107,7 +107,8 @@ def _getLifeTime(**kwargs): if 'accToken' in kwargs: accToken = kwargs['accToken'] debug.debug("accessToken is {{ {} }}".format(accToken)) - retVal = {"value": accToken.lifetime} + retVal = {"value": accToken.lifetime, + "group": accToken.user.toJSON()['group']} debug.info( "return get lifetime from accesstoken {{ {} }}".format(retVal)) return jsonify(retVal) @@ -132,7 +133,8 @@ def _saveLifeTime(**kwargs): accToken.lifetime = lifetime debug.info("update accesstoken timestamp") accToken.updateTimestamp() - retVal = {"value": accToken.lifetime} + retVal = {"value": accToken.lifetime, + "group": accToken.user.toJSON()['group']} debug.info( "return save lifetime for accessToken {{ {} }}".format(retVal)) return jsonify(retVal) From 44155b967810de0aabccada8d5f2b444502bfc65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 4 Jun 2020 20:56:20 +0200 Subject: [PATCH 088/111] fixed bug ##258 --- geruecht/baruser/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 76903d1..61e793c 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -45,7 +45,8 @@ def _bar(**kwargs): "lastname": user.lastname, "amount": all, "locked": user.locked, - "type": type + "type": type, + "limit": user.limit } debug.debug("return {{ {} }}".format(dic)) return jsonify(dic) From 068abb43a221cc9fedee703ba37938514beb6564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 4 Jun 2020 21:20:38 +0200 Subject: [PATCH 089/111] fixed bug ##259 --- .../databaseController/dbJobKindController.py | 20 +++++++++++++++++++ .../databaseController/dbWorkerController.py | 20 +++++++++++-------- .../mainController/mainWorkerController.py | 2 +- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/geruecht/controller/databaseController/dbJobKindController.py b/geruecht/controller/databaseController/dbJobKindController.py index c243f0d..d131eb3 100644 --- a/geruecht/controller/databaseController/dbJobKindController.py +++ b/geruecht/controller/databaseController/dbJobKindController.py @@ -101,6 +101,26 @@ class Base: self.db.connection.rollback() raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + def getJobKindDate(self, date, job_kind): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from job_kind_dates where daydate='{}' and job_kind={}".format(date, job_kind['id'])) + item = cursor.fetchone() + if item: + item['job_kind'] = self.getJobKind(item['job_kind']) if item['job_kind'] != None else None + item['daydate'] = {'year': item['daydate'].year, 'month': item['daydate'].month, 'day': item['daydate'].day} + else: + item = { + 'job_kind': self.getJobKind(1), + 'daydate': {'year': date.year, 'month': date.month, 'day': date.day}, + 'maxpersons': 2 + } + return item + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + def deleteJobKindDates(self, jobkinddates): try: cursor = self.db.connection.cursor() diff --git a/geruecht/controller/databaseController/dbWorkerController.py b/geruecht/controller/databaseController/dbWorkerController.py index 7a2e917..3d7652d 100644 --- a/geruecht/controller/databaseController/dbWorkerController.py +++ b/geruecht/controller/databaseController/dbWorkerController.py @@ -22,14 +22,18 @@ class Base: cursor.execute("select * from bardienste where startdatetime='{}'".format(date)) data = cursor.fetchall() retVal = [] - # for work in data: - # user = self.getUserById(work['user_id']).toJSON() - # startdatetime = work['startdatetime'] - # enddatetime = work['enddatetime'] - # start = { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day} - # job_kind = self.getJobKind(work['job_kind']) if work['job_kind'] != None else None - # retVal.append({'user': user, 'startdatetime': startdatetime, 'enddatetime': enddatetime, 'start': start, 'job_kind': job_kind}) - # return retVal + return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}, "job_kind": self.getJobKind(work['job_kind']) if work['job_kind'] != None else None} for work in data] + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def getWorkersWithJobKind(self, date, job_kind): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from bardienste where startdatetime='{}' and job_kind={} {}".format(date, job_kind['id'], "or job_kind='null'" if job_kind['id'] is 1 else '')) + data = cursor.fetchall() + retVal = [] return [{"user": self.getUserById(work['user_id']).toJSON(), "startdatetime": work['startdatetime'], "enddatetime": work['enddatetime'], "start": { "year": work['startdatetime'].year, "month": work['startdatetime'].month, "day": work['startdatetime'].day}, "job_kind": self.getJobKind(work['job_kind']) if work['job_kind'] != None else None} for work in data] except Exception as err: traceback.print_exc() diff --git a/geruecht/controller/mainController/mainWorkerController.py b/geruecht/controller/mainController/mainWorkerController.py index 4ae0d16..91a3068 100644 --- a/geruecht/controller/mainController/mainWorkerController.py +++ b/geruecht/controller/mainController/mainWorkerController.py @@ -32,7 +32,7 @@ class Base: user = self.getUser(username) debug.debug("user is {{ {} }}".format(user)) debug.debug("check if user has job on date") - if (not db.getWorker(user, date)): + if (not db.getWorker(user, date) and len(db.getWorkersWithJobKind(date, job_kind)) < db.getJobKindDate(date.date(), job_kind)['maxpersons']): debug.debug("set job to user") db.setWorker(user, date, job_kind=job_kind) retVal = self.getWorker(date, username=username) From a70904ceaccc80dad4304ca7cdd5e003cabaa73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 4 Jun 2020 23:03:39 +0200 Subject: [PATCH 090/111] accessToken werden nun in der datenbank gespeichert lifetime kann auch neu gesetzt werden. --- geruecht/controller/accesTokenController.py | 21 +++--- .../controller/databaseController/__init__.py | 3 +- .../dbAccessTokenController.py | 68 +++++++++++++++++++ .../mainController/mainUserController.py | 3 +- geruecht/model/accessToken.py | 5 +- geruecht/routes.py | 10 +-- 6 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 geruecht/controller/databaseController/dbAccessTokenController.py diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 6c683c4..a662e56 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -1,6 +1,7 @@ from geruecht.model.accessToken import AccessToken import geruecht.controller as gc import geruecht.controller.mainController as mc +import geruecht.controller.databaseController as dc from geruecht.model import BAR from datetime import datetime, timedelta import hashlib @@ -10,6 +11,7 @@ from geruecht.logger import getDebugLogger debug = getDebugLogger() mainController = mc.MainController() +db = dc.DatabaseController() class AccesTokenController(metaclass=Singleton): """ Control all createt AccesToken @@ -30,7 +32,6 @@ class AccesTokenController(metaclass=Singleton): """ debug.info("init accesstoken controller") self.lifetime = gc.accConfig - self.tokenList = [] def checkBar(self, user): debug.info("check if user {{ {} }} is baruser".format(user)) @@ -57,7 +58,7 @@ class AccesTokenController(metaclass=Singleton): An the AccesToken for this given Token or False. """ debug.info("check token {{ {} }} is valid") - for accToken in self.tokenList: + for accToken in db.getAccessTokens(): debug.debug("accesstoken is {}".format(accToken)) endTime = accToken.timestamp + timedelta(seconds=accToken.lifetime) now = datetime.now() @@ -69,19 +70,16 @@ class AccesTokenController(metaclass=Singleton): debug.debug("check if accestoken {{ {} }} has group {{ {} }}".format(accToken, group)) if self.isSameGroup(accToken, group): accToken.updateTimestamp() + db.updateAccessToken(accToken) debug.debug("found accesstoken {{ {} }} with token: {{ {} }} and group: {{ {} }}".format(accToken, token, group)) return accToken else: debug.debug("accesstoken is {{ {} }} out of date".format(accToken)) - self.deleteAccessToken(accToken) + db.deleteAccessToken(accToken) debug.debug("no valid accesstoken with token: {{ {} }} and group: {{ {} }}".format(token, group)) return False - def deleteAccessToken(self, accToken): - debug.info("delete accesstoken {{ {} }}".format(accToken)) - self.tokenList.remove(accToken) - - def createAccesToken(self, user, ldap_conn): + def createAccesToken(self, user): """ Create an AccessToken Create an AccessToken for an User and add it to the tokenList. @@ -96,9 +94,8 @@ class AccesTokenController(metaclass=Singleton): now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() self.checkBar(user) - accToken = AccessToken(user, token, ldap_conn, self.lifetime, datetime.now()) + accToken = db.createAccessToken(user, token, self.lifetime, datetime.now(), lock_bar=False) debug.debug("accesstoken is {{ {} }}".format(accToken)) - self.tokenList.append(accToken) return token def isSameGroup(self, accToken, groups): @@ -117,3 +114,7 @@ class AccesTokenController(metaclass=Singleton): for group in groups: if group in accToken.user.group: return True return False + + def updateAccessToken(self, accToken): + accToken.updateTimestamp() + return db.updateAccessToken(accToken) diff --git a/geruecht/controller/databaseController/__init__.py b/geruecht/controller/databaseController/__init__.py index 9ab55bb..08ee142 100644 --- a/geruecht/controller/databaseController/__init__.py +++ b/geruecht/controller/databaseController/__init__.py @@ -1,6 +1,6 @@ from ..mainController import Singleton from geruecht import db -from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController +from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController, dbAccessTokenController from geruecht.exceptions import DatabaseExecption import traceback from MySQLdb._exceptions import IntegrityError @@ -13,6 +13,7 @@ class DatabaseController(dbUserController.Base, dbJobKindController.Base, dbJobInviteController.Base, dbJobRequesController.Base, + dbAccessTokenController.Base, metaclass=Singleton): ''' DatabaesController diff --git a/geruecht/controller/databaseController/dbAccessTokenController.py b/geruecht/controller/databaseController/dbAccessTokenController.py new file mode 100644 index 0000000..230dd37 --- /dev/null +++ b/geruecht/controller/databaseController/dbAccessTokenController.py @@ -0,0 +1,68 @@ +import traceback +from geruecht.exceptions import DatabaseExecption +from geruecht.model.accessToken import AccessToken + + +class Base: + + def getAccessToken(self, item): + try: + cursor = self.db.connection.cursor() + if type(item) == str: + sql = "select * from session where token='{}'".format(item) + elif type(item) == int: + sql = 'select * from session where id={}'.format(item) + else: + raise DatabaseExecption("item as no type int or str. name={}, type={}".format(item, type(item))) + cursor.execute(sql) + session = cursor.fetchone() + retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp']) if session != None else None + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) + + def getAccessTokens(self): + try: + cursor = self.db.connection.cursor() + cursor.execute("select * from session") + sessions = cursor.fetchall() + retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp']) for session in sessions] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def createAccessToken(self, user, token, lifetime, timestamp, lock_bar): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into session (user, timestamp, lock_bar, token, lifetime) VALUES ({}, '{}', {}, '{}', {})".format(user.id, timestamp, lock_bar, token, lifetime)) + self.db.connection.commit() + return self.getAccessToken(token) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def updateAccessToken(self, accToken): + try: + cursor = self.db.connection.cursor() + cursor.execute("update session set timestamp='{}', lock_bar={}, lifetime={} where id={}".format(accToken.timestamp, accToken.lock_bar, accToken.lifetime, accToken.id)) + self.db.connection.commit() + return self.getAccessToken(accToken.id) + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def deleteAccessToken(self, accToken): + try: + cursor = self.db.connection.cursor() + cursor.execute("delete from session where id={}".format(accToken.id)) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py index c0c70aa..b46f7e7 100644 --- a/geruecht/controller/mainController/mainUserController.py +++ b/geruecht/controller/mainController/mainUserController.py @@ -153,8 +153,7 @@ class Base: debug.debug("user is {{ {} }}".format(user)) user.password = password ldap.login(username, password) - ldap_conn = ldap.bind(user, password) - return user, ldap_conn + return user except PermissionDenied as err: debug.debug("permission is denied", exc_info=True) raise err \ No newline at end of file diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 0e7746c..05f18ce 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -15,9 +15,8 @@ class AccessToken(): timestamp = None user = None token = None - ldap_conn = None - def __init__(self, user, token, ldap_conn, lifetime, timestamp=datetime.now()): + def __init__(self, id, user, token, lifetime, timestamp=datetime.now()): """ Initialize Class AccessToken No more to say. @@ -28,11 +27,11 @@ class AccessToken(): timestamp: Default current time, but can set to an other datetime-Object. """ debug.debug("init accesstoken") + self.id = id self.user = user self.timestamp = timestamp self.lifetime = lifetime self.token = token - self.ldap_conn = ldap_conn self.lock_bar = False debug.debug("accesstoken is {{ {} }}".format(self)) diff --git a/geruecht/routes.py b/geruecht/routes.py index a79e402..6a911c4 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -132,7 +132,8 @@ def _saveLifeTime(**kwargs): lifetime, accToken)) accToken.lifetime = lifetime debug.info("update accesstoken timestamp") - accToken.updateTimestamp() + accToken = accesTokenController.updateAccessToken(accToken) + accToken = accesTokenController.validateAccessToken(accToken.token, [USER, EXTERN]) retVal = {"value": accToken.lifetime, "group": accToken.user.toJSON()['group']} debug.info( @@ -178,10 +179,9 @@ def _login(): debug.debug("username is {{ {} }}".format(username)) try: debug.info("search {{ {} }} in database".format(username)) - user, ldap_conn = mainController.loginUser(username, password) + user = mainController.loginUser(username, password) debug.debug("user is {{ {} }}".format(user)) - user.password = password - token = accesTokenController.createAccesToken(user, ldap_conn) + token = accesTokenController.createAccesToken(user) debug.debug("accesstoken is {{ {} }}".format(token)) debug.info("validate accesstoken") dic = accesTokenController.validateAccessToken( @@ -194,6 +194,6 @@ def _login(): except PermissionDenied as err: debug.warning("permission denied exception in logout", exc_info=True) return jsonify({"error": str(err)}), 401 - except Exception: + except Exception as err: debug.warning("exception in logout.", exc_info=True) return jsonify({"error": "permission denied"}), 401 From c957195ffb83fc946eb35480e5d66913a47c1923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 5 Jun 2020 00:34:32 +0200 Subject: [PATCH 091/111] =?UTF-8?q?user=20kann=20seine=20accessToken=20abr?= =?UTF-8?q?ufen=20und=20l=C3=B6schen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/controller/accesTokenController.py | 10 +++++-- .../dbAccessTokenController.py | 26 ++++++++++++++----- geruecht/model/accessToken.py | 25 +++++++++++++++++- geruecht/routes.py | 4 +-- geruecht/user/routes.py | 25 +++++++++++++++++- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index a662e56..58675e2 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -79,7 +79,7 @@ class AccesTokenController(metaclass=Singleton): debug.debug("no valid accesstoken with token: {{ {} }} and group: {{ {} }}".format(token, group)) return False - def createAccesToken(self, user): + def createAccesToken(self, user, user_agent=None): """ Create an AccessToken Create an AccessToken for an User and add it to the tokenList. @@ -94,7 +94,7 @@ class AccesTokenController(metaclass=Singleton): now = datetime.ctime(datetime.now()) token = hashlib.md5((now + user.dn).encode('utf-8')).hexdigest() self.checkBar(user) - accToken = db.createAccessToken(user, token, self.lifetime, datetime.now(), lock_bar=False) + accToken = db.createAccessToken(user, token, self.lifetime, datetime.now(), lock_bar=False, user_agent=user_agent) debug.debug("accesstoken is {{ {} }}".format(accToken)) return token @@ -115,6 +115,12 @@ class AccesTokenController(metaclass=Singleton): if group in accToken.user.group: return True return False + def getAccessTokensFromUser(self, user): + return db.getAccessTokensFromUser(user) + + def deleteAccessToken(self, accToken): + db.deleteAccessToken(accToken) + def updateAccessToken(self, accToken): accToken.updateTimestamp() return db.updateAccessToken(accToken) diff --git a/geruecht/controller/databaseController/dbAccessTokenController.py b/geruecht/controller/databaseController/dbAccessTokenController.py index 230dd37..2182976 100644 --- a/geruecht/controller/databaseController/dbAccessTokenController.py +++ b/geruecht/controller/databaseController/dbAccessTokenController.py @@ -16,29 +16,43 @@ class Base: raise DatabaseExecption("item as no type int or str. name={}, type={}".format(item, type(item))) cursor.execute(sql) session = cursor.fetchone() - retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp']) if session != None else None + retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp'], browser=session['browser'], platform=session['platform']) if session != None else None return retVal except Exception as err: traceback.print_exc() self.db.connection.rollback() raise DatabaseExecption("Something went worng with Databes: {}".format(err)) - def getAccessTokens(self): + def getAccessTokensFromUser(self, user): try: cursor = self.db.connection.cursor() - cursor.execute("select * from session") + cursor.execute("select * from session where user={}".format(user.id)) sessions = cursor.fetchall() - retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp']) for session in sessions] + retVal = [ + AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], + session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] return retVal except Exception as err: traceback.print_exc() self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) - def createAccessToken(self, user, token, lifetime, timestamp, lock_bar): + def getAccessTokens(self): try: cursor = self.db.connection.cursor() - cursor.execute("insert into session (user, timestamp, lock_bar, token, lifetime) VALUES ({}, '{}', {}, '{}', {})".format(user.id, timestamp, lock_bar, token, lifetime)) + cursor.execute("select * from session") + sessions = cursor.fetchall() + retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] + return retVal + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + + def createAccessToken(self, user, token, lifetime, timestamp, lock_bar, user_agent=None): + try: + cursor = self.db.connection.cursor() + cursor.execute("insert into session (user, timestamp, lock_bar, token, lifetime, browser, platform) VALUES ({}, '{}', {}, '{}', {}, '{}', '{}')".format(user.id, timestamp, lock_bar, token, lifetime, user_agent.browser if user_agent else 'NULL', user_agent.platform if user_agent else 'NULL')) self.db.connection.commit() return self.getAccessToken(token) except Exception as err: diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 05f18ce..91586c9 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -16,7 +16,7 @@ class AccessToken(): user = None token = None - def __init__(self, id, user, token, lifetime, timestamp=datetime.now()): + def __init__(self, id, user, token, lifetime, timestamp=datetime.now(), browser=None, platform=None): """ Initialize Class AccessToken No more to say. @@ -33,6 +33,8 @@ class AccessToken(): self.lifetime = lifetime self.token = token self.lock_bar = False + self.browser = browser + self.platform = platform debug.debug("accesstoken is {{ {} }}".format(self)) def updateTimestamp(self): @@ -43,6 +45,27 @@ class AccessToken(): debug.debug("update timestamp from accesstoken {{ {} }}".format(self)) self.timestamp = datetime.now() + def toJSON(self): + """ Create Dic to dump in JSON + + Returns: + A Dic with static Attributes. + """ + dic = { + "id": self.id, + "timestamp": {'year': self.timestamp.year, + 'month': self.timestamp.month, + 'day': self.timestamp.day, + 'hour': self.timestamp.hour, + 'minute': self.timestamp.minute, + 'second': self.timestamp.second + }, + "lifetime": self.lifetime, + "browser": self.browser, + "platform": self.platform + } + return dic + def __eq__(self, token): return True if self.token == token else False diff --git a/geruecht/routes.py b/geruecht/routes.py index 6a911c4..c8f7810 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -144,7 +144,6 @@ def _saveLifeTime(**kwargs): "exception in save lifetime for accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 - @app.route("/logout", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER], bar=True) def _logout(**kwargs): @@ -178,10 +177,11 @@ def _login(): password = data['password'] debug.debug("username is {{ {} }}".format(username)) try: + user_agent = request.user_agent debug.info("search {{ {} }} in database".format(username)) user = mainController.loginUser(username, password) debug.debug("user is {{ {} }}".format(user)) - token = accesTokenController.createAccesToken(user) + token = accesTokenController.createAccesToken(user, user_agent=user_agent) debug.debug("accesstoken is {{ {} }}".format(token)) debug.info("validate accesstoken") dic = accesTokenController.validateAccessToken( diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index e433798..6744d21 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -1,14 +1,17 @@ from flask import Blueprint, request, jsonify from geruecht.decorator import login_required import geruecht.controller.mainController as mc +import geruecht.controller.accesTokenController as ac from geruecht.model import USER from datetime import datetime, time, date from geruecht.exceptions import DayLocked from geruecht.logger import getDebugLogger, getCreditLogger, getJobsLogger +from geruecht.model.accessToken import AccessToken user = Blueprint("user", __name__) mainController = mc.MainController() +accesTokenController = ac.AccesTokenController() debug = getDebugLogger() creditL = getCreditLogger() @@ -385,4 +388,24 @@ def _deleteJobRequest(**kwargs): return jsonify(retVal) except Exception as err: debug.debug("exception", exc_info=True) - return jsonify({"error": str(err)}), 500 \ No newline at end of file + return jsonify({"error": str(err)}), 500 + + +@user.route("/user/getAccessTokens", methods=['GET', 'POST']) +@login_required(groups=[USER]) +def _getAccessTokens(**kwargs): + try: + debug.info("/user/getAccessTokens") + if request.method == 'POST': + data = request.get_json() + delAccToken = AccessToken(data['id'], kwargs['accToken'].user, None, None, None) + accesTokenController.deleteAccessToken(delAccToken) + tokens = accesTokenController.getAccessTokensFromUser(kwargs['accToken'].user) + retVal = [] + for token in tokens: + retVal.append(token.toJSON()) + debug.debug("return {{ {} }}".format(retVal)) + return jsonify(retVal) + except Exception as err: + debug.debug("exception", exc_info=True) + return jsonify({"error": str(err)}), 500 From 2803831784d879c065933ed1ee3330f83232402f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 5 Jun 2020 01:17:39 +0200 Subject: [PATCH 092/111] =?UTF-8?q?user=20kann=20ab=20jetzt=20sein=20passw?= =?UTF-8?q?ord=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mainController/mainUserController.py | 41 ++++++++++++------- geruecht/user/routes.py | 4 +- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py index b46f7e7..a021a11 100644 --- a/geruecht/controller/mainController/mainUserController.py +++ b/geruecht/controller/mainController/mainUserController.py @@ -1,3 +1,5 @@ +from ldap3.core.exceptions import LDAPPasswordIsMandatoryError, LDAPBindError + from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, PermissionDenied import geruecht.controller.databaseController as dc import geruecht.controller.ldapController as lc @@ -114,22 +116,27 @@ class Base: debug.debug("user is {{ {} }}".format(user)) return user - def modifyUser(self, user, ldap_conn, attributes): - debug.info("modify user {{ {} }} with attributes {{ {} }} with ldap_conn {{ {} }}".format( - user, attributes, ldap_conn)) + def modifyUser(self, user, attributes, password): + debug.info("modify user {{ {} }} with attributes {{ {} }}".format( + user, attributes)) + try: - if 'username' in attributes: - debug.debug("change username, so change first in database") - db.changeUsername(user, attributes['username']) - ldap.modifyUser(user, ldap_conn, attributes) - if 'username' in attributes: - retVal = self.getUser(attributes['username']) - debug.debug("user is {{ {} }}".format(retVal)) - return retVal - else: - retVal = self.getUser(user.uid) - debug.debug("user is {{ {} }}".format(retVal)) - return retVal + ldap_conn = ldap.bind(user, password) + if attributes: + if 'username' in attributes: + debug.debug("change username, so change first in database") + db.changeUsername(user, attributes['username']) + ldap.modifyUser(user, ldap_conn, attributes) + if 'username' in attributes: + retVal = self.getUser(attributes['username']) + debug.debug("user is {{ {} }}".format(retVal)) + return retVal + else: + retVal = self.getUser(user.uid) + debug.debug("user is {{ {} }}".format(retVal)) + return retVal + return self.getUser(user.uid) + except UsernameExistLDAP as err: debug.debug( "username exists on ldap, rechange username on database", exc_info=True) @@ -139,6 +146,10 @@ class Base: if 'username' in attributes: db.changeUsername(user, user.uid) raise Exception(err) + except LDAPPasswordIsMandatoryError as err: + raise Exception('Password wurde nicht gesetzt!!') + except LDAPBindError as err: + raise Exception('Password ist falsch') except Exception as err: raise Exception(err) diff --git a/geruecht/user/routes.py b/geruecht/user/routes.py index 6744d21..6b5e3c2 100644 --- a/geruecht/user/routes.py +++ b/geruecht/user/routes.py @@ -69,8 +69,10 @@ def _saveConfig(**kwargs): if 'accToken' in kwargs: accToken = kwargs['accToken'] data = request.get_json() + password = data['acceptedPassword'] + data.pop('acceptedPassword') accToken.user = mainController.modifyUser( - accToken.user, accToken.ldap_conn, data) + accToken.user, data, password) retVal = accToken.user.toJSON() retVal['creditList'] = {credit.year: credit.toJSON() for credit in accToken.user.geruechte} From 93fcbe72ae515641b35ba2fc369d57b5918a8b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 5 Jun 2020 22:53:27 +0200 Subject: [PATCH 093/111] fixed ##279 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wenn user noch nicht in der datenbank existiert, wird er hinzugefügt. wenn er noch kein gerücht besitzt, wird eins angelegt. --- geruecht/baruser/routes.py | 1 + .../databaseController/dbCreditListController.py | 10 +++++++--- .../mainController/mainUserController.py | 1 + required.txt | 15 +++++++-------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 61e793c..e86a6c4 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -210,3 +210,4 @@ def _lockbar(**kwargs): accToken.lock_bar = data['value'] debug.debug('return {{ "value": {} }}'.format(accToken.lock_bar)) return jsonify({'value': accToken.lock_bar}) + diff --git a/geruecht/controller/databaseController/dbCreditListController.py b/geruecht/controller/databaseController/dbCreditListController.py index f8098b5..ddbc262 100644 --- a/geruecht/controller/databaseController/dbCreditListController.py +++ b/geruecht/controller/databaseController/dbCreditListController.py @@ -3,6 +3,7 @@ from datetime import datetime from geruecht.exceptions import DatabaseExecption from geruecht.model.creditList import CreditList +from geruecht.model.user import User class Base: @@ -10,12 +11,14 @@ class Base: try: cursor = self.db.connection.cursor() if 'year' in kwargs: - sql = "select * from creditList where user_id={} and year_date={}".format(user.id, kwargs['year']) + sql = "select * from creditList where user_id={} and year_date={}".format(user.id if type(user) is User else user, kwargs['year']) else: - sql = "select * from creditList where user_id={}".format(user.id) + sql = "select * from creditList where user_id={}".format(user.id if type(user) is User else user) cursor.execute(sql) data = cursor.fetchall() - if len(data) == 1: + if len(data) == 0: + return self.createCreditList(user_id=user.id, year=datetime.now().year) + elif len(data) == 1: return [CreditList(data[0])] else: return [CreditList(value) for value in data] @@ -30,6 +33,7 @@ class Base: cursor = self.db.connection.cursor() cursor.execute("insert into creditList (year_date, user_id) values ({},{})".format(year, user_id)) self.db.connection.commit() + return self.getCreditListFromUser(user_id) except Exception as err: traceback.print_exc() self.db.connection.rollback() diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py index a021a11..03371b8 100644 --- a/geruecht/controller/mainController/mainUserController.py +++ b/geruecht/controller/mainController/mainUserController.py @@ -4,6 +4,7 @@ from geruecht.exceptions import UsernameExistLDAP, LDAPExcetpion, PermissionDeni import geruecht.controller.databaseController as dc import geruecht.controller.ldapController as lc from geruecht.logger import getDebugLogger +from geruecht.model.user import User db = dc.DatabaseController() ldap = lc.LDAPController() diff --git a/required.txt b/required.txt index 9b2b3bc..1a818f2 100644 --- a/required.txt +++ b/required.txt @@ -1,15 +1,14 @@ -Click==7.0 -Flask==1.1.1 +click==7.1.2 +Flask==1.1.2 Flask-Cors==3.0.8 Flask-LDAPConn==0.10.1 Flask-MySQLdb==0.2.0 itsdangerous==1.1.0 -Jinja2==2.11.0 -ldap3==2.6.1 +Jinja2==2.11.2 +ldap3==2.7 MarkupSafe==1.1.1 mysqlclient==1.4.6 pyasn1==0.4.8 -PyMySQL==0.9.3 -PyYAML==5.3 -six==1.14.0 -Werkzeug==0.16.1 +PyYAML==5.3.1 +six==1.15.0 +Werkzeug==1.0.1 From 7d19c130713f04c3dca330716a5b1669afe09b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 5 Jun 2020 23:26:15 +0200 Subject: [PATCH 094/111] synchronisations mit Ldap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wenn userlänge des ldap nicht mit der der datenbank übereinstimmt wird die datenbank mit dem ldap synchronisiert. --- .../databaseController/dbCreditListController.py | 2 ++ geruecht/controller/mainController/mainUserController.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/geruecht/controller/databaseController/dbCreditListController.py b/geruecht/controller/databaseController/dbCreditListController.py index ddbc262..6e75cb7 100644 --- a/geruecht/controller/databaseController/dbCreditListController.py +++ b/geruecht/controller/databaseController/dbCreditListController.py @@ -9,6 +9,8 @@ from geruecht.model.user import User class Base: def getCreditListFromUser(self, user, **kwargs): try: + if user.uid == 'extern': + return [] cursor = self.db.connection.cursor() if 'year' in kwargs: sql = "select * from creditList where user_id={} and year_date={}".format(user.id if type(user) is User else user, kwargs['year']) diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py index 03371b8..5e6550f 100644 --- a/geruecht/controller/mainController/mainUserController.py +++ b/geruecht/controller/mainController/mainUserController.py @@ -78,8 +78,16 @@ class Base: debug.debug("updated config of user is {{ {} }}".format(retVal)) return retVal + def syncLdap(self): + debug.info('sync Users from Ldap') + ldap_users = ldap.getAllUser() + for user in ldap_users: + self.getUser(user['username']) + def getAllUsersfromDB(self, extern=True): debug.info("get all users from database") + if (len(ldap.getAllUser()) != len(db.getAllUser())): + self.syncLdap() users = db.getAllUser() debug.debug("users are {{ {} }}".format(users)) for user in users: From 18785dad91140cc331a2f3a50e49254cb9002173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 5 Jun 2020 23:43:16 +0200 Subject: [PATCH 095/111] fixed ##261 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hier wird nun der username auf casesensitiv überprüft. --- geruecht/controller/databaseController/dbUserController.py | 5 ++++- geruecht/controller/ldapController.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/geruecht/controller/databaseController/dbUserController.py b/geruecht/controller/databaseController/dbUserController.py index 08f2d9d..e88940a 100644 --- a/geruecht/controller/databaseController/dbUserController.py +++ b/geruecht/controller/databaseController/dbUserController.py @@ -38,7 +38,10 @@ class Base: retVal.initGeruechte(creditLists) if workgroups: retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) - return retVal + if retVal.uid == username: + return retVal + else: + return None except Exception as err: traceback.print_exc() self.db.connection.rollback() diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index ca204b2..6769995 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -52,12 +52,15 @@ class LDAPController(metaclass=Singleton): 'dn': self.ldap.connection.response[0]['dn'], 'firstname': user['givenName'][0], 'lastname': user['sn'][0], - 'uid': username, + 'uid': user['uid'][0], } if user['mail']: retVal['mail'] = user['mail'][0] debug.debug("user is {{ {} }}".format(retVal)) - return retVal + if retVal['uid'] == username: + return retVal + else: + raise Exception() except: debug.warning("exception in get user data from ldap", exc_info=True) raise PermissionDenied("No User exists with this uid.") From 622bbc546fb0afc96b96d1656461811769b880e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 6 Jun 2020 13:17:18 +0200 Subject: [PATCH 096/111] fixed set locked_bar in database, if user is no bar_user locked_bar is set automaticly to false --- geruecht/baruser/routes.py | 6 +++++- geruecht/controller/accesTokenController.py | 5 ++++- .../databaseController/dbAccessTokenController.py | 6 +++--- geruecht/model/accessToken.py | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index e86a6c4..2fc7b59 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -1,8 +1,9 @@ from flask import Blueprint, request, jsonify import geruecht.controller.ldapController as lc import geruecht.controller.mainController as mc +import geruecht.controller.accesTokenController as ac from datetime import datetime -from geruecht.model import BAR, MONEY, USER, VORSTAND +from geruecht.model import BAR, MONEY, USER, VORSTAND, EXTERN from geruecht.decorator import login_required from geruecht.logger import getDebugLogger, getCreditLogger @@ -13,6 +14,7 @@ baruser = Blueprint("baruser", __name__) ldap = lc.LDAPController() mainController = mc.MainController() +accesTokenController = ac.AccesTokenController() @baruser.route("/bar") @@ -208,6 +210,8 @@ def _lockbar(**kwargs): if request.method == "POST": data = request.get_json() accToken.lock_bar = data['value'] + accToken = accesTokenController.updateAccessToken(accToken) + accToken = accesTokenController.validateAccessToken(accToken.token, [USER, EXTERN]) debug.debug('return {{ "value": {} }}'.format(accToken.lock_bar)) return jsonify({'value': accToken.lock_bar}) diff --git a/geruecht/controller/accesTokenController.py b/geruecht/controller/accesTokenController.py index 58675e2..bec7703 100644 --- a/geruecht/controller/accesTokenController.py +++ b/geruecht/controller/accesTokenController.py @@ -39,10 +39,12 @@ class AccesTokenController(metaclass=Singleton): if BAR not in user.group: debug.debug("append bar to user {{ {} }}".format(user)) user.group.append(BAR) + return True else: while BAR in user.group: debug.debug("delete bar from user {{ {} }}".format(user)) user.group.remove(BAR) + return False debug.debug("user {{ {} }} groups are {{ {} }}".format(user, user.group)) def validateAccessToken(self, token, group): @@ -66,7 +68,8 @@ class AccesTokenController(metaclass=Singleton): if now <= endTime: debug.debug("check if token {{ {} }} is same as {{ {} }}".format(token, accToken)) if accToken == token: - self.checkBar(accToken.user) + if not self.checkBar(accToken.user): + accToken.lock_bar = False debug.debug("check if accestoken {{ {} }} has group {{ {} }}".format(accToken, group)) if self.isSameGroup(accToken, group): accToken.updateTimestamp() diff --git a/geruecht/controller/databaseController/dbAccessTokenController.py b/geruecht/controller/databaseController/dbAccessTokenController.py index 2182976..13ae442 100644 --- a/geruecht/controller/databaseController/dbAccessTokenController.py +++ b/geruecht/controller/databaseController/dbAccessTokenController.py @@ -16,7 +16,7 @@ class Base: raise DatabaseExecption("item as no type int or str. name={}, type={}".format(item, type(item))) cursor.execute(sql) session = cursor.fetchone() - retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp'], browser=session['browser'], platform=session['platform']) if session != None else None + retVal = AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], lock_bar=bool(session['lock_bar']),timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) if session != None else None return retVal except Exception as err: traceback.print_exc() @@ -30,7 +30,7 @@ class Base: sessions = cursor.fetchall() retVal = [ AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], - session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] + lock_bar=bool(session['lock_bar']), timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] return retVal except Exception as err: traceback.print_exc() @@ -42,7 +42,7 @@ class Base: cursor = self.db.connection.cursor() cursor.execute("select * from session") sessions = cursor.fetchall() - retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] + retVal = [AccessToken(session['id'], self.getUserById(session['user']), session['token'], session['lifetime'], lock_bar=bool(session['lock_bar']),timestamp=session['timestamp'], browser=session['browser'], platform=session['platform']) for session in sessions] return retVal except Exception as err: traceback.print_exc() diff --git a/geruecht/model/accessToken.py b/geruecht/model/accessToken.py index 91586c9..6e777f7 100644 --- a/geruecht/model/accessToken.py +++ b/geruecht/model/accessToken.py @@ -16,7 +16,7 @@ class AccessToken(): user = None token = None - def __init__(self, id, user, token, lifetime, timestamp=datetime.now(), browser=None, platform=None): + def __init__(self, id, user, token, lifetime, lock_bar=False, timestamp=datetime.now(), browser=None, platform=None): """ Initialize Class AccessToken No more to say. @@ -32,7 +32,7 @@ class AccessToken(): self.timestamp = timestamp self.lifetime = lifetime self.token = token - self.lock_bar = False + self.lock_bar = lock_bar self.browser = browser self.platform = platform debug.debug("accesstoken is {{ {} }}".format(self)) From 93978395e63e13ed6d7cf1f2e13cace8cb0e0598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 6 Jun 2020 15:51:14 +0200 Subject: [PATCH 097/111] =?UTF-8?q?emails=20werden=20jetzt=20versendet,=20?= =?UTF-8?q?wenn=20jemand=20jemanden=20einl=C3=A4dt=20oder=20nach=20einer?= =?UTF-8?q?=20job=C3=BCbertragung=20bittet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/controller/emailController.py | 22 +++++++++++++++---- .../mainController/mainJobInviteController.py | 6 ++++- .../mainJobRequestController.py | 8 +++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index 190ac07..b49b0e6 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -38,12 +38,24 @@ class EmailController(): def jobTransact(self, user, jobtransact): debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user)) - date = '{}.{}.{}'.format(jobtransact['date'].day, jobtransact['date'].month, jobtransact['date'].year) - from_user = '{} {}'.format(jobtransact['from_user'].firstname, jobtransact['from_user'].lastname) - subject = 'Bardienstanfrage am {}'.format(date) + date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year']) + from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname']) + job_kind = jobtransact['job_kind'] + subject = 'Dienstanfrage am {}'.format(date) text = MIMEText( "Hallo {} {},\n" - "{} fragt, ob du am {} zum Bardienst teilnehmen willst. Beantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'utf-8') + "{} fragt, ob du am {} den Dienst {} übernehmen willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date, job_kind['name']), 'utf-8') + debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) + return (subject, text) + + def jobInvite(self, user, jobtransact): + debug.info("create email jobtransact {{ {} }}for user {{ {} }}".format(jobtransact, user)) + date = '{}.{}.{}'.format(jobtransact['on_date']['day'], jobtransact['on_date']['month'], jobtransact['on_date']['year']) + from_user = '{} {}'.format(jobtransact['from_user']['firstname'], jobtransact['from_user']['lastname']) + subject = 'Diensteinladung am {}'.format(date) + text = MIMEText( + "Hallo {} {},\n" + "{} fragt, ob du am {} mit Dienst haben willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'utf-8') debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) @@ -77,6 +89,8 @@ class EmailController(): subject, text = self.credit(user) elif type == 'jobtransact': subject, text = self.jobTransact(user, jobtransact) + elif type == 'jobinvite': + subject, text = self.jobInvite(user, jobtransact) else: raise Exception("Fail to send Email. No type is set. user={}, type={} , jobtransact={}".format(user, type, jobtransact)) diff --git a/geruecht/controller/mainController/mainJobInviteController.py b/geruecht/controller/mainController/mainJobInviteController.py index 47a1ea0..ed8e50c 100644 --- a/geruecht/controller/mainController/mainJobInviteController.py +++ b/geruecht/controller/mainController/mainJobInviteController.py @@ -1,10 +1,12 @@ from datetime import date import geruecht.controller.databaseController as dc +import geruecht.controller.emailController as ec from geruecht import getDebugLogger db = dc.DatabaseController() debug = getDebugLogger() +emailController = ec.EmailController() class Base: def getJobInvites(self, from_user, to_user, date): @@ -25,7 +27,9 @@ class Base: to_user = jobInvite['to_user'] on_date = date(jobInvite['date']['year'], jobInvite['date']['month'], jobInvite['date']['day']) debug.info("set new JobInvite from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}") - retVal.append(db.setJobInvite(from_user, to_user, on_date)) + setJobInvite = db.setJobInvite(from_user, to_user, on_date) + retVal.append(setJobInvite) + emailController.sendMail(db.getUserById(to_user['id'], False), type='jobinvite', jobtransact=setJobInvite) debug.debug("seted JobInvites are {{ {} }}".format(retVal)) return retVal diff --git a/geruecht/controller/mainController/mainJobRequestController.py b/geruecht/controller/mainController/mainJobRequestController.py index 05529a6..e31845e 100644 --- a/geruecht/controller/mainController/mainJobRequestController.py +++ b/geruecht/controller/mainController/mainJobRequestController.py @@ -1,10 +1,11 @@ from datetime import date, time, datetime - +import geruecht.controller.emailController as ec import geruecht.controller.databaseController as dc from geruecht import getDebugLogger db = dc.DatabaseController() debug = getDebugLogger() +emailController = ec.EmailController() class Base: def getJobRequests(self, from_user, to_user, date): @@ -26,7 +27,10 @@ class Base: on_date = date(jobRequest['date']['year'], jobRequest['date']['month'], jobRequest['date']['day']) job_kind = jobRequest['job_kind'] debug.info("set new JobRequest from_user {{ {} }}, to_user {{ {} }}, on_date {{ {} }}") - retVal.append(db.setJobRequest(from_user, to_user, on_date, job_kind)) + setJobRequest = db.setJobRequest(from_user, to_user, on_date, job_kind) + retVal.append(setJobRequest) + emailController.sendMail(db.getUserById(to_user['id']), type='jobtransact', jobtransact=setJobRequest) + debug.debug("seted JobRequests are {{ {} }}".format(retVal)) return retVal From f1b957c6ea8fefa16eef45349b3cf587a5638875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 7 Jun 2020 23:28:27 +0200 Subject: [PATCH 098/111] =?UTF-8?q?last=5Fseen=20hinzugef=C3=BCgt.=20diese?= =?UTF-8?q?r=20wert=20sagt=20an,=20wann=20das=20letzte=20mal=20vom=20bardi?= =?UTF-8?q?enst=20etwas=20hinzugef=C3=BCgt=20wird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/baruser/routes.py | 3 +++ .../databaseController/dbUserController.py | 15 ++++++++++++++- .../mainController/mainCreditListController.py | 2 ++ geruecht/model/user.py | 3 +++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 2fc7b59..d078bc6 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -50,6 +50,7 @@ def _bar(**kwargs): "type": type, "limit": user.limit } + dic[user.uid]['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, "hour": user.last_seen.hour, "minute": user.last_seen.minute, "second": user.last_seen.second} if user.last_seen else None debug.debug("return {{ {} }}".format(dic)) return jsonify(dic) except Exception as err: @@ -89,6 +90,7 @@ def _baradd(**kwargs): dic = user.toJSON() dic['amount'] = all dic['type'] = type + dic['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, "hour": user.last_seen.hour, "minute": user.last_seen.minute, "second": user.last_seen.second} if user.last_seen else None debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} fügt {} {} {} € Schulden hinzu.".format( date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, user.firstname, user.lastname, amountl/100)) @@ -178,6 +180,7 @@ def _getUser(**kwargs): retVal = user.toJSON() retVal['amount'] = amount retVal['type'] = type + debug.debug("return {{ {} }}".format(retVal)) return jsonify(retVal) except Exception as err: diff --git a/geruecht/controller/databaseController/dbUserController.py b/geruecht/controller/databaseController/dbUserController.py index e88940a..bc00b16 100644 --- a/geruecht/controller/databaseController/dbUserController.py +++ b/geruecht/controller/databaseController/dbUserController.py @@ -92,7 +92,6 @@ class Base: def updateUser(self, user): try: cursor = self.db.connection.cursor() - print('uid: {}; group: {}'.format(user.uid, user.group)) groups = self._convertGroupToString(user.group) sql = "update user set dn='{}', firstname='{}', lastname='{}', gruppe='{}', lockLimit={}, locked={}, autoLock={}, mail='{}' where uid='{}'".format( user.dn, user.firstname, user.lastname, groups, user.limit, user.locked, user.autoLock, user.mail, user.uid) @@ -104,6 +103,20 @@ class Base: self.db.connection.rollback() raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + def updateLastSeen(self, user, time): + try: + cursor = self.db.connection.cursor() + sql = "update user set last_seen='{}' where uid='{}'".format( + time, user.uid) + print(sql) + cursor.execute(sql) + self.db.connection.commit() + + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Datatabase: {}".format(err)) + def changeUsername(self, user, newUsername): try: cursor= self.db.connection.cursor() diff --git a/geruecht/controller/mainController/mainCreditListController.py b/geruecht/controller/mainController/mainCreditListController.py index f6e24c2..844206b 100644 --- a/geruecht/controller/mainController/mainCreditListController.py +++ b/geruecht/controller/mainController/mainCreditListController.py @@ -41,6 +41,8 @@ class Base: debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format( user.locked, finanzer)) user.addAmount(amount, year=year, month=month) + user.last_seen = datetime.now() + db.updateLastSeen(user, user.last_seen) creditLists = user.updateGeruecht() debug.debug("creditList is {{ {} }}".format(creditLists)) for creditList in creditLists: diff --git a/geruecht/model/user.py b/geruecht/model/user.py index 1af0ffe..543859c 100644 --- a/geruecht/model/user.py +++ b/geruecht/model/user.py @@ -28,6 +28,9 @@ class User(): self.firstname = data['firstname'] self.lastname = data['lastname'] self.group = data['gruppe'] + self.last_seen = None + if 'last_seen' in data: + self.last_seen = data['last_seen'] if 'statusgroup' in data: self.statusgroup = data['statusgroup'] else: From f4ab34c298198e8ae9ad606e7f2f2abed6e70634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 7 Jun 2020 23:43:52 +0200 Subject: [PATCH 099/111] =?UTF-8?q?erstellt=20verzeichnisse=20f=C3=BCrs=20?= =?UTF-8?q?log,=20falls=20diese=20nicht=20existieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/logger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/geruecht/logger.py b/geruecht/logger.py index f63ef04..c428a18 100644 --- a/geruecht/logger.py +++ b/geruecht/logger.py @@ -1,13 +1,15 @@ import logging import logging.config import yaml -from os import path +from os import path, makedirs, getcwd if not path.exists("geruecht/log/debug"): a = path.join(path.curdir, "geruecht", "log", "debug") + makedirs(a) if not path.exists("geruecht/log/info"): b = path.join(path.curdir, "geruecht", "log", "info") + makedirs(b) with open("geruecht/logging.yml", 'rt') as file: From c7e1c981e6aa94025a90ec895a44c992964880c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 8 Jun 2020 00:03:27 +0200 Subject: [PATCH 100/111] =?UTF-8?q?fixed=20bug,=20dass=20auch=20der=20fina?= =?UTF-8?q?nzer=20schulden=20hinzuf=C3=BCgen=20kann=20ohne=20dass=20last?= =?UTF-8?q?=5Fseen=20aktualisiert=20wird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/baruser/routes.py | 2 +- .../controller/mainController/mainCreditListController.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index d078bc6..484cf4f 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -77,7 +77,7 @@ def _baradd(**kwargs): amountl = amount date = datetime.now() mainController.addAmount( - userID, amount, year=date.year, month=date.month) + userID, amount, year=date.year, month=date.month, bar=True) user = mainController.getUser(userID) geruecht = user.getGeruecht(year=date.year) month = geruecht.getMonth(month=date.month) diff --git a/geruecht/controller/mainController/mainCreditListController.py b/geruecht/controller/mainController/mainCreditListController.py index 844206b..7e6f19f 100644 --- a/geruecht/controller/mainController/mainCreditListController.py +++ b/geruecht/controller/mainController/mainCreditListController.py @@ -29,7 +29,7 @@ class Base: user.updateData({'locked': False}) db.updateUser(user) - def addAmount(self, username, amount, year, month, finanzer=False): + def addAmount(self, username, amount, year, month, finanzer=False, bar=False): debug.info("add amount {{ {} }} to user {{ {} }} no month {{ {} }}, year {{ {} }}".format( amount, username, month, year)) user = self.getUser(username) @@ -41,8 +41,9 @@ class Base: debug.debug("user is not locked {{ {} }} or is finanzer execution {{ {} }}".format( user.locked, finanzer)) user.addAmount(amount, year=year, month=month) - user.last_seen = datetime.now() - db.updateLastSeen(user, user.last_seen) + if bar: + user.last_seen = datetime.now() + db.updateLastSeen(user, user.last_seen) creditLists = user.updateGeruecht() debug.debug("creditList is {{ {} }}".format(creditLists)) for creditList in creditLists: From ba16743fdec66107d576bade1ac06b6e1d78e686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 8 Jun 2020 16:41:48 +0200 Subject: [PATCH 101/111] fixed bug, sodass auch ein user sich einloggen kann, wenn er noch nicht in der datenbank ist --- .../controller/databaseController/dbCreditListController.py | 5 +++-- geruecht/controller/databaseController/dbUserController.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/geruecht/controller/databaseController/dbCreditListController.py b/geruecht/controller/databaseController/dbCreditListController.py index 6e75cb7..62a3528 100644 --- a/geruecht/controller/databaseController/dbCreditListController.py +++ b/geruecht/controller/databaseController/dbCreditListController.py @@ -9,8 +9,9 @@ from geruecht.model.user import User class Base: def getCreditListFromUser(self, user, **kwargs): try: - if user.uid == 'extern': - return [] + if type(user) is User: + if user.uid == 'extern': + return [] cursor = self.db.connection.cursor() if 'year' in kwargs: sql = "select * from creditList where user_id={} and year_date={}".format(user.id if type(user) is User else user, kwargs['year']) diff --git a/geruecht/controller/databaseController/dbUserController.py b/geruecht/controller/databaseController/dbUserController.py index bc00b16..3e419d4 100644 --- a/geruecht/controller/databaseController/dbUserController.py +++ b/geruecht/controller/databaseController/dbUserController.py @@ -38,8 +38,9 @@ class Base: retVal.initGeruechte(creditLists) if workgroups: retVal.workgroups = self.getWorkgroupsOfUser(retVal.id) - if retVal.uid == username: - return retVal + if retVal: + if retVal.uid == username: + return retVal else: return None except Exception as err: From 4384d18fab4d8ce23eceb038156ef1fa89862aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Jun 2020 20:25:29 +0200 Subject: [PATCH 102/111] =?UTF-8?q?registrierung=20wurde=20hinzugef=C3=BCg?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/__init__.py | 2 ++ .../controller/databaseController/__init__.py | 3 +- .../dbRegistrationController.py | 32 +++++++++++++++++++ .../controller/mainController/__init__.py | 3 +- .../mainRegistrationController.py | 14 ++++++++ geruecht/registration_route.py | 15 +++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 geruecht/controller/databaseController/dbRegistrationController.py create mode 100644 geruecht/controller/mainController/mainRegistrationController.py create mode 100644 geruecht/registration_route.py diff --git a/geruecht/__init__.py b/geruecht/__init__.py index 0b4d1da..d2eb11d 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -39,6 +39,7 @@ from geruecht.finanzer.routes import finanzer from geruecht.user.routes import user from geruecht.vorstand.routes import vorstand from geruecht.gastro.routes import gastrouser +from geruecht.registration_route import registration DEBUG.info("Registrate bluebrints") app.register_blueprint(baruser) @@ -46,3 +47,4 @@ app.register_blueprint(finanzer) app.register_blueprint(user) app.register_blueprint(vorstand) app.register_blueprint(gastrouser) +app.register_blueprint(registration) diff --git a/geruecht/controller/databaseController/__init__.py b/geruecht/controller/databaseController/__init__.py index 08ee142..8f783fd 100644 --- a/geruecht/controller/databaseController/__init__.py +++ b/geruecht/controller/databaseController/__init__.py @@ -1,6 +1,6 @@ from ..mainController import Singleton from geruecht import db -from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController, dbAccessTokenController +from ..databaseController import dbUserController, dbCreditListController, dbJobKindController, dbPricelistController, dbWorkerController, dbWorkgroupController, dbJobInviteController, dbJobRequesController, dbAccessTokenController, dbRegistrationController from geruecht.exceptions import DatabaseExecption import traceback from MySQLdb._exceptions import IntegrityError @@ -14,6 +14,7 @@ class DatabaseController(dbUserController.Base, dbJobInviteController.Base, dbJobRequesController.Base, dbAccessTokenController.Base, + dbRegistrationController.Base, metaclass=Singleton): ''' DatabaesController diff --git a/geruecht/controller/databaseController/dbRegistrationController.py b/geruecht/controller/databaseController/dbRegistrationController.py new file mode 100644 index 0000000..39aa2e1 --- /dev/null +++ b/geruecht/controller/databaseController/dbRegistrationController.py @@ -0,0 +1,32 @@ +import traceback +from geruecht.exceptions import DatabaseExecption + +class Base: + def setNewRegistration(self, data): + try: + cursor = self.db.connection.cursor() + if data['entryDate']: + sql = "insert into registration_list (firstname, lastname, clubname, email, keynumber, birthdate, entrydate) VALUES ('{}', '{}', '{}', '{}', {}, '{}', '{}')".format( + data['firstName'], + data['lastName'], + data['clubName'] if data['clubName'] else 'NULL', + data['mail'], + data['keynumber'] if data['keynumber'] else 'NULL', + data['birthDate'], + data['entryDate'] + ) + else: + sql = "insert into registration_list (firstname, lastname, clubname, email, keynumber, birthdate) VALUES ('{}', '{}', '{}', '{}', {}, '{}')".format( + data['firstName'], + data['lastName'], + data['clubName'] if data['clubName'] else 'NULL', + data['mail'], + data['keynumber'] if data['keynumber'] else 'NULL', + data['birthDate'] + ) + cursor.execute(sql) + self.db.connection.commit() + except Exception as err: + traceback.print_exc() + self.db.connection.rollback() + raise DatabaseExecption("Something went worng with Databes: {}".format(err)) \ No newline at end of file diff --git a/geruecht/controller/mainController/__init__.py b/geruecht/controller/mainController/__init__.py index 41f2a7f..602f6ab 100644 --- a/geruecht/controller/mainController/__init__.py +++ b/geruecht/controller/mainController/__init__.py @@ -5,7 +5,7 @@ import geruecht.controller.emailController as ec from geruecht.model.user import User from datetime import datetime, timedelta from geruecht.logger import getDebugLogger -from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController +from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController, mainRegistrationController db = dc.DatabaseController() ldap = lc.LDAPController() @@ -22,6 +22,7 @@ class MainController(mainJobKindController.Base, mainWorkgroupController.Base, mainJobInviteController.Base, mainJobRequestController.Base, + mainRegistrationController.Base, metaclass=Singleton): def __init__(self): diff --git a/geruecht/controller/mainController/mainRegistrationController.py b/geruecht/controller/mainController/mainRegistrationController.py new file mode 100644 index 0000000..a32893f --- /dev/null +++ b/geruecht/controller/mainController/mainRegistrationController.py @@ -0,0 +1,14 @@ +from datetime import date + +import geruecht.controller.databaseController as dc +from geruecht.logger import getDebugLogger + +db = dc.DatabaseController() +debug = getDebugLogger() + +class Base: + def setNewRegistration(self, data): + debug.info("set new registration {{ {} }}".format(data)) + data['birthDate'] = date(int(data['birthDate']['year']), int(data['birthDate']['month']), int(data['birthDate']['day'])) + data['entryDate'] = date(int(data['entryDate']['year']), int(data['entryDate']['month']), int(data['entryDate']['day'])) if data['entryDate'] else None + db.setNewRegistration(data) \ No newline at end of file diff --git a/geruecht/registration_route.py b/geruecht/registration_route.py new file mode 100644 index 0000000..8a4bed1 --- /dev/null +++ b/geruecht/registration_route.py @@ -0,0 +1,15 @@ +from flask import Blueprint, request, jsonify +import geruecht.controller.mainController as mc +from geruecht.logger import getDebugLogger + +registration = Blueprint("registration", __name__) + +mainController = mc.MainController() + +debug = getDebugLogger() + +@registration.route("/registration", methods=['PUT']) +def __registration(): + data = request.get_json() + mainController.setNewRegistration(data) + return jsonify({"ok":"ok"}) \ No newline at end of file From 2a3e32f70b50ccdca08db19ccc4898dce76896a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Jun 2020 20:51:35 +0200 Subject: [PATCH 103/111] read as byte --- geruecht/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geruecht/logger.py b/geruecht/logger.py index c428a18..0348262 100644 --- a/geruecht/logger.py +++ b/geruecht/logger.py @@ -12,7 +12,7 @@ if not path.exists("geruecht/log/info"): makedirs(b) -with open("geruecht/logging.yml", 'rt') as file: +with open("geruecht/logging.yml", 'rb') as file: config = yaml.safe_load(file.read()) logging.config.dictConfig(config) From 8fa46f0eb3ccb2dd9e42e54002e4fb9e4cb8b85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Jun 2020 22:31:33 +0200 Subject: [PATCH 104/111] ldapgruppen werden richtig ausgelesen --- geruecht/controller/ldapController.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index 6769995..e90f374 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -98,6 +98,8 @@ class LDAPController(metaclass=Singleton): retVal.append(BAR) elif group_name == 'vorstand': retVal.append(VORSTAND) + elif group_name == 'ldap-user': + retVal.append(USER) debug.debug("groups are {{ {} }}".format(retVal)) return retVal except Exception as err: From 4481f4707d65b84a11978e208b31411d96791c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Fri, 19 Jun 2020 22:23:46 +0200 Subject: [PATCH 105/111] fixed bug dass last_seen status auch beim stornieren gesendet wird --- geruecht/baruser/routes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index 484cf4f..ababcfc 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -154,6 +154,9 @@ def _storno(**kwargs): dic = user.toJSON() dic['amount'] = all dic['type'] = type + dic['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, + "hour": user.last_seen.hour, "minute": user.last_seen.minute, + "second": user.last_seen.second} if user.last_seen else None debug.debug("return {{ {} }}".format(dic)) creditL.info("{} Baruser {} {} storniert {} € von {} {}".format( date, kwargs['accToken'].user.firstname, kwargs['accToken'].user.lastname, amountl/100, user.firstname, user.lastname)) From 0d04bcbce51d3392cf79f0761285f7e49e25c7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 23 Jun 2020 22:00:09 +0200 Subject: [PATCH 106/111] sendet dem baruser auch den autolock status des users zu --- geruecht/baruser/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geruecht/baruser/routes.py b/geruecht/baruser/routes.py index ababcfc..dc7c383 100644 --- a/geruecht/baruser/routes.py +++ b/geruecht/baruser/routes.py @@ -48,7 +48,8 @@ def _bar(**kwargs): "amount": all, "locked": user.locked, "type": type, - "limit": user.limit + "limit": user.limit, + "autoLock": user.autoLock } dic[user.uid]['last_seen'] = {"year": user.last_seen.year, "month": user.last_seen.month, "day": user.last_seen.day, "hour": user.last_seen.hour, "minute": user.last_seen.minute, "second": user.last_seen.second} if user.last_seen else None debug.debug("return {{ {} }}".format(dic)) From d474ef49e8f8adc70e2f2ac0a6d86336b01b8bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 23 Jun 2020 22:23:07 +0200 Subject: [PATCH 107/111] =?UTF-8?q?fixed=20bug,dass=20passw=C3=B6rter=20ni?= =?UTF-8?q?cht=20mit=20log=20auftauchen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geruecht/controller/ldapController.py | 4 ++-- geruecht/controller/mainController/mainUserController.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index e90f374..bc85d8f 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -170,7 +170,7 @@ class LDAPController(metaclass=Singleton): return retVal def modifyUser(self, user, conn, attributes): - debug.info("modify ldap data from user {{ {} }} with attributes {{ {} }}".format(user, attributes)) + debug.info("modify ldap data from user {{ {} }} with attributes (can't show because here can be a password)".format(user)) try: if 'username' in attributes: debug.debug("change username") @@ -191,7 +191,7 @@ class LDAPController(metaclass=Singleton): if 'password' in attributes: salted_password = hashed(HASHED_SALTED_MD5, attributes['password']) mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])] - debug.debug("modyfier are {{ {} }}".format(mody)) + debug.debug("modyfier are (can't show because here can be a password)") conn.modify(user.dn, mody) except Exception as err: debug.warning("exception in modify user data from ldap", exc_info=True) diff --git a/geruecht/controller/mainController/mainUserController.py b/geruecht/controller/mainController/mainUserController.py index 5e6550f..135663d 100644 --- a/geruecht/controller/mainController/mainUserController.py +++ b/geruecht/controller/mainController/mainUserController.py @@ -126,8 +126,8 @@ class Base: return user def modifyUser(self, user, attributes, password): - debug.info("modify user {{ {} }} with attributes {{ {} }}".format( - user, attributes)) + debug.info("modify user {{ {} }} with attributes (can't show because here can be a password)".format( + user)) try: ldap_conn = ldap.bind(user, password) From f87d7b9e5d18d52df3df6ddb203f116ab7d31c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 28 Jun 2020 12:31:58 +0200 Subject: [PATCH 108/111] =?UTF-8?q?passwordreset=20hinzugef=C3=BCgt=20und?= =?UTF-8?q?=20gitignore=20verbessert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + geruecht/config.yml.example | 19 +++++++++ geruecht/configparser.py | 15 +++++++ geruecht/controller/emailController.py | 21 ++++++++-- .../controller/mainController/__init__.py | 3 +- .../mainController/mainPasswordReset.py | 39 +++++++++++++++++++ geruecht/routes.py | 16 ++++++++ 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 geruecht/config.yml.example create mode 100644 geruecht/controller/mainController/mainPasswordReset.py diff --git a/.gitignore b/.gitignore index 24662c6..f48f2f7 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,4 @@ dmypy.json # custom test_pricelist/ test_project/ +geruecht.config.yml diff --git a/geruecht/config.yml.example b/geruecht/config.yml.example new file mode 100644 index 0000000..06f3e71 --- /dev/null +++ b/geruecht/config.yml.example @@ -0,0 +1,19 @@ +AccessTokenLifeTime: 1800 +Database: + URL: + user: + passwd: + database: +LDAP: + URL: + dn: + USER_DN: + ADMIN_DN: + ADMIN_SECRET: +Mail: + URL: + port: + user: + passwd: + email: + crypt: SSL/STARTLS \ No newline at end of file diff --git a/geruecht/configparser.py b/geruecht/configparser.py index 2106e93..b2835c7 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -42,6 +42,21 @@ class ConifgParser(): DEBUG.info( 'No Config for port in LDAP found. Set it to default: {}'.format(389)) self.config['LDAP']['port'] = 389 + if 'ADMIN_DN' not in self.config['LDAP']: + DEBUG.info( + 'No Config for ADMIN_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) + ) + self.config['LDAP']['ADMIN_DN'] = None + if 'ADMIN_SECRET' not in self.config['LDAP']: + DEBUG.info( + 'No Config for ADMIN_SECRET in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) + ) + self.config['LDAP']['ADMIN_SECRET'] = None + if 'USER_DN' not in self.config['LDAP']: + DEBUG.info( + 'No Config for USER_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) + ) + self.config['LDAP']['USER_DN'] = None self.ldap = self.config['LDAP'] DEBUG.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: diff --git a/geruecht/controller/emailController.py b/geruecht/controller/emailController.py index b49b0e6..067a34d 100644 --- a/geruecht/controller/emailController.py +++ b/geruecht/controller/emailController.py @@ -44,7 +44,7 @@ class EmailController(): subject = 'Dienstanfrage am {}'.format(date) text = MIMEText( "Hallo {} {},\n" - "{} fragt, ob du am {} den Dienst {} übernehmen willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date, job_kind['name']), 'utf-8') + "{} fragt, ob du am {} den Dienst {} übernehmen willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date, job_kind['name']), 'plain') debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) @@ -55,7 +55,7 @@ class EmailController(): subject = 'Diensteinladung am {}'.format(date) text = MIMEText( "Hallo {} {},\n" - "{} fragt, ob du am {} mit Dienst haben willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'utf-8') + "{} fragt, ob du am {} mit Dienst haben willst.\nBeantworte die Anfrage im Userportal von Flaschengeist.".format(user.firstname, user.lastname, from_user, date), 'plain') debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) @@ -71,11 +71,22 @@ class EmailController(): add = '' text = MIMEText( "Hallo {} {},\nDu hast {} im Wert von {:.2f} €. {}\n\nDiese Nachricht wurde automatisch erstellt.".format( - user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain', 'utf-8') + user.firstname, user.lastname, type, abs(sum) / 100, add), 'plain') debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) return (subject, text) - def sendMail(self, user, type='credit', jobtransact=None): + def passwordReset(self, user, data): + debug.info("create email passwort reset for user {{ {} }}".format(user)) + subject = Header("Password vergessen") + text = MIMEText( + "Hallo {} {},\nDu hast dein Password vergessen!\nDies wurde nun mit Flaschengeist zurückgesetzt.\nDein neues Passwort lautet:\n{}\n\nBitte ändere es sofort in deinem Flaschengeistprolif in https://flaschengeist.wu5.de.".format( + user.firstname, user.lastname, data['password'] + ), 'plain' + ) + debug.debug("subject is {{ {} }}, text is {{ {} }}".format(subject, text.as_string())) + return (subject, text) + + def sendMail(self, user, type='credit', jobtransact=None, **kwargs): debug.info("send email to user {{ {} }}".format(user)) try: if user.mail == 'None' or not user.mail: @@ -91,6 +102,8 @@ class EmailController(): subject, text = self.jobTransact(user, jobtransact) elif type == 'jobinvite': subject, text = self.jobInvite(user, jobtransact) + elif type == 'passwordReset': + subject, text = self.passwordReset(user, kwargs) else: raise Exception("Fail to send Email. No type is set. user={}, type={} , jobtransact={}".format(user, type, jobtransact)) diff --git a/geruecht/controller/mainController/__init__.py b/geruecht/controller/mainController/__init__.py index 602f6ab..ae3f73a 100644 --- a/geruecht/controller/mainController/__init__.py +++ b/geruecht/controller/mainController/__init__.py @@ -5,7 +5,7 @@ import geruecht.controller.emailController as ec from geruecht.model.user import User from datetime import datetime, timedelta from geruecht.logger import getDebugLogger -from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController, mainRegistrationController +from ..mainController import mainJobKindController, mainCreditListController, mainPricelistController, mainUserController, mainWorkerController, mainWorkgroupController, mainJobInviteController, mainJobRequestController, mainRegistrationController, mainPasswordReset db = dc.DatabaseController() ldap = lc.LDAPController() @@ -23,6 +23,7 @@ class MainController(mainJobKindController.Base, mainJobInviteController.Base, mainJobRequestController.Base, mainRegistrationController.Base, + mainPasswordReset.Base, metaclass=Singleton): def __init__(self): diff --git a/geruecht/controller/mainController/mainPasswordReset.py b/geruecht/controller/mainController/mainPasswordReset.py new file mode 100644 index 0000000..f82b6a4 --- /dev/null +++ b/geruecht/controller/mainController/mainPasswordReset.py @@ -0,0 +1,39 @@ +from geruecht import ldap, ldapConfig, getDebugLogger +import geruecht.controller.emailController as ec +from ldap3.utils.hashed import hashed +from ldap3 import HASHED_SALTED_MD5, MODIFY_REPLACE +import string +import random + +emailController = ec.EmailController() +debug = getDebugLogger() + +def randomString(stringLength=8): + letters = string.ascii_letters + string.digits + return ''.join(random.choice(letters) for i in range(stringLength)) + +class Base: + def resetPassword(self, data): + debug.info("forgot password {{ {} }}".format(data)) + adminConn = ldap.connect(ldapConfig['ADMIN_DN'], ldapConfig['ADMIN_SECRET']) + if 'username' in data: + search = 'uid={}'.format(data['username'].lower()) + elif 'mail' in data: + search = 'mail={}'.format(data['mail'].lower()) + else: + debug.error("username or mail not set") + raise Exception('username or mail not set') + adminConn.search(ldapConfig['USER_DN'], '(&(objectClass=person)({}))'.format(search), + attributes=['cn', 'sn', 'givenName', 'uid', 'mail']) + for user in adminConn.response: + user_dn = user['dn'] + uid = user['attributes']['uid'][0] + mail = user['attributes']['mail'][0] + mody = {} + password = randomString() + salted_password = hashed(HASHED_SALTED_MD5, password) + mody['userPassword'] = [(MODIFY_REPLACE, [salted_password])] + debug.info("reset password for {{ {} }}".format(user_dn)) + adminConn.modify(user_dn, mody) + emailController.sendMail(self.getUser(uid), type='passwordReset', password=password) + return mail \ No newline at end of file diff --git a/geruecht/routes.py b/geruecht/routes.py index c8f7810..298b36d 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -144,6 +144,22 @@ def _saveLifeTime(**kwargs): "exception in save lifetime for accesstoken.", exc_info=True) return jsonify({"error": str(err)}), 500 +@app.route("/passwordReset", methods=['POST']) +def _passwordReset(): + try: + debug.info('password reset') + data = request.get_json() + mail = mainController.resetPassword(data) + index = mail.find('@') + for i in range(index): + if i == 0: + continue + mail = mail.replace(mail[i], "*", 1) + return jsonify({"ok": "ok", "mail": mail}) + except Exception as err: + debug.warning("excetpion in password reset", exc_info=True) + return jsonify({"error": str(err)}), 409 + @app.route("/logout", methods=['GET']) @login_required(groups=[MONEY, GASTRO, VORSTAND, EXTERN, USER], bar=True) def _logout(**kwargs): From 7baffec406bc5bd26ce6c32765bdba130229eee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 28 Jun 2020 12:59:06 +0200 Subject: [PATCH 109/111] =?UTF-8?q?add=20config=20f=C3=BCr=20LDAPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + geruecht/__init__.py | 13 +++++++++++-- geruecht/config.yml.example | 5 ++++- geruecht/configparser.py | 27 ++++++++++++++++++++++----- geruecht/controller/ldapController.py | 2 +- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index f48f2f7..ed93d09 100644 --- a/.gitignore +++ b/.gitignore @@ -124,4 +124,5 @@ dmypy.json # custom test_pricelist/ test_project/ +config.yml geruecht.config.yml diff --git a/geruecht/__init__.py b/geruecht/__init__.py index d2eb11d..2687a9a 100644 --- a/geruecht/__init__.py +++ b/geruecht/__init__.py @@ -8,6 +8,7 @@ from .logger import getDebugLogger from geruecht.controller import dbConfig, ldapConfig from flask_mysqldb import MySQL from flask_ldapconn import LDAPConn +import ssl DEBUG = getDebugLogger() DEBUG.info("Initialize App") @@ -25,9 +26,17 @@ app.config['MYSQL_PASSWORD'] = dbConfig['passwd'] app.config['MYSQL_DB'] = dbConfig['database'] app.config['MYSQL_CURSORCLASS'] = 'DictCursor' app.config['LDAP_SERVER'] = ldapConfig['URL'] -app.config['LDAP_PORT'] = ldapConfig['port'] -app.config['LDAP_BINDDN'] = ldapConfig['dn'] +app.config['LDAP_PORT'] = ldapConfig['PORT'] +if ldapConfig['BIND_DN']: + app.config['LDAP_BINDDN'] = ldapConfig['BIND_DN'] +else: + app.config['LDAP_BINDDN'] = ldapConfig['DN'] +if ldapConfig['BIND_SECRET']: + app.config['LDAP_SECRET'] = ldapConfig['BIND_SECRET'] app.config['LDAP_USE_TLS'] = False +app.config['LDAP_USE_SSL'] = ldapConfig['SSL'] +app.config['LDAP_TLS_VERSION'] = ssl.PROTOCOL_TLSv1_2 +app.config['LDAP_REQUIRE_CERT'] = ssl.CERT_NONE app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] = True ldap = LDAPConn(app) diff --git a/geruecht/config.yml.example b/geruecht/config.yml.example index 06f3e71..62a16a2 100644 --- a/geruecht/config.yml.example +++ b/geruecht/config.yml.example @@ -6,7 +6,10 @@ Database: database: LDAP: URL: - dn: + DN: + BIND_DN: + BIND_SECRET: + SSL: USER_DN: ADMIN_DN: ADMIN_SECRET: diff --git a/geruecht/configparser.py b/geruecht/configparser.py index b2835c7..1fbe90c 100644 --- a/geruecht/configparser.py +++ b/geruecht/configparser.py @@ -34,14 +34,14 @@ class ConifgParser(): if 'LDAP' not in self.config: self.__error__( - 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') - if 'URL' not in self.config['LDAP'] or 'dn' not in self.config['LDAP']: + 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "BIND_DN"') + if 'URL' not in self.config['LDAP'] or 'DN' not in self.config['LDAP']: self.__error__( - 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "dn"') - if 'port' not in self.config['LDAP']: + 'Wrong Configuration for LDAP. You should configure ldapconfig with "URL" and "BIND_DN"') + if 'PORT' not in self.config['LDAP']: DEBUG.info( 'No Config for port in LDAP found. Set it to default: {}'.format(389)) - self.config['LDAP']['port'] = 389 + self.config['LDAP']['PORT'] = 389 if 'ADMIN_DN' not in self.config['LDAP']: DEBUG.info( 'No Config for ADMIN_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) @@ -57,6 +57,23 @@ class ConifgParser(): 'No Config for USER_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) ) self.config['LDAP']['USER_DN'] = None + if 'BIND_DN' not in self.config['LDAP']: + DEBUG.info( + 'No Config for BIND_DN in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) + ) + self.config['LDAP']['BIND_DN'] = None + if 'BIND_SECRET' not in self.config['LDAP']: + DEBUG.info( + 'No Config for BIND_SECRET in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(None) + ) + self.config['LDAP']['BIND_SECRET'] = None + if 'SSL' not in self.config['LDAP']: + DEBUG.info( + 'No Config for SSL in LDAP found. Set it to default {}. (Maybe Password reset not working)'.format(False) + ) + self.config['LDAP']['SSL'] = False + else: + self.config['LDAP']['SSL'] = bool(self.config['LDAP']['SSL']) self.ldap = self.config['LDAP'] DEBUG.info("Set LDAPconfig: {}".format(self.ldap)) if 'AccessTokenLifeTime' in self.config: diff --git a/geruecht/controller/ldapController.py b/geruecht/controller/ldapController.py index bc85d8f..da3044e 100644 --- a/geruecht/controller/ldapController.py +++ b/geruecht/controller/ldapController.py @@ -17,7 +17,7 @@ class LDAPController(metaclass=Singleton): def __init__(self): debug.info("init ldap controller") - self.dn = ldapConfig['dn'] + self.dn = ldapConfig['DN'] self.ldap = ldap debug.debug("base dn is {{ {} }}".format(self.dn)) debug.debug("ldap is {{ {} }}".format(self.ldap)) From 964865a731755b558a9d12973f229d5302698e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 28 Jun 2020 13:20:02 +0200 Subject: [PATCH 110/111] fix DN in resetPassword --- geruecht/controller/mainController/mainPasswordReset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geruecht/controller/mainController/mainPasswordReset.py b/geruecht/controller/mainController/mainPasswordReset.py index f82b6a4..8cc031d 100644 --- a/geruecht/controller/mainController/mainPasswordReset.py +++ b/geruecht/controller/mainController/mainPasswordReset.py @@ -23,7 +23,7 @@ class Base: else: debug.error("username or mail not set") raise Exception('username or mail not set') - adminConn.search(ldapConfig['USER_DN'], '(&(objectClass=person)({}))'.format(search), + adminConn.search(ldapConfig['DN'], '(&(objectClass=person)({}))'.format(search), attributes=['cn', 'sn', 'givenName', 'uid', 'mail']) for user in adminConn.response: user_dn = user['dn'] From fac8afab0395e2f4fcddf88fa1942bd30bd03921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 27 Jul 2020 10:01:01 +0200 Subject: [PATCH 111/111] Status Lockbar bei GetLifeTime wird mitgesendet --- geruecht/config.yml | 9 --------- geruecht/routes.py | 3 ++- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 geruecht/config.yml diff --git a/geruecht/config.yml b/geruecht/config.yml deleted file mode 100644 index 3df779a..0000000 --- a/geruecht/config.yml +++ /dev/null @@ -1,9 +0,0 @@ -AccessTokenLifeTime: 1800 -Database: - URL: 192.168.5.128 - user: wu5 - passwd: E1n$tein - database: geruecht -LDAP: - URL: ldap://192.168.5.128 - dn: dc=ldap,dc=example,dc=local \ No newline at end of file diff --git a/geruecht/routes.py b/geruecht/routes.py index 298b36d..07b78ce 100644 --- a/geruecht/routes.py +++ b/geruecht/routes.py @@ -108,7 +108,8 @@ def _getLifeTime(**kwargs): accToken = kwargs['accToken'] debug.debug("accessToken is {{ {} }}".format(accToken)) retVal = {"value": accToken.lifetime, - "group": accToken.user.toJSON()['group']} + "group": accToken.user.toJSON()['group'], + "lock_bar": accToken.lock_bar} debug.info( "return get lifetime from accesstoken {{ {} }}".format(retVal)) return jsonify(retVal)
- - - - - {% if current_user.is_authenticated %} - Logout - Übersicht - {% else %} - Finanzer - {% endif %} -