Compare commits
3 commits
42f40ad9a0
...
8cba035e93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cba035e93 | ||
|
|
fe5f1e7ecd | ||
|
|
5dafe72895 |
13 changed files with 714 additions and 150 deletions
|
|
@ -14,5 +14,6 @@ class Config(object):
|
|||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
||||
|
|
|
|||
15
app/forms.py
15
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.
|
||||
|
|
@ -24,8 +24,8 @@ class DeleteWishlist(FlaskForm):
|
|||
|
||||
|
||||
class EditWishlistInfo(FlaskForm):
|
||||
title = StringField("Title", validators=[DataRequired()])
|
||||
description = StringField("Description", validators=[DataRequired()])
|
||||
title = StringField("Title")
|
||||
description = TextAreaField("Description")
|
||||
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()])
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
166
app/scrapers.py
Normal file
166
app/scrapers.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
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"<ScraperResult name:{self.name} price:{ self.price } image:{self.image}>"
|
||||
)
|
||||
|
||||
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 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:
|
||||
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
|
||||
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}",
|
||||
),
|
||||
]
|
||||
67
app/templates/base.html
Normal file
67
app/templates/base.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Wishthat</title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/daisyui@5"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
{% endblock head %}
|
||||
</head>
|
||||
<body class="m-0 p-0 w-full h-screen bg-base-200 flex flex-col">
|
||||
|
||||
{% block aboveNavbar %}
|
||||
{% endblock aboveNavbar %}
|
||||
|
||||
<div class="w-full flex-grow min-h-screen flex flex-col">
|
||||
<nav class="w-full p-2 flex-none">
|
||||
<div class="navbar bg-base-100 shadow-sm rounded-md">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-4 w-44 p-2 shadow"
|
||||
>
|
||||
<li><a class="btn btn-soft btn-success w-40 mb-2" href="/new">New wishlist</a></li>
|
||||
{% block middleNavPhone %}
|
||||
{% endblock middleNavPhone %}
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl" href="/">wishthat</a>
|
||||
</div>
|
||||
<div class="navbar-middle">
|
||||
{% block middleNav %}
|
||||
{% endblock middleNav %}
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<a class="btn btn-soft btn-success hidden lg:inline-flex" href="/new">New wishlist</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
5
app/templates/components.html
Normal file
5
app/templates/components.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{% macro mainCenter() %}
|
||||
<main class="w-full h-screen flex justify-center items-center">
|
||||
{{ caller() }}
|
||||
</main>
|
||||
{% endmacro %}
|
||||
|
|
@ -1,98 +1,263 @@
|
|||
{% set cpath = url_for("edit", id=wishlist.editId) %}
|
||||
|
||||
<h1>Metadata</h1>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_wl_editinfo.hidden_tag() }}
|
||||
|
||||
{{ form_wl_editinfo.title.label }}
|
||||
{{ form_wl_editinfo.title(placeholder=wishlist.title) }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_wl_editinfo.description.label }}
|
||||
{{ form_wl_editinfo.description(placeholder=wishlist.description) }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_wl_editinfo.wl_edit_submit() }}
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<h1>Urls</h1>
|
||||
<ul>
|
||||
<li>
|
||||
View: <a href={{ url_for("view", id=wishlist.viewId) }}>{{ wishlist.viewId }}</a>
|
||||
</li>
|
||||
<li>
|
||||
Edit: <a href={{ url_for("edit", id=wishlist.editId) }}>{{ wishlist.editId }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_wl_reseturls.hidden_tag() }}
|
||||
{{ form_wl_reseturls.wl_reset_submit() }}
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<h1>New item</h1>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_it_new.hidden_tag() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.title.label }}
|
||||
{{ form_it_new.title() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.description.label }}
|
||||
{{ form_it_new.description() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.price.label }}
|
||||
{{ form_it_new.price() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.url.label }}
|
||||
{{ form_it_new.url() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.image.label }}
|
||||
{{ form_it_new.image() }}
|
||||
|
||||
<!-- <br> -->
|
||||
{{ form_it_new.it_new_submit() }}
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<h1>Delete items</h1>
|
||||
|
||||
{% if wishlist.items|length == 0 %}
|
||||
<p>No items yet</p>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for value in wishlist.items %}
|
||||
{% extends "base.html" %}
|
||||
{% block middleNav %}
|
||||
<a class="btn btn-soft w-40 hidden lg:inline-flex" href="/view/{{ wishlist.viewId }}">View wishlist</a>
|
||||
{% endblock middleNav %}
|
||||
{% block middleNavPhone %}
|
||||
<li><a class="btn btn-soft w-40" href="/view/{{ wishlist.viewId }}">View wishlist</a></li>
|
||||
{% endblock middleNavPhone %}
|
||||
<li><a class="btn btn-soft w-40" href="/view/{{ wishlist.viewId }}">View wishlist</a></li>
|
||||
{% block content %}
|
||||
<main class="flex flex-col justify-end sm:justify-center items-center h-screen w-full">
|
||||
<div class="flex flex-col-reverse sm:flex-row rounded-lg border-2 border-base-300 bg-base-100 p-6 shadow-lg w-[34em] max-h-[35em] sm:w-[40em] md:w-[45em] scale-65 sm:scale-90 md:scale-100 ">
|
||||
<ul class="menu menu-horizontal sm:menu-vertical w-full sm:w-min rounded-box bg-base-200 flex-none gap-1" id="menu">
|
||||
<li>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_it_delete.csrf_token }}
|
||||
{{ form_it_delete.index(value=loop.index) }}
|
||||
{{ form_it_delete.it_del_submit() }}
|
||||
</form>
|
||||
{{ value.title }}
|
||||
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<li>
|
||||
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Urls">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="w-5 h-5">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Wishes">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M4 22V11H2V5h5.2q-.125-.225-.162-.475T7 4q0-1.25.875-2.125T10 1q.575 0 1.075.213T12 1.8q.425-.4.925-.6T14 1q1.25 0 2.125.875T17 4q0 .275-.05.513T16.8 5H22v6h-2v11zM14 3q-.425 0-.712.288T13 4t.288.713T14 5t.713-.288T15 4t-.288-.712T14 3M9 4q0 .425.288.713T10 5t.713-.288T11 4t-.288-.712T10 3t-.712.288T9 4M4 7v2h7V7zm7 13v-9H6v9zm2 0h5v-9h-5zm7-11V7h-7v2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="divider divider-vertical sm:divider-horizontal"></div>
|
||||
<div class="flex-grow w-full overflow-y-scroll p-2" id="tabs">
|
||||
<div class="contents">
|
||||
<!-- INFO -->
|
||||
<h1 class="text-2xl">Info</h1>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
<!-- {{ form_wl_editinfo.hidden_tag() }} -->
|
||||
<legend class="fieldset-legend">{{ form_wl_editinfo.title.label.text }}</legend>
|
||||
{{ form_wl_editinfo.title(placeholder=wishlist.title, class="w-full
|
||||
input validator mt-1 mb-4") }}
|
||||
<!-- <br> -->
|
||||
<legend class="fieldset-legend">{{ form_wl_editinfo.description.label.text }}</legend>
|
||||
{{ form_wl_editinfo.description(placeholder=wishlist.description,
|
||||
class="w-full textarea validator mt-1 mb-2") }}
|
||||
<div class="validator-hint">Please make sure that both inputs are filled.</div>
|
||||
<!-- <br> -->
|
||||
<div class="join">
|
||||
<form action="{{ cpath }}" method="POST" class="contents">
|
||||
{{ form_wl_delete.hidden_tag() }} {{ form_wl_delete.wl_del_submit(class="btn btn-soft btn-error
|
||||
join-item w-full") }}
|
||||
</form>
|
||||
{{ form_wl_editinfo.wl_edit_submit(class="btn btn-soft btn-success
|
||||
join-item w-full") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<!-- URLS -->
|
||||
<h1 class="text-2xl">Urls</h1>
|
||||
<ul class="list shadow-sm rounded-md my-4">
|
||||
<li class="list-row flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
class="opacity-30 size-[1.8em] flex-none">
|
||||
<defs>
|
||||
<mask id="ipSPreviewOpen0">
|
||||
<g fill="none" stroke-linejoin="round" stroke-width="4">
|
||||
<path fill="#fff" stroke="#fff" d="M24 36c11.046 0 20-12 20-12s-8.954-12-20-12S4 24 4 24s8.954 12 20 12Z" />
|
||||
<path fill="#000" stroke="#000" d="M24 29a5 5 0 1 0 0-10a5 5 0 0 0 0 10Z" />
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSPreviewOpen0)" />
|
||||
</svg>
|
||||
<p class="flex-grow">{{ wishlist.viewId }}</p>
|
||||
<a class="btn btn-square btn-ghost flex-none"
|
||||
href="{{ url_for('view', id=wishlist.viewId) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="opacity-30 size-[1.8em]">
|
||||
<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4t-.288.713T11 5H5v14h14v-6q0-.425.288-.712T20 12t.713.288T21 13v6q0 .825-.587 1.413T19 21zM19 6.4L10.4 15q-.275.275-.7.275T9 15t-.275-.7t.275-.7L17.6 5H15q-.425 0-.712-.288T14 4t.288-.712T15 3h5q.425 0 .713.288T21 4v5q0 .425-.288.713T20 10t-.712-.288T19 9z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-row flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 24 24"
|
||||
class="opacity-30 size-[1.8em] flex-none">
|
||||
<path fill="currentColor" d="M5 19h1.425L16.2 9.225L14.775 7.8L5 17.575zm-2 2v-4.25L16.2 3.575q.3-.275.663-.425t.762-.15t.775.15t.65.45L20.425 5q.3.275.438.65T21 6.4q0 .4-.137.763t-.438.662L7.25 21zM19 6.4L17.6 5zm-3.525 2.125l-.7-.725L16.2 9.225z" />
|
||||
</svg>
|
||||
<p class="flex-grow">{{ wishlist.editId }}</p>
|
||||
<a class="btn btn-square btn-ghost flex-none"
|
||||
href='{{ url_for("edit", id=wishlist.editId) }}'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="opacity-30 size-[1.8em]">
|
||||
<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4t-.288.713T11 5H5v14h14v-6q0-.425.288-.712T20 12t.713.288T21 13v6q0 .825-.587 1.413T19 21zM19 6.4L10.4 15q-.275.275-.7.275T9 15t-.275-.7t.275-.7L17.6 5H15q-.425 0-.712-.288T14 4t.288-.712T15 3h5q.425 0 .713.288T21 4v5q0 .425-.288.713T20 10t-.712-.288T19 9z" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_wl_reseturls.hidden_tag() }} {{ form_wl_reseturls.wl_reset_submit(class="btn btn-soft btn-warning")
|
||||
}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<!-- ITEMS -->
|
||||
<h1 class="text-xl mb-2">Add/Delete items</h1>
|
||||
<ul class="list bg-base-100 rounded-md shadow-sm">
|
||||
<li class="opacity-60 list-row">
|
||||
<span class="flex flex-row items-center font-bold">Create item</span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<button type="submit"
|
||||
class="btn btn-square btn-ghost group"
|
||||
onclick="add_item_modal.showModal()">
|
||||
<svg class="size-[1.8em] group-hover:fill-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M11 13v3q0 .425.288.713T12 17t.713-.288T13 16v-3h3q.425 0 .713-.288T17 12t-.288-.712T16 11h-3V8q0-.425-.288-.712T12 7t-.712.288T11 8v3H8q-.425 0-.712.288T7 12t.288.713T8 13zm-6 8q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5H5zM5 5v14z" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
{% for value in wishlist.items %}
|
||||
<li class="list-row bg-base-100">
|
||||
<div>
|
||||
<img class="size-10 rounded-box" src="{{ value.image }}" />
|
||||
</div>
|
||||
<div class="text-lg">{{ value.title }}</div>
|
||||
<p class="list-col-wrap text-xs">{{ value.description }}</p>
|
||||
<form action="{{ cpath }}" method="POST" class="contents">
|
||||
{{ form_it_delete.csrf_token }}
|
||||
{{ form_it_delete.index(value=loop.index) }}
|
||||
<button type="submit" class="btn btn-square btn-ghost btn-error group">
|
||||
<svg class="size-[1.8em] group-hover:fill-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="m9.4 16.5l2.6-2.6l2.6 2.6l1.4-1.4l-2.6-2.6L16 9.9l-1.4-1.4l-2.6 2.6l-2.6-2.6L8 9.9l2.6 2.6L8 15.1zM7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM7 6v13z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span></span>
|
||||
</main>
|
||||
<dialog id="add_item_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" id="close">✕</button>
|
||||
</form>
|
||||
<h1 class="text-xl font-bold">
|
||||
Item creation
|
||||
</h3>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_it_new.hidden_tag() }}
|
||||
<legend class="fieldset-legend">{{ form_it_new.it_new_title.label.text }}</legend>
|
||||
{{ form_it_new.it_new_title(class="w-full input validator mt-1 mb-4") }}
|
||||
<!-- <br> -->
|
||||
<legend class="fieldset-legend">{{ form_it_new.description.label.text }}</legend>
|
||||
{{ form_it_new.description(class="w-full input validator mt-1 mb-2") }}
|
||||
<legend class="fieldset-legend">{{ form_it_new.price.label.text }}</legend>
|
||||
{{ form_it_new.price(class="w-full input validator mt-1 mb-2", pattern="(?:€|$)?\d*(?:,|.)\d*") }}
|
||||
<legend class="fieldset-legend">{{ form_it_new.url.label.text }}</legend>
|
||||
{{ form_it_new.url(class="w-full input validator mt-1 mb-2") }}
|
||||
<legend class="fieldset-legend">{{ form_it_new.image.label.text }}</legend>
|
||||
{{ form_it_new.image(class="w-full input validator mt-1 mb-2") }}
|
||||
<div class="validator-hint">Please make sure that both inputs are filled and are valid.</div>
|
||||
<div class="join modal-actions">
|
||||
<button class="btn btn-soft join-item sm:w-full" id="scrape">Autofill</button>
|
||||
{{ form_it_new.it_new_submit(class="btn btn-soft btn-success
|
||||
join-item sm:w-full") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<script>
|
||||
const $all = (...v) => document.querySelectorAll(...v)
|
||||
const menu = $all("#menu > li > a")
|
||||
const tabs = $all("#tabs > div")
|
||||
|
||||
<br>
|
||||
<h1>Delete wishlist</h1>
|
||||
<form action="{{ cpath }}" method="POST">
|
||||
{{ form_wl_delete.hidden_tag() }}
|
||||
{{ form_wl_delete.wl_del_submit() }}
|
||||
</form>
|
||||
const def = 0
|
||||
let activeMenu = menu[def]
|
||||
let activeTab = tabs[def]
|
||||
|
||||
<style>
|
||||
form {
|
||||
display:grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
grid-gap:5px;
|
||||
}
|
||||
form label { text-align:right; }
|
||||
form label:after { content: ":"; }
|
||||
</style>
|
||||
activeMenu.classList.add("menu-active")
|
||||
for (let i of Array.from(tabs).filter((_, i) => i != def)) i.classList.add("hidden")
|
||||
|
||||
for (let [index, elem] of Object.entries(menu)) {
|
||||
index = +index
|
||||
|
||||
elem.addEventListener("click", e => {
|
||||
activeMenu.classList.remove("menu-active")
|
||||
elem.classList.add("menu-active")
|
||||
|
||||
activeTab.classList.add("hidden")
|
||||
tabs[index].classList.remove("hidden")
|
||||
|
||||
|
||||
activeMenu = elem
|
||||
activeTab = tabs[index]
|
||||
})
|
||||
}
|
||||
|
||||
const $q = (...i) => document.querySelector(...i);
|
||||
const title = $q("#it_new_title")
|
||||
const price = $q("#price")
|
||||
const url = $q("#url")
|
||||
const image = $q("#image")
|
||||
// const description = $q("#description")
|
||||
|
||||
$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;
|
||||
})
|
||||
|
||||
$q("#close").addEventListener("click", e => {
|
||||
$all("dialog > div > form > input").forEach(v => v.value = "")
|
||||
})
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
|
|
|||
74
app/templates/index.html
Normal file
74
app/templates/index.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block aboveNavbar %}
|
||||
|
||||
<div
|
||||
class="hero min-h-screen"
|
||||
style="background-image: url(https://images.unsplash.com/photo-1558018754-1f1b019ece9d?q=80&w=1227&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D);"
|
||||
>
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content text-neutral-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-5xl font-bold">Hello there</h1>
|
||||
<p class="mb-5">
|
||||
Welcome to WishThat. The place to create and share wishlists online.
|
||||
</p>
|
||||
<button class="btn btn-primary" onclick="content.scrollIntoView({ behavior: 'smooth', block: 'end' })">Learn more</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock aboveNavbar %}
|
||||
|
||||
{% block content %}
|
||||
<main class="w-full h-full px-20 pt-5 flex flex-col" id="content">
|
||||
<h2 class="text-3xl font-bold mb-5">Why Use Wishthat?</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Avoid Duplicate Purchases</h3>
|
||||
<p>Our system helps you keep track of what has been purchased, ensuring that you don't buy the same item twice.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">No Account Needed</h3>
|
||||
<p>Start creating wishlists immediately without the hassle of signing up or logging in.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Price and Image Scraping</h3>
|
||||
<p>We scrape websites for the latest prices and images, making sure you don't have to painstakingly create wish items manually!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Easy to Use</h3>
|
||||
<p>Our user-friendly interface allows you to create and manage your wishlists effortlessly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Fully Open Source</h3>
|
||||
<p>Wishthat is fully open-source and FOSS, allowing anyone to check our security.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow-md flex flex-col">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mobile friendly design</h3>
|
||||
<p>The software has been designed with mobile friendlyness in mind. Meaning that your phone addicted friends may enjoy this site as well.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex-grow"></span>
|
||||
|
||||
<footer class="footer footer-center p-10 bg-base-100 text-base-content">
|
||||
<div>
|
||||
<p>© 2025 jsw. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,11 +1,22 @@
|
|||
<form action="{{ url_for("new") }}" method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{{ form.title.label }}
|
||||
{{ form.title() }}
|
||||
{% block content %}
|
||||
|
||||
{{ form.description.label }}
|
||||
{{ form.description() }}
|
||||
<main class="w-full flex-grow flex justify-center items-end md:items-center">
|
||||
<form action="{{ url_for("new") }}" method="POST" class="border-2 border-base-300 w-full md:w-md p-6 m-4 rounded-lg shadow-lg bg-base-100">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ form.submit() }}
|
||||
</form>
|
||||
<h1 class="text-3xl font-semibold text-center text-info-content mb-4">New wishlist</h1>
|
||||
|
||||
<legend class="fieldset-legend">{{ form.title.label.text }}</legend>
|
||||
{{ form.title(class="w-full input validator mt-1 mb-4", placeholder="Wishlist Title") }}
|
||||
|
||||
<legend class="fieldset-legend">{{ form.description.label.text }}</legend>
|
||||
{{ form.description(class="w-full textarea validator mt-1 mb-2", placeholder="Wishlist Description") }}
|
||||
<div class="validator-hint">Please make sure that both inputs are filled.</div>
|
||||
|
||||
{{ form.submit(class="btn btn-soft btn-success w-full") }}
|
||||
</form>
|
||||
</main>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
|||
16
app/templates/post_new.html
Normal file
16
app/templates/post_new.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<dialog class="modal modal-bottom md:modal-middle" open>
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Before you continue</h3>
|
||||
<p class="py-4">Wishthat operates using two distinct passcodes: one for viewing the wishlist and another for editing it. Think of it as a combination of a username and password. Please navigate to the edit page and <b>bookmark the URL</b>, as you will <b>not be able to edit your wishlist if you lose this passcode</b>. You can also find the passcode for viewing the wishlist on the edit page.</p>
|
||||
<div class="modal-action">
|
||||
<a class="btn" href="/view/{{viewId}}">View</a>
|
||||
<a class="btn btn-primary" href="/edit/{{editId}}">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,46 +1,76 @@
|
|||
<h1>{{wishlist.title}}</h1>
|
||||
<sub>{{wishlist.description}}</sub>
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<main class="w-full h-full min-h-0 px-20 pt-5 overflow-y-scroll">
|
||||
<!-- Wishlist Title Card -->
|
||||
<div class="w-full bg-base-100 p-5 rounded-lg shadow-md mb-5">
|
||||
<h1 class="text-2xl font-bold">{{wishlist.title}}</h1>
|
||||
<p class="text-gray-600">{{wishlist.description}}</p>
|
||||
</div>
|
||||
|
||||
<a href="{{url_for('edit', id=wishlist.editId)}}">edit</a>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 overflow-y-scroll mb-5">
|
||||
{% for item in wishlist.items %}
|
||||
<div class="card bg-base-100 image-full shadow-md h-full flex flex-col">
|
||||
<figure class="flex-grow">
|
||||
<img
|
||||
src="{{item.image}}"
|
||||
alt="{{ item.title }}"
|
||||
class="object-cover h-full w-full rounded-t-lg" />
|
||||
</figure>
|
||||
<div class="card-body flex flex-col flex-grow">
|
||||
<h2 class="card-title">{{ item.title }}</h2>
|
||||
<p class="flex-grow">{{ item.description }}</p>
|
||||
<div class="card-actions justify-end">
|
||||
<div class="{{ "join" if not item.bought }}"><a class="btn btn-soft {{ "join-item" if not item.bought }}" href="{{ item.url }}" {{ "hidden" if item.bought }}>{{ "€" ~ item.price if not item.bought }}</a>
|
||||
<form method="POST" action="{{ url_for('view', id=wishlist.viewId) }}" class="contents">
|
||||
<button class="buy btn btn-soft btn-primary {{ "join-item" if not item.bought }}" {{ "disabled" if item.bought }}>{{ "Reserved" if item.bought else "Reserve" }}</button>
|
||||
{{ form.csrf_token }}
|
||||
{{ form.num(value=loop.index) }}
|
||||
</form></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ul>
|
||||
{% if wishlist.items|length == 0 %}
|
||||
<p>No items yet</p>
|
||||
{% endif %}
|
||||
{% for item in wishlist.items %}
|
||||
<li>
|
||||
<form method="POST" href="{{ url_for("view", id=wishlist.editId) }}">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.num(value = loop.index) }}
|
||||
|
||||
<input type=checkbox {{ "checked disabled" if item.bought}}>
|
||||
{{ item.title }}: {{ item.description }}
|
||||
<dialog class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Important!</h3>
|
||||
<p class="py-4">Are you sure you bought this product? You won't be able to undo this.</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="join">
|
||||
<button id="dialog-confirm-no" class="btn btn-soft join-item">Close</button>
|
||||
<button id="dialog-confirm-ok" class="btn btn-soft btn-primary join-item">OK</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<dialog>
|
||||
<p>Are you sure you bought this product. You won't be able to undo this.</p>
|
||||
<form method="dialog">
|
||||
<button id="dialog-confirm-ok">OK</button>
|
||||
<button id="dialog-confirm-no">Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog class="modal" {{ "open" if wishlist.items|length < 1 }}>
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Still empty!</h3>
|
||||
<p class="py-4">Hi there. It seems like you are here a bit too early, the wishlist is still empty. Please come back later.</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<a class="btn" href="/">Go back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const $ = (...f) => document.querySelector(...f);
|
||||
const $all = (...f) => document.querySelectorAll(...f);
|
||||
|
||||
const items = Array.from($all("input[type=checkbox]"));
|
||||
const dialog = $("dialog")
|
||||
const dialogOk = $("#dialog-confirm-ok")
|
||||
const dialogNo = $("#dialog-confirm-no")
|
||||
const items = Array.from($all(".buy"));
|
||||
const dialog = $("dialog");
|
||||
const dialogOk = $("#dialog-confirm-ok");
|
||||
const dialogNo = $("#dialog-confirm-no");
|
||||
|
||||
for (let checkbox of items) {
|
||||
checkbox.addEventListener("click", e => {
|
||||
for (let button of items) {
|
||||
button.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
const onclick = () => checkbox.form.submit();
|
||||
const onclick = () => button.form.submit();
|
||||
|
||||
dialogOk.addEventListener("click", onclick);
|
||||
dialogNo.addEventListener(
|
||||
|
|
@ -48,9 +78,8 @@
|
|||
() => dialogOk.removeEventListener("click", onclick)
|
||||
);
|
||||
|
||||
console.log(dialog)
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
|
|
|||
45
app/views.py
45
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,26 +13,36 @@ from app.forms import (
|
|||
)
|
||||
from app.models import Wishlist, Item
|
||||
from uuid import UUID, uuid4 as uuid
|
||||
from app.scrapers import scrapeSite
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return "hello"
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@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/<viewId>/<editId>")
|
||||
def postNew(viewId: str, editId: str):
|
||||
|
||||
return render_template("post_new.html", viewId=viewId, editId=editId)
|
||||
|
||||
|
||||
@app.route("/edit/<id>", methods=["GET", "POST"])
|
||||
def edit(id: str):
|
||||
wishlist: Wishlist = db.one_or_404(
|
||||
|
|
@ -54,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))
|
||||
|
|
@ -69,12 +85,13 @@ 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
|
||||
|
||||
item = Item(
|
||||
str(
|
||||
f.title.data,
|
||||
f.it_new_title.data,
|
||||
),
|
||||
str(
|
||||
f.description.data,
|
||||
|
|
@ -91,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)
|
||||
|
|
@ -118,6 +135,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 +149,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__)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
};
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue