From 5dafe7289594c5c9e1b06edef80e98526afebcfd Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Sun, 22 Jun 2025 19:42:20 +0200 Subject: [PATCH 1/3] shit and i'm lazy --- app/configuration.py | 1 + app/forms.py | 12 +- app/scrapers.py | 162 +++++++++++++++++++++++++++ app/templates/components.html | 5 + app/templates/edit.html | 205 +++++++++++++++++++--------------- app/templates/footer.html | 2 + app/templates/header.html | 11 ++ app/templates/index.html | 0 app/templates/new.html | 25 +++-- app/templates/post_new.html | 14 +++ app/templates/view.html | 4 + app/views.py | 33 +++++- flake.nix | 2 +- instance/application.db | Bin 12288 -> 12288 bytes 14 files changed, 367 insertions(+), 109 deletions(-) create mode 100644 app/scrapers.py create mode 100644 app/templates/components.html create mode 100644 app/templates/footer.html create mode 100644 app/templates/header.html create mode 100644 app/templates/index.html create mode 100644 app/templates/post_new.html diff --git a/app/configuration.py b/app/configuration.py index 092b4a8..150124b 100644 --- a/app/configuration.py +++ b/app/configuration.py @@ -14,5 +14,6 @@ class Config(object): class DevelopmentConfig(Config): DEBUG = True + class TestingConfig(Config): TESTING = True diff --git a/app/forms.py b/app/forms.py index 5cf3bcd..f8c70dd 100644 --- a/app/forms.py +++ b/app/forms.py @@ -3,19 +3,19 @@ from flask_wtf import FlaskForm from wtforms import ( StringField, SubmitField, - IntegerField, HiddenField, FloatField, URLField, + TextAreaField, ) from wtforms.validators import DataRequired class NewWishlist(FlaskForm): - title = StringField("Title:", validators=[DataRequired()]) - description = StringField("Description:", validators=[DataRequired()]) - submit = SubmitField("Submit") + title = StringField("Title", validators=[DataRequired()]) + description = TextAreaField("Description", validators=[DataRequired()]) + submit = SubmitField("Create") # Each submit needs a different page fot it to work on the same page. @@ -25,7 +25,7 @@ class DeleteWishlist(FlaskForm): class EditWishlistInfo(FlaskForm): title = StringField("Title", validators=[DataRequired()]) - description = StringField("Description", validators=[DataRequired()]) + description = TextAreaField("Description", validators=[DataRequired()]) wl_edit_submit = SubmitField("Submit") @@ -34,7 +34,7 @@ class ResetWishlistUrls(FlaskForm): class NewItem(FlaskForm): - title = StringField("Title", validators=[DataRequired()]) + it_new_title = StringField("Title", validators=[DataRequired()]) description = StringField("Description", validators=[DataRequired()]) price = FloatField("Price", validators=[DataRequired()]) url = URLField("Url", validators=[DataRequired()]) diff --git a/app/scrapers.py b/app/scrapers.py new file mode 100644 index 0000000..ca99499 --- /dev/null +++ b/app/scrapers.py @@ -0,0 +1,162 @@ +from abc import ABC, abstractmethod +from typing import Callable, override +from bs4 import BeautifulSoup +from requests import get +from re import findall, match, search + +noReturnLambda: Callable[[str], str] = lambda x: x + + +class ScrapeError(Exception): + pass + + +class ScraperResult: + def __init__(self, name: str, price: float, image: str): + self.name = name + self.price = price + self.image = image + + @override + def __repr__(self) -> str: + return ( + f"" + ) + + name: str + price: float + image: str + + +class ScraperLike(ABC): + name: str + urlRegex: str + + @abstractmethod + def scrape(self, url: str) -> ScraperResult: + pass + + +class GenericScraper(ScraperLike): + name: str + urlRegex: str + _nameQuery: str + _priceQuery: str + _imageQuery: str + priceParser: Callable[[str], str] + imageParser: Callable[[str], str] + + def __init__( + self, + name: str, + baseUrl: str, + nameQuery: str, + priceQuery: str, + imageQuery: str, + priceParser: Callable[[str], str] = noReturnLambda, + imageParser: Callable[[str], str] = noReturnLambda, + ): + self.name = name + self.urlRegex = baseUrl + self._nameQuery = nameQuery + self._priceQuery = priceQuery + self._imageQuery = imageQuery + self.priceParser = priceParser + self.imageParser = imageParser + + @override + def scrape(self, url: str) -> ScraperResult: + res = get( + url, + headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0" + }, + ) + if res.status_code != 200: + raise ScrapeError("Failed to fetch page.") + + soup = BeautifulSoup(res.text, features="html.parser") + name = soup.select_one(self._nameQuery) + price = soup.select_one(self._priceQuery) + image = soup.select_one(self._imageQuery) + + if name is None or price is None or image is None: + raise ScrapeError( + f"Failed to scrape site. Invalid webpage or queries: N:{name},P:{price},I:{image}" + ) + + name = name.text.strip() + image = image.get("src") + try: + x = self.priceParser(price.text) + reg = search(r"([0-9]+)(?:(?:\.|,)([0-9]+))?", x) + if not reg: + raise ValueError + x = reg.group(1) + + g2 = reg.group(2) + if g2: + x += "." + g2 + + price = float(x) + except ValueError: + print(price) + raise ScrapeError(f"Failed to scrape site. Error while parsing price.") + if not isinstance(image, str): + raise ScrapeError(f"Failed to scrape site. Error while parsing image.") + + return ScraperResult(name, price, self.imageParser(image)) + + +def scrapeSite(url: str) -> ScraperResult | None: + scraped: ScraperResult | None = None + + for i in scrapers: + if match(i.urlRegex, url) is None: + continue + + scraped = i.scrape(url) + + return scraped + + +scrapers = [ + GenericScraper( + "Amazon", + r"^https?:\/\/(www\.)?((amazon)|(amzn))\.\w*", + "#productTitle", + "#corePrice_feature_div > div:nth-child(1) > div:nth-child(1) > span:nth-child(1) > span:nth-child(1)", + "#landingImage", + ), + GenericScraper( + "Bol.com", + r"^https?:\/\/(www\.)?bol.com", + ".page-heading > span:nth-child(1)", + ".promo-price", + "div.container-item:nth-child(2) > wsp-selected-item-image-zoom-modal-application:nth-child(1) > button:nth-child(2) > img:nth-child(1)", + priceParser=lambda x: x.replace("\n ", "."), + ), + GenericScraper( + "MediaMarkt", + r"^https?:\/\/(www\.)?mediamarkt.\w*", + "h1.sc-d571b66f-0", + ".sc-6db49389-0 > span:nth-child(2)", + "div.sc-hLBbgP:nth-child(2) > div:nth-child(3) > ul:nth-child(1) > li:nth-child(1) > div:nth-child(1) > div:nth-child(1) > button:nth-child(1) > img:nth-child(1)", + priceParser=lambda x: x.replace("€", ""), + ), + GenericScraper( + "Coolblue", + r"^https?:\/\/(www\.)?coolblue.\w*", + ".css-1o2kclk", + ".css-puih25 > span:nth-child(1)", + ".css-ptvba5", + ), + GenericScraper( + "Megekko", + r"^https?:\/\/(www\.)?megekko.nl", + "#prd_title", + "a.prsPrice:nth-child(1) > div:nth-child(1)", + "#prd_afbeeldingen > div:nth-child(1) > img:nth-child(1)", + imageParser=lambda x: f"https://www.megekko.nl/{x}", + ), +] diff --git a/app/templates/components.html b/app/templates/components.html new file mode 100644 index 0000000..c3a910c --- /dev/null +++ b/app/templates/components.html @@ -0,0 +1,5 @@ +{% macro mainCenter() %} +
+ {{ caller() }} +
+{% endmacro %} diff --git a/app/templates/edit.html b/app/templates/edit.html index 263a54e..61e8b90 100644 --- a/app/templates/edit.html +++ b/app/templates/edit.html @@ -1,98 +1,121 @@ {% set cpath = url_for("edit", id=wishlist.editId) %} +
+

