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
1 change: 1 addition & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=no-member,redefined-outer-name,unused-argument
import logging
from logging.config import fileConfig

Expand Down
16 changes: 14 additions & 2 deletions migrations/versions/1c96ddd37bf6_add_action_detail_column.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
Create Date: 2026-04-20 10:58:06.359349

"""
# pylint: disable=no-member
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect


# revision identifiers, used by Alembic.
Expand All @@ -17,8 +19,18 @@


def upgrade():
op.add_column('action', sa.Column('detail', sa.JSON(), nullable=True))
bind = op.get_bind()
inspector = inspect(bind)
columns = {column["name"] for column in inspector.get_columns("action")}

if "detail" not in columns:
op.add_column("action", sa.Column("detail", sa.JSON(), nullable=True))


def downgrade():
op.drop_column('action', 'detail')
bind = op.get_bind()
inspector = inspect(bind)
columns = {column["name"] for column in inspector.get_columns("action")}

if "detail" in columns:
op.drop_column("action", "detail")
53 changes: 53 additions & 0 deletions migrations/versions/7b6a6f0d9d62_add_wishlist_items_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""add wishlist items table

Revision ID: 7b6a6f0d9d62
Revises: 1c96ddd37bf6
Create Date: 2026-05-07 14:00:00.000000

"""
# pylint: disable=no-member,duplicate-code
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect


# revision identifiers, used by Alembic.
revision = "7b6a6f0d9d62"
down_revision = "1c96ddd37bf6"
branch_labels = None
depends_on = None


def upgrade():
bind = op.get_bind()
inspector = inspect(bind)

if "wishlist_item" not in inspector.get_table_names():
op.create_table(
"wishlist_item",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("roadmap_item_id", sa.String(length=255), nullable=False),
sa.Column("title", sa.String(length=255), nullable=False),
sa.Column("section", sa.String(length=100), nullable=True),
sa.Column("summary", sa.Text(), nullable=True),
sa.Column("href", sa.String(length=500), nullable=True),
sa.Column("priority", sa.String(length=20), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"user_id",
"roadmap_item_id",
name="uq_wishlist_user_roadmap_item",
),
)


def downgrade():
bind = op.get_bind()
inspector = inspect(bind)

if "wishlist_item" in inspector.get_table_names():
op.drop_table("wishlist_item")
97 changes: 97 additions & 0 deletions migrations/versions/c4e8d0f9a1b2_backfill_wishlist_item_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""backfill wishlist item columns

Revision ID: c4e8d0f9a1b2
Revises: 7b6a6f0d9d62
Create Date: 2026-05-07 21:05:00.000000

