From 7e99dd7f6717c6b5c9c7d9d6665fe41d55d18a5a Mon Sep 17 00:00:00 2001 From: LoretoConcha <109807122+LoretoConcha@users.noreply.github.com> Date: Fri, 22 Aug 2025 05:22:27 +0000 Subject: [PATCH 01/13] feat(models): add Task, Category, TaskOffered, TaskDealed, Payment --- src/api/models.py | 213 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 208 insertions(+), 5 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..14402510d6 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,222 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean, Integer, ForeignKey, Date, Text, Numeric, DateTime, func, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship +from datetime import date, datetime +from decimal import Decimal + db = SQLAlchemy() +task_categories = db.Table( + "task_categories", + db.Column("task_id", Integer, db.ForeignKey( + "tasks.id", ondelete="CASCADE"), primary_key=True), + db.Column("category_id", Integer, db.ForeignKey( + "categories.id", ondelete="CASCADE"), primary_key=True), +) + + class User(db.Model): + __tablename__ = "users" + id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) + email: Mapped[str] = mapped_column( + String(120), unique=True, nullable=False) password: Mapped[str] = mapped_column(nullable=False) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) - def serialize(self): return { "id": self.id, "email": self.email, # do not serialize the password, its a security breach - } \ No newline at end of file + } + + +class Task(db.Model): + __tablename__ = "tasks" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + title: Mapped[str | None] = mapped_column(String(120), nullable=True) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + location: Mapped[str | None] = mapped_column(String(120), nullable=True) + price: Mapped[Decimal | None] = mapped_column( + Numeric(10, 2), nullable=True) + + # dates + due_at: Mapped[datetime | None] = mapped_column( + DateTime, nullable=True) # timestamp + # DB fills posted_at automatically with current_date() + posted_at: Mapped[date] = mapped_column( + Date, nullable=False, server_default=func.current_date()) + assigned_at: Mapped[date | None] = mapped_column(Date, nullable=True) + completed_at: Mapped[date | None] = mapped_column(Date, nullable=True) + + # business-defined value (app.py) + status: Mapped[str] = mapped_column( + String(30), nullable=False, server_default="pending") + + # FK + relationship (1 User -> many Tasks) + publisher_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, index=True) + publisher: Mapped["User"] = relationship(backref="tasks") + + # relationship categories + categories: Mapped[list["Category"]] = relationship( + "Category", secondary=task_categories, back_populates="tasks") + + def serialize(self): + return { + "id": self.id, + "title": self.title, + } + + def serialize_all_data(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "location": self.location, + "price": float(self.price) if self.price is not None else None, + "due_at": self.due_at.isoformat() if self.due_at else None, + "status": self.status, + "posted_at": self.posted_at.isoformat() if self.posted_at else None, + "assigned_at": self.assigned_at.isoformat() if self.assigned_at else None, + "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "publisher_id": self.publisher_id, + } + + +class Category(db.Model): + __tablename__ = "categories" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + name: Mapped[str] = mapped_column( + String(50), nullable=False, unique=True) + + # relationship + tasks: Mapped[list["Task"]] = relationship( + "Task", secondary=task_categories, back_populates="categories") + # return type + + def serialize(self): + return { + "id": self.id, + "name": self.name, + } + + +class TaskOffered(db.Model): + __tablename__ = "tasks_offered" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + # the values will be defined in the business layer(app.py) + status: Mapped[Decimal | None] = mapped_column( + Numeric(10, 2), nullable=True) + # dates + created_at: Mapped[date] = mapped_column( + Date, nullable=False, server_default=func.current_date()) + updated_at: Mapped[date] = mapped_column( + Date, nullable=False, + server_default=func.current_date(), + server_onupdate=func.current_date() + ) + # FK + task_id: Mapped[int] = mapped_column( + ForeignKey("tasks.id"), nullable=False, index=True) + tasker_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, index=True) + + # one offer per (task, tasker) in pair + __table_args__ = ( + UniqueConstraint("task_id", "tasker_id", + name="uq_tasks_offered_task_tasker"), + ) + + # return type + + def serialize(self): + return { + "id": self.id, + "task_id": self.task_id, + "tasker_id": self.tasker_id, + "status": float(self.status) if self.status is not None else None, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + } + + +class TaskDealed(db.Model): + __tablename__ = "tasks_dealed" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + fixed_price: Mapped[Decimal | None] = mapped_column( + Numeric(10, 2), nullable=True) + + # the values will be defined in the business layer(app.py) + status: Mapped[str] = mapped_column(String(30), nullable=False) + # dates + accepted_at: Mapped[date | None] = mapped_column(Date, nullable=True) + delivered_at: Mapped[date | None] = mapped_column(Date, nullable=True) + cancelled_at: Mapped[date | None] = mapped_column(Date, nullable=True) + # FK + task_id: Mapped[int] = mapped_column( + ForeignKey("tasks.id"), nullable=False, index=True + ) + offer_id: Mapped[int] = mapped_column( + ForeignKey("tasks_offered.id"), nullable=False, index=True + ) + client_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, index=True + ) + tasker_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, index=True + ) + + def serialize(self): + return { + "id": self.id, + "task_id": self.task_id, + "offer_id": self.offer_id, + "client_id": self.client_id, + "tasker_id": self.tasker_id, + "fixed_price": float(self.fixed_price) if self.fixed_price is not None else None, + "status": self.status, + "accepted_at": self.accepted_at.isoformat() if self.accepted_at else None, + "delivered_at": self.delivered_at.isoformat() if self.delivered_at else None, + "cancelled_at": self.cancelled_at.isoformat() if self.cancelled_at else None + # do not serialize the password, its a security breach + } + + +class Payment(db.Model): + __tablename__ = "payments" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) + # the values will be defined in the business layer(app.py) + status: Mapped[str] = mapped_column(String(20), nullable=False) + # dates + created_at: Mapped[date] = mapped_column( + Date, nullable=False, server_default=func.current_date() + ) + updated_at: Mapped[date] = mapped_column( + Date, nullable=False, + server_default=func.current_date(), + server_onupdate=func.current_date() + ) + + # FK + dealed_id: Mapped[int] = mapped_column( + ForeignKey("tasks_dealed.id", ondelete="CASCADE"), unique=True, nullable=False, index=True + ) + + def serialize(self): + return { + "id": self.id, + "dealed_id": self.dealed_id, + "amount": float(self.amount), + "status": self.status, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + } From 9b7f5274f046d91d4d9e2fb6a5f2834efe4c1abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaqueline=20T=C3=A9llez?= <78996295+jaquenwn@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:24:22 +0000 Subject: [PATCH 02/13] tablas jaqueline --- src/api/models.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/api/models.py b/src/api/models.py index 14402510d6..5d0ec29300 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -220,3 +220,79 @@ def serialize(self): "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), } +class Review(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + review: Mapped[str] = mapped_column(String(10000), nullable=True) + rate: Mapped[int] = mapped_column(Integer, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + publisher_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + worker_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + task_id: Mapped[int] = mapped_column(ForeignKey('task.id'), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "review": self.review, + "rate": self.rate, + "created_at": self.created_at, + "worker_id": self.worker_id, + "task_id": self.task_id, + } + +class Message(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + body: Mapped[str] = mapped_column(String(100000), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + dealed_id: Mapped[int] = mapped_column(ForeignKey('task_dealed.id'), unique=True, nullable=False) + sender_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "body": self.body, + "created_at": self.created_at, + "dealed_id": self.dealed_id, + "sender_id": self.sender_id, + } + +class Dispute(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + reason: Mapped[str] = mapped_column(String(120), nullable=False) + details: Mapped[str] = mapped_column(String(1000), nullable=False) + status: Mapped[str] = mapped_column(String(30), nullable=False) + resolution: Mapped[str] = mapped_column(String(1000), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + dealed_id: Mapped[int] = mapped_column(ForeignKey('task_dealed.id'), unique=True, nullable=False) + raised_by: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + resolved_by_admin_user: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=True) + + def serialize(self): + return { + "id": self.id, + "reason": self.reason, + "details": self.details, + "status": self.status, + "resolution": self.resolution, + "created_at": self.created_at, + "updated_at": self.updated_at, + "dealed_id": self.dealed_id, + "raised_by": self.raised_by, + "resolved_by_admin_user": self.resolved_by_admin_user, + } + +class Admin_action(db.Model): + id: Mapped[int] = mapped_column(primary_key=True) + action: Mapped[str] = mapped_column(String(60), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + dispute_id: Mapped[int] = mapped_column(ForeignKey('dispute.id'), unique=True, nullable=False) + admin_user: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + + def serialize(self): + return { + "id": self.id, + "action": self.action, + "created_at": self.created_at, + "dispute_id": self.dispute_id, + "admin_user": self.admin_user, + } \ No newline at end of file From 66b571568ec75238f2c1116091126414be615f77 Mon Sep 17 00:00:00 2001 From: LoretoConcha <109807122+LoretoConcha@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:55:30 +0000 Subject: [PATCH 03/13] elimine tabla fake user --- src/api/models.py | 58 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 5d0ec29300..38ea831e17 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -16,23 +16,6 @@ ) -class User(db.Model): - __tablename__ = "users" - - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column( - String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) - - def serialize(self): - return { - "id": self.id, - "email": self.email, - # do not serialize the password, its a security breach - } - - class Task(db.Model): __tablename__ = "tasks" @@ -220,14 +203,19 @@ def serialize(self): "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), } + + class Review(db.Model): id: Mapped[int] = mapped_column(primary_key=True) review: Mapped[str] = mapped_column(String(10000), nullable=True) rate: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - publisher_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) - worker_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) - task_id: Mapped[int] = mapped_column(ForeignKey('task.id'), unique=True, nullable=False) + publisher_id: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=False) + worker_id: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=False) + task_id: Mapped[int] = mapped_column( + ForeignKey('task.id'), unique=True, nullable=False) def serialize(self): return { @@ -238,13 +226,16 @@ def serialize(self): "worker_id": self.worker_id, "task_id": self.task_id, } - + + class Message(db.Model): id: Mapped[int] = mapped_column(primary_key=True) body: Mapped[str] = mapped_column(String(100000), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dealed_id: Mapped[int] = mapped_column(ForeignKey('task_dealed.id'), unique=True, nullable=False) - sender_id: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + dealed_id: Mapped[int] = mapped_column(ForeignKey( + 'task_dealed.id'), unique=True, nullable=False) + sender_id: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=False) def serialize(self): return { @@ -255,6 +246,7 @@ def serialize(self): "sender_id": self.sender_id, } + class Dispute(db.Model): id: Mapped[int] = mapped_column(primary_key=True) reason: Mapped[str] = mapped_column(String(120), nullable=False) @@ -263,9 +255,12 @@ class Dispute(db.Model): resolution: Mapped[str] = mapped_column(String(1000), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dealed_id: Mapped[int] = mapped_column(ForeignKey('task_dealed.id'), unique=True, nullable=False) - raised_by: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) - resolved_by_admin_user: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=True) + dealed_id: Mapped[int] = mapped_column(ForeignKey( + 'task_dealed.id'), unique=True, nullable=False) + raised_by: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=False) + resolved_by_admin_user: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=True) def serialize(self): return { @@ -280,13 +275,16 @@ def serialize(self): "raised_by": self.raised_by, "resolved_by_admin_user": self.resolved_by_admin_user, } - + + class Admin_action(db.Model): id: Mapped[int] = mapped_column(primary_key=True) action: Mapped[str] = mapped_column(String(60), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dispute_id: Mapped[int] = mapped_column(ForeignKey('dispute.id'), unique=True, nullable=False) - admin_user: Mapped[int] = mapped_column(ForeignKey('user.id'), unique=True, nullable=False) + dispute_id: Mapped[int] = mapped_column( + ForeignKey('dispute.id'), unique=True, nullable=False) + admin_user: Mapped[int] = mapped_column( + ForeignKey('user.id'), unique=True, nullable=False) def serialize(self): return { @@ -295,4 +293,4 @@ def serialize(self): "created_at": self.created_at, "dispute_id": self.dispute_id, "admin_user": self.admin_user, - } \ No newline at end of file + } From 33eecf8fb40c2c1d75a6616d166e9032521f2ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaqueline=20T=C3=A9llez?= <78996295+jaquenwn@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:16:35 +0000 Subject: [PATCH 04/13] review no unique ids y task_dealed --- src/api/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 38ea831e17..30d473776b 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -211,11 +211,12 @@ class Review(db.Model): rate: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) publisher_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=False) + ForeignKey('user.id'), nullable=False) worker_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=False) - task_id: Mapped[int] = mapped_column( - ForeignKey('task.id'), unique=True, nullable=False) + ForeignKey('user.id'), nullable=False) + task_dealed_id: Mapped[int] = mapped_column( + ForeignKey('task_dealed.id'), unique=True, nullable=False) + def serialize(self): return { @@ -236,7 +237,7 @@ class Message(db.Model): 'task_dealed.id'), unique=True, nullable=False) sender_id: Mapped[int] = mapped_column( ForeignKey('user.id'), unique=True, nullable=False) - + user = db.relationship('User', secondary=users_messages, back_populates="messages") def serialize(self): return { "id": self.id, From 6007f32674166e7ce9b5e50d5c881dfc863c41f7 Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Fri, 22 Aug 2025 22:14:41 +0000 Subject: [PATCH 05/13] Parte de maximiliano casi lista --- src/api/models.py | 90 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..40bbd69d1d 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -4,16 +4,92 @@ db = SQLAlchemy() -class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) +UserRole = db.Table( + db.Model.metadata, + db.Column('user_id', db.Integer, db.ForeignKey( + 'user.id'), primary_key=True), + db.Column('role_id', db.Integer, db.ForeignKey( + 'rol.id'), primary_key=True) +) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) + roles = db.relationship('Rol', secondary=UserRole,) def serialize(self): return { "id": self.id, "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + "username": self.username, + "created_at": self.created_at, + "modified_at": self.modified_at, + # no serializar "password" para no exponerlo + } + + +class Rol(db.Model): + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(50), unique=True, nullable=False) + user = db.relationship('User', secondary=UserRole) + + def serialize(self): + return { + "id": self.id, + "type": self.type, + } + + +class Profile(db.Model): + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + name = db.Column(db.String(100), nullable=False) + last_name = db.Column(db.String(100), nullable=False) + avatar = db.Column(db.String(100), nullable=True) + city = db.Column(db.String(50), nullable=False) + birth_date = db.Column(db.Date, nullable=True) + bio = db.Column(db.String(250), nullable=True) + skills = db.Column(db.String(250), nullable=True) + rating_avg = db.Column(db.Float, nullable=True) + created_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) + + def serialize(self): + return { + "user_id": self.user_id, + "name": self.name, + "last_name": self.last_name, + "avatar": self.avatar, + "city": self.city, + "birth_date": self.birth_date, + "bio": self.bio, + "skills": self.skills, + "rating_avg": self.rating_avg, + "created_at": self.created_at, + "modified_at": self.modified_at, + } + + +class Account_settings(db.Model): + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + phone = db.Column(db.String(20), nullable=True) + billing_info = db.Column(db.String(250), nullable=True) + language = db.Column(db.String(50), nullable=True) + marketing_emails = db.Column(db.Boolean, nullable=True) + crated_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) + + def serialize(self): + return { + "user_id": self.user_id, + "phone": self.phone, + "billing_info": self.billing_info, + "language": self.language, + "marketing_emails": self.marketing_emails, + "created_at": self.crated_at, + "modified_at": self.modified_at, + } From 260ec6a44d95ee7af35d4007fc005f4408d571f1 Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Fri, 22 Aug 2025 22:52:40 +0000 Subject: [PATCH 06/13] Refactorizar modelos --- Pipfile | 2 +- Pipfile.lock | 277 ++++++++++++++++++++++------------------------ src/api/models.py | 71 ++++++------ 3 files changed, 166 insertions(+), 184 deletions(-) diff --git a/Pipfile b/Pipfile index 44e04f14ff..a0e0872ccb 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,6 @@ verify_ssl = true [packages] flask = "*" -flask-sqlalchemy = "*" flask-migrate = "*" flask-swagger = "*" psycopg2-binary = "*" @@ -19,6 +18,7 @@ flask-admin = "*" typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" +flask-sqlalchemy = "*" sqlalchemy = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..ba1d6c4120 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -42,11 +42,11 @@ }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.1" }, "cloudinary": { "hashes": [ @@ -58,11 +58,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.2" }, "flask-admin": { "hashes": [ @@ -102,6 +103,7 @@ "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.1.1" }, "flask-swagger": { @@ -114,82 +116,63 @@ }, "greenlet": { "hashes": [ - "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", - "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", - "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", - "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", - "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", - "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", - "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", - "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", - "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", - "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", - "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", - "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", - "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", - "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", - "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", - "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", - "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", - "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", - "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", - "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", - "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", - "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", - "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", - "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", - "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", - "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", - "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", - "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", - "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", - "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", - "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", - "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", - "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", - "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", - "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", - "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", - "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", - "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", - "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", - "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", - "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", - "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", - "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", - "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", - "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", - "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", - "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", - "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", - "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", - "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", - "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", - "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", - "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", - "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", - "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", - "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", - "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", - "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", - "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", - "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", - "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", - "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", - "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", - "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", - "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", - "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", - "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", - "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", - "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", - "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", - "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", - "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", - "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" ], - "markers": "python_version < '3.14' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", - "version": "==3.1.1" + "markers": "python_version >= '3.9'", + "version": "==3.2.4" }, "gunicorn": { "hashes": [ @@ -209,11 +192,11 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ @@ -457,74 +440,76 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d", - "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03", - "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea", - "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50", - "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", - "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", - "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1", - "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", - "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68", - "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149", - "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06", - "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7", - "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca", - "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5", - "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3", - "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3", - "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443", - "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff", - "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", - "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6", - "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", - "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2", - "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297", - "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578", - "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728", - "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178", - "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2", - "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", - "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9", - "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8", - "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", - "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4", - "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", - "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079", - "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", - "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373", - "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248", - "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", - "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", - "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6", - "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", - "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444", - "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d", - "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4", - "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc", - "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7", - "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c", - "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba", - "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", - "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", - "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", - "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", - "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", - "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", - "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63", - "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2", - "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae" + "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", + "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7", + "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c", + "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227", + "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf", + "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed", + "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a", + "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", + "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", + "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", + "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", + "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", + "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", + "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed", + "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b", + "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", + "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e", + "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad", + "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", + "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782", + "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f", + "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", + "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", + "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a", + "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", + "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", + "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", + "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", + "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", + "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185", + "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", + "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547", + "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", + "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", + "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", + "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", + "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b", + "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", + "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", + "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", + "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", + "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414", + "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", + "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c", + "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", + "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34", + "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", + "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", + "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32", + "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443", + "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7", + "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512", + "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", + "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", + "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", + "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", + "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d" ], "index": "pypi", - "version": "==2.0.38" + "markers": "python_version >= '3.7'", + "version": "==2.0.43" }, "typing-extensions": { "hashes": [ - "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", - "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", + "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" ], "index": "pypi", - "version": "==4.12.2" + "markers": "python_version >= '3.9'", + "version": "==4.14.1" }, "urllib3": { "hashes": [ diff --git a/src/api/models.py b/src/api/models.py index 99e45bb120..3686d35825 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,16 @@ -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy import String, Boolean from flask_sqlalchemy import SQLAlchemy from sqlalchemy import String, Boolean, Integer, ForeignKey, Date, Text, Numeric, DateTime, func, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import relationship, Mapped, mapped_column from datetime import date, datetime from decimal import Decimal - db = SQLAlchemy() db = SQLAlchemy() -#Tabla que relaciona user con rol +# Tabla que relaciona user con rol UserRole = db.Table( 'user_rol', db.Model.metadata, @@ -26,13 +23,13 @@ class User(db.Model): __tablename__ = 'user' - id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False) - username = db.Column(db.String(80), unique=True, nullable=False) - password = db.Column(db.String(200), nullable=False) - created_at = db.Column(db.DateTime, nullable=False) - modified_at = db.Column(db.DateTime, nullable=False) - roles = db.relationship('Rol', secondary=UserRole,) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + email: Mapped[String] = mapped_column(String(120), unique=True, nullable=False) + username: Mapped[String] = mapped_column(String(80), unique=True, nullable=False) + password: Mapped[String] = mapped_column(String(200), nullable=False) + created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + roles: Mapped[list['Rol']] = relationship('Rol', secondary='user_rol',) def serialize(self): return { @@ -47,9 +44,9 @@ def serialize(self): class Rol(db.Model): __tablename__ = 'rol' - id = db.Column(db.Integer, primary_key=True) - type = db.Column(db.String(50), unique=True, nullable=False) - user = db.relationship('User', secondary=UserRole) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + type: Mapped[String] = mapped_column(String(50), unique=True, nullable=False) + user: Mapped[list['User']] = relationship('User', secondary='user_rol') def serialize(self): return { @@ -60,17 +57,17 @@ def serialize(self): class Profile(db.Model): __tablename__ = 'profile' - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) - name = db.Column(db.String(100), nullable=False) - last_name = db.Column(db.String(100), nullable=False) - avatar = db.Column(db.String(100), nullable=True) - city = db.Column(db.String(50), nullable=False) - birth_date = db.Column(db.Date, nullable=True) - bio = db.Column(db.String(250), nullable=True) - skills = db.Column(db.String(250), nullable=True) - rating_avg = db.Column(db.Float, nullable=True) - created_at = db.Column(db.DateTime, nullable=False) - modified_at = db.Column(db.DateTime, nullable=False) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), primary_key=True) + name: Mapped[String] = mapped_column(String(100), nullable=False) + last_name: Mapped[String] = mapped_column(String(100), nullable=False) + avatar: Mapped[String] = mapped_column(String(100), nullable=True) + city: Mapped[String] = mapped_column(String(50), nullable=False) + birth_date: Mapped[date | None] = mapped_column(Date, nullable=True) + bio: Mapped[String | None] = mapped_column(String(250), nullable=True) + skills: Mapped[String | None] = mapped_column(String(250), nullable=True) + rating_avg: Mapped[float | None] = mapped_column(Float, nullable=True) + created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) def serialize(self): return { @@ -90,13 +87,13 @@ def serialize(self): class AccountSettings(db.Model): __tablename__ = 'account_settings' - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) - phone = db.Column(db.String(20), nullable=True) - billing_info = db.Column(db.String(250), nullable=True) - language = db.Column(db.String(50), nullable=True) - marketing_emails = db.Column(db.Boolean, nullable=True) - crated_at = db.Column(db.DateTime, nullable=False) - modified_at = db.Column(db.DateTime, nullable=False) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), primary_key=True) + phone: Mapped[String | None] = mapped_column(String(20), nullable=True) + billing_info: Mapped[String | None] = mapped_column(String(250), nullable=True) + language: Mapped[String | None] = mapped_column(String(50), nullable=True) + marketing_emails: Mapped[bool | None] = mapped_column(Boolean, nullable=True) + created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) def serialize(self): return { @@ -145,7 +142,7 @@ class Task(db.Model): # FK + relationship (1 User -> many Tasks) publisher_id: Mapped[int] = mapped_column( - ForeignKey("users.id"), nullable=False, index=True) + ForeignKey("user.id"), nullable=False, index=True) publisher: Mapped["User"] = relationship(backref="tasks") # relationship categories @@ -212,7 +209,7 @@ class TaskOffered(db.Model): task_id: Mapped[int] = mapped_column( ForeignKey("tasks.id"), nullable=False, index=True) tasker_id: Mapped[int] = mapped_column( - ForeignKey("users.id"), nullable=False, index=True) + ForeignKey("user.id"), nullable=False, index=True) # one offer per (task, tasker) in pair __table_args__ = ( @@ -254,10 +251,10 @@ class TaskDealed(db.Model): ForeignKey("tasks_offered.id"), nullable=False, index=True ) client_id: Mapped[int] = mapped_column( - ForeignKey("users.id"), nullable=False, index=True + ForeignKey("user.id"), nullable=False, index=True ) tasker_id: Mapped[int] = mapped_column( - ForeignKey("users.id"), nullable=False, index=True + ForeignKey("user.id"), nullable=False, index=True ) def serialize(self): From 5c950bffa00d14d37df15a41c8bef7d2c07a02ea Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Fri, 22 Aug 2025 23:55:16 +0000 Subject: [PATCH 07/13] Resolver errores --- src/api/models.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 3686d35825..3682e262a3 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -6,10 +6,6 @@ db = SQLAlchemy() - -db = SQLAlchemy() - - # Tabla que relaciona user con rol UserRole = db.Table( 'user_rol', @@ -24,8 +20,10 @@ class User(db.Model): __tablename__ = 'user' id: Mapped[int] = mapped_column(Integer, primary_key=True) - email: Mapped[String] = mapped_column(String(120), unique=True, nullable=False) - username: Mapped[String] = mapped_column(String(80), unique=True, nullable=False) + email: Mapped[String] = mapped_column( + String(120), unique=True, nullable=False) + username: Mapped[String] = mapped_column( + String(80), unique=True, nullable=False) password: Mapped[String] = mapped_column(String(200), nullable=False) created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) @@ -45,7 +43,8 @@ def serialize(self): class Rol(db.Model): __tablename__ = 'rol' id: Mapped[int] = mapped_column(Integer, primary_key=True) - type: Mapped[String] = mapped_column(String(50), unique=True, nullable=False) + type: Mapped[String] = mapped_column( + String(50), unique=True, nullable=False) user: Mapped[list['User']] = relationship('User', secondary='user_rol') def serialize(self): @@ -57,7 +56,8 @@ def serialize(self): class Profile(db.Model): __tablename__ = 'profile' - user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), primary_key=True) + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey('user.id'), primary_key=True) name: Mapped[String] = mapped_column(String(100), nullable=False) last_name: Mapped[String] = mapped_column(String(100), nullable=False) avatar: Mapped[String] = mapped_column(String(100), nullable=True) @@ -87,11 +87,14 @@ def serialize(self): class AccountSettings(db.Model): __tablename__ = 'account_settings' - user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), primary_key=True) + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey('user.id'), primary_key=True) phone: Mapped[String | None] = mapped_column(String(20), nullable=True) - billing_info: Mapped[String | None] = mapped_column(String(250), nullable=True) + billing_info: Mapped[String | None] = mapped_column( + String(250), nullable=True) language: Mapped[String | None] = mapped_column(String(50), nullable=True) - marketing_emails: Mapped[bool | None] = mapped_column(Boolean, nullable=True) + marketing_emails: Mapped[bool | None] = mapped_column( + Boolean, nullable=True) created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) @@ -102,7 +105,7 @@ def serialize(self): "billing_info": self.billing_info, "language": self.language, "marketing_emails": self.marketing_emails, - "created_at": self.crated_at, + "created_at": self.created_at, "modified_at": self.modified_at, } @@ -118,7 +121,7 @@ def serialize(self): class Task(db.Model): - __tablename__ = "tasks" + __tablename__ = "task" id: Mapped[int] = mapped_column(Integer, primary_key=True) title: Mapped[str | None] = mapped_column(String(120), nullable=True) @@ -336,9 +339,9 @@ class Message(db.Model): dealed_id: Mapped[int] = mapped_column(ForeignKey( 'task_dealed.id'), unique=True, nullable=False) sender_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=False) + ForeignKey('user.id'), nullable=False) user = db.relationship( - 'User', secondary=users_messages, back_populates="messages") + 'user', secondary=users_messages, back_populates="messages") #REVISAR! def serialize(self): return { @@ -361,9 +364,9 @@ class Dispute(db.Model): dealed_id: Mapped[int] = mapped_column(ForeignKey( 'task_dealed.id'), unique=True, nullable=False) raised_by: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=False) + ForeignKey('user.id'), nullable=False) resolved_by_admin_user: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=True) + ForeignKey('user.id'), nullable=True) def serialize(self): return { @@ -385,7 +388,7 @@ class Admin_action(db.Model): action: Mapped[str] = mapped_column(String(60), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) dispute_id: Mapped[int] = mapped_column( - ForeignKey('dispute.id'), unique=True, nullable=False) + ForeignKey('dispute.id'), nullable=False) admin_user: Mapped[int] = mapped_column( ForeignKey('user.id'), unique=True, nullable=False) From ae7d62a5207c006e12a5dd7a054b273115f6effa Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Mon, 25 Aug 2025 16:08:52 +0000 Subject: [PATCH 08/13] Arreglado errores --- src/api/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 3682e262a3..074f5999a7 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -272,7 +272,6 @@ def serialize(self): "accepted_at": self.accepted_at.isoformat() if self.accepted_at else None, "delivered_at": self.delivered_at.isoformat() if self.delivered_at else None, "cancelled_at": self.cancelled_at.isoformat() if self.cancelled_at else None - # do not serialize the password, its a security breach } @@ -340,8 +339,8 @@ class Message(db.Model): 'task_dealed.id'), unique=True, nullable=False) sender_id: Mapped[int] = mapped_column( ForeignKey('user.id'), nullable=False) - user = db.relationship( - 'user', secondary=users_messages, back_populates="messages") #REVISAR! + user: Mapped['User'] = mapped_column( + ForeignKey('user.id'), nullable=False) def serialize(self): return { From 47efb8847e01bc1341d2e72eb41730a9628dd1a8 Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Mon, 25 Aug 2025 18:11:59 +0000 Subject: [PATCH 09/13] Refactorizacion completa de los modelos --- src/api/models.py | 256 +++++++++++++++++++++------------------------- 1 file changed, 119 insertions(+), 137 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index 074f5999a7..43438d564d 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,5 +1,5 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean, Integer, ForeignKey, Date, Text, Numeric, DateTime, func, UniqueConstraint +from sqlalchemy import String, Boolean, Integer, ForeignKey, Date, Text, Numeric, DateTime, func, UniqueConstraint, Float from sqlalchemy.orm import relationship, Mapped, mapped_column from datetime import date, datetime from decimal import Decimal @@ -19,15 +19,14 @@ class User(db.Model): __tablename__ = 'user' - id: Mapped[int] = mapped_column(Integer, primary_key=True) - email: Mapped[String] = mapped_column( - String(120), unique=True, nullable=False) - username: Mapped[String] = mapped_column( - String(80), unique=True, nullable=False) - password: Mapped[String] = mapped_column(String(200), nullable=False) - created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) - modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) - roles: Mapped[list['Rol']] = relationship('Rol', secondary='user_rol',) + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(200), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) + roles = db.relationship('Rol', secondary='user_rol',) + messages = db.relationship('Message', back_populates='user') def serialize(self): return { @@ -42,10 +41,9 @@ def serialize(self): class Rol(db.Model): __tablename__ = 'rol' - id: Mapped[int] = mapped_column(Integer, primary_key=True) - type: Mapped[String] = mapped_column( - String(50), unique=True, nullable=False) - user: Mapped[list['User']] = relationship('User', secondary='user_rol') + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(50), unique=True, nullable=False) + user = db.relationship('User', secondary='user_rol') def serialize(self): return { @@ -56,18 +54,17 @@ def serialize(self): class Profile(db.Model): __tablename__ = 'profile' - user_id: Mapped[int] = mapped_column( - Integer, ForeignKey('user.id'), primary_key=True) - name: Mapped[String] = mapped_column(String(100), nullable=False) - last_name: Mapped[String] = mapped_column(String(100), nullable=False) - avatar: Mapped[String] = mapped_column(String(100), nullable=True) - city: Mapped[String] = mapped_column(String(50), nullable=False) - birth_date: Mapped[date | None] = mapped_column(Date, nullable=True) - bio: Mapped[String | None] = mapped_column(String(250), nullable=True) - skills: Mapped[String | None] = mapped_column(String(250), nullable=True) - rating_avg: Mapped[float | None] = mapped_column(Float, nullable=True) - created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) - modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + user_id = db.Column(db.Integer, ForeignKey('user.id'), primary_key=True) + name = db.Column(db.String(100), nullable=False) + last_name = db.Column(db.String(100), nullable=False) + avatar = db.Column(db.String(100), nullable=True) + city = db.Column(db.String(50), nullable=False) + birth_date = db.Column(db.Date, nullable=True) + bio = db.Column(db.String(250), nullable=True) + skills = db.Column(db.String(250), nullable=True) + rating_avg = db.Column(db.Float, nullable=True) + created_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) def serialize(self): return { @@ -87,16 +84,13 @@ def serialize(self): class AccountSettings(db.Model): __tablename__ = 'account_settings' - user_id: Mapped[int] = mapped_column( - Integer, ForeignKey('user.id'), primary_key=True) - phone: Mapped[String | None] = mapped_column(String(20), nullable=True) - billing_info: Mapped[String | None] = mapped_column( - String(250), nullable=True) - language: Mapped[String | None] = mapped_column(String(50), nullable=True) - marketing_emails: Mapped[bool | None] = mapped_column( - Boolean, nullable=True) - created_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) - modified_at: Mapped[DateTime] = mapped_column(DateTime, nullable=False) + user_id = db.Column(db.Integer, ForeignKey('user.id'), primary_key=True) + phone = db.Column(db.String(20), nullable=True) + billing_info = db.Column(db.String(250), nullable=True) + language = db.Column(db.String(50), nullable=True) + marketing_emails = db.Column(db.Boolean, nullable=True) + created_at = db.Column(db.DateTime, nullable=False) + modified_at = db.Column(db.DateTime, nullable=False) def serialize(self): return { @@ -114,7 +108,7 @@ def serialize(self): task_categories = db.Table( "task_categories", db.Column("task_id", Integer, db.ForeignKey( - "tasks.id", ondelete="CASCADE"), primary_key=True), + "task.id", ondelete="CASCADE"), primary_key=True), db.Column("category_id", Integer, db.ForeignKey( "categories.id", ondelete="CASCADE"), primary_key=True), ) @@ -123,33 +117,30 @@ def serialize(self): class Task(db.Model): __tablename__ = "task" - id: Mapped[int] = mapped_column(Integer, primary_key=True) - title: Mapped[str | None] = mapped_column(String(120), nullable=True) - description: Mapped[str | None] = mapped_column(Text, nullable=True) - location: Mapped[str | None] = mapped_column(String(120), nullable=True) - price: Mapped[Decimal | None] = mapped_column( - Numeric(10, 2), nullable=True) + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(120), nullable=True) + description = db.Column(db.Text, nullable=True) + location = db.Column(db.String(120), nullable=True) + price = db.Column(db.Numeric(10, 2), nullable=True) # dates - due_at: Mapped[datetime | None] = mapped_column( - DateTime, nullable=True) # timestamp + due_at = db.Column(db.DateTime, nullable=True) # timestamp # DB fills posted_at automatically with current_date() - posted_at: Mapped[date] = mapped_column( - Date, nullable=False, server_default=func.current_date()) - assigned_at: Mapped[date | None] = mapped_column(Date, nullable=True) - completed_at: Mapped[date | None] = mapped_column(Date, nullable=True) + posted_at = db.Column(db.Date, nullable=False, + server_default=func.current_date()) + assigned_at = db.Column(db.Date, nullable=True) + completed_at = db.Column(db.Date, nullable=True) # business-defined value (app.py) - status: Mapped[str] = mapped_column( - String(30), nullable=False, server_default="pending") + status = db.Column(db.String(30), nullable=False, server_default="pending") # FK + relationship (1 User -> many Tasks) - publisher_id: Mapped[int] = mapped_column( - ForeignKey("user.id"), nullable=False, index=True) - publisher: Mapped["User"] = relationship(backref="tasks") + publisher_id = db.Column(db.Integer, ForeignKey( + "user.id"), nullable=False, index=True) + publisher = db.relationship("User", backref="tasks") # relationship categories - categories: Mapped[list["Category"]] = relationship( + categories = db.relationship( "Category", secondary=task_categories, back_populates="tasks") def serialize(self): @@ -177,12 +168,11 @@ def serialize_all_data(self): class Category(db.Model): __tablename__ = "categories" - id: Mapped[int] = mapped_column(Integer, primary_key=True) - name: Mapped[str] = mapped_column( - String(50), nullable=False, unique=True) + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), nullable=False, unique=True) # relationship - tasks: Mapped[list["Task"]] = relationship( + tasks = db.relationship( "Task", secondary=task_categories, back_populates="categories") # return type @@ -196,23 +186,22 @@ def serialize(self): class TaskOffered(db.Model): __tablename__ = "tasks_offered" - id: Mapped[int] = mapped_column(Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True) # the values will be defined in the business layer(app.py) - status: Mapped[Decimal | None] = mapped_column( - Numeric(10, 2), nullable=True) + status = db.Column(db.Numeric(10, 2), nullable=True) # dates - created_at: Mapped[date] = mapped_column( - Date, nullable=False, server_default=func.current_date()) - updated_at: Mapped[date] = mapped_column( - Date, nullable=False, + created_at = db.Column( + db.Date, nullable=False, server_default=func.current_date()) + updated_at = db.Column( + db.Date, nullable=False, server_default=func.current_date(), server_onupdate=func.current_date() ) # FK - task_id: Mapped[int] = mapped_column( - ForeignKey("tasks.id"), nullable=False, index=True) - tasker_id: Mapped[int] = mapped_column( - ForeignKey("user.id"), nullable=False, index=True) + task_id = db.Column(db.Integer, ForeignKey( + "task.id"), nullable=False, index=True) + tasker_id = db.Column(db.Integer, ForeignKey( + "user.id"), nullable=False, index=True) # one offer per (task, tasker) in pair __table_args__ = ( @@ -234,31 +223,27 @@ def serialize(self): class TaskDealed(db.Model): - __tablename__ = "tasks_dealed" + __tablename__ = "task_dealed" - id: Mapped[int] = mapped_column(Integer, primary_key=True) - fixed_price: Mapped[Decimal | None] = mapped_column( - Numeric(10, 2), nullable=True) + id = db.Column(db.Integer, primary_key=True) + fixed_price = db.Column(db.Numeric(10, 2), nullable=True) # the values will be defined in the business layer(app.py) - status: Mapped[str] = mapped_column(String(30), nullable=False) + status = db.Column(db.String(30), nullable=False) # dates - accepted_at: Mapped[date | None] = mapped_column(Date, nullable=True) - delivered_at: Mapped[date | None] = mapped_column(Date, nullable=True) - cancelled_at: Mapped[date | None] = mapped_column(Date, nullable=True) + accepted_at = db.Column(db.Date, nullable=True) + delivered_at = db.Column(db.Date, nullable=True) + cancelled_at = db.Column(db.Date, nullable=True) # FK - task_id: Mapped[int] = mapped_column( - ForeignKey("tasks.id"), nullable=False, index=True - ) - offer_id: Mapped[int] = mapped_column( - ForeignKey("tasks_offered.id"), nullable=False, index=True - ) - client_id: Mapped[int] = mapped_column( - ForeignKey("user.id"), nullable=False, index=True - ) - tasker_id: Mapped[int] = mapped_column( - ForeignKey("user.id"), nullable=False, index=True + task_id = db.Column( + db.Integer, ForeignKey("task.id"), nullable=False, index=True ) + offer_id = db.Column(db.Integer, ForeignKey( + "tasks_offered.id"), nullable=False, index=True) + client_id = db.Column(db.Integer, ForeignKey( + "user.id"), nullable=False, index=True) + tasker_id = db.Column(db.Integer, ForeignKey( + "user.id"), nullable=False, index=True) def serialize(self): return { @@ -278,24 +263,23 @@ def serialize(self): class Payment(db.Model): __tablename__ = "payments" - id: Mapped[int] = mapped_column(Integer, primary_key=True) - amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False) + id = db.Column(db.Integer, primary_key=True) + amount = db.Column(db.Numeric(10, 2), nullable=False) # the values will be defined in the business layer(app.py) - status: Mapped[str] = mapped_column(String(20), nullable=False) + status = db.Column(db.String(20), nullable=False) # dates - created_at: Mapped[date] = mapped_column( - Date, nullable=False, server_default=func.current_date() + created_at = db.Column( + db.Date, nullable=False, server_default=func.current_date() ) - updated_at: Mapped[date] = mapped_column( - Date, nullable=False, + updated_at = db.Column( + db.Date, nullable=False, server_default=func.current_date(), server_onupdate=func.current_date() ) # FK - dealed_id: Mapped[int] = mapped_column( - ForeignKey("tasks_dealed.id", ondelete="CASCADE"), unique=True, nullable=False, index=True - ) + dealed_id = db.Column(db.Integer, ForeignKey( + "task_dealed.id", ondelete="CASCADE"), unique=True, nullable=False, index=True) def serialize(self): return { @@ -309,16 +293,17 @@ def serialize(self): class Review(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - review: Mapped[str] = mapped_column(String(10000), nullable=True) - rate: Mapped[int] = mapped_column(Integer, nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - publisher_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), nullable=False) - worker_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), nullable=False) - task_dealed_id: Mapped[int] = mapped_column( - ForeignKey('task_dealed.id'), unique=True, nullable=False) + id = db.Column(db.Integer, primary_key=True) + review = db.Column(db.String(10000), nullable=True) + rate = db.Column(db.Numeric(3, 2), nullable=False) + created_at = db.Column(db.DateTime(timezone=True)) + publisher_id = db.Column( + db.Integer, ForeignKey('user.id'), nullable=False) + worker_id = db.Column( + db.Integer, ForeignKey('user.id'), nullable=False) + task_dealed_id = db.Column( + db.Integer, ForeignKey('task_dealed.id'), unique=True, nullable=False) + task_id = db.Column(db.Integer, ForeignKey('task.id'), nullable=False) def serialize(self): return { @@ -332,40 +317,37 @@ def serialize(self): class Message(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - body: Mapped[str] = mapped_column(String(100000), nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dealed_id: Mapped[int] = mapped_column(ForeignKey( + id = db.Column(db.Integer, primary_key=True) + body = db.Column(db.String(10000), nullable=True) + created_at = db.Column(db.DateTime(timezone=True)) + dealer_id = db.Column(db.Integer, ForeignKey( 'task_dealed.id'), unique=True, nullable=False) - sender_id: Mapped[int] = mapped_column( - ForeignKey('user.id'), nullable=False) - user: Mapped['User'] = mapped_column( - ForeignKey('user.id'), nullable=False) + sender_id = db.Column(db.Integer, ForeignKey( + 'user.id'), nullable=False) def serialize(self): return { "id": self.id, "body": self.body, "created_at": self.created_at, - "dealed_id": self.dealed_id, + "dealer_id": self.dealer_id, "sender_id": self.sender_id, } class Dispute(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - reason: Mapped[str] = mapped_column(String(120), nullable=False) - details: Mapped[str] = mapped_column(String(1000), nullable=False) - status: Mapped[str] = mapped_column(String(30), nullable=False) - resolution: Mapped[str] = mapped_column(String(1000), nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dealed_id: Mapped[int] = mapped_column(ForeignKey( + id = db.Column(db.Integer, primary_key=True) + reason = db.Column(db.String(120), nullable=False) + details = db.Column(db.String(1000), nullable=False) + status = db.Column(db.String(30), nullable=False) + resolution = db.Column(db.String(1000), nullable=True) + created_at = db.Column(db.DateTime(timezone=True)) + updated_at = db.Column(db.DateTime(timezone=True)) + dealed_id = db.Column(db.Integer, ForeignKey( 'task_dealed.id'), unique=True, nullable=False) - raised_by: Mapped[int] = mapped_column( - ForeignKey('user.id'), nullable=False) - resolved_by_admin_user: Mapped[int] = mapped_column( - ForeignKey('user.id'), nullable=True) + raised_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + resolved_by_admin_user = db.Column( + db.Integer, db.ForeignKey('user.id'), nullable=True) def serialize(self): return { @@ -383,13 +365,13 @@ def serialize(self): class Admin_action(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - action: Mapped[str] = mapped_column(String(60), nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) - dispute_id: Mapped[int] = mapped_column( - ForeignKey('dispute.id'), nullable=False) - admin_user: Mapped[int] = mapped_column( - ForeignKey('user.id'), unique=True, nullable=False) + id = db.Column(db.Integer, primary_key=True) + action = db.Column(db.String(60), nullable=False) + created_at = db.Column(db.DateTime(timezone=True)) + dispute_id = db.Column( + db.Integer, ForeignKey('dispute.id'), nullable=False) + admin_user = db.Column( + db.Integer, ForeignKey('user.id'), unique=True, nullable=False) def serialize(self): return { From 0cb2a3df8c8f9234eea210550049d05c6fc6ecc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaqueline=20T=C3=A9llez?= <78996295+jaquenwn@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:20:33 +0000 Subject: [PATCH 10/13] vista crear tarea --- src/front/pages/NewTask.jsx | 73 +++++++++++++++++++++++++++++++++++++ src/front/routes.jsx | 2 + 2 files changed, 75 insertions(+) create mode 100644 src/front/pages/NewTask.jsx diff --git a/src/front/pages/NewTask.jsx b/src/front/pages/NewTask.jsx new file mode 100644 index 0000000000..121bf2a83f --- /dev/null +++ b/src/front/pages/NewTask.jsx @@ -0,0 +1,73 @@ +import { Link } from "react-router-dom"; +import useGlobalReducer from "../hooks/useGlobalReducer"; // Custom hook for accessing the global state. + +export const NewTask = () => { + // Access the global state and dispatch function using the useGlobalReducer hook. + const { store, dispatch } = useGlobalReducer() + + return ( +
+ + + +
+

Publica una nueva tarea

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • + +
+
+
+ + +
+ +
+
+ ); +}; diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..4aa52c54ae 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,6 +9,7 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import { NewTask } from "./pages/NewTask"; export const router = createBrowserRouter( createRoutesFromElements( @@ -25,6 +26,7 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> + } /> ) ); \ No newline at end of file From 1c72a79dcf3195cdc088cbda5c3c8fccf8382fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaqueline=20T=C3=A9llez?= <78996295+jaquenwn@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:51:02 +0000 Subject: [PATCH 11/13] formulario tarea --- src/front/pages/NewTask.jsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/front/pages/NewTask.jsx b/src/front/pages/NewTask.jsx index 121bf2a83f..031ce2d8a9 100644 --- a/src/front/pages/NewTask.jsx +++ b/src/front/pages/NewTask.jsx @@ -24,7 +24,20 @@ export const NewTask = () => {
- +
+
+ + +
+
+ + +
+
+ + +
+
@@ -59,7 +72,7 @@ export const NewTask = () => { - +
From 1f50d844de3addd32bf16f1291feed05d633d8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaqueline=20T=C3=A9llez?= <78996295+jaquenwn@users.noreply.github.com> Date: Tue, 26 Aug 2025 02:08:07 +0000 Subject: [PATCH 12/13] vista basica admin --- src/front/components/AdminCard.jsx | 20 +++++++++++ src/front/pages/Admin.jsx | 56 ++++++++++++++++++++++++++++++ src/front/pages/Demo.jsx | 1 + src/front/routes.jsx | 2 ++ 4 files changed, 79 insertions(+) create mode 100644 src/front/components/AdminCard.jsx create mode 100644 src/front/pages/Admin.jsx diff --git a/src/front/components/AdminCard.jsx b/src/front/components/AdminCard.jsx new file mode 100644 index 0000000000..6184b8ddfe --- /dev/null +++ b/src/front/components/AdminCard.jsx @@ -0,0 +1,20 @@ +export const AdminCard = () => ( +
+
+
+ ... +
+
+
+
Nombre Apellido
+

Administrador principal

+

lol@email.com

+

Miembro desde xd

+
+
+

Ultima conexión
3 mins ago

+
+
+
+
+); \ No newline at end of file diff --git a/src/front/pages/Admin.jsx b/src/front/pages/Admin.jsx new file mode 100644 index 0000000000..a98c7ded76 --- /dev/null +++ b/src/front/pages/Admin.jsx @@ -0,0 +1,56 @@ +// Import necessary components from react-router-dom and other parts of the application. +import { Link } from "react-router-dom"; +import useGlobalReducer from "../hooks/useGlobalReducer"; // Custom hook for accessing the global state. +import { AdminCard } from "../components/AdminCard" +export const Admin = () => { + // Access the global state and dispatch function using the useGlobalReducer hook. + const { store, dispatch } = useGlobalReducer() + + const filterList = ["Todo", "Pendientes", "Abiertas", "Cerradas"] + const tableHeader= ["ID","Título","Usuario", "Estado","Prioridad","Categoría","Fecha creación"] + const apiVars= ["id","title", "user","status","priority","category","created_at"] + + return ( +
+

Panel de Administrador

+ +
+
Gestión de disputas
+

Administra y resuelve las disputas de los usuarios

+ +
+ {filterList.map((filtro, index) => +
+ +
)} +
+ + + + + + {tableHeader.map((header, index) => + )} + + + + + + + {apiVars.map((fet, index) => + + )} + + + + +
{header}Acciones
{fet}Otto
+
+
+ + + + +
+ ); +}; \ No newline at end of file diff --git a/src/front/pages/Demo.jsx b/src/front/pages/Demo.jsx index 34250a45b7..a067d60af8 100644 --- a/src/front/pages/Demo.jsx +++ b/src/front/pages/Demo.jsx @@ -2,6 +2,7 @@ import { Link } from "react-router-dom"; import useGlobalReducer from "../hooks/useGlobalReducer"; // Custom hook for accessing the global state. + export const Demo = () => { // Access the global state and dispatch function using the useGlobalReducer hook. const { store, dispatch } = useGlobalReducer() diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 4aa52c54ae..9cb39d4cb8 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -10,6 +10,7 @@ import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; import { NewTask } from "./pages/NewTask"; +import { Admin } from "./pages/Admin"; export const router = createBrowserRouter( createRoutesFromElements( @@ -27,6 +28,7 @@ export const router = createBrowserRouter( } /> {/* Dynamic route for single items */} } /> } /> + } /> ) ); \ No newline at end of file From b15a3259872cf9a1e4ec9dcd3bfb9170d07ecac5 Mon Sep 17 00:00:00 2001 From: Maximiliano Manriquez Date: Tue, 26 Aug 2025 15:49:52 +0000 Subject: [PATCH 13/13] Avances en los endpoints --- Pipfile | 6 +- Pipfile.lock | 30 +- calhost | 0 migrations/versions/0763d677d453_.py | 35 --- migrations/versions/3bb1f1f3f9b9_.py | 245 ++++++++++++++++ migrations/versions/4aed268b8be9_.py | 0 return | 21 ++ src/api/models.py | 15 +- src/api/routes.py | 409 ++++++++++++++++++++++++++- 9 files changed, 697 insertions(+), 64 deletions(-) create mode 100644 calhost delete mode 100644 migrations/versions/0763d677d453_.py create mode 100644 migrations/versions/3bb1f1f3f9b9_.py create mode 100644 migrations/versions/4aed268b8be9_.py create mode 100644 return diff --git a/Pipfile b/Pipfile index a0e0872ccb..d35986da62 100644 --- a/Pipfile +++ b/Pipfile @@ -6,8 +6,6 @@ verify_ssl = true [dev-packages] [packages] -flask = "*" -flask-migrate = "*" flask-swagger = "*" psycopg2-binary = "*" python-dotenv = "*" @@ -16,10 +14,12 @@ gunicorn = "*" cloudinary = "*" flask-admin = "*" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" +flask-jwt-extended = "*" +flask-migrate = "*" flask-sqlalchemy = "*" sqlalchemy = "*" +flask = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index ba1d6c4120..88842036f2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "1e3cc14c317d2e1d7cccecd1fcc7afd5b60c23767c0d3997b1db5574a144d218" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "alembic": { "hashes": [ - "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", - "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213" + "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", + "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2" ], - "markers": "python_version >= '3.8'", - "version": "==1.14.1" + "markers": "python_version >= '3.9'", + "version": "==1.16.4" }, "blinker": { "hashes": [ @@ -83,11 +83,12 @@ }, "flask-jwt-extended": { "hashes": [ - "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", - "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" + "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", + "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" ], "index": "pypi", - "version": "==4.6.0" + "markers": "python_version >= '3.9' and python_version < '4'", + "version": "==4.7.1" }, "flask-migrate": { "hashes": [ @@ -95,6 +96,7 @@ "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==4.1.0" }, "flask-sqlalchemy": { @@ -200,11 +202,11 @@ }, "mako": { "hashes": [ - "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", - "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac" + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" ], "markers": "python_version >= '3.8'", - "version": "==1.3.9" + "version": "==1.3.10" }, "markupsafe": { "hashes": [ @@ -504,12 +506,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", - "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.14.1" + "version": "==4.15.0" }, "urllib3": { "hashes": [ diff --git a/calhost b/calhost new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/migrations/versions/3bb1f1f3f9b9_.py b/migrations/versions/3bb1f1f3f9b9_.py new file mode 100644 index 0000000000..2e6d88b4c1 --- /dev/null +++ b/migrations/versions/3bb1f1f3f9b9_.py @@ -0,0 +1,245 @@ +"""empty message + +Revision ID: 3bb1f1f3f9b9 +Revises: +Create Date: 2025-08-25 21:49:48.718476 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3bb1f1f3f9b9' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('category', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('rol', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('type') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('password', sa.String(length=200), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('modified_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('account_settings', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('phone', sa.String(length=20), nullable=True), + sa.Column('billing_info', sa.String(length=250), nullable=True), + sa.Column('language', sa.String(length=50), nullable=True), + sa.Column('marketing_emails', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('modified_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id') + ) + op.create_table('profile', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('last_name', sa.String(length=100), nullable=False), + sa.Column('avatar', sa.String(length=100), nullable=True), + sa.Column('city', sa.String(length=50), nullable=False), + sa.Column('birth_date', sa.Date(), nullable=True), + sa.Column('bio', sa.String(length=250), nullable=True), + sa.Column('skills', sa.String(length=250), nullable=True), + sa.Column('rating_avg', sa.Float(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('modified_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id') + ) + op.create_table('task', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=120), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('location', sa.String(length=120), nullable=True), + sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('due_at', sa.DateTime(), nullable=True), + sa.Column('posted_at', sa.Date(), server_default=sa.text('CURRENT_DATE'), nullable=False), + sa.Column('assigned_at', sa.Date(), nullable=True), + sa.Column('completed_at', sa.Date(), nullable=True), + sa.Column('status', sa.String(length=30), server_default='pending', nullable=False), + sa.Column('publisher_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['publisher_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('task', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_task_publisher_id'), ['publisher_id'], unique=False) + + op.create_table('user_rol', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['rol.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'role_id') + ) + op.create_table('task_categories', + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('category_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['category.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['task_id'], ['task.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('task_id', 'category_id') + ) + op.create_table('tasks_offered', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('status', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('created_at', sa.Date(), server_default=sa.text('CURRENT_DATE'), nullable=False), + sa.Column('updated_at', sa.Date(), server_default=sa.text('CURRENT_DATE'), nullable=False), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('tasker_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['task_id'], ['task.id'], ), + sa.ForeignKeyConstraint(['tasker_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('task_id', 'tasker_id', name='uq_tasks_offered_task_tasker') + ) + with op.batch_alter_table('tasks_offered', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_tasks_offered_task_id'), ['task_id'], unique=False) + batch_op.create_index(batch_op.f('ix_tasks_offered_tasker_id'), ['tasker_id'], unique=False) + + op.create_table('task_dealed', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('fixed_price', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('status', sa.String(length=30), nullable=False), + sa.Column('accepted_at', sa.Date(), nullable=True), + sa.Column('delivered_at', sa.Date(), nullable=True), + sa.Column('cancelled_at', sa.Date(), nullable=True), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('offer_id', sa.Integer(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('tasker_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['client_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['offer_id'], ['tasks_offered.id'], ), + sa.ForeignKeyConstraint(['task_id'], ['task.id'], ), + sa.ForeignKeyConstraint(['tasker_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('task_dealed', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_task_dealed_client_id'), ['client_id'], unique=False) + batch_op.create_index(batch_op.f('ix_task_dealed_offer_id'), ['offer_id'], unique=False) + batch_op.create_index(batch_op.f('ix_task_dealed_task_id'), ['task_id'], unique=False) + batch_op.create_index(batch_op.f('ix_task_dealed_tasker_id'), ['tasker_id'], unique=False) + + op.create_table('dispute', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('reason', sa.String(length=120), nullable=False), + sa.Column('details', sa.String(length=1000), nullable=False), + sa.Column('status', sa.String(length=30), nullable=False), + sa.Column('resolution', sa.String(length=1000), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('dealed_id', sa.Integer(), nullable=False), + sa.Column('raised_by', sa.Integer(), nullable=False), + sa.Column('resolved_by_admin_user', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dealed_id'], ['task_dealed.id'], ), + sa.ForeignKeyConstraint(['raised_by'], ['user.id'], ), + sa.ForeignKeyConstraint(['resolved_by_admin_user'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('dealed_id') + ) + op.create_table('message', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('body', sa.String(length=10000), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.Column('dealer_id', sa.Integer(), nullable=False), + sa.Column('sender_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['dealer_id'], ['task_dealed.id'], ), + sa.ForeignKeyConstraint(['sender_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('dealer_id') + ) + op.create_table('payments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('created_at', sa.Date(), server_default=sa.text('CURRENT_DATE'), nullable=False), + sa.Column('updated_at', sa.Date(), server_default=sa.text('CURRENT_DATE'), nullable=False), + sa.Column('dealed_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['dealed_id'], ['task_dealed.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('payments', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_payments_dealed_id'), ['dealed_id'], unique=True) + + op.create_table('review', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('review', sa.String(length=10000), nullable=True), + sa.Column('rate', sa.Numeric(precision=3, scale=2), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('publisher_id', sa.Integer(), nullable=False), + sa.Column('worker_id', sa.Integer(), nullable=False), + sa.Column('task_dealed_id', sa.Integer(), nullable=False), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['publisher_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['task_dealed_id'], ['task_dealed.id'], ), + sa.ForeignKeyConstraint(['task_id'], ['task.id'], ), + sa.ForeignKeyConstraint(['worker_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('task_dealed_id') + ) + op.create_table('admin_action', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action', sa.String(length=60), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('dispute_id', sa.Integer(), nullable=False), + sa.Column('admin_user', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['admin_user'], ['user.id'], ), + sa.ForeignKeyConstraint(['dispute_id'], ['dispute.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('admin_user') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('admin_action') + op.drop_table('review') + with op.batch_alter_table('payments', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_payments_dealed_id')) + + op.drop_table('payments') + op.drop_table('message') + op.drop_table('dispute') + with op.batch_alter_table('task_dealed', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_task_dealed_tasker_id')) + batch_op.drop_index(batch_op.f('ix_task_dealed_task_id')) + batch_op.drop_index(batch_op.f('ix_task_dealed_offer_id')) + batch_op.drop_index(batch_op.f('ix_task_dealed_client_id')) + + op.drop_table('task_dealed') + with op.batch_alter_table('tasks_offered', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_tasks_offered_tasker_id')) + batch_op.drop_index(batch_op.f('ix_tasks_offered_task_id')) + + op.drop_table('tasks_offered') + op.drop_table('task_categories') + op.drop_table('user_rol') + with op.batch_alter_table('task', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_task_publisher_id')) + + op.drop_table('task') + op.drop_table('profile') + op.drop_table('account_settings') + op.drop_table('user') + op.drop_table('rol') + op.drop_table('category') + # ### end Alembic commands ### diff --git a/migrations/versions/4aed268b8be9_.py b/migrations/versions/4aed268b8be9_.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/return b/return new file mode 100644 index 0000000000..4eb01ad6d6 --- /dev/null +++ b/return @@ -0,0 +1,21 @@ + List of relations + Schema | Name | Type | Owner +--------+------------------+-------+-------- + public | account_settings | table | gitpod + public | admin_action | table | gitpod + public | alembic_version | table | gitpod + public | categories | table | gitpod + public | dispute | table | gitpod + public | message | table | gitpod + public | payments | table | gitpod + public | profile | table | gitpod + public | review | table | gitpod + public | rol | table | gitpod + public | task | table | gitpod + public | task_categories | table | gitpod + public | task_dealed | table | gitpod + public | tasks_offered | table | gitpod + public | user | table | gitpod + public | user_rol | table | gitpod +(16 rows) + diff --git a/src/api/models.py b/src/api/models.py index 43438d564d..283c5d0f9c 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -23,8 +23,10 @@ class User(db.Model): email = db.Column(db.String(120), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(200), nullable=False) - created_at = db.Column(db.DateTime, nullable=False) - modified_at = db.Column(db.DateTime, nullable=False) + created_at = db.Column(db.DateTime, nullable=False, + server_default=func.current_timestamp()) + modified_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp( + ), server_onupdate=func.current_timestamp()) roles = db.relationship('Rol', secondary='user_rol',) messages = db.relationship('Message', back_populates='user') @@ -110,7 +112,7 @@ def serialize(self): db.Column("task_id", Integer, db.ForeignKey( "task.id", ondelete="CASCADE"), primary_key=True), db.Column("category_id", Integer, db.ForeignKey( - "categories.id", ondelete="CASCADE"), primary_key=True), + "category.id", ondelete="CASCADE"), primary_key=True), ) @@ -162,11 +164,12 @@ def serialize_all_data(self): "assigned_at": self.assigned_at.isoformat() if self.assigned_at else None, "completed_at": self.completed_at.isoformat() if self.completed_at else None, "publisher_id": self.publisher_id, + "categories": [categories.serialize() for categories in self.categories] if self.categories else [], } class Category(db.Model): - __tablename__ = "categories" + __tablename__ = "category" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False, unique=True) @@ -319,11 +322,13 @@ def serialize(self): class Message(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String(10000), nullable=True) - created_at = db.Column(db.DateTime(timezone=True)) + created_at = db.Column(db.DateTime(timezone=True), + server_default=func.current_timestamp()) dealer_id = db.Column(db.Integer, ForeignKey( 'task_dealed.id'), unique=True, nullable=False) sender_id = db.Column(db.Integer, ForeignKey( 'user.id'), nullable=False) + user = db.relationship('User', back_populates='messages') def serialize(self): return { diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..67d6782e62 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,21 +2,416 @@ This module takes care of starting the API Server, Loading the DB and Adding the endpoints """ from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User +from api.models import db, User, Task, Category, TaskOffered, Profile, AccountSettings, TaskDealed, Payment, Review, Message, Dispute, Admin_action from api.utils import generate_sitemap, APIException from flask_cors import CORS +from datetime import datetime api = Blueprint('api', __name__) # Allow CORS requests to this API CORS(api) +# Endpoint para obtener todos los usuarios -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } +@api.route('/users', methods=['GET']) +def get_users(): + users = User.query.all() + users_list = [user.serialize() for user in users] + return jsonify(users_list), 200 + +# Endpoint para obtener un usuario por su id + + +@api.route('/users/', methods=['GET']) +def get_user(user_id): + user = User.query.get(user_id) + if user: + return jsonify(user.serialize()), 200 + return jsonify({"error": "Usuario no encontrado"}), 404 + +# Endpoint para crear un nuevo usuario + + +@api.route('/users', methods=['POST']) +def create_user(): + data = request.get_json() + if not data or 'email' not in data: + return jsonify({"error": "Se requiere un Email valido"}), 400 + if not data or 'password' not in data: + return jsonify({"error": "Se requiere una contraseña valida"}), 400 + if not data or 'username' not in data: + return jsonify({"error": "Se requiere un nombre de usuario valido"}), 400 + + # Importar datetime y crear current_time + current_time = datetime.utcnow() + + new_user = User( + email=data['email'], password=data['password'], username=data['username'], + created_at=current_time, modified_at=current_time + ) + db.session.add(new_user) + db.session.commit() + return jsonify(new_user.serialize()), 201 + +# Endpoint para actualizar un usuario + +@api.route('/users/', methods=['PUT']) +def update_user(user_id): + user = User.query.get(user_id) + if not user: + return jsonify({"error": "Usuario no encontrado"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "Datos inválidos"}), 400 + + user.email = data.get('email', user.email) + user.username = data.get('username', user.username) + user.password = data.get('password', user.password) + user.modified_at = datetime.utcnow() + + db.session.commit() + return jsonify(user.serialize()), 200 + +# Endpoint para eliminar un usuario + + +@api.route('/users/', methods=['DELETE']) +def delete_user(user_id): + user = User.query.get(user_id) + if user: + db.session.delete(user) + db.session.commit() + return jsonify({"message": "Usuario eliminado"}), 200 + return jsonify({"error": "Usuario no encontrado"}), 404 + +# Endpoint para obtener todas las tareas + + +@api.route('/tasks', methods=['GET']) +def get_tasks(): + tasks = Task.query.all() + tasks_list = [task.serialize() for task in tasks] + + return jsonify(tasks_list), 200 + +# Endpoint para obtener una tarea en concreto + + +@api.route('/tasks/', methods=['GET']) +def get_task(task_id): + task = Task.query.get(task_id) + if task: + return jsonify(task.serialize()), 200 + return jsonify({"error": "Tarea no encontrada"}), 404 + +# Endpoint para eliminar una tarea en concreto + + +@api.route('/tasks/', methods=['DELETE']) +def delete_tasks(task_id): + task = Task.query.get(task_id) + if task: + db.session.delete(task) + db.session.commit() + return jsonify({"message": "Tarea eliminada"}), 200 + return jsonify({"error": "Tarea no encontrada"}), 404 + +# Endpoint para crear una tarea + + +@api.route('/tasks', methods=['POST']) +def create_task(): + try: + data = request.get_json() + if not data: + return jsonify({"error": "Datos inválidos"}), 400 + if 'title' not in data: + return jsonify({"error": "Se requiere un título válido"}), 400 + if 'description' not in data: + return jsonify({"error": "Se requiere una descripción válida"}), 400 + if 'publisher_id' not in data: + return jsonify({"error": "Se requiere un ID de publicador válido"}), 400 + + current_time = datetime.utcnow() + new_task = Task( + title=data['title'], + description=data['description'], + publisher_id=data['publisher_id'], + location=data.get('location'), + price=data.get('price'), + due_at=current_time, + # Valor por defecto 'pending' si no se proporciona + status=data.get('status', 'pending') + ) + + db.session.add(new_task) + db.session.commit() + return jsonify(new_task.serialize_all_data()), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error al crear la tarea: {str(e)}"}), 500 + +# Endpoint para actualizar una tarea ya existente + + +@api.route('/tasks/', methods=['PUT']) +def update_task(task_id): + try: + task = Task.query.get(task_id) + if not task: + return jsonify({"error": "Tarea no encontrada"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "Datos inválidos"}), 400 + + # Actualizar campos + task.title = data.get('title', task.title) + task.description = data.get('description', task.description) + task.location = data.get('location', task.location) + task.price = data.get('price', task.price) + task.due_at = data.get('due_at', task.due_at) + task.status = data.get('status', task.status) + task.publisher_id = data.get('publisher_id', task.publisher_id) + + # Manejar categorías (relación n:n) + if 'category_id' in data: + # Primero verificamos si la categoría existe + category = Category.query.get(data['category_id']) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + # Limpiar categorías actuales y agregar la nueva + task.categories = [category] + + db.session.commit() + return jsonify(task.serialize_all_data()), 200 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error al actualizar la tarea: {str(e)}"}), 500 + +# Endpoint para obtener las tareas de un usuario en concreto + + +@api.route('/users//tasks', methods=['GET']) +def get_tasks_by_user(user_id): + user = User.query.get(user_id) + # Si el usuario no existe, da error + if user is None: + return jsonify({"error": "Usuario no encontrado"}), 404 + + query = Task.query.filter_by(publisher_id=user_id) + + # Filtrar por estado + status = request.args.get('status') + if status: + query = query.filter_by(status=status) + + # Filtrar por fechas + from_date = request.args.get('from_date') + if from_date: + query = query.filter(Task.created_at >= from_date) + + to_date = request.args.get('to_date') + if to_date: + query = query.filter(Task.created_at <= to_date) + + tasks = query.all() + tasks_list = [task.serialize_all_data() for task in tasks] + return jsonify(tasks_list), 200 + +# Endpoint para eliminar una tarea en concreto de un usuario en concreto + + +@api.route('/users//tasks/', methods=['DELETE']) +def delete_tasks_by_user(user_id, task_id): + user = User.query.get(user_id) + if not user: + return jsonify({"error": "usuario inválido"}), 404 + + task = Task.query.filter_by(id=task_id, publisher_id=user_id).first() + if not task: + return jsonify({"error": "tarea inválida o no pertenece al usuario"}), 404 + + db.session.delete(task) + db.session.commit() + return jsonify({"message": "Tarea eliminada"}), 200 + +# Endpoint para obtener todas las categorías + + +@api.route('/categories', methods=['GET']) +def get_categories(): + categories = Category.query.all() + categories_list = [category.serialize() for category in categories] + return jsonify(categories_list), 200 + +# Endpoint para obtener una categoría específica + + +@api.route('/categories/', methods=['GET']) +def get_category(category_id): + category = Category.query.get(category_id) + if category: + return jsonify(category.serialize()), 200 + return jsonify({"error": "Categoría no encontrada"}), 404 + +# Endpoint para crear una nueva categoría + + +@api.route('/categories', methods=['POST']) +def create_category(): + try: + data = request.get_json() + if not data or 'name' not in data: + return jsonify({"error": "Se requiere un nombre válido para la categoría"}), 400 + + new_category = Category(name=data['name']) + db.session.add(new_category) + db.session.commit() + return jsonify(new_category.serialize()), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error al crear la categoría: {str(e)}"}), 500 + +# Endpoint para eliminar una categoria en concreto + + +@api.route('/categories/', methods=['DELETE']) +def delete_category(category_id): + category = Category.query.get(category_id) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + db.session.delete(category) + db.session.commit() + return jsonify({"message": "Categoría eliminada"}), 200 + +# Endpoint para actualizar una categoría ya existente + + +@api.route('/categories/', methods=['PUT']) +def update_category(category_id): + category = Category.query.get(category_id) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + data = request.get_json() + if not data or 'name' not in data: + return jsonify({"error": "Se requiere un nombre válido para la categoría"}), 400 + + category.name = data['name'] + db.session.commit() + return jsonify(category.serialize()), 200 + +# Endpoint para obtener todas las categorías de una tarea específica + + +@api.route('/tasks//categories', methods=['GET']) +def get_categories_by_task(task_id): + task = Task.query.get(task_id) + if not task: + return jsonify({"error": "Tarea no encontrada"}), 404 + + categories = task.categories + return jsonify([category.serialize() for category in categories]), 200 + +# Endpoint para obtener todas las tareas de una categoría específica + + +@api.route('/categories//tasks', methods=['GET']) +def get_tasks_by_category(category_id): + category = Category.query.get(category_id) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + tasks = category.tasks + return jsonify([task.serialize() for task in tasks]), 200 + +# Endpoint para agregar una categoría a una tarea + + +@api.route('/tasks//categories', methods=['POST']) +def add_category_to_task(task_id): + task = Task.query.get(task_id) + if not task: + return jsonify({"error": "Tarea no encontrada"}), 404 + + data = request.get_json() + if not data or 'category_id' not in data: + return jsonify({"error": "Se requiere un ID de categoría válido"}), 400 + + category = Category.query.get(data['category_id']) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + task.categories.append(category) + db.session.commit() + return jsonify(task.serialize()), 200 + +# Endpoint para eliminar una categoria de una tarea en especifico + + +@api.route('/tasks//categories', methods=['DELETE']) +def remove_category_from_task(task_id): + task = Task.query.get(task_id) + if not task: + return jsonify({"error": "Tarea no encontrada"}), 404 + + data = request.get_json() + if not data or 'category_id' not in data: + return jsonify({"error": "Se requiere un ID de categoría válido"}), 400 + + category = Category.query.get(data['category_id']) + if not category: + return jsonify({"error": "Categoría no encontrada"}), 404 + + if category in task.categories: + task.categories.remove(category) + db.session.commit() + return jsonify(task.serialize()), 200 + else: + return jsonify({"error": "La categoría no está asociada a la tarea"}), 400 + +# Endpoint para obtener el perfil de un usuario + + +@api.route('/users//profile', methods=['GET']) +def get_profile(user_id): + profile = Profile.query.get(user_id) + if not profile: + return jsonify({"error": "Perfil no encontrado"}), 404 + + return jsonify(profile.serialize()), 200 + +# Endpoint para modificar el perfil de un usuario + + +@api.route('/users//profile', methods=['PUT']) +def update_profile(user_id): + profile = Profile.query.get(user_id) + if not profile: + return jsonify({"error": "Perfil no encontrado"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "Datos inválidos"}), 400 + + current_time = datetime.utcnow() + + profile.name = data.get("name", profile.name) + profile.last_name = data.get("last_name", profile.last_name) + profile.birth_date = data.get("birth_date", profile.birth_date) + profile.bio = data.get("bio", profile.bio) + profile.avatar = data.get("avatar", profile.avatar) + profile.skills = data.get("skills", profile.skills) + profile.rating_avg = data.get("rating_avg", profile.rating_avg) + profile.city = data.get("city", profile.city) + profile.modified_at = current_time + + db.session.commit() + return jsonify(profile.serialize()), 200 - return jsonify(response_body), 200