Edit '{{wishlist.title}}'

+ Manage your wishlist details and items +
+
+ {{ form_wl_editinfo.hidden_tag() }} + {{ form_wl_editinfo.title.label }} + {{ form_wl_editinfo.title(placeholder=wishlist.title) }} + + {{ form_wl_editinfo.description.label }} + {{ form_wl_editinfo.description(placeholder=wishlist.description) }} + + {{ form_wl_editinfo.wl_edit_submit() }} +
+
+

Urls

+ +
+ {{ form_wl_reseturls.hidden_tag() }} + {{ form_wl_reseturls.wl_reset_submit() }} +
+
+

New item

+
+ {{ form_it_new.hidden_tag() }} + + {{ form_it_new.it_new_title.label }} + {{ form_it_new.it_new_title() }} + + {{ form_it_new.description.label }} + {{ form_it_new.description() }} + + {{ form_it_new.price.label }} + {{ form_it_new.price() }} + + {{ form_it_new.url.label }} + {{ form_it_new.url() }} + + {{ form_it_new.image.label }} + {{ form_it_new.image() }} + + {{ form_it_new.it_new_submit() }} + +
+
+

Delete items

+ {% if wishlist.items|length == 0 %}

No items yet

{% endif %} +
    + {% for value in wishlist.items %} +
  • +
    + {{ form_it_delete.csrf_token }} + {{ form_it_delete.index(value=loop.index) }} + {{ form_it_delete.it_del_submit() }} +
    + {{ value.title }} +
  • + {% endfor %} +
