Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions migrations/versions/8939202cfc94_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""empty message

Revision ID: 8939202cfc94
Revises: 972faf56c454
Create Date: 2025-07-01 19:01:24.422826

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '8939202cfc94'
down_revision = '972faf56c454'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('lead',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=120), nullable=False),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('phone', sa.String(length=15), nullable=False),
sa.Column('company', sa.String(length=50), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('phone')
)
op.drop_table('user')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('email', sa.VARCHAR(length=120), autoincrement=False, nullable=False),
sa.Column('name', sa.VARCHAR(length=120), autoincrement=False, nullable=False),
sa.Column('phone', sa.VARCHAR(length=15), autoincrement=False, nullable=False),
sa.Column('company', sa.VARCHAR(length=50), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('id', name='user_pkey'),
sa.UniqueConstraint('email', name='user_email_key'),
sa.UniqueConstraint('phone', name='user_phone_key')
)
op.drop_table('lead')
# ### end Alembic commands ###
42 changes: 42 additions & 0 deletions migrations/versions/972faf56c454_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""empty message

Revision ID: 972faf56c454
Revises: 0763d677d453
Create Date: 2025-07-01 16:24:18.129115

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '972faf56c454'
down_revision = '0763d677d453'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('name', sa.String(length=120), nullable=False))
batch_op.add_column(sa.Column('phone', sa.String(length=15), nullable=False))
batch_op.add_column(sa.Column('company', sa.String(length=50), nullable=True))
batch_op.create_unique_constraint(None, ['phone'])
batch_op.drop_column('password')
batch_op.drop_column('is_active')

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False))
batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=False))
batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('company')
batch_op.drop_column('phone')
batch_op.drop_column('name')

# ### end Alembic commands ###
4 changes: 2 additions & 2 deletions src/api/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import os
from flask_admin import Admin
from .models import db, User
from .models import db, Lead
from flask_admin.contrib.sqla import ModelView

def setup_admin(app):
Expand All @@ -11,7 +11,7 @@ def setup_admin(app):


# Add your models here, for example this is how we add a the User model to the admin
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Lead, db.session))

# You can duplicate that line to add mew models
# admin.add_view(ModelView(YourModelName, db.session))
11 changes: 6 additions & 5 deletions src/api/commands.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@

import click
from api.models import db, User
from api.models import db, Lead

"""
In this file, you can add as many commands as you want using the @app.cli.command decorator
Flask commands are usefull to run cronjobs or tasks outside of the API but sill in integration
with youy database, for example: Import the price of bitcoin every night as 12am
"""


def setup_commands(app):

"""
This is an example command "insert-test-users" that you can run from the command line
by typing: $ flask insert-test-users 5
Note: 5 is the number of users to add
"""
@app.cli.command("insert-test-users") # name of our command
@click.argument("count") # argument of out command
@app.cli.command("insert-test-users") # name of our command
@click.argument("count") # argument of out command
def insert_test_users(count):
print("Creating test users")
for x in range(1, int(count) + 1):
Expand All @@ -31,4 +32,4 @@ def insert_test_users(count):

@app.cli.command("insert-test-data")
def insert_test_data():
pass
pass
20 changes: 13 additions & 7 deletions src/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@

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)

class Lead(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(
String(120), unique=False, nullable=False)
email: Mapped[str] = mapped_column(
String(120), unique=True, nullable=False)
phone: Mapped[str] = mapped_column(String(15), unique=True, nullable=False)
company: Mapped[str] = mapped_column(
String(50), unique=False, nullable=True)

def serialize(self):
return {
"id": self.id,
"name": self.name,
"email": self.email,
# do not serialize the password, its a security breach
}
"phone": self.phone,
"company": self.company
}
91 changes: 85 additions & 6 deletions src/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,100 @@
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, Lead
from api.utils import generate_sitemap, APIException
from flask_cors import CORS
from sqlalchemy.exc import IntegrityError

api = Blueprint('api', __name__)

# Allow CORS requests to this API
CORS(api)


@api.route('/hello', methods=['POST', 'GET'])
def handle_hello():
@api.route('/leads', methods=['GET'])
def get_leads():
all_leads = Lead.query.all()
serialized_leads = [lead.serialize() for lead in all_leads]

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"
return jsonify(serialized_leads), 200


def validate_lead_data(data):
errors = {}

name = data.get("name")
email = data.get("email")
phone = data.get("phone")
company = data.get("company")

if not name or not name.strip():
errors["name"] = "Name field is required."

if not email or not email.strip():
errors["email"] = "Email field is required."
elif "@" not in email or "." not in email or len(email) < 5:
errors["email"] = "Invalid email format."

if not phone or not phone.strip():
errors["phone"] = "Phone field is required"
elif len(phone) < 9:
errors["phone"] = "Phone number must have at least nine digits"

return errors, {
"name": name.strip() if name else None,
"email": email.strip() if email else None,
"phone": phone.strip() if phone else None,
"company": company.strip() if company else None
}

return jsonify(response_body), 200

@api.route('/contact', methods=['POST'])
def add_lead():
lead_data = request.get_json()

if not lead_data:
return jsonify({"message": "Invalid JSON or empty request body"}), 400

validation_errors, clean_data = validate_lead_data(lead_data)

if validation_errors:
return jsonify({
"status": "error",
"message": "Validation failed",
"errors": validation_errors
}), 400

try:
new_lead = Lead(
name=clean_data["name"],
email=clean_data["email"],
phone=clean_data["phone"],
company=clean_data["company"]
)

db.session.add(new_lead)
db.session.commit()

return jsonify({
"message": "Lead received successfully!",
"lead_id": new_lead.id,
"lead": new_lead.serialize()
}), 201

except IntegrityError as e:
db.session.rollback()
print(f"Registration error (Duplicate): {e}")
if "lead_email_key" in str(e):
return jsonify({
"status": "error",
"message": "Validation failed",
"errors": {"email": "This email is already registered"}
}), 409
else:
return jsonify({"message": "An integrity error ocurred"}), 400

except Exception as e:
db.session.rollback()
print(f"Registration error (General): {e}")
return jsonify({"message": "Unable to process your request at this time"}), 500
43 changes: 0 additions & 43 deletions src/front/pages/Demo.jsx

This file was deleted.

36 changes: 0 additions & 36 deletions src/front/pages/Single.jsx

This file was deleted.