diff --git a/migrations/env.py b/migrations/env.py index 4c97092..7fc75a3 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,3 +1,4 @@ +# pylint: disable=no-member,redefined-outer-name,unused-argument import logging from logging.config import fileConfig diff --git a/migrations/versions/1c96ddd37bf6_add_action_detail_column.py b/migrations/versions/1c96ddd37bf6_add_action_detail_column.py index 9aef553..c6feb3c 100644 --- a/migrations/versions/1c96ddd37bf6_add_action_detail_column.py +++ b/migrations/versions/1c96ddd37bf6_add_action_detail_column.py @@ -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. @@ -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") diff --git a/migrations/versions/7b6a6f0d9d62_add_wishlist_items_table.py b/migrations/versions/7b6a6f0d9d62_add_wishlist_items_table.py new file mode 100644 index 0000000..9104dad --- /dev/null +++ b/migrations/versions/7b6a6f0d9d62_add_wishlist_items_table.py @@ -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") diff --git a/migrations/versions/c4e8d0f9a1b2_backfill_wishlist_item_columns.py b/migrations/versions/c4e8d0f9a1b2_backfill_wishlist_item_columns.py new file mode 100644 index 0000000..c11ab60 --- /dev/null +++ b/migrations/versions/c4e8d0f9a1b2_backfill_wishlist_item_columns.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index 272c59a..49a4671 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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() diff --git a/website/__init__.py b/website/__init__.py index 21445ac..cce8c30 100644 --- a/website/__init__.py +++ b/website/__init__.py @@ -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")) @@ -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 ( diff --git a/website/models/user.py b/website/models/user.py index aef1ac7..7c0c924 100644 --- a/website/models/user.py +++ b/website/models/user.py @@ -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.""" @@ -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, + } diff --git a/website/static/css/roadmap-specific.css b/website/static/css/roadmap-specific.css index ec46c84..5b94a53 100644 --- a/website/static/css/roadmap-specific.css +++ b/website/static/css/roadmap-specific.css @@ -757,57 +757,45 @@ body { } .rm-priority-col { - background-color: #ffffff; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); + background: #ffffff; + border: 1.5px solid var(--border); + border-radius: 12px; padding: 24px; - box-shadow: var(--card-shadow); display: flex; flex-direction: column; + min-height: 280px; } .rm-col-header { font-family: 'Poppins', sans-serif; font-weight: 700; font-size: 1rem; - color: #2d3748; + color: var(--text); margin-bottom: 20px; padding-bottom: 12px; - border-bottom: 2px solid #f1f5f9; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 10px; } -.rm-feature-card { - background: #ffffff; - border: 1px solid #edf2f7; - border-radius: 12px; - overflow: hidden; - margin-bottom: 20px; +.rm-priority-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex: 0 0 auto; } -.rm-feature-img { - width: 100%; - height: 150px; - object-fit: cover; +.rm-priority-dot--high { + background: #ef4444; } -.rm-feature-body { - padding: 16px; - display: flex; - gap: 12px; +.rm-priority-dot--medium { + background: #f59e0b; } -.rm-card-icon { - flex-shrink: 0; - background-color: var(--primary-blue); - color: #fff; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 0.85rem; +.rm-priority-dot--low { + background: #22c55e; } .rm-checklist { @@ -839,6 +827,101 @@ body { background-color: #fff; } +.rm-three-column-grid--wishlist { + align-items: stretch; +} + +.rm-checklist--wishlist { + display: flex; + flex-direction: column; + gap: 14px; +} + +.rm-checklist--wishlist li { + padding: 0; + border-bottom: 0; +} + +.rm-checklist--wishlist li::before { + display: none; +} + +.rm-wishlist-item { + display: block; + padding: 14px 16px; + border: 1.5px solid rgba(18, 123, 228, 0.22); + border-bottom: 3px solid rgba(18, 123, 228, 0.34); + border-radius: 4px; + background: #fff; + cursor: grab; + transition: + border-color 0.18s ease, + border-bottom-color 0.18s ease, + background-color 0.18s ease; +} + +.rm-wishlist-item:hover, +.rm-wishlist-item:focus-within { + border-color: rgba(18, 123, 228, 0.34); + border-bottom-color: rgba(18, 123, 228, 0.5); + background-color: #f8fbff; +} + +.rm-wishlist-item-title { + color: var(--text); + font-size: 0.96rem; + font-weight: 800; + line-height: 1.45; + text-decoration: none; + display: inline-block; +} + +.rm-wishlist-item-title-link { + color: var(--accent); +} + +.rm-wishlist-item-title-link:hover { + color: var(--accent-dark); + text-decoration: underline; +} + +.rm-wishlist-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + min-height: 420px; + padding: 48px 24px; + border: 1.5px solid var(--border); + border-radius: 20px; + background: #fff; + text-align: center; +} + +.rm-wishlist-empty-art { + width: min(100%, 420px); + max-height: 320px; + height: auto; + object-fit: contain; +} + +.rm-wishlist-empty-title { + margin: 0; + color: var(--text); + font-family: 'Poppins', sans-serif; + font-size: 1.4rem; + font-weight: 700; +} + +.rm-wishlist-empty-copy { + max-width: 460px; + margin: 0; + color: var(--muted); + font-size: 0.96rem; + line-height: 1.7; +} + @media (max-width: 1000px) { .rm-path-wrap { overflow-x: auto; @@ -847,4 +930,3 @@ body { min-width: 900px; } } - diff --git a/website/static/img/wishlist-empty-state.svg b/website/static/img/wishlist-empty-state.svg new file mode 100644 index 0000000..08f160b --- /dev/null +++ b/website/static/img/wishlist-empty-state.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/js/roadmap-tracking.js b/website/static/js/roadmap-tracking.js index c23394a..477f8c6 100644 --- a/website/static/js/roadmap-tracking.js +++ b/website/static/js/roadmap-tracking.js @@ -19,6 +19,27 @@ }).catch(function () {}); } + function saveWishlistItem(detail, node) { + if (!detail || !node) return; + + var titleNode = node.querySelector(".item-title-link, .item-title"); + var summaryNode = node.querySelector(".item-meta"); + var linkNode = node.querySelector(".item-title-link"); + + fetch("/wishlist/items/from-roadmap", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + roadmap_item_id: detail.item_id || "", + title: titleNode ? (titleNode.textContent || "").trim() : detail.label || "", + section: detail.section || "", + summary: summaryNode ? (summaryNode.textContent || "").trim() : "", + href: linkNode ? linkNode.href || "" : "", + checked: !!detail.checked, + }), + }).catch(function () {}); + } + function reportRoadmapTime() { if (hasReportedRoadmapTime) return; @@ -56,12 +77,15 @@ var li = t.closest(".roadmap-node"); if (!li || !grid.contains(li)) return; - postTrack("roadmap_checkbox", { + var checkboxDetail = { section: li.getAttribute("data-rm-section") || "", label: li.getAttribute("data-rm-label") || "", item_id: li.getAttribute("data-rm-id") || "", checked: !!t.checked, - }); + }; + + postTrack("roadmap_checkbox", checkboxDetail); + saveWishlistItem(checkboxDetail, li); } if (t.classList.contains("rm-status-select")) { @@ -105,4 +129,4 @@ }); window.addEventListener("pagehide", reportRoadmapTime); -})(); \ No newline at end of file +})(); diff --git a/website/static/js/wishlist.js b/website/static/js/wishlist.js index f179c61..f2edc16 100644 --- a/website/static/js/wishlist.js +++ b/website/static/js/wishlist.js @@ -1,21 +1,31 @@ function allowDrop(ev) { - ev.preventDefault(); + ev.preventDefault(); } function drag(ev) { - ev.dataTransfer.setData("text", ev.target.id); + ev.dataTransfer.setData("text", ev.currentTarget.id); } function drop(ev) { - ev.preventDefault(); - const data = ev.dataTransfer.getData("text"); - const draggedElement = document.getElementById(data); - - - const column = ev.target.closest(".rm-priority-col"); - - if (column) { - const targetList = column.querySelector("ul"); - targetList.appendChild(draggedElement); - } -} \ No newline at end of file + ev.preventDefault(); + + const data = ev.dataTransfer.getData("text"); + const draggedElement = document.getElementById(data); + const column = ev.target.closest(".rm-priority-col"); + + if (!draggedElement || !column) return; + + const targetList = column.querySelector("ul"); + const itemId = draggedElement.getAttribute("data-item-id"); + const priority = column.getAttribute("data-priority"); + + if (!targetList || !itemId || !priority) return; + + targetList.appendChild(draggedElement); + + fetch("/wishlist/items/" + itemId + "/priority", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ priority: priority }), + }).catch(function () {}); +} diff --git a/website/templates/majorSpecific/cs.html b/website/templates/majorSpecific/cs.html index caa7ca9..bb0eb60 100644 --- a/website/templates/majorSpecific/cs.html +++ b/website/templates/majorSpecific/cs.html @@ -31,6 +31,7 @@
Edit answers + Wishlist AI Mentor Feedback
diff --git a/website/templates/majorSpecific/econ.html b/website/templates/majorSpecific/econ.html index e490af0..09109f3 100644 --- a/website/templates/majorSpecific/econ.html +++ b/website/templates/majorSpecific/econ.html @@ -31,6 +31,7 @@
Edit answers + Wishlist AI Mentor Feedback
@@ -214,4 +215,4 @@

