From f4e6d818d7588e1dfccc4cd5bcec9890f5abae45 Mon Sep 17 00:00:00 2001
From: b4-sharp <Bradley2.Sharp@live.uwe.ac.uk>
Date: Tue, 28 Mar 2023 01:21:44 +0100
Subject: [PATCH] Implement item search

---
 store/forms.py              |  5 ++++
 store/routes.py             | 18 +++++++++++++
 store/templates/base.html   | 26 +++++++++---------
 store/templates/search.html | 53 +++++++++++++++++++++++++++++++++++++
 store/utility.py            | 33 +++++++++--------------
 unit_tests.py               |  1 +
 6 files changed, 104 insertions(+), 32 deletions(-)
 create mode 100644 store/templates/search.html

diff --git a/store/forms.py b/store/forms.py
index 61ef12b..ec125c5 100644
--- a/store/forms.py
+++ b/store/forms.py
@@ -32,3 +32,8 @@ class LoginForm(FlaskForm):
         "Enter your Favourite Colour", [validators.Length(min=3, max=35)]
     )
     submit = SubmitField("Login", render_kw={"class": "button"})
+
+
+class SearchForm(FlaskForm):
+    query = StringField("Query", [validators.DataRequired()])
+    submit = SubmitField("Submit")
diff --git a/store/routes.py b/store/routes.py
index a7e6463..d807d21 100644
--- a/store/routes.py
+++ b/store/routes.py
@@ -457,3 +457,21 @@ def ItemPage(item_id):
         item_description=item_description,
         item_id=item_id,
     )
+
+
+@app.context_processor
+def base():
+    """So that search works on every page, and so that the search form does not need to be passed
+    to every template a context processor is used to inject a variable (in this case search_form)
+    into every template.
+    """
+    form = SearchForm()
+    return dict(search_form=form)
+
+
+@app.route("/search", methods=["POST"])
+def search():
+    form = SearchForm()
+    if form.validate_on_submit():
+        items = get_by_keyword(form.query.data)
+        return render_template("search.html", form=form, items=items)
diff --git a/store/templates/base.html b/store/templates/base.html
index e91b02e..3cfeeca 100644
--- a/store/templates/base.html
+++ b/store/templates/base.html
@@ -1,3 +1,4 @@
+{% from "_formhelpers.html" import render_field %}
 <!DOCTYPE html>
 <html lang="en">
 
@@ -8,9 +9,9 @@
 </head>
 
 <body>
-    <header class="header">
-      <a href="{{ url_for('index')}}">ANTIQUES ONLINE</a>
-    </header>
+  <header class="header">
+    <a href="{{ url_for('index')}}">ANTIQUES ONLINE</a>
+  </header>
 
   <nav class="navbar">
     <ul class="navbar-list">
@@ -27,9 +28,10 @@
       <li><a href="{{ url_for('register')}}">Register</a></li>
       {% endif %}
 
-      <form class="search">
-        <button type="submit">Search</button>
-        <input type="text" placeholder="Search...">
+      <form method="POST" action="{{url_for('search')}}" class="search">
+        {{search_form.hidden_tag()}}
+        {{render_field(search_form.query) }}
+        {{search_form.submit()}}
       </form>
     </ul>
   </nav>
@@ -48,12 +50,12 @@
 
   {% endblock %} <!-- Where HTML will go when extended. -->
 
-    <footer class="footer">
-      <br>
-      <p>Contact Info: mail@mail.com</p>
-      <p>Phone Number 0000 000 0000</p>
-      <br>
-    </footer>
+  <footer class="footer">
+    <br>
+    <p>Contact Info: mail@mail.com</p>
+    <p>Phone Number 0000 000 0000</p>
+    <br>
+  </footer>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/store/templates/search.html b/store/templates/search.html
new file mode 100644
index 0000000..0922e01
--- /dev/null
+++ b/store/templates/search.html
@@ -0,0 +1,53 @@
+{%extends 'base.html' %}
+{% block title %} Search | Antiques Online {% endblock %}
+{% block content %}
+Searching for {{form.query.data}}
+<style>
+    .grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+        grid-gap: 10px;
+        grid-auto-rows: minmax(100px, auto);
+    }
+
+    .grid-item {
+        grid-row: span 1;
+        grid-column: span 1;
+        padding: 10px;
+    }
+
+    .grid-item img {
+        max-width: 100%;
+        height: auto;
+    }
+
+    .grid-item h2 {
+        margin: 10px 0;
+        font-size: 18px;
+
+    }
+
+    .grid-item p {
+        margin: 5px 0;
+        font-size: 14px;
+    }
+</style>
+
+<div class="grid">
+    {% if items %}
+    {% for item in items %}
+    <div class="grid-item">
+
+        <img src="static\image_placeholder.png" alt="{{ item.description }}">
+        <h2>{{ item.description }}</h2>
+        <p>£{{ item.price }}</p>
+        <a href="{{url_for('ItemPage', item_id = item.id)}}">View
+            Details</a>
+
+    </div>
+    {% endfor %}
+    {% else %}
+    <p>No items available</p>
+    {% endif %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/store/utility.py b/store/utility.py
index 89384d8..9e77b4c 100644
--- a/store/utility.py
+++ b/store/utility.py
@@ -2,7 +2,7 @@
 
 from difflib import SequenceMatcher
 from store.models import *
-from sqlalchemy import select
+from sqlalchemy import select, func
 import datetime
 from store import *
 import re
@@ -84,26 +84,17 @@ def remove_item(item):
 #     db.session.Item.query()
 
 
-# Search desciption for matching substring.
-# This is most useful for a user keyword search.
 def get_by_keyword(substring):
-    to_search = get_items() + get_item_sets()
-    found = []
-    for descriptable in to_search:
-        if substring.lower() in descriptable.description.lower():
-            found.append(descriptable)
-    return found
-
-
-# Search desciption for matching substring.
-# This is most useful for a user keyword search.
-def get_by_keyword(substring):
-    to_search = get_items() + get_item_sets()
-    found = []
-    for descriptable in to_search:
-        if substring.lower() in descriptable.description.lower():
-            found.append(descriptable)
-    return found
+    """Search desciption for matching substring. Case insensitive.
+    This is most useful for a user keyword search.
+    """
+    substring = substring.lower()
+    return (
+        Item.query.filter(func.lower(Item.description.contains(substring))).all()
+        + ItemSet.query.filter(
+            func.lower(ItemSet.description.contains(substring))
+        ).all()
+    )
 
 
 def get_itemsets_by_item_id(item_id):
@@ -134,6 +125,8 @@ def add_item(description, price, date_sold=None):
 
 
 def update_set(reference_item):
+    """Update the set that the passed item belongs to by removing it from the set and adding a similar item."""
+
     # We need the owning item set to avoid adding duplicates.
     owning_itemsets = get_itemsets_by_item_id(reference_item.id)
     # Sort by closest match. Could also do it based of the description of the set.
diff --git a/unit_tests.py b/unit_tests.py
index f710e56..eb6d6f7 100644
--- a/unit_tests.py
+++ b/unit_tests.py
@@ -1,4 +1,5 @@
 from store import app
+
 """ An instance of the database needs to exist to unit test,
 as the database is seeded from file this can be used as a test database.
 Really there should be an inmemory database for testing purposes, but
-- 
GitLab