+
+

Delete wishlist

+
+ {{ form_wl_delete.hidden_tag() }} + {{ form_wl_delete.wl_del_submit() }} +
+ +
-

Metadata

-
- {{ form_wl_editinfo.hidden_tag() }} + diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..b605728 --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,2 @@ + + diff --git a/app/templates/header.html b/app/templates/header.html new file mode 100644 index 0000000..e3dce15 --- /dev/null +++ b/app/templates/header.html @@ -0,0 +1,11 @@ + + + + + + Wishthat + + + + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/new.html b/app/templates/new.html index 1979102..54184df 100644 --- a/app/templates/new.html +++ b/app/templates/new.html @@ -1,11 +1,20 @@ - - {{ form.hidden_tag() }} +{% include 'header.html' %} - {{ form.title.label }} - {{ form.title() }} +
+ + {{ form.hidden_tag() }} - {{ form.description.label }} - {{ form.description() }} +

New wishlist

- {{ form.submit() }} - + {{ form.title.label.text }} + {{ form.title(class="w-full input validator mt-1 mb-4", placeholder="Wishlist Title") }} + + {{ form.description.label.text }} + {{ form.description(class="w-full textarea validator mt-1 mb-2", placeholder="Wishlist Description") }} +
Please make sure that both inputs are filled.
+ + {{ form.submit(class="btn btn-soft w-full") }} + +
+ +{% include 'footer.html' %} diff --git a/app/templates/post_new.html b/app/templates/post_new.html new file mode 100644 index 0000000..d8df650 --- /dev/null +++ b/app/templates/post_new.html @@ -0,0 +1,14 @@ +{% include 'header.html' %} + + + + + +{% include 'footer.html' %} diff --git a/app/templates/view.html b/app/templates/view.html index 5df40ec..742ebe1 100644 --- a/app/templates/view.html +++ b/app/templates/view.html @@ -1,3 +1,5 @@ +{% include 'header.html' %} +

{{wishlist.title}}