Resources & Notes

{% include 'footer.html' %} - \ No newline at end of file + diff --git a/website/templates/wishlist.html b/website/templates/wishlist.html index 3709af4..7808802 100644 --- a/website/templates/wishlist.html +++ b/website/templates/wishlist.html @@ -5,9 +5,9 @@ - CS Roadmap · Blueprint + Wishlist · Blueprint - + - - + +
- - Blueprint + + Blueprint
+ Edit answers AI Mentor
@@ -34,10 +35,11 @@
+

Saved roadmap items

Your Wishlist

-

All your checked courses are saved in one place!

+

Checked roadmap items land in low priority first, then you can move them where they belong.

-
+
{% if class_year %}
{{ class_year }}
{% endif %} @@ -65,42 +67,112 @@

Your Wishlist

+ {% set has_items = wishlist_total > 0 %}
-
-
-

High Priority

-
    -
  • Review Data Structures
  • -
  • LeetCode 75 Challenge
  • -
-
- -
-

Medium Priority

-
    -
  • Personal Project
  • -
-
- -
-

Low Priority

-
    -
  • Network
  • -
-
-
+ {% if has_items %} +
+
+

+ + High Priority +

+
    + {% for item in wishlist_items.high %} +
  • + {% if item.href %} + {{ item.title }} + {% else %} + {{ item.title }} + {% endif %} + {% if item.href %} + {% endif %} +
  • + {% endfor %} +
