diff --git a/.gitignore b/.gitignore index 15201ac..b37a122 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +entorno/ # C extensions *.so diff --git a/app.py b/app.py index 4720bd1..0a3ddae 100644 --- a/app.py +++ b/app.py @@ -1,22 +1,13 @@ from flask import Flask, render_template, request, redirect, url_for, render_template_string from flask import session -from data.db import DB -from dotenv import load_dotenv +from data import Users, Roles, Permissions, Modules import os import psycopg2 app = Flask(__name__) -load_dotenv() app.secret_key = os.environ.get('SECRET_KEY') -base = { - 'dbname': os.environ.get('DB_NAME'), - 'user': os.environ.get('DB_USER'), - 'password': os.environ.get('DB_PASSWORD'), - 'host': os.environ.get('DB_HOST'), - 'port': os.environ.get('DB_PORT') -} @app.route('/', methods=['GET', 'POST']) def login(): @@ -30,26 +21,16 @@ def login(): else: error = "El Usuario o Contraseña son Incorrectos." - return render_template_string(""" - {% if error %} -

{{ error }}

- {% endif %} -
- Usuario:
- Contraseña:
- -
- """, error=error) + return render_template('login.html', error=error) def is_valid_user(username :str, password :str) -> bool: try: - db = DB(base) - user = db.connect().get_user_by_email(username) + user = Users().find(email=username) if not user: return False - if not user.login(password): + if user.password != password: return False session['user_id'] = user.id @@ -62,32 +43,170 @@ def is_valid_user(username :str, password :str) -> bool: @app.route('/datos') def show_data(): try: - connection = psycopg2.connect(**base) - cursor = connection.cursor() + if not session.get('user_id'): + return redirect(url_for('login')) + user = Users().find(id=session.get('user_id')) + return render_template('welcome.html', user=user) + + except Exception as e: + return f"Error: {e}" + +@app.route('/modules') +def show_modules(): + try: if not session.get('user_id'): - return redirect(url_for('login')) + return redirect(url_for('login')) + user = Users().find(id=session.get('user_id')) + modules = Modules().all() + + return render_template('modules.html', user=user, modules=modules) + + except Exception as e: + return f"Error: {e}" + + +@app.route('/modules/create') +def create_modules(): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + return render_template('modules_form.html', id=0) + + except Exception as e: + return f"Error: {e}" + + + +@app.route('/modules/edit/') +def edit_modules(id: int): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + module = Modules().find(id=id) + return render_template('modules_form.html', id=id, module=module) + + except Exception as e: + return f"Error: {e}" + - query = """ - SELECT - a.id, a.name, a.lastname, b."name", d."name", - c.can_read, c.can_write, c.can_update, c.can_delete - FROM - users a - JOIN roles b ON a.role_id = b.id - JOIN permissions c ON c.role_id = b.id - JOIN modules d ON c.module_id = d.id WHERE a.id = %s - """ - - cursor.execute(query, (session['user_id'],)) - records = cursor.fetchall() - - cursor.close() - connection.close() - return render_template('table.html', records=records) + +@app.route('/modules/save', methods=['POST']) +def save_modules(): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + id = int(request.form.get('id')) + name = request.form.get('name') + if name is None or name == '': + return render_template('modules_form.html', id=id, error="El nombre del módulo es requerido.") + + if id == 0: + module = Modules(name=name) + else: + module = Modules().find(id=id) + module.name = name + + module.save() + return redirect(url_for('show_modules')) except Exception as e: return f"Error: {e}" + + + +@app.route('/modules/delete/', methods=['GEt']) +def delete_modules(id: int): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + module = Modules().find(id=id) + if module: + module.delete() + return redirect(url_for('show_modules')) + + except Exception as e: + return f"Error: {e}" + +if __name__ == '__main__': + app.run(debug=True) + + +@app.route('/roles') +def show_roles(): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + roles = Roles().all() + + return render_template('roles.html', roles=roles) + + except Exception as e: + return f"Error: {e}" + +@app.route('/roles/create') +def create_roles(): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + return render_template('roles_form.html', id=0) + + except Exception as e: + return f"Error: {e}" + +@app.route('/roles/edit/') +def edit_roles(id: int): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + role = Roles().find(id=id) + return render_template('roles_form.html', id=id, role=role) + + except Exception as e: + return f"Error: {e}" + +@app.route('/roles/save', methods=['POST']) +def save_roles(): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + id = int(request.form.get('id')) + name = request.form.get('name') + if name is None or name == '': + return render_template('roles_form.html', id=id, error="El nombre del rol es requerido.") + + if id == 0: + role = Roles(name=name) + else: + role = Roles().find(id=id) + role.name = name + + role.save() + return redirect(url_for('show_roles')) + + except Exception as e: + return f"Error: {e}" + +@app.route('/roles/delete/', methods=['GET']) +def delete_roles(id: int): + try: + if not session.get('user_id'): + return redirect(url_for('login')) + + role = Roles().find(id=id) + if role: + role.delete() + return redirect(url_for('show_roles')) + + except Exception as e: + return f"Error: {e}" + if __name__ == '__main__': app.run(debug=True) \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py index c6736f4..01f69a0 100644 --- a/data/__init__.py +++ b/data/__init__.py @@ -1 +1,2 @@ -from .db import User, Permission, DBException, DB \ No newline at end of file +from .orm import ORM +from .models import Users, Modules, Roles, Permissions \ No newline at end of file diff --git a/data/db.py b/data/db.py deleted file mode 100644 index b7d2ebb..0000000 --- a/data/db.py +++ /dev/null @@ -1,68 +0,0 @@ -import psycopg2 - -from .models import User, Permission - - -class DBException(Exception): - pass - - -class DB(): - db_params = None - connection = None - - def __init__(self, db_params): - self.db_params = db_params - - - def connect(self): - self.connection = psycopg2.connect(**self.db_params) - return self - - - def tuple_to_user(self, data): - user = User() - user.id = data[0] - user.name = data[1] - user.lastname = data[2] - user.email = data[3] - user.password = data[4] - user.role = data[5] - user.created_at = data[6] - user.updated_at = data[7] - return user - - - def get_user(self, id): - query = "SELECT id, name, lastname, email, password, role_id, created_at, update_at FROM users WHERE id = %s" - cursor = self.connection.cursor() - cursor.execute(query, (str(id))) - result = cursor.fetchone() - if not result: - raise DBException("User not found") - self.connection.close() - return self.tuple_to_user(result) - - - def get_user_by_email(self, email): - query = "SELECT id, name, lastname, email, password, role_id, created_at, update_at FROM users WHERE email = %s" - cursor = self.connection.cursor() - cursor.execute(query, (email, )) - result = cursor.fetchone() - if not result: - raise DBException("User not found") - self.connection.close() - return self.tuple_to_user(result) - - - def get_users(self): - query = "SELECT id, name, lastname, email, password, role_id, created_at, update_at FROM users" - cursor = self.connection.cursor() - cursor.execute(query) - result = cursor.fetchone() - if not result: - raise DBException("User not found") - self.connection.close() - return [self.tuple_to_user(row) for row in result] - - diff --git a/data/models.py b/data/models.py index 1066b37..e2e2d63 100644 --- a/data/models.py +++ b/data/models.py @@ -1,23 +1,16 @@ -import datetime +from .orm import ORM -class Permission(): - name: str - can_update: bool - can_write: bool - can_delete: bool - can_read: bool +class Users(ORM): + pass -class User(): - name : str - lastname :str - email : str - role : str - password : str - created_at : datetime - updated_at : datetime - permissions : [Permission] # type: ignore +class Modules(ORM): + pass - def login(self, password): - return self.password == password - + +class Roles(ORM): + pass + + +class Permissions(ORM): + pass diff --git a/data/orm.py b/data/orm.py new file mode 100644 index 0000000..0c21a59 --- /dev/null +++ b/data/orm.py @@ -0,0 +1,92 @@ + +import psycopg2 +from psycopg2.extras import RealDictCursor +import datetime +from dotenv import load_dotenv +import os + + +class ORM(): + _table_name = None + _exclude_fields = ['created_at', 'updated_at', 'update_at', 'id'] + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + + @classmethod + def _get_connection(cls): + return psycopg2.connect( + dbname = os.environ.get('DB_NAME'), + user = os.environ.get('DB_USER'), + password = os.environ.get('DB_PASSWORD'), + host = os.environ.get('DB_HOST'), + port = os.environ.get('DB_PORT') + ) + + + @classmethod + def find(cls, **kwargs): + conn = cls._get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + where_clause = ' AND '.join([f"{key} = %s" for key in kwargs]) + query = f"SELECT * FROM {cls._table_name or cls.__name__.lower()} WHERE {where_clause}" + cur.execute(query, tuple(kwargs.values())) + result = cur.fetchone() + ins = cls(**result) if result else None + return ins + finally: + conn.close() + + + @classmethod + def all(cls, **kwargs)-> list: + conn = cls._get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + query = f"SELECT * FROM {cls._table_name or cls.__name__.lower()}" + cur.execute(query, tuple(kwargs.values())) + result = cur.fetchall() + return [] if not result else [cls(**row) for row in result] + finally: + conn.close() + + + + def save(self): + conn = self._get_connection() + try: + with conn.cursor() as cur: + attributes = { k: v for k, v in self.__dict__.items() if k not in self._exclude_fields } + if hasattr(self, 'id'): + set_clause = ", ".join([f"{key} = %s" for key in attributes if key != 'id']) + query = f"UPDATE {self._table_name or self.__class__.__name__.lower()} SET {set_clause} WHERE id = %s" + cur.execute(query, tuple(attributes.values()) + (self.id,)) + conn.commit() + else: + columns = ", ".join(attributes.keys()) + placeholders = ", ".join(["%s"] * len(attributes)) + query = f"INSERT INTO {self._table_name or self.__class__.__name__.lower()} ({columns}) VALUES ({placeholders}) RETURNING id" + cur.execute(query, tuple(attributes.values())) + self.id = cur.fetchone()[0] + conn.commit() + finally: + conn.close() + + + def delete(self): + if not hasattr(self, 'id') or not self.id: + raise ValueError("Cannot delete unsaved record") + conn = self._get_connection() + try: + with conn.cursor() as cur: + query = f"DELETE FROM {self._table_name or self.__class__.__name__.lower()} WHERE id = %s" + cur.execute(query, (self.id,)) + conn.commit() + finally: + conn.close() + + + diff --git a/requirements.txt b/requirements.txt index 6f40933..31ec4f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,16 @@ blinker==1.9.0 click==8.1.8 +exceptiongroup==1.2.2 Flask==3.1.0 +iniconfig==2.0.0 itsdangerous==2.2.0 Jinja2==3.1.5 MarkupSafe==3.0.2 +packaging==24.2 +pluggy==1.5.0 psycopg2-binary==2.9.10 +pytest==8.3.4 +pytest-dotenv==0.5.2 python-dotenv==1.0.1 +tomli==2.2.1 Werkzeug==3.1.3 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..224da51 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,42 @@ + + + + + + {% block title %}Login App{% endblock %} + + + + + +
+ {% block content %} + {% endblock %} +
+ + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..109a577 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Login

+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/modules.html b/templates/modules.html new file mode 100644 index 0000000..996ed5c --- /dev/null +++ b/templates/modules.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% block content %} +

+
+ +
+
+

Modules

+
+ + + + + + + + + + + {% for module in modules %} + + + + + + + {% endfor %} + +
IDNameCreated AtActions
{{ module.id }}{{ module.name }}{{ module.created_at }} + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/modules_form.html b/templates/modules_form.html new file mode 100644 index 0000000..1583ee2 --- /dev/null +++ b/templates/modules_form.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% if id == 0 %} +

Creating a new module

+ {% else %} +

Editing module with ID: {{ id }}

+ {% endif %} +
+ +
+ + +
+ {% if error%} + + {% endif %} + + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/roles.html b/templates/roles.html new file mode 100644 index 0000000..be25030 --- /dev/null +++ b/templates/roles.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% block content %} +

+
+ +
+
+

Roles

+
+ + + + + + + + + + + {% for roles in roles %} + + + + + + + {% endfor %} + +
IDNameCreated AtActions
{{ roles.id }}{{ roles.name }}{{ roles.created_at }} + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/roles_form.html b/templates/roles_form.html new file mode 100644 index 0000000..dbb8353 --- /dev/null +++ b/templates/roles_form.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% if id == 0 %} +

Creating a new role

+ {% else %} +

Editing role with ID: {{ id }}

+ {% endif %} +
+ +
+ + +
+ {% if error%} + + {% endif %} + + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/welcome.html b/templates/welcome.html new file mode 100644 index 0000000..c85d4a5 --- /dev/null +++ b/templates/welcome.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + + +{% block content %} +
+
+

Bienvenido

+ {{ user.name }} {{ user.lastname }} +

We are glad to have you here. Enjoy your stay!

+
+
+{% endblock %} \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..3a6b03d --- /dev/null +++ b/test.py @@ -0,0 +1,35 @@ +from data import ORM +from data.models import Users, Modules, Roles, Permissions + +if __name__ == '__main__': + + module = Modules() + module.name = 'Testing' + module.save() + + role = Roles() + role.name = 'Tester' + role.save() + + permission = Permissions() + permission.role_id = role.id + permission.module_id = module.id + permission.can_write = True + permission.can_read = True + permission.can_update = True + permission.can_delete = True + permission.save() + + data = { + 'name': 'Tester', + 'lastname': 'Test lastname', + 'email': 'tester@gogo.com' , + 'password': 'asdasdfsadff', + 'hash': 'asdasdfsadff', + 'role_id': role.id + } + + user = Users(**data) + user.save() + print(user.__dict__) + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..7533091 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,49 @@ +import pytest +from data import Users, Modules, Roles, Permissions + + +def test_user_flow(): + module = Modules() + module.name = 'Testing' + module.save() + + assert type(module.id) is int + + role = Roles() + role.name = 'Tester' + role.save() + + assert type(role.id) is int + + + permission = Permissions() + permission.role_id = role.id + permission.module_id = module.id + permission.can_write = True + permission.can_read = True + permission.can_update = True + permission.can_delete = True + permission.save() + + assert type(permission.id) is int + + + data = { + 'name': 'Tester', + 'lastname': 'Test lastname', + 'email': 'lllllll@gogo.com' , + 'password': 'asdasdfsadff', + 'hash': 'asdasdfsadff', + 'role_id': role.id + } + + user = Users(**data) + user.save() + + assert type(user.id) is int + print(user.__dict__) + + user.delete() + permission.delete() + role.delete() + module.delete() \ No newline at end of file