{{wishlist.description}} @@ -54,3 +56,5 @@ } + +{% include 'footer.html' %} diff --git a/app/views.py b/app/views.py index c168c6a..e1d55c8 100644 --- a/app/views.py +++ b/app/views.py @@ -1,4 +1,5 @@ -from flask import url_for, redirect, render_template, abort +import json +from flask import request, url_for, redirect, render_template, abort from app import app, db from app.forms import ( NewWishlist, @@ -12,6 +13,9 @@ from app.forms import ( ) from app.models import Wishlist, Item from uuid import UUID, uuid4 as uuid +from json import JSONEncoder + +from app.scrapers import scrapeSite @app.route("/") @@ -22,16 +26,25 @@ def index(): @app.route("/new", methods=["GET", "POST"]) def new(): form = NewWishlist() + if form.validate_on_submit(): wishlist = Wishlist(str(form.title.data), str(form.description.data)) db.session.add(wishlist) db.session.commit() - return redirect(url_for("view", id=wishlist.viewId)) + return redirect( + url_for("postNew", viewId=wishlist.viewId, editId=wishlist.editId) + ) return render_template("new.html", form=form) +@app.route("/post_new//") +def postNew(viewId: str, editId: str): + + return render_template("post_new.html", viewId=viewId, editId=editId) + + @app.route("/edit/", methods=["GET", "POST"]) def edit(id: str): wishlist: Wishlist = db.one_or_404( @@ -74,7 +87,7 @@ def edit(id: str): item = Item( str( - f.title.data, + f.it_new_title.data, ), str( f.description.data, @@ -118,6 +131,7 @@ def view(id: str): db.select(Wishlist).filter_by(viewId=UUID(id)), description="Failed to get wishlist. Are you sure this is the correct url?", ) + checkform = CheckItem() checkform.num if checkform.validate_on_submit(): @@ -131,3 +145,16 @@ def view(id: str): return redirect(url_for("view", id=id)) return render_template("view.html", wishlist=wishlist, form=checkform) + + +@app.route("/scrape", methods=["GET"]) +def scrape(): + url = request.args.get("url") + if url is None: + abort(400) + + scraped = scrapeSite(url) + if scraped is None: + abort(404) + + return json.dumps(scraped.__dict__) diff --git a/flake.nix b/flake.nix index 6fa056c..029325b 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,7 @@ { nativeBuildInputs = [ (pkgs.python3.withPackages - (x: [x.flask x.flask-wtf x.wtforms x.flask-sqlalchemy])) + (x: [x.flask x.flask-wtf x.wtforms x.flask-sqlalchemy x.beautifulsoup4 x.types-beautifulsoup4 x.requests])) pkgs.entr ]; }; diff --git a/instance/application.db b/instance/application.db index f0c5b911dcbb9a98c52cca9331c38198a4695c9f..5e34ed5a3baa084af424db9310e9144f4cf2de15 100644 GIT binary patch delta 1636 zcmZojXh@hK%_uri#+gxcW5N=C86NI84E!tkZTMdCP2zLp{lGhm*M;XD&r}{8?l&6? z&v4iK@vt%Y`br9>n5HF~S(+GGB&VjBB%7yNSQ?sJ8mAbT85pIe7@Hd;TO=8oSR^MJ znG)px$uQxCwAVsct)Vw#1exuHc$N}7pTTB1d=rA3OFrGc4oa&n@vrG=@Xg=MOV zrJ+$`TB1pkL9&H~nPEzrQL4FtL29yri7AS~^&pc&I6)?>3K|0t_q*x{=r&^jABpN5DnkAd1rWmBACYxB8m|Iw;Bqyer7^EZ_o1~f>rh-h) zN-YArIU_TK17fO4vRPV+QEFmJijkQ~l7)Fva+;B;rGZ6~nT3I2eOjuesi8%pvAJoA znYp2fiKT&IqD6{HqLEpWk%>u?k*S#lnz8Hw3#4J|E9%uLeK43d&lO-u|; z6OGa=QVonuEmO^s49ra}%~LFmjZDld%QPc%3k%b<6w{PM^HfVCOAC`^6EjdU zF-x^DNlr7cFf=tcfI2oK)1MV$V48W7fr*7dnkgs@O-xcj-cCsbMV3*jrD45=VVa?# znVEU2fsv)Lv5B#%S(1sRNve^7g}F(xMQTzKEL1Zx!&ty3%3CBE8d;c`nV6(nn3*J} znph-Sm>QdyryNwhFbG)^-#vouIDNi#P}N=Y+M zF-u8k}63JK0A51P=?(8V3Hg{2Ba8 zJZm-<&f%$VDQ4keU~Cc%Wp7PwRODcZ%rDK&%t=)+GEfL`boN&8%}g%JR|rndNX$;n zNlYwKaCBBMbW|`jGF1puFb>GfQwR=l2~qI#SI}1Qa|SCc%F9eGQt(L4OG(X9@XgOn z%_~VPs#I_(D$UN$%P)W$>6)3BQk+~5H@7T5zevHcD5W$rFCT1aad~D*az<*gLP@?t zX>qDTT7Hp2ZlyvZM2Vh*(4qbqjjx9@N=gcft@QQF%ggl=a}%rb^Yrp^^r7C+MK~PH z1UpR^?j~K=dax^Cj?hmj(04L0k92YKF!wOFFGz_mN{KJGEi5vvtWrzP&nrpIE78qN zu}#da$}`j}uFTalGBh1X)HPuZtO|jHXGfgzJNKP>^O*KnWHFkt+ zNY+bEv^23Wur$<7G&4!jtv4|>NY%A4OEK3?HZ(I#PBb<)Ha1UED@X%bU0_R$`B240 zwnnBQf$j#5;V$N3j!wb8Q4t}Lk!ny+q}wKWMj9GJxka|7hNhMVX_ob-x@jgB2D&C@ zsRp`9smZCjrUn)ksY%A>=4NJwP}P~Xh(Lh_cdlM;YD#9JE;PuK^KVW_N delta 59 zcmZojXh@hK&B!@X#+i|GW5N=C4kmsZ2L6@&HX92ocsDleV4Q3te}aRFKZAjPEq?|? L@CoDO4E-4Z4^ Date: Tue, 24 Jun 2025 09:45:17 +0200 Subject: [PATCH 2/3] progress or shit, don't feel like devlogging --- app/forms.py | 4 +- app/templates/base.html | 63 ++++++++++ app/templates/edit.html | 236 ++++++++++++++++++------------------ app/templates/footer.html | 2 - app/templates/header.html | 11 -- app/templates/index.html | 1 + app/templates/new.html | 10 +- app/templates/post_new.html | 6 +- app/templates/view.html | 6 +- app/views.py | 13 +- instance/application.db | Bin 12288 -> 12288 bytes 11 files changed, 208 insertions(+), 144 deletions(-) create mode 100644 app/templates/base.html delete mode 100644 app/templates/footer.html delete mode 100644 app/templates/header.html diff --git a/app/forms.py b/app/forms.py index f8c70dd..6197d7b 100644 --- a/app/forms.py +++ b/app/forms.py @@ -24,8 +24,8 @@ class DeleteWishlist(FlaskForm): class EditWishlistInfo(FlaskForm): - title = StringField("Title", validators=[DataRequired()]) - description = TextAreaField("Description", validators=[DataRequired()]) + title = StringField("Title") + description = TextAreaField("Description") wl_edit_submit = SubmitField("Submit") diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..83340ad --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,63 @@ + + + + {% block head %} + + + Wishthat + + + {% endblock head %} + + + + + {% block content %} + {% endblock content %} + + diff --git a/app/templates/edit.html b/app/templates/edit.html index 61e8b90..bef1766 100644 --- a/app/templates/edit.html +++ b/app/templates/edit.html @@ -1,121 +1,125 @@ {% set cpath = url_for("edit", id=wishlist.editId) %} -
-

Edit '{{wishlist.title}}'

- Manage your wishlist details and items -
-
- {{ form_wl_editinfo.hidden_tag() }} - {{ form_wl_editinfo.title.label }} - {{ form_wl_editinfo.title(placeholder=wishlist.title) }} - - {{ form_wl_editinfo.description.label }} - {{ form_wl_editinfo.description(placeholder=wishlist.description) }} - - {{ form_wl_editinfo.wl_edit_submit() }} -
-
-

Urls

- -
- {{ form_wl_reseturls.hidden_tag() }} - {{ form_wl_reseturls.wl_reset_submit() }} -
-
-

New item

-
- {{ form_it_new.hidden_tag() }} - - {{ form_it_new.it_new_title.label }} - {{ form_it_new.it_new_title() }} - - {{ form_it_new.description.label }} - {{ form_it_new.description() }} - - {{ form_it_new.price.label }} - {{ form_it_new.price() }} - - {{ form_it_new.url.label }} - {{ form_it_new.url() }} - - {{ form_it_new.image.label }} - {{ form_it_new.image() }} - - {{ form_it_new.it_new_submit() }} - -
-
-

Delete items

- {% if wishlist.items|length == 0 %}

No items yet

{% endif %} -
    - {% for value in wishlist.items %} -
  • -
    - {{ form_it_delete.csrf_token }} - {{ form_it_delete.index(value=loop.index) }} - {{ form_it_delete.it_del_submit() }} -
    - {{ value.title }} -
  • - {% endfor %} -
-
-

Delete wishlist

-
- {{ form_wl_delete.hidden_tag() }} - {{ form_wl_delete.wl_del_submit() }} -
- -
+{% extends "base.html" %} - + $q("#scrape").addEventListener("click", async e => { + e.preventDefault() + const tUrl = url.value.trim(); + + if (!tUrl) { + alert("Please provide a valid url.") //TODO: Replace with daisyui modal + return + } + + const res = await fetch( + "/scrape?" + new URLSearchParams({ + url: tUrl + }).toString(), + { + method: "get", + } + ) + + if (res.status !== 200) { + alert("Failed to scrape site.") + return + } + + const json = await res.json() + title.value = json.name; + image.value = json.image; + price.value = json.price; + }) + +{% endblock content %} diff --git a/app/templates/footer.html b/app/templates/footer.html deleted file mode 100644 index b605728..0000000 --- a/app/templates/footer.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/templates/header.html b/app/templates/header.html deleted file mode 100644 index e3dce15..0000000 --- a/app/templates/header.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Wishthat - - - - - diff --git a/app/templates/index.html b/app/templates/index.html index e69de29..94d9808 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/app/templates/new.html b/app/templates/new.html index 54184df..f04c1a6 100644 --- a/app/templates/new.html +++ b/app/templates/new.html @@ -1,6 +1,8 @@ -{% include 'header.html' %} +{% extends "base.html" %} -
+{% block content %} + +
{{ form.hidden_tag() }} @@ -13,8 +15,8 @@ {{ form.description(class="w-full textarea validator mt-1 mb-2", placeholder="Wishlist Description") }}
Please make sure that both inputs are filled.
- {{ form.submit(class="btn btn-soft w-full") }} + {{ form.submit(class="btn btn-soft btn-success w-full") }}
-{% include 'footer.html' %} +{% endblock content %} diff --git a/app/templates/post_new.html b/app/templates/post_new.html index d8df650..5c7ff1c 100644 --- a/app/templates/post_new.html +++ b/app/templates/post_new.html @@ -1,4 +1,6 @@ -{% include 'header.html' %} +{% extends "base.html" %} + +{% block content %} -{% include 'footer.html' %} +{% endblock content %} diff --git a/app/templates/view.html b/app/templates/view.html index 742ebe1..432990b 100644 --- a/app/templates/view.html +++ b/app/templates/view.html @@ -1,4 +1,6 @@ -{% include 'header.html' %} +{% extends "base.html" %} + +{% block content %}

{{wishlist.title}}

{{wishlist.description}} @@ -57,4 +59,4 @@ -{% include 'footer.html' %} +{% endblock content %} diff --git a/app/views.py b/app/views.py index e1d55c8..0a81987 100644 --- a/app/views.py +++ b/app/views.py @@ -13,14 +13,12 @@ from app.forms import ( ) from app.models import Wishlist, Item from uuid import UUID, uuid4 as uuid -from json import JSONEncoder - from app.scrapers import scrapeSite @app.route("/") def index(): - return "hello" + return render_template("index.html") @app.route("/new", methods=["GET", "POST"]) @@ -67,8 +65,13 @@ def edit(id: str): return redirect(url_for("index")) elif form_wl_editinfo.validate_on_submit() and form_wl_editinfo.wl_edit_submit.data: - wishlist.title = str(form_wl_editinfo.title.data) - wishlist.description = str(form_wl_editinfo.description.data) + if form_wl_editinfo.title.data != "" and form_wl_editinfo.title.data != None: + wishlist.title = str(form_wl_editinfo.title.data) + if ( + form_wl_editinfo.description.data != "" + and form_wl_editinfo.description.data != None + ): + wishlist.description = str(form_wl_editinfo.description.data) db.session.commit() return redirect(url_for("edit", id=id)) diff --git a/instance/application.db b/instance/application.db index 5e34ed5a3baa084af424db9310e9144f4cf2de15..d3dec6517c4314420e8ddce17051f8229440973b 100644 GIT binary patch delta 447 zcmZojXh@hK&8Rq0#+gxZW5N=C6#=dW2L6@&Hhi!6Ch%#MnXDW{k_Z#jR z+%8ONKQ;kO-eCJO0q~c zNi<5eG&D>!OEEREFf}nrO0h6cGe|QrH8V0vvotnKGXoixnU|iGTC9+rn3tVe6vPKI zSyD35)G#&GEZNjBEiKW+$kNy{(a_M)z$C@Q!Ynn}z#t{r(%8tt*wP}&%)->f+$hb$ zxZcFrG%3X(DcQo*%rZ3%Vq8|DLRNly5HG|~3qu3bOAGVFG$Uh!loa!n+{Enc)S`_1)FNIk YKOV4|f+?nHiDs52Mi!g@N;8W90M7e?IRF3v delta 55 zcmZojXh@hK%_uri#+gxcW5N=C86NI84E!tkZTMdCP2zLp{lGhm*M;XD&r}{8?l&6? M&v0*kBP}8V0Nc0`{{R30 From 8cba035e93c43004aa67de441bcf2f6cc39e6f1f Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Wed, 25 Jun 2025 11:35:13 +0200 Subject: [PATCH 3/3] Finished user interface. Added mobile-ish support. Added homepage. Added viewpage. Fixed some scrapers --- app/forms.py | 1 - app/scrapers.py | 8 +- app/templates/base.html | 92 +++++----- app/templates/edit.html | 352 +++++++++++++++++++++++++++------------ app/templates/index.html | 73 ++++++++ app/templates/view.html | 95 +++++++---- app/views.py | 3 +- instance/application.db | Bin 12288 -> 20480 bytes 8 files changed, 433 insertions(+), 191 deletions(-) diff --git a/app/forms.py b/app/forms.py index 6197d7b..b85c80e 100644 --- a/app/forms.py +++ b/app/forms.py @@ -48,7 +48,6 @@ class CheckItem(FlaskForm): class DeleteItem(FlaskForm): index = HiddenField() - it_del_submit = SubmitField("Delete item") def parseHiddenIndex(field: HiddenField, array: list[Any]) -> int | None: diff --git a/app/scrapers.py b/app/scrapers.py index ca99499..34bda9d 100644 --- a/app/scrapers.py +++ b/app/scrapers.py @@ -80,7 +80,7 @@ class GenericScraper(ScraperLike): price = soup.select_one(self._priceQuery) image = soup.select_one(self._imageQuery) - if name is None or price is None or image is None: + if name is None or image is None: raise ScrapeError( f"Failed to scrape site. Invalid webpage or queries: N:{name},P:{price},I:{image}" ) @@ -88,7 +88,11 @@ class GenericScraper(ScraperLike): name = name.text.strip() image = image.get("src") try: - x = self.priceParser(price.text) + if price is None: + price = "0" + else: + price = price.text + x = self.priceParser(price) reg = search(r"([0-9]+)(?:(?:\.|,)([0-9]+))?", x) if not reg: raise ValueError diff --git a/app/templates/base.html b/app/templates/base.html index 83340ad..f3ac209 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,50 +14,54 @@ {% endblock head %} - + + {% block aboveNavbar %} + {% endblock aboveNavbar %} - {% block content %} - {% endblock content %} +
+ + + {% block content %} + {% endblock content %} +
diff --git a/app/templates/edit.html b/app/templates/edit.html index bef1766..1d877de 100644 --- a/app/templates/edit.html +++ b/app/templates/edit.html @@ -1,125 +1,263 @@ {% set cpath = url_for("edit", id=wishlist.editId) %} {% extends "base.html" %} - +{% block middleNav %} + +{% endblock middleNav %} +{% block middleNavPhone %} +
  • View wishlist
  • +{% endblock middleNavPhone %} +
  • View wishlist
  • {% block content %} -
    -

    Edit '{{wishlist.title}}'

    - Manage your wishlist details and items -
    -
    - {{ form_wl_editinfo.hidden_tag() }} - {{ form_wl_editinfo.title.label }} - {{ form_wl_editinfo.title(placeholder=wishlist.title) }} - - {{ form_wl_editinfo.description.label }} - {{ form_wl_editinfo.description(placeholder=wishlist.description) }} - - {{ form_wl_editinfo.wl_edit_submit() }} -
    -
    -

    Urls

    -
      +
      +
      + -
      - {{ form_wl_reseturls.hidden_tag() }} - {{ form_wl_reseturls.wl_reset_submit() }} -
      -
      -

      New item

      -
      - {{ form_it_new.hidden_tag() }} - - {{ form_it_new.it_new_title.label }} - {{ form_it_new.it_new_title() }} - - {{ form_it_new.description.label }} - {{ form_it_new.description() }} - - {{ form_it_new.price.label }} - {{ form_it_new.price() }} - - {{ form_it_new.url.label }} - {{ form_it_new.url() }} - - {{ form_it_new.image.label }} - {{ form_it_new.image() }} - - {{ form_it_new.it_new_submit() }} - -
      -
      -

      Delete items

      - {% if wishlist.items|length == 0 %}

      No items yet

      {% endif %} -
        - {% for value in wishlist.items %} -
      • -
        - {{ form_it_delete.csrf_token }} - {{ form_it_delete.index(value=loop.index) }} - {{ form_it_delete.it_del_submit() }} -
        - {{ value.title }} +
      • + + + + + +
      • +
      +
      +
      +
      + +

      Info

      +
      + + {{ form_wl_editinfo.title.label.text }} + {{ form_wl_editinfo.title(placeholder=wishlist.title, class="w-full + input validator mt-1 mb-4") }} + + {{ form_wl_editinfo.description.label.text }} + {{ form_wl_editinfo.description(placeholder=wishlist.description, + class="w-full textarea validator mt-1 mb-2") }} +
      Please make sure that both inputs are filled.
      + +
      + + {{ form_wl_delete.hidden_tag() }} {{ form_wl_delete.wl_del_submit(class="btn btn-soft btn-error + join-item w-full") }} + + {{ form_wl_editinfo.wl_edit_submit(class="btn btn-soft btn-success + join-item w-full") }} +
      + +
      +
      + +

      Urls

      +
        +
      • + + + + + + + + + + + +

        {{ wishlist.viewId }}

        + + + + +
      • - {% endfor %} -
      -
      -

      Delete wishlist

      -
      - {{ form_wl_delete.hidden_tag() }} - {{ form_wl_delete.wl_del_submit() }} -
      - +
    • + + + +

      {{ wishlist.editId }}

      + + + + + +
    • +
    +
    + {{ form_wl_reseturls.hidden_tag() }} {{ form_wl_reseturls.wl_reset_submit(class="btn btn-soft btn-warning") + }} +
    + +
    + +

    Add/Delete items

    +
      +
    • + Create item + + + +
    • + {% for value in wishlist.items %} +
    • +
      + +
      +
      {{ value.title }}
      +

      {{ value.description }}

      +
      + {{ form_it_delete.csrf_token }} + {{ form_it_delete.index(value=loop.index) }} + +
      +
    • + {% endfor %} +
    +
    + + +
    - + + + {% endblock content %} diff --git a/app/templates/index.html b/app/templates/index.html index 94d9808..52b9159 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1 +1,74 @@ {% extends "base.html" %} + +{% block aboveNavbar %} + +
    +
    +
    +
    +

    Hello there

    +

    + Welcome to WishThat. The place to create and share wishlists online. +

    + +
    +
    +
    + +{% endblock aboveNavbar %} + +{% block content %} +
    +

    Why Use Wishthat?

    +
    +
    +
    +

    Avoid Duplicate Purchases

    +

    Our system helps you keep track of what has been purchased, ensuring that you don't buy the same item twice.

    +
    +
    +
    +
    +

    No Account Needed

    +

    Start creating wishlists immediately without the hassle of signing up or logging in.

    +
    +
    +
    +
    +

    Price and Image Scraping

    +

    We scrape websites for the latest prices and images, making sure you don't have to painstakingly create wish items manually!

    +
    +
    +
    +
    +

    Easy to Use

    +

    Our user-friendly interface allows you to create and manage your wishlists effortlessly.

    +
    +
    +
    +
    +

    Fully Open Source

    +

    Wishthat is fully open-source and FOSS, allowing anyone to check our security.

    +
    +
    +
    +
    +

    Mobile friendly design

    +

    The software has been designed with mobile friendlyness in mind. Meaning that your phone addicted friends may enjoy this site as well.

    +
    +
    +
    + + + +
    +
    +

    © 2025 jsw. All rights reserved.

    +
    +
    +
    + +{% endblock content %} diff --git a/app/templates/view.html b/app/templates/view.html index 432990b..cb0e3ba 100644 --- a/app/templates/view.html +++ b/app/templates/view.html @@ -1,50 +1,76 @@ {% extends "base.html" %} - {% block content %} +
    + +
    +

    {{wishlist.title}}

    +

    {{wishlist.description}}

    +
    -

    {{wishlist.title}}

    -{{wishlist.description}} +
    + {% for item in wishlist.items %} +
    +
    + {{ item.title }} +
    +
    +

    {{ item.title }}

    +

    {{ item.description }}

    +
    +
    {{ "€" ~ item.price if not item.bought }} +
    + + {{ form.csrf_token }} + {{ form.num(value=loop.index) }} +
    +
    +
    +
    + {% endfor %} +
    +
    -edit -
      - {% if wishlist.items|length == 0 %} -

      No items yet

      - {% endif %} - {% for item in wishlist.items %} -
    • -
      - {{ form.csrf_token }} - {{ form.num(value = loop.index) }} - - - {{ item.title }}: {{ item.description }} + +
    • - {% endfor %} -
    - - -

    Are you sure you bought this product. You won't be able to undo this.

    -
    - - -
    + + +
    + + - {% endblock content %} diff --git a/app/views.py b/app/views.py index 0a81987..fabb07e 100644 --- a/app/views.py +++ b/app/views.py @@ -85,6 +85,7 @@ def edit(id: str): return redirect(url_for("edit", id=wishlist.editId)) elif form_it_new.validate_on_submit() and form_it_new.it_new_submit.data: + print("NEW") f = form_it_new price = f.price.data if f.price.data != None else 0 @@ -107,7 +108,7 @@ def edit(id: str): db.session.commit() return redirect(url_for("edit", id=id)) - elif form_it_delete.validate_on_submit() and form_it_delete.it_del_submit.data: + elif form_it_delete.validate_on_submit() and form_it_delete.index.data: index = parseHiddenIndex(form_it_delete.index, wishlist.items) if index == None: return abort(400) diff --git a/instance/application.db b/instance/application.db index d3dec6517c4314420e8ddce17051f8229440973b..b7835f895378c740c095c00921ad09ab922413d3 100644 GIT binary patch literal 20480 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