+
+ +
+

+ + Medium Priority +

+
    + {% for item in wishlist_items.medium %} +
  • + {% if item.href %} + {{ item.title }} + {% else %} + {{ item.title }} + {% endif %} + {% if item.href %} + {% endif %} +
  • + {% endfor %} +
+
+ +
+

+ + Low Priority +

+
    + {% for item in wishlist_items.low %} +
  • + {% if item.href %} + {{ item.title }} + {% else %} + {{ item.title }} + {% endif %} + {% if item.href %} + {% endif %} +
  • + {% endfor %} +
+
+
+ {% else %} +
+ Wishlist empty state illustration +

No wishlist items yet

+

+ Check a roadmap item to save it here. +

+
+ {% endif %}
- - + {% include 'footer.html' %} - \ No newline at end of file + diff --git a/website/views/auth_views.py b/website/views/auth_views.py index 1376f40..6f06b6e 100644 --- a/website/views/auth_views.py +++ b/website/views/auth_views.py @@ -1,11 +1,10 @@ from urllib.parse import urlparse +import os from flask import Blueprint, redirect, render_template, request, session, url_for from authlib.integrations.flask_client import OAuth from flask_login import login_required, login_user, logout_user -from website.models.temp_user import TempUser -import os from website import db @@ -62,6 +61,8 @@ def _safe_next_url(next_url: str | None) -> str | None: def init_oauth(app): """Initialize OAuth and register Google provider.""" + # OAuth registration is stored at module scope for route handlers to reuse. + # pylint: disable=global-statement global google oauth.init_app(app) diff --git a/website/views/landing_views.py b/website/views/landing_views.py index 24de692..91ca557 100644 --- a/website/views/landing_views.py +++ b/website/views/landing_views.py @@ -14,11 +14,13 @@ session, url_for, ) -from flask_login import current_user +from flask_login import current_user, login_required +from sqlalchemy import func from .dashboard_views import build_roadmap_dashboard_context from ..consts import HTML_EXTENSION, LANDING_DEFAULT_NAME, PREFIX from ..models.tracking import Action, Feedback, User, Visit, db +from ..models.user import WishlistItem from ..onboarding_config import ( CAREER_GOAL_LABELS, CAREER_STAGE_LABELS, @@ -300,8 +302,103 @@ def cookie_policy(): return render_template("cookies.html") @landing_blueprint.route("/wishlist") +@login_required def wishlist(): - return render_template("wishlist.html") + grouped_items = {"high": [], "medium": [], "low": []} + + items = ( + WishlistItem.query.filter_by(user_id=current_user.id) + .order_by(WishlistItem.created_at.asc(), WishlistItem.id.asc()) + .all() + ) + + for item in items: + priority_key = (item.priority or "low").lower() + if priority_key not in grouped_items: + priority_key = "low" + grouped_items[priority_key].append(item) + + return render_template( + "wishlist.html", + class_year=current_user.year or "", + career_goal=CAREER_GOAL_LABELS.get(current_user.career_goal or "", ""), + career_stage=CAREER_STAGE_LABELS.get(current_user.career_stage or "", ""), + priority=PRIORITY_LABELS.get(current_user.priority or "", ""), + wishlist_items=grouped_items, + wishlist_total=len(items), + ) + + +@landing_blueprint.route("/wishlist/items/from-roadmap", methods=["POST"]) +def save_wishlist_item_from_roadmap(): + if not current_user.is_authenticated: + return jsonify({"status": "unauthorized"}), 401 + + data = request.get_json(silent=True) or {} + roadmap_item_id = (data.get("roadmap_item_id") or "").strip()[:255] + title = (data.get("title") or "").strip()[:255] + checked = bool(data.get("checked")) + + if not roadmap_item_id: + return ( + jsonify( + {"status": "error", "message": "roadmap_item_id required"} + ), + 400, + ) + + item = WishlistItem.query.filter_by( + user_id=current_user.id, + roadmap_item_id=roadmap_item_id, + ).first() + + if not checked: + if item is not None: + db.session.delete(item) + db.session.commit() + return jsonify({"status": "success", "removed": True}), 200 + + if not title: + return jsonify({"status": "error", "message": "title required"}), 400 + + if item is None: + item = WishlistItem( + user_id=current_user.id, + roadmap_item_id=roadmap_item_id, + label=title, + title=title, + priority="low", + ) + db.session.add(item) + + item.label = title + item.title = title + item.section = (data.get("section") or "").strip()[:100] or None + item.summary = (data.get("summary") or "").strip() or None + item.href = (data.get("href") or "").strip()[:500] or None + + db.session.commit() + return jsonify({"status": "success", "item": item.to_dict()}), 200 + + +@landing_blueprint.route("/wishlist/items//priority", methods=["POST"]) +def update_wishlist_item_priority(item_id: int): + if not current_user.is_authenticated: + return jsonify({"status": "unauthorized"}), 401 + + item = WishlistItem.query.filter_by(id=item_id, user_id=current_user.id).first() + if item is None: + return jsonify({"status": "error", "message": "Item not found"}), 404 + + data = request.get_json(silent=True) or {} + priority_value = (data.get("priority") or "").strip().lower() + if priority_value not in {"high", "medium", "low"}: + return jsonify({"status": "error", "message": "Invalid priority"}), 400 + + item.priority = priority_value + db.session.commit() + + return jsonify({"status": "success"}), 200 @landing_blueprint.route("/feedback", methods=["POST"])