"""
# pylint: disable=no-member,duplicate-code
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect


# revision identifiers, used by Alembic.
revision = "c4e8d0f9a1b2"
down_revision = "7b6a6f0d9d62"
branch_labels = None
depends_on = None


def upgrade():
bind = op.get_bind()
inspector = inspect(bind)

if "wishlist_item" not in inspector.get_table_names():
return

columns = {column["name"] for column in inspector.get_columns("wishlist_item")}

if "roadmap_item_id" not in columns:
op.add_column(
"wishlist_item",
sa.Column("roadmap_item_id", sa.String(length=255), nullable=True),
)
if "title" not in columns:
op.add_column(
"wishlist_item",
sa.Column("title", sa.String(length=255), nullable=True),
)
if "section" not in columns:
op.add_column(
"wishlist_item",
sa.Column("section", sa.String(length=100), nullable=True),
)
if "summary" not in columns:
op.add_column(
"wishlist_item",
sa.Column("summary", sa.Text(), nullable=True),
)
if "href" not in columns:
op.add_column(
"wishlist_item",
sa.Column("href", sa.String(length=500), nullable=True),
)
if "priority" not in columns:
op.add_column(
"wishlist_item",
sa.Column(
"priority",
sa.String(length=20),
nullable=False,
server_default="low",
),
)
if "created_at" not in columns:
op.add_column(
"wishlist_item",
sa.Column("created_at", sa.DateTime(), nullable=True),
)
if "updated_at" not in columns:
op.add_column(
"wishlist_item",
sa.Column("updated_at", sa.DateTime(), nullable=True),
)


def downgrade():
bind = op.get_bind()
inspector = inspect(bind)

if "wishlist_item" not in inspector.get_table_names():
return

columns = {column["name"] for column in inspector.get_columns("wishlist_item")}

for name in (
"updated_at",
"created_at",
"priority",
"href",
"summary",
"section",
"title",
"roadmap_item_id",
):
if name in columns:
op.drop_column("wishlist_item", name)
28 changes: 12 additions & 16 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,29 @@ def app_ctx(app):


@pytest.fixture(autouse=True)
def cleanup_db(app_ctx):
def cleanup_db(_app_ctx):
"""Automatically clean up database before and after each test."""
from website.models import Note, CameraGear, LabEquipment, Consumable, User
table_names = set(db.metadata.tables.keys())

# Clean up before test
db.session.rollback()
try:
# Delete in reverse order of dependencies
Note.query.delete()
CameraGear.query.delete()
LabEquipment.query.delete()
Consumable.query.delete()
User.query.delete()
# Delete in reverse dependency order for all known tables.
for table in reversed(db.metadata.sorted_tables):
if table.name in table_names:
db.session.execute(table.delete())
db.session.commit()
except Exception: # pragma: no cover
db.session.rollback()

yield

# Clean up after test
db.session.rollback()
try:
Note.query.delete()
CameraGear.query.delete()
LabEquipment.query.delete()
Consumable.query.delete()
User.query.delete()
for table in reversed(db.metadata.sorted_tables):
if table.name in table_names:
db.session.execute(table.delete())
db.session.commit()
except Exception: # pragma: no cover
db.session.rollback()
Expand Down
10 changes: 1 addition & 9 deletions website/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@
db = SQLAlchemy()


from .views import (
dashboard_blueprint,
landing_blueprint,
roadmap_blueprint,
auth_blueprint,
)


# Project root: parent of the `website` package
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
Expand Down Expand Up @@ -95,7 +87,7 @@ def load_user(user_id):
return User.query.get(int(user_id))

db.init_app(app)
migrate = Migrate(app, db)
Migrate(app, db)
with app.app_context():
import website.models.tracking # pylint: disable=unused-import
from .views import (
Expand Down
42 changes: 41 additions & 1 deletion website/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class User(db.Model, UserMixin):
career_goal = db.Column(db.String(50))
career_stage = db.Column(db.String(50))
priority = db.Column(db.String(50))
wishlist_items = db.relationship(
"WishlistItem",
backref="user",
lazy=True,
cascade="all, delete-orphan",
)

def __repr__(self):
"""Return a concise representation for debugging."""
Expand All @@ -47,8 +53,42 @@ def save(self):
"""Save the user to the database"""
db.session.add(self)
db.session.commit()



class WishlistItem(db.Model):
"""Wishlist item saved from roadmap interactions for an authenticated user."""

id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
roadmap_item_id = db.Column(db.String(255), nullable=False)
label = db.Column(db.String(255), nullable=True)
title = db.Column(db.String(255), nullable=False)
section = db.Column(db.String(100), nullable=True)
summary = db.Column(db.Text, nullable=True)
href = db.Column(db.String(500), nullable=True)
priority = db.Column(db.String(20), nullable=False, default="low")
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
updated_at = db.Column(
db.DateTime,
default=db.func.current_timestamp(),
onupdate=db.func.current_timestamp(),
)

__table_args__ = (
db.UniqueConstraint(
"user_id", "roadmap_item_id", name="uq_wishlist_user_roadmap_item"
),
)

def to_dict(self):
"""Return a JSON-serializable representation of the wishlist item."""
return {
"id": self.id,
"roadmap_item_id": self.roadmap_item_id,
"label": self.label or self.title,
"title": self.title,
"section": self.section,
"summary": self.summary,
"href": self.href,
"priority": self.priority,
}
Loading
Loading