From bd017079d5ad9a48c0edffb8d8e02498a10fa34b Mon Sep 17 00:00:00 2001
From: Daoud Clarke
Date: Tue, 24 Oct 2023 10:32:06 +0100
Subject: [PATCH 01/55] Add login using allauth
---
mwmbl/settings_bg_prod.py | 2 +-
mwmbl/settings_common.py | 16 +++
mwmbl/settings_dev.py | 4 +-
mwmbl/templates/base.html | 22 ++++
mwmbl/templates/home.html | 5 +
mwmbl/templates/profile.html | 8 ++
mwmbl/templates/registration/login.html | 26 ++++
mwmbl/templates/signup.html | 10 ++
mwmbl/urls.py | 14 ++-
mwmbl/views.py | 24 ++++
poetry.lock | 158 +++++++++++++++++++++++-
pyproject.toml | 1 +
12 files changed, 285 insertions(+), 5 deletions(-)
create mode 100644 mwmbl/templates/base.html
create mode 100644 mwmbl/templates/home.html
create mode 100644 mwmbl/templates/profile.html
create mode 100644 mwmbl/templates/registration/login.html
create mode 100644 mwmbl/templates/signup.html
create mode 100644 mwmbl/views.py
diff --git a/mwmbl/settings_bg_prod.py b/mwmbl/settings_bg_prod.py
index 070bbf9..51d035e 100644
--- a/mwmbl/settings_bg_prod.py
+++ b/mwmbl/settings_bg_prod.py
@@ -5,4 +5,4 @@ ALLOWED_HOSTS = ["api.mwmbl.org", "mwmbl.org"]
DATA_PATH = "/app/storage"
RUN_BACKGROUND_PROCESSES = True
-NUM_PAGES = 10240000
+NUM_PAGES = 10240000
\ No newline at end of file
diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py
index 645ebd4..14a1e5a 100644
--- a/mwmbl/settings_common.py
+++ b/mwmbl/settings_common.py
@@ -33,6 +33,10 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'mwmbl',
+
+ 'allauth',
+ 'allauth.account',
+ 'allauth.socialaccount',
]
MIDDLEWARE = [
@@ -43,6 +47,8 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+
+ "allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'mwmbl.urls'
@@ -120,3 +126,13 @@ print("Static files", STATICFILES_DIRS)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+AUTHENTICATION_BACKENDS = [
+ # Needed to login by username in Django admin, regardless of `allauth`
+ 'django.contrib.auth.backends.ModelBackend',
+
+ # `allauth` specific authentication methods, such as login by email
+ 'allauth.account.auth_backends.AuthenticationBackend',
+]
+
+ACCOUNT_EMAIL_REQUIRED = True
+ACCOUNT_EMAIL_VERIFICATION = "mandatory"
diff --git a/mwmbl/settings_dev.py b/mwmbl/settings_dev.py
index ee82351..c7cd281 100644
--- a/mwmbl/settings_dev.py
+++ b/mwmbl/settings_dev.py
@@ -3,7 +3,9 @@ from mwmbl.settings_common import *
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
DATA_PATH = "./devdata"
-RUN_BACKGROUND_PROCESSES = True
+RUN_BACKGROUND_PROCESSES = False
NUM_PAGES = 2560
diff --git a/mwmbl/templates/base.html b/mwmbl/templates/base.html
new file mode 100644
index 0000000..b397195
--- /dev/null
+++ b/mwmbl/templates/base.html
@@ -0,0 +1,22 @@
+
+
+
+
+ {% block title %}Simple is Better Than Complex{% endblock %}
+
+
+
+ My Site
+ {% if user.is_authenticated %}
+ logout
+ {% else %}
+ login / signup
+ {% endif %}
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
diff --git a/mwmbl/templates/home.html b/mwmbl/templates/home.html
new file mode 100644
index 0000000..ed59579
--- /dev/null
+++ b/mwmbl/templates/home.html
@@ -0,0 +1,5 @@
+{% extends 'base.html' %}
+
+{% block content %}
+ Welcome, {{ user.username }}!
+{% endblock %}
diff --git a/mwmbl/templates/profile.html b/mwmbl/templates/profile.html
new file mode 100644
index 0000000..2923d40
--- /dev/null
+++ b/mwmbl/templates/profile.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% block title %}Profile Page{% endblock title %}
+{% block content %}
+
+
This is the profile page for {{user.username}}
+
+
+{% endblock content %}
diff --git a/mwmbl/templates/registration/login.html b/mwmbl/templates/registration/login.html
new file mode 100644
index 0000000..a5f87a3
--- /dev/null
+++ b/mwmbl/templates/registration/login.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+
+{% block content %}
+ Log in to My Site
+ {% if form.errors %}
+ Your username and password didn't match. Please try again.
+ {% endif %}
+
+ {% endfor %}
+
+ New to My Site? Sign up
+
+{% endblock %}
diff --git a/mwmbl/templates/signup.html b/mwmbl/templates/signup.html
new file mode 100644
index 0000000..b858ff6
--- /dev/null
+++ b/mwmbl/templates/signup.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+
+{% block content %}
+ Sign up
+
+{% endblock %}
diff --git a/mwmbl/urls.py b/mwmbl/urls.py
index 7d8bff6..b416085 100644
--- a/mwmbl/urls.py
+++ b/mwmbl/urls.py
@@ -15,12 +15,22 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
-from django.urls import path
+from django.contrib.auth import login, logout
+from django.template.defaulttags import url
+from django.urls import path, include
from mwmbl.api import api_original as api, api_v1
+from mwmbl.views import signup, profile
urlpatterns = [
path('admin/', admin.site.urls),
path('', api.urls),
- path('api/v1/', api_v1.urls)
+ path('api/v1/', api_v1.urls),
+ path('accounts/', include('allauth.urls')),
+
+ # path("accounts/", include("django.contrib.auth.urls")),
+ # path('accounts/new/', signup, name='signup'),
+ path('accounts/profile/', profile, name='profile'),
+ # path('login/', login, {'template_name': 'login.html'}, name='login'),
+ # path('logout/', logout, {'next_page': 'login'}, name='logout'),
]
diff --git a/mwmbl/views.py b/mwmbl/views.py
new file mode 100644
index 0000000..0ae3ba8
--- /dev/null
+++ b/mwmbl/views.py
@@ -0,0 +1,24 @@
+from django.contrib.auth import authenticate, login
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.forms import UserCreationForm
+from django.shortcuts import redirect, render
+
+
+def signup(request):
+ if request.method == 'POST':
+ form = UserCreationForm(request.POST)
+ if form.is_valid():
+ form.save()
+ username = form.cleaned_data.get('username')
+ raw_password = form.cleaned_data.get('password1')
+ user = authenticate(username=username, password=raw_password)
+ login(request, user)
+ return redirect('/')
+ else:
+ form = UserCreationForm()
+ return render(request, 'signup.html', {'form': form})
+
+
+@login_required
+def profile(request):
+ return render(request, 'profile.html')
diff --git a/poetry.lock b/poetry.lock
index d7a0647..f4395b2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -419,6 +419,52 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+[[package]]
+name = "cryptography"
+version = "41.0.4"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"},
+ {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"},
+ {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"},
+ {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"},
+ {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"},
+ {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"},
+ {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"},
+]
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
+nox = ["nox"]
+pep8test = ["black", "check-sdist", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
+
[[package]]
name = "cymem"
version = "2.0.8"
@@ -462,6 +508,18 @@ files = [
{file = "cymem-2.0.8.tar.gz", hash = "sha256:8fb09d222e21dcf1c7e907dc85cf74501d4cea6c4ed4ac6c9e016f98fb59cbbf"},
]
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
[[package]]
name = "django"
version = "4.2.6"
@@ -483,6 +541,28 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
+[[package]]
+name = "django-allauth"
+version = "0.57.0"
+description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "django-allauth-0.57.0.tar.gz", hash = "sha256:a095ef0db7de305d9175772c78e765ebd5fceb004ae61c1383d7fc1af0f7c5b1"},
+]
+
+[package.dependencies]
+Django = ">=3.2"
+pyjwt = {version = ">=1.7", extras = ["crypto"]}
+python3-openid = ">=3.0.8"
+requests = ">=2.0.0"
+requests-oauthlib = ">=0.3.0"
+
+[package.extras]
+mfa = ["qrcode (>=7.0.0)"]
+saml = ["python3-saml (>=1.15.0,<2.0.0)"]
+
[[package]]
name = "django-ninja"
version = "0.22.2"
@@ -1104,6 +1184,23 @@ files = [
{file = "numpy-1.26.0.tar.gz", hash = "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"},
]
+[[package]]
+name = "oauthlib"
+version = "3.2.2"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
+ {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
+]
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
+
[[package]]
name = "packaging"
version = "23.2"
@@ -1454,6 +1551,27 @@ typing-extensions = ">=3.7.4.3"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
+[[package]]
+name = "pyjwt"
+version = "2.8.0"
+description = "JSON Web Token implementation in Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
+ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
[[package]]
name = "pyspark"
version = "3.2.0"
@@ -1530,6 +1648,25 @@ files = [
[package.dependencies]
six = ">=1.5"
+[[package]]
+name = "python3-openid"
+version = "3.2.0"
+description = "OpenID support for modern servers and consumers."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
+ {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
+]
+
+[package.dependencies]
+defusedxml = "*"
+
+[package.extras]
+mysql = ["mysql-connector-python"]
+postgresql = ["psycopg2"]
+
[[package]]
name = "pytz"
version = "2023.3.post1"
@@ -1732,6 +1869,25 @@ redis = ["redis (>=3)"]
security = ["itsdangerous (>=2.0)"]
yaml = ["pyyaml (>=5.4)"]
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.1"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
+ {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
+]
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
[[package]]
name = "s3transfer"
version = "0.7.0"
@@ -2443,4 +2599,4 @@ indexer = ["ujson", "warcio", "idna", "beautifulsoup4", "lxml", "langdetect", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.11"
-content-hash = "fe5f238c57ec2d09acb6bdf8f46f33c7bbe499f68a7e34ab7bca1336e0ae881c"
+content-hash = "13572a7df206102ce30a6deb1eecd22b5b217a96f864a7dd6c558a7ca263d520"
diff --git a/pyproject.toml b/pyproject.toml
index 4a4a725..a0de7fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,7 @@ django = "^4.2.4"
django-ninja = "^0.22.2"
requests-cache = "^1.1.0"
redis = {extras = ["hiredis"], version = "^5.0.1"}
+django-allauth = "^0.57.0"
[tool.poetry.extras]
indexer = [
From bb9e6aa4bd555adcef8ae512a48830144dd62475 Mon Sep 17 00:00:00 2001
From: Daoud Clarke
Date: Wed, 25 Oct 2023 16:39:42 +0100
Subject: [PATCH 02/55] Implement curation API using Django Ninja
---
mwmbl/admin.py | 8 ++
mwmbl/api.py | 7 +-
mwmbl/apps.py | 15 +--
mwmbl/migrations/0001_initial.py | 58 ++++++++++
mwmbl/migrations/__init__.py | 0
mwmbl/models.py | 15 +++
mwmbl/platform/curate.py | 82 +++++++++++++
mwmbl/platform/data.py | 45 ++++++++
mwmbl/platform/user.py | 190 -------------------------------
mwmbl/settings_common.py | 6 +-
10 files changed, 227 insertions(+), 199 deletions(-)
create mode 100644 mwmbl/admin.py
create mode 100644 mwmbl/migrations/0001_initial.py
create mode 100644 mwmbl/migrations/__init__.py
create mode 100644 mwmbl/models.py
create mode 100644 mwmbl/platform/curate.py
create mode 100644 mwmbl/platform/data.py
delete mode 100644 mwmbl/platform/user.py
diff --git a/mwmbl/admin.py b/mwmbl/admin.py
new file mode 100644
index 0000000..74af7ac
--- /dev/null
+++ b/mwmbl/admin.py
@@ -0,0 +1,8 @@
+from django.contrib.admin import ModelAdmin
+from django.contrib.auth.admin import UserAdmin
+from django.contrib import admin
+
+from mwmbl.models import MwmblUser, UserCuration
+
+admin.site.register(MwmblUser, UserAdmin)
+admin.site.register(UserCuration, ModelAdmin)
diff --git a/mwmbl/api.py b/mwmbl/api.py
index 4a9cf08..bb2fe75 100644
--- a/mwmbl/api.py
+++ b/mwmbl/api.py
@@ -7,6 +7,7 @@ from ninja import NinjaAPI
import mwmbl.crawler.app as crawler
from mwmbl.indexer.batch_cache import BatchCache
from mwmbl.indexer.paths import INDEX_NAME, BATCH_DIR_NAME
+from mwmbl.platform import curate
from mwmbl.tinysearchengine import search
from mwmbl.tinysearchengine.completer import Completer
from mwmbl.tinysearchengine.indexer import TinyIndex, Document
@@ -24,13 +25,17 @@ batch_cache = BatchCache(Path(settings.DATA_PATH) / BATCH_DIR_NAME)
def create_api(version):
- api = NinjaAPI(version=version)
+ # Set csrf to True to all cookie-based authentication
+ api = NinjaAPI(version=version, csrf=True)
search_router = search.create_router(ranker)
api.add_router("/search/", search_router)
crawler_router = crawler.create_router(batch_cache=batch_cache, queued_batches=queued_batches)
api.add_router("/crawler/", crawler_router)
+
+ curation_router = curate.create_router(index_path)
+ api.add_router("/curation/", curation_router)
return api
diff --git a/mwmbl/apps.py b/mwmbl/apps.py
index f829ecc..dff27b6 100644
--- a/mwmbl/apps.py
+++ b/mwmbl/apps.py
@@ -6,19 +6,20 @@ from pathlib import Path
from django.apps import AppConfig
from django.conf import settings
-from mwmbl.api import queued_batches
-from mwmbl import background
-from mwmbl.indexer.paths import INDEX_NAME
-from mwmbl.indexer.update_urls import update_urls_continuously
-from mwmbl.tinysearchengine.indexer import TinyIndex, Document, PAGE_SIZE
-from mwmbl.url_queue import update_queue_continuously
-
class MwmblConfig(AppConfig):
name = "mwmbl"
verbose_name = "Mwmbl Application"
def ready(self):
+ # Imports here to avoid AppRegistryNotReady exception
+ from mwmbl.api import queued_batches
+ from mwmbl import background
+ from mwmbl.indexer.paths import INDEX_NAME
+ from mwmbl.indexer.update_urls import update_urls_continuously
+ from mwmbl.tinysearchengine.indexer import TinyIndex, Document, PAGE_SIZE
+ from mwmbl.url_queue import update_queue_continuously
+
index_path = Path(settings.DATA_PATH) / INDEX_NAME
try:
existing_index = TinyIndex(item_factory=Document, index_path=index_path)
diff --git a/mwmbl/migrations/0001_initial.py b/mwmbl/migrations/0001_initial.py
new file mode 100644
index 0000000..1dc8d43
--- /dev/null
+++ b/mwmbl/migrations/0001_initial.py
@@ -0,0 +1,58 @@
+# Generated by Django 4.2.6 on 2023-10-25 11:55
+
+from django.conf import settings
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='MwmblUser',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+ ],
+ options={
+ 'verbose_name': 'user',
+ 'verbose_name_plural': 'users',
+ 'abstract': False,
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UserCuration',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timestamp', models.DateTimeField()),
+ ('url', models.CharField(max_length=300)),
+ ('results', models.JSONField()),
+ ('curation_type', models.CharField(max_length=20)),
+ ('curation', models.JSONField()),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/mwmbl/migrations/__init__.py b/mwmbl/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mwmbl/models.py b/mwmbl/models.py
new file mode 100644
index 0000000..30d6e01
--- /dev/null
+++ b/mwmbl/models.py
@@ -0,0 +1,15 @@
+from django.db import models
+from django.contrib.auth.models import AbstractUser
+
+
+class MwmblUser(AbstractUser):
+ pass
+
+
+class UserCuration(models.Model):
+ user = models.ForeignKey(MwmblUser, on_delete=models.CASCADE)
+ timestamp = models.DateTimeField()
+ url = models.CharField(max_length=300)
+ results = models.JSONField()
+ curation_type = models.CharField(max_length=20)
+ curation = models.JSONField()
diff --git a/mwmbl/platform/curate.py b/mwmbl/platform/curate.py
new file mode 100644
index 0000000..92b1ebd
--- /dev/null
+++ b/mwmbl/platform/curate.py
@@ -0,0 +1,82 @@
+import json
+from urllib.parse import urljoin, parse_qs
+
+import requests
+from ninja import Router
+from ninja.security import django_auth
+
+from mwmbl.indexer.update_urls import get_datetime_from_timestamp
+from mwmbl.models import UserCuration
+from mwmbl.platform.data import CurateBegin, CurateMove, CurateDelete, CurateAdd, CurateValidate, Curation
+from mwmbl.tinysearchengine.indexer import TinyIndex, Document
+from mwmbl.tokenizer import tokenize
+
+
+RESULT_URL = "https://mwmbl.org/?q="
+MAX_CURATED_SCORE = 1_111_111.0
+
+
+def create_router(index_path: str) -> Router:
+ router = Router(tags=["user"])
+
+ @router.post("/begin", auth=django_auth)
+ def user_begin_curate(request, curate_begin: CurateBegin):
+ return _curate(request, "curate_begin", curate_begin)
+
+ @router.post("/move", auth=django_auth)
+ def user_move_result(request, curate_move: Curation[CurateMove]):
+ return _curate(request, "curate_move", curate_move)
+
+ @router.post("/delete", auth=django_auth)
+ def user_delete_result(request, curate_delete: Curation[CurateDelete]):
+ return _curate(request, "curate_delete", curate_delete)
+
+ @router.post("/add", auth=django_auth)
+ def user_add_result(request, curate_add: Curation[CurateAdd]):
+ return _curate(request, "curate_add", curate_add)
+
+ @router.post("/validate", auth=django_auth)
+ def user_add_result(request, curate_validate: Curation[CurateValidate]):
+ return _curate(request, "curate_validate", curate_validate)
+
+ def _curate(request, curation_type: str, curation: Curation):
+ user_curation = UserCuration(
+ user=request.user,
+ timestamp=get_datetime_from_timestamp(curation.timestamp),
+ url=curation.url,
+ results=curation.dict()["results"],
+ curation_type=curation_type,
+ curation=curation.curation.dict(),
+ )
+ user_curation.save()
+
+ with TinyIndex(Document, index_path, 'w') as indexer:
+ query_string = parse_qs(curation.url)
+ if len(query_string) > 1:
+ raise ValueError(f"Should be one query string in the URL: {curation.url}")
+
+ queries = next(iter(query_string.values()))
+ if len(queries) > 1:
+ raise ValueError(f"Should be one query value in the URL: {curation.url}")
+
+ query = queries[0]
+ print("Query", query)
+ tokens = tokenize(query)
+ print("Tokens", tokens)
+ term = " ".join(tokens)
+ print("Key", term)
+
+ documents = [
+ Document(result.title, result.url, result.extract, MAX_CURATED_SCORE - i, term, result.curated)
+ for i, result in enumerate(curation.results)
+ ]
+ page_index = indexer.get_key_page_index(term)
+ print("Page index", page_index)
+ print("Storing documents", documents)
+ indexer.store_in_page(page_index, documents)
+
+ return {"curation": "ok"}
+
+ return router
+
+
diff --git a/mwmbl/platform/data.py b/mwmbl/platform/data.py
new file mode 100644
index 0000000..1f0b0c1
--- /dev/null
+++ b/mwmbl/platform/data.py
@@ -0,0 +1,45 @@
+from datetime import datetime
+from typing import TypeVar, Generic
+
+from ninja import Schema
+
+
+class Result(Schema):
+ url: str
+ title: str
+ extract: str
+ curated: bool
+
+
+class CurateBegin(Schema):
+ pass
+
+
+class CurateMove(Schema):
+ old_index: int
+ new_index: int
+
+
+class CurateDelete(Schema):
+ delete_index: int
+
+
+class CurateAdd(Schema):
+ insert_index: int
+ url: str
+
+
+class CurateValidate(Schema):
+ validate_index: int
+ is_validated: bool
+
+
+T = TypeVar('T', CurateBegin, CurateAdd, CurateDelete, CurateMove, CurateValidate)
+
+
+class Curation(Schema, Generic[T]):
+ timestamp: int
+ url: str
+ results: list[Result]
+ curation: T
+
diff --git a/mwmbl/platform/user.py b/mwmbl/platform/user.py
deleted file mode 100644
index bbdcb0e..0000000
--- a/mwmbl/platform/user.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import json
-import os
-from typing import TypeVar, Generic
-from urllib.parse import urljoin, parse_qs
-
-import requests
-from fastapi import APIRouter, Response
-from pydantic import BaseModel
-
-from mwmbl.tinysearchengine.indexer import TinyIndex, Document
-from mwmbl.tokenizer import tokenize
-
-
-LEMMY_URL = os.environ["LEMMY_URL"]
-RESULT_URL = "https://mwmbl.org/?q="
-MAX_CURATED_SCORE = 1_111_111.0
-
-
-class Register(BaseModel):
- username: str
- email: str
- password: str
- password_verify: str
-
-
-class Login(BaseModel):
- username_or_email: str
- password: str
-
-
-class Result(BaseModel):
- url: str
- title: str
- extract: str
- curated: bool
-
-
-class BeginCurate(BaseModel):
- auth: str
- url: str
- results: list[Result]
-
-
-class CurateMove(BaseModel):
- old_index: int
- new_index: int
-
-
-class CurateDelete(BaseModel):
- delete_index: int
-
-
-class CurateAdd(BaseModel):
- insert_index: int
- url: str
-
-
-class CurateValidate(BaseModel):
- validate_index: int
- is_validated: bool
-
-
-T = TypeVar('T', CurateAdd, CurateDelete, CurateMove, CurateValidate)
-
-
-class Curation(BaseModel, Generic[T]):
- auth: str
- curation_id: int
- url: str
- results: list[Result]
- curation: T
-
-
-def create_router(index_path: str) -> APIRouter:
- router = APIRouter(prefix="/user", tags=["user"])
-
- # TODO: reinstate
- # community_id = get_community_id()
- community_id = 0
-
- @router.post("/register")
- def user_register(register: Register) -> Response:
- lemmy_register = {
- "username": register.username,
- "email": register.email,
- "password": register.password,
- "password_verify": register.password_verify,
- "answer": "not applicable",
- "captcha_answer": None,
- "captcha_uuid": None,
- "honeypot": None,
- "show_nsfw": False,
- }
- request = requests.post(urljoin(LEMMY_URL, "api/v3/user/register"), json=lemmy_register)
- if request.status_code != 200:
- return Response(content=request.content, status_code=request.status_code, media_type="text/json")
-
- @router.post("/login")
- def user_login(login: Login) -> Response:
- request = requests.post(urljoin(LEMMY_URL, "api/v3/user/login"), json=login.dict())
- return Response(content=request.content, status_code=request.status_code, media_type="text/json")
-
- @router.post("/curation/begin")
- def user_begin_curate(begin_curate: BeginCurate):
- results = begin_curate.dict()["results"]
- body = json.dumps({"original_results": results}, indent=2)
- create_post = {
- "auth": begin_curate.auth,
- "body": body,
- "community_id": community_id,
- "honeypot": None,
- "language_id": None,
- "name": begin_curate.url,
- "nsfw": None,
- "url": begin_curate.url,
- }
- request = requests.post(urljoin(LEMMY_URL, "api/v3/post"), json=create_post)
- if request.status_code != 200:
- return Response(content=request.content, status_code=request.status_code, media_type="text/json")
- data = request.json()
- curation_id = data["post_view"]["post"]["id"]
- return {"curation_id": curation_id}
-
- @router.post("/curation/move")
- def user_move_result(curate_move: Curation[CurateMove]):
- return _curate("curate_move", curate_move)
-
- @router.post("/curation/delete")
- def user_delete_result(curate_delete: Curation[CurateDelete]):
- return _curate("curate_delete", curate_delete)
-
- @router.post("/curation/add")
- def user_add_result(curate_add: Curation[CurateAdd]):
- return _curate("curate_add", curate_add)
-
- @router.post("/curation/validate")
- def user_add_result(curate_validate: Curation[CurateValidate]):
- return _curate("curate_validate", curate_validate)
-
- def _curate(curation_type: str, curation: Curation):
- content = json.dumps({
- "curation_type": curation_type,
- "curation": curation.curation.dict(),
- }, indent=2)
- create_comment = {
- "auth": curation.auth,
- "content": json.dumps(content, indent=2),
- "form_id": None,
- "language_id": None,
- "parent_id": None,
- "post_id": curation.curation_id,
- }
- request = requests.post(urljoin(LEMMY_URL, "api/v3/comment"), json=create_comment)
-
- with TinyIndex(Document, index_path, 'w') as indexer:
- query_string = parse_qs(curation.url)
- if len(query_string) > 1:
- raise ValueError(f"Should be one query string in the URL: {curation.url}")
-
- queries = next(iter(query_string.values()))
- if len(queries) > 1:
- raise ValueError(f"Should be one query value in the URL: {curation.url}")
-
- query = queries[0]
- print("Query", query)
- tokens = tokenize(query)
- print("Tokens", tokens)
- term = " ".join(tokens)
- print("Key", term)
-
- documents = [
- Document(result.title, result.url, result.extract, MAX_CURATED_SCORE - i, term, result.curated)
- for i, result in enumerate(curation.results)
- ]
- page_index = indexer.get_key_page_index(term)
- print("Page index", page_index)
- print("Storing documents", documents)
- indexer.store_in_page(page_index, documents)
-
- return Response(content=request.content, status_code=request.status_code, media_type="text/json")
-
- return router
-
-
-def get_community_id() -> str:
- request = requests.get(urljoin(LEMMY_URL, "api/v3/community?name=main"))
- community = request.json()
- return community["community_view"]["community"]["id"]
-
-
diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py
index 14a1e5a..e223c1b 100644
--- a/mwmbl/settings_common.py
+++ b/mwmbl/settings_common.py
@@ -119,7 +119,6 @@ USE_TZ = True
STATIC_URL = 'static/'
STATICFILES_DIRS = [str(Path(__file__).parent.parent / "front-end" / "dist")]
-print("Static files", STATICFILES_DIRS)
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
@@ -134,5 +133,10 @@ AUTHENTICATION_BACKENDS = [
'allauth.account.auth_backends.AuthenticationBackend',
]
+
+AUTH_USER_MODEL = "mwmbl.MwmblUser"
+
+
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
+
From 4d823497a68512830258dd895783ea8e3b14ecab Mon Sep 17 00:00:00 2001
From: Daoud Clarke
Date: Wed, 25 Oct 2023 19:17:02 +0100
Subject: [PATCH 03/55] Add original curation front-end
---
front-end/assets/css/global.css | 94 ++++++++++
front-end/assets/opensearch.xml | 10 +-
front-end/config.js | 1 +
front-end/package-lock.json | 82 +++------
front-end/package.json | 3 -
front-end/src/components/app.js | 14 +-
front-end/src/components/login.js | 69 ++++++++
.../src/components/molecules/add-button.js | 24 +++
.../src/components/molecules/add-result.js | 69 ++++++++
.../src/components/molecules/delete-button.js | 35 ++++
front-end/src/components/molecules/result.js | 22 ++-
.../components/molecules/validate-button.js | 53 ++++++
front-end/src/components/organisms/footer.js | 10 +-
front-end/src/components/organisms/results.js | 166 +++++++++++++++++-
front-end/src/components/organisms/save.js | 122 +++++++++++++
.../src/components/organisms/search-bar.js | 6 +-
front-end/src/components/register.js | 84 +++++++++
front-end/src/index.html | 13 +-
front-end/src/index.js | 3 +
front-end/src/stats/index.html | 12 +-
front-end/vite.config.js | 9 +-
21 files changed, 795 insertions(+), 106 deletions(-)
create mode 100644 front-end/src/components/login.js
create mode 100644 front-end/src/components/molecules/add-button.js
create mode 100644 front-end/src/components/molecules/add-result.js
create mode 100644 front-end/src/components/molecules/delete-button.js
create mode 100644 front-end/src/components/molecules/validate-button.js
create mode 100644 front-end/src/components/organisms/save.js
create mode 100644 front-end/src/components/register.js
diff --git a/front-end/assets/css/global.css b/front-end/assets/css/global.css
index d21eff3..b6f04dd 100644
--- a/front-end/assets/css/global.css
+++ b/front-end/assets/css/global.css
@@ -117,6 +117,10 @@ mwmbl-results, footer {
padding: 10px;
}
+.result {
+ min-height: 120px;
+}
+
.result a {
display: block;
text-decoration: none;
@@ -229,4 +233,94 @@ a {
font-weight: var(--bold-font-weight);
color: var(--primary-color);
text-decoration: underline;
+}
+
+.result-container {
+ display: flex;
+}
+
+.curation-buttons {
+ padding: 20px;
+}
+
+.curation-button {
+ opacity: 0;
+ color: inherit;
+ border: none;
+ padding: 0;
+ font: inherit;
+ outline: inherit;
+ cursor: pointer;
+
+ background: darkgrey;
+ box-shadow: 3px 3px 3px lightgrey;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex; /* or inline-flex */
+ align-items: center;
+ justify-content: center;
+ margin: 10px 0 10px 0;
+}
+
+.result:hover .curation-button {
+ opacity: 70%;
+ transition:
+ opacity 200ms ease-in-out;
+}
+
+.result:hover .curation-button:hover {
+ opacity: 100%;
+}
+
+.curate-delete {
+ margin-top: 0;
+}
+
+.validated {
+ background: lightgreen;
+ opacity: 100%;
+}
+
+.curate-add {
+ margin-bottom: 0;
+}
+
+
+.modal {
+ /*display: none; !* Hidden by default *!*/
+ position: fixed; /* Stay in place */
+ z-index: 100; /* Sit on top */
+ left: 0;
+ top: 0;
+ width: 100%; /* Full width */
+ height: 100%; /* Full height */
+ overflow: auto; /* Enable scroll if needed */
+ background-color: rgb(0,0,0); /* Fallback color */
+ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+}
+
+/* Modal Content/Box */
+.modal-content {
+ background-color: #fefefe;
+ margin: 15% auto; /* 15% from the top and centered */
+ padding: 20px;
+ border: 1px solid #888;
+ max-width: 800px;
+ width: 80%; /* Could be more or less, depending on screen size */
+}
+
+/* The Close Button */
+.close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+}
+
+.close:hover,
+.close:focus {
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/front-end/assets/opensearch.xml b/front-end/assets/opensearch.xml
index bf0fe92..a9bdbcf 100644
--- a/front-end/assets/opensearch.xml
+++ b/front-end/assets/opensearch.xml
@@ -1,10 +1,10 @@
- MWMBL
- Search MWMBL
-
-
- MWMBL Search
+ MWMBL Local
+ Search MWMBL Local
+
+
+ MWMBL Search Local
data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDkzNzUgOTM3NSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNTsiPgo8c3R5bGU+CgkJcGF0aCB7CgkJCWZpbGw6ICMwMDA7CgkJfQoJCUBtZWRpYSAoIHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrICkgewoJCQlwYXRoIHsKCQkJCWZpbGw6ICNmZmYgIWltcG9ydGFudDsKCQkJfQoJCX0KCTwvc3R5bGU+CjxwYXRoIGQ9Ik02MTI4LjcyLDgyNTEuNTZjNDk1LjY1LDAgOTE5LjY5NywtMTc2LjIyMiAxMjcyLjEzLC01MjguNjU5YzM1Mi40MzcsLTM1Mi40MzggNTI4LjY1OSwtNzc2LjQ4NCA1MjguNjU5LC0xMjcyLjEzbC0wLC0zMzU4Ljc1Yy0wLC05NC42NDQgLTM1LjQ5MiwtMTc2Ljg0MSAtMTA2LjQ4MiwtMjQ2LjU4MWMtNzAuOTg1LC02OS43MzkgLTE1My44MDEsLTEwNC42MTIgLTI0OC40NDUsLTEwNC42MTJjLTk5LjYzNCwtMCAtMTg0LjMxNCwzNC44NzMgLTI1NC4wNTQsMTA0LjYxMmMtNjkuNzQ2LDY5Ljc0IC0xMDQuNjEyLDE1MS45MzcgLTEwNC42MTIsMjQ2LjU4MWwtMCwzMzU4Ljc1Yy0wLDMwMS4zNzMgLTEwNS44NTcsNTU3LjkyMyAtMzE3LjU3MSw3NjkuNjNjLTIxMS43MDgsMjExLjcxNCAtNDY4LjI1MSwzMTcuNTcxIC03NjkuNjMsMzE3LjU3MWMtMjk4Ljg5LDAgLTU1NC44MDgsLTEwNS44NTcgLTc2Ny43NjYsLTMxNy41NzFjLTIxMi45NTgsLTIxMS43MDcgLTMxOS40MzQsLTQ2OC4yNTcgLTMxOS40MzQsLTc2OS42M2wtMCwtMzM1OC43NWMtMCwtOTQuNjQ0IC0zNC44NzMsLTE3Ni44NDEgLTEwNC42MTMsLTI0Ni41ODFjLTY5LjczOSwtNjkuNzM5IC0xNTQuNDI2LC0xMDQuNjEyIC0yNTQuMDU0LC0xMDQuNjEyYy05NC42NDksLTAgLTE3Ni44NDEsMzQuODczIC0yNDYuNTgsMTA0LjYxMmMtNjkuNzQsNjkuNzQgLTEwNC42MTMsMTUxLjkzNyAtMTA0LjYxMywyNDYuNTgxbDAsMzM1OC43NWMwLDMwMS4zNzMgLTEwNi40NzYsNTU3LjkyMyAtMzE5LjQzNCw3NjkuNjNjLTIxMi45NTksMjExLjcxNCAtNDY4Ljg4MywzMTcuNTcxIC03NjcuNzY2LDMxNy41NzFjLTMwMS4zNzksMCAtNTU3LjkyMywtMTA1Ljg1NyAtNzY5LjYzNiwtMzE3LjU3MWMtMjExLjcwOCwtMjExLjcwNyAtMzE3LjU2NSwtNDY4LjI1NyAtMzE3LjU2NSwtNzY5LjYzbDAsLTMzNTguNzVjMCwtOTQuNjQ0IC0zNC44NzMsLTE3Ni44NDEgLTEwNC42MTIsLTI0Ni41ODFjLTY5Ljc0LC02OS43MzkgLTE1NC40MjcsLTEwNC42MTIgLTI1NC4wNTQsLTEwNC42MTJjLTk0LjY1LC0wIC0xNzYuODQxLDM0Ljg3MyAtMjQ2LjU4MSwxMDQuNjEyYy02OS43MzksNjkuNzQgLTEwNC42MTIsMTUxLjkzNyAtMTA0LjYxMiwyNDYuNTgxbC0wLDMzNTguNzVjLTAsMzI2LjI4MyA4MC4zMjcsNjI3LjY2MiAyNDAuOTc2LDkwNC4xMzFjMTYwLjY1NiwyNzYuNDY5IDM3OC41OTMsNDk1LjAzMSA2NTMuODE3LDY1NS42ODZjMjc1LjIyNCwxNjAuNjQ5IDU3NS45ODQsMjQwLjk3NyA5MDIuMjY3LDI0MC45NzdjMjkxLjQxNiwwIDU2My41MjUsLTY0Ljc2MSA4MTYuMzM1LC0xOTQuMjc3YzI1Mi44MSwtMTI5LjUxNyA0NjAuMTU4LC0zMDcuNjA4IDYyMi4wNTgsLTUzNC4yNjNjMTY2Ljg3OCwyMjYuNjU1IDM3Ni43MjIsNDA0Ljc0NiA2MjkuNTMyLDUzNC4yNjNjMjUyLjgwOSwxMjkuNTE2IDUyNC45MTksMTk0LjI3NyA4MTYuMzM1LDE5NC4yNzdabS0wLjk2LC0xNjE3LjM5bC0wLjU4MiwtMGMtOTkuNjI3LC0wIC0xODQuMzE0LC0zNC44NzMgLTI1NC4wNTQsLTEwNC42MTJjLTY5LjczOSwtNjkuNzQgLTEwNC42MTIsLTE1MS45MzggLTEwNC42MTIsLTI0Ni41ODFsLTAsLTMzNTguNzRjLTAsLTMwMS4zNzMgLTEwNS44NTcsLTU1Ny45MjMgLTMxNy41NjUsLTc2OS42M2MtMjEwLjY5OCwtMjEwLjY5OSAtNDY1Ljc5OSwtMzE2LjU0OSAtNzY1LjMyLC0zMTcuNTU5Yy0yOTkuNTIxLDEuMDEgLTU1NC42MjIsMTA2Ljg2IC03NjUuMzE0LDMxNy41NTljLTIxMS43MTQsMjExLjcwNyAtMzE3LjU3MSw0NjguMjU3IC0zMTcuNTcxLDc2OS42M2wwLDMzNTguNzVjMCw5NC42NDQgLTM0Ljg2NiwxNzYuODQxIC0xMDQuNjA2LDI0Ni41ODFjLTY5LjczOSw2OS43MzkgLTE1NC40MjYsMTA0LjYxMiAtMjU0LjA1NCwxMDQuNjEybC04LjYzOCwwYy05NC42NDMsMCAtMTc2Ljg0MSwtMzQuODczIC0yNDYuNTgsLTEwNC42MTJjLTY5Ljc0LC02OS43NCAtMTA0LjYxMywtMTUxLjkzNyAtMTA0LjYxMywtMjQ2LjU4MWwwLC0zMzU4Ljc1YzAsLTMwMS4zNzMgLTEwNi40NzYsLTU1Ny45MjMgLTMxOS40MzQsLTc2OS42M2MtMjEyLjk1OSwtMjExLjcxNCAtNDY4Ljg3NiwtMzE3LjU3MSAtNzY3Ljc2NiwtMzE3LjU3MWMtMzAxLjM3OSwtMCAtNTU3LjkyMiwxMDUuODU3IC03NjkuNjMsMzE3LjU3MWMtMjExLjcxNCwyMTEuNzA3IC0zMTcuNTcxLDQ2OC4yNTcgLTMxNy41NzEsNzY5LjYzbDAsMzM1OC43NWMwLDk0LjY0NCAtMzQuODY3LDE3Ni44NDEgLTEwNC42MTIsMjQ2LjU4MWMtNjkuNzQsNjkuNzM5IC0xNTQuNDIsMTA0LjYxMiAtMjU0LjA1NCwxMDQuNjEyYy05NC42NDQsMCAtMTc2Ljg0MSwtMzQuODczIC0yNDYuNTgxLC0xMDQuNjEyYy02OS43MzksLTY5Ljc0IC0xMDQuNjA2LC0xNTEuOTM3IC0xMDQuNjA2LC0yNDYuNTgxbDAsLTMzNTguNzVjMCwtMzI2LjI4MyA4MC4zMjEsLTYyNy42NjIgMjQwLjk3NywtOTA0LjEzMWMxNjAuNjQ5LC0yNzYuNDY5IDM3OC41ODYsLTQ5NS4wMzEgNjUzLjgxNiwtNjU1LjY4NmMyNzUuMjI0LC0xNjAuNjQ5IDU3NS45NzgsLTI0MC45NzcgOTAyLjI2MSwtMjQwLjk3N2MyOTEuNDE2LC0wIDU2My41MjYsNjQuNzYxIDgxNi4zMzUsMTk0LjI3N2MyNTIuODEsMTI5LjUxNyA0NjAuMTY0LDMwNy42MDggNjIyLjA1OCw1MzQuMjYzYzE2Ni44NzgsLTIyNi42NTUgMzc2LjcyMiwtNDA0Ljc0NiA2MjkuNTMyLC01MzQuMjYzYzI1Mi44MDksLTEyOS41MTYgNTI0LjkxOSwtMTk0LjI3NyA4MTYuMzM1LC0xOTQuMjc3bDguNjM4LC0wYzE2NC44MjIsLTAgMzIzLjQ3MiwyMC43MTggNDc1Ljk0MSw2Mi4xNTRsNS4yMzksMS40MzFjNDEuMTE0LDExLjI2MyA4MS42MDksMjQuMDI0IDEyMS40OTcsMzguMjg0YzcyLjY4NywyNS44NyAxNDMuOTA3LDU2LjY3NSAyMTMuNjUyLDkyLjQwOGMyNTAuNjM2LDEyOC40MDggNDU2LjU5MiwzMDQuNTQ5IDYxNy44NjYsNTI4LjQxMmw0LjMyOCw1LjY2NWMxNjYuODcyLC0yMjYuNTggMzc2LjY2NywtNDA0LjU5OCA2MjkuMzk2LC01MzQuMDc3YzI1Mi44MDksLTEyOS41MTYgNTI0LjkyNSwtMTk0LjI3NyA4MTYuMzM1LC0xOTQuMjc3YzQ5NS42NTcsLTAgOTE5LjcwNCwxNzYuMjIyIDEyNzIuMTQsNTI4LjY1OWMzNTIuNDM3LDM1Mi40MzggNTI4LjY1Myw3NzYuNDg0IDUyOC42NTMsMTI3Mi4xM2wwLDMzNTguNzVjMCw5NC42NDQgLTM1LjQ5MiwxNzYuODQxIC0xMDYuNDc2LDI0Ni41ODFjLTcwLjk4NCw2OS43MzkgLTE1My44MDEsMTA0LjYxMiAtMjQ4LjQ1MSwxMDQuNjEyYy05OS42MjcsMCAtMTg0LjMxNCwtMzQuODczIC0yNTQuMDU0LC0xMDQuNjEyYy02OS43MzksLTY5Ljc0IC0xMDQuNjEyLC0xNTEuOTM3IC0xMDQuNjEyLC0yNDYuNTgxbC0wLC0zMzU4Ljc1Yy0wLC0zMDEuMzczIC0xMDUuODUxLC01NTcuOTIzIC0zMTcuNTY1LC03NjkuNjNjLTIxMS43MTMsLTIxMS43MTQgLTQ2OC4yNTcsLTMxNy41NzEgLTc2OS42MzYsLTMxNy41NzFjLTI5OC44ODMsLTAgLTU1NC44MDcsMTA1Ljg1NyAtNzY3Ljc2NiwzMTcuNTcxYy0yMTIuOTUyLDIxMS43MDcgLTMxOS40MzQsNDY4LjI1NyAtMzE5LjQzNCw3NjkuNjNsLTAsMzM1OC43NWMtMCw5NC42NDQgLTM0Ljg2NywxNzYuODQxIC0xMDQuNjA2LDI0Ni41ODFjLTY5Ljc0Niw2OS43MzkgLTE1NC40MjcsMTA0LjYxMiAtMjU0LjA1NSwxMDQuNjEybC0wLjU4MiwtMC4wMDZaIiBzdHlsZT0ic3Ryb2tlOiMxODVBREI7c3Ryb2tlLXdpZHRoOjQuMTdweDsiLz48L3N2Zz4K
open
diff --git a/front-end/config.js b/front-end/config.js
index c42b4a8..1c7c23c 100644
--- a/front-end/config.js
+++ b/front-end/config.js
@@ -9,6 +9,7 @@
export default {
componentPrefix: 'mwmbl',
publicApiURL: 'https://api.mwmbl.org/',
+ // publicApiURL: 'http://localhost:5000/',
searchQueryParam: 'q',
footerLinks: [
{
diff --git a/front-end/package-lock.json b/front-end/package-lock.json
index 285df52..277dfb4 100644
--- a/front-end/package-lock.json
+++ b/front-end/package-lock.json
@@ -5,9 +5,6 @@
"packages": {
"": {
"name": "front-end",
- "dependencies": {
- "chart.js": "^4.4.0"
- },
"devDependencies": {
"@vitejs/plugin-legacy": "^2.3.1",
"terser": "^5.16.1",
@@ -113,11 +110,6 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
- "node_modules/@kurkle/color": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
- "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
- },
"node_modules/@vitejs/plugin-legacy": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.3.1.tgz",
@@ -156,17 +148,6 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
- "node_modules/chart.js": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz",
- "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==",
- "dependencies": {
- "@kurkle/color": "^0.3.0"
- },
- "engines": {
- "pnpm": ">=7"
- }
- },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -598,16 +579,10 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
+ "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -628,9 +603,9 @@
"dev": true
},
"node_modules/postcss": {
- "version": "8.4.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "version": "8.4.19",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+ "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
"dev": true,
"funding": [
{
@@ -640,14 +615,10 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
- "nanoid": "^3.3.6",
+ "nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@@ -765,9 +736,9 @@
}
},
"node_modules/vite": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
- "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz",
+ "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
@@ -884,11 +855,6 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
- "@kurkle/color": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
- "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
- },
"@vitejs/plugin-legacy": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-2.3.1.tgz",
@@ -914,14 +880,6 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
- "chart.js": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz",
- "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==",
- "requires": {
- "@kurkle/color": "^0.3.0"
- }
- },
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -1145,9 +1103,9 @@
}
},
"nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
+ "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
"path-parse": {
@@ -1163,12 +1121,12 @@
"dev": true
},
"postcss": {
- "version": "8.4.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "version": "8.4.19",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
+ "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==",
"dev": true,
"requires": {
- "nanoid": "^3.3.6",
+ "nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
@@ -1252,9 +1210,9 @@
}
},
"vite": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz",
- "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==",
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz",
+ "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==",
"dev": true,
"requires": {
"esbuild": "^0.15.9",
diff --git a/front-end/package.json b/front-end/package.json
index 53a57e0..76e8d53 100644
--- a/front-end/package.json
+++ b/front-end/package.json
@@ -11,8 +11,5 @@
"@vitejs/plugin-legacy": "^2.3.1",
"terser": "^5.16.1",
"vite": "^3.2.3"
- },
- "dependencies": {
- "chart.js": "^4.4.0"
}
}
diff --git a/front-end/src/components/app.js b/front-end/src/components/app.js
index 9644a6b..d53cb3b 100644
--- a/front-end/src/components/app.js
+++ b/front-end/src/components/app.js
@@ -1,16 +1,22 @@
import define from '../utils/define.js';
+import addResult from "./molecules/add-result.js";
+import save from "./organisms/save.js";
const template = () => /*html*/`
-
-
-
+
+
+
+
`;
diff --git a/front-end/src/components/login.js b/front-end/src/components/login.js
new file mode 100644
index 0000000..9546695
--- /dev/null
+++ b/front-end/src/components/login.js
@@ -0,0 +1,69 @@
+import define from '../utils/define.js';
+import config from "../../config.js";
+
+const template = () => /*html*/`
+
+`;
+
+export default define('login', class extends HTMLElement {
+ constructor() {
+ super();
+ this.loginForm = null;
+ this.emailOrUsernameInput = null;
+ this.passwordInput = null;
+ this.__setup();
+ this.__events();
+ }
+
+ __setup() {
+ this.innerHTML = template();
+ this.loginForm = this.querySelector('form');
+ this.emailOrUsernameInput = this.querySelector('#login-email-or-username');
+ this.passwordInput = this.querySelector('#login-password');
+ }
+
+ __events() {
+ this.loginForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ this.__handleLogin(e);
+ });
+ }
+
+ __handleLogin = async () => {
+ const response = await fetch(`${config.publicApiURL}user/login`, {
+ method: 'POST',
+ cache: 'no-cache',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ "username_or_email": this.emailOrUsernameInput.value,
+ "password": this.passwordInput.value,
+ })
+ });
+ if (response.status === 200) {
+ const loginData = await response.json();
+ console.log("Login data", loginData);
+ document.cookie = `jwt=${loginData["jwt"]}; SameSite=Strict`;
+ console.log("Login success");
+ } else {
+ console.log("Login error", response);
+ }
+ }
+});
\ No newline at end of file
diff --git a/front-end/src/components/molecules/add-button.js b/front-end/src/components/molecules/add-button.js
new file mode 100644
index 0000000..d59884a
--- /dev/null
+++ b/front-end/src/components/molecules/add-button.js
@@ -0,0 +1,24 @@
+import define from "../../utils/define.js";
+import {globalBus} from "../../utils/events.js";
+import addResult from "./add-result.js";
+import emptyResult from "./empty-result.js";
+
+
+export default define('add-button', class extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.__setup();
+ }
+
+ __setup() {
+ this.__events();
+ }
+
+ __events() {
+ this.addEventListener('click', (e) => {
+ console.log("Add button");
+ document.querySelector('.modal').style.display = 'block';
+ document.querySelector('.modal input').focus();
+ })
+ }
+}, { extends: 'button' });
diff --git a/front-end/src/components/molecules/add-result.js b/front-end/src/components/molecules/add-result.js
new file mode 100644
index 0000000..6e9ce17
--- /dev/null
+++ b/front-end/src/components/molecules/add-result.js
@@ -0,0 +1,69 @@
+import define from '../../utils/define.js';
+import config from "../../../config.js";
+import {globalBus} from "../../utils/events.js";
+
+
+const FETCH_URL = `${config['publicApiURL']}crawler/fetch?`
+
+
+const template = () => /*html*/`
+
+`;
+
+export default define('add-result', class extends HTMLDivElement {
+ constructor() {
+ super();
+ this.classList.add('modal');
+ this.__setup();
+ }
+
+ __setup() {
+ this.innerHTML = template();
+ this.__events();
+ this.style.display = 'none';
+ }
+
+ __events() {
+ this.querySelector('.close').addEventListener('click', e => {
+ if (e.target === this) {
+ this.style.display = 'none';
+ }
+ });
+
+ this.addEventListener('click', e => {
+ this.style.display = 'none';
+ });
+
+ this.querySelector('form').addEventListener('click', e => {
+ // Clicking on the form shouldn't close it
+ e.stopPropagation();
+ });
+
+ this.addEventListener('submit', this.__urlSubmitted.bind(this));
+ }
+
+ async __urlSubmitted(e) {
+ e.preventDefault();
+ const value = this.querySelector('input').value;
+ console.log("Input value", value);
+
+ const query = document.querySelector('.search-bar input').value;
+
+ const url = `${FETCH_URL}url=${encodeURIComponent(value)}&query=${encodeURIComponent(query)}`;
+ const response = await fetch(url);
+ if (response.status === 200) {
+ const data = await response.json();
+ console.log("Data", data);
+
+ const addResultEvent = new CustomEvent('curate-add-result', {detail: data});
+ globalBus.dispatch(addResultEvent);
+ } else {
+ console.log("Bad response", response);
+ // TODO
+ }
+ }
+}, { extends: 'div' });
diff --git a/front-end/src/components/molecules/delete-button.js b/front-end/src/components/molecules/delete-button.js
new file mode 100644
index 0000000..1914684
--- /dev/null
+++ b/front-end/src/components/molecules/delete-button.js
@@ -0,0 +1,35 @@
+import define from "../../utils/define.js";
+import {globalBus} from "../../utils/events.js";
+
+
+export default define('delete-button', class extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.__setup();
+ }
+
+ __setup() {
+ this.__events();
+ }
+
+ __events() {
+ this.addEventListener('click', (e) => {
+ console.log("Delete button");
+
+ const result = this.closest('.result');
+ const parent = result.parentNode;
+
+ const index = Array.prototype.indexOf.call(parent.children, result);
+ console.log("Delete index", index);
+
+ const beginCuratingEvent = new CustomEvent('curate-delete-result', {
+ detail: {
+ data: {
+ delete_index: index
+ }
+ }
+ });
+ globalBus.dispatch(beginCuratingEvent);
+ })
+ }
+}, { extends: 'button' });
diff --git a/front-end/src/components/molecules/result.js b/front-end/src/components/molecules/result.js
index 696a7a1..2a2859e 100644
--- a/front-end/src/components/molecules/result.js
+++ b/front-end/src/components/molecules/result.js
@@ -1,13 +1,25 @@
import define from '../../utils/define.js';
import escapeString from '../../utils/escapeString.js';
import { globalBus } from '../../utils/events.js';
+import deleteButton from "./delete-button.js";
+import validateButton from "./validate-button.js";
+import addButton from "./add-button.js";
const template = ({ data }) => /*html*/`
-
- ${data.url}
- ${data.title}
-
-
+
`;
export default define('result', class extends HTMLLIElement {
diff --git a/front-end/src/components/molecules/validate-button.js b/front-end/src/components/molecules/validate-button.js
new file mode 100644
index 0000000..65e3b10
--- /dev/null
+++ b/front-end/src/components/molecules/validate-button.js
@@ -0,0 +1,53 @@
+import define from "../../utils/define.js";
+import {globalBus} from "../../utils/events.js";
+
+
+const VALIDATED_CLASS = "validated";
+
+export default define('validate-button', class extends HTMLButtonElement {
+ constructor() {
+ super();
+ this.__setup();
+ }
+
+ __setup() {
+ this.__events();
+ }
+
+ __events() {
+ this.addEventListener('click', (e) => {
+ console.log("Validate button");
+
+ const result = this.closest('.result');
+ const parent = result.parentNode;
+
+ const index = Array.prototype.indexOf.call(parent.children, result);
+ console.log("Validate index", index);
+
+ const curationValidateEvent = new CustomEvent('curate-validate-result', {
+ detail: {
+ data: {
+ validate_index: index
+ }
+ }
+ });
+ globalBus.dispatch(curationValidateEvent);
+ })
+ }
+
+ isValidated() {
+ return this.classList.contains(VALIDATED_CLASS);
+ }
+
+ validate() {
+ this.classList.add(VALIDATED_CLASS);
+ }
+
+ unvalidate() {
+ this.classList.remove(VALIDATED_CLASS);
+ }
+
+ toggleValidate() {
+ this.classList.toggle(VALIDATED_CLASS);
+ }
+}, { extends: 'button' });
diff --git a/front-end/src/components/organisms/footer.js b/front-end/src/components/organisms/footer.js
index 73344cb..4b8d62f 100644
--- a/front-end/src/components/organisms/footer.js
+++ b/front-end/src/components/organisms/footer.js
@@ -4,14 +4,14 @@ import config from '../../../config.js';
const template = ({ data }) => /*html*/`
`;
@@ -22,7 +22,7 @@ export default define('footer', class extends HTMLElement {
}
__setup() {
- this.innerHTML = template({
+ this.innerHTML = template({
data: {
links: config.footerLinks
}
@@ -31,6 +31,6 @@ export default define('footer', class extends HTMLElement {
}
__events() {
-
+
}
}, { extends: 'footer' });
\ No newline at end of file
diff --git a/front-end/src/components/organisms/results.js b/front-end/src/components/organisms/results.js
index 40f558c..37fefe5 100644
--- a/front-end/src/components/organisms/results.js
+++ b/front-end/src/components/organisms/results.js
@@ -1,5 +1,5 @@
import define from '../../utils/define.js';
-import { globalBus } from '../../utils/events.js';
+import {globalBus} from '../../utils/events.js';
// Components
import result from '../molecules/result.js';
@@ -17,6 +17,8 @@ export default define('results', class extends HTMLElement {
constructor() {
super();
this.results = null;
+ this.oldIndex = null;
+ this.curating = false;
this.__setup();
}
@@ -31,7 +33,7 @@ export default define('results', class extends HTMLElement {
this.results.innerHTML = '';
let resultsHTML = '';
if (!e.detail.error) {
- // If there is no details the input is empty
+ // If there is no details the input is empty
if (!e.detail.results) {
resultsHTML = /*html*/`
@@ -42,7 +44,7 @@ export default define('results', class extends HTMLElement {
for(const resultData of e.detail.results) {
resultsHTML += /*html*/`
{
this.results.firstElementChild.firstElementChild.focus();
- })
+ });
+
+ globalBus.on('curate-delete-result', (e) => {
+ console.log("Curate delete result event", e);
+ this.__beginCurating.bind(this)();
+
+ const children = this.results.getElementsByClassName('result');
+ let deleteIndex = e.detail.data.delete_index;
+ const child = children[deleteIndex];
+ this.results.removeChild(child);
+ const newResults = this.__getResults();
+
+ const curationSaveEvent = new CustomEvent('save-curation', {
+ detail: {
+ type: 'delete',
+ data: {
+ url: document.location.href,
+ results: newResults,
+ curation: {
+ delete_index: deleteIndex
+ }
+ }
+ }
+ });
+ globalBus.dispatch(curationSaveEvent);
+ });
+
+ globalBus.on('curate-validate-result', (e) => {
+ console.log("Curate validate result event", e);
+ this.__beginCurating.bind(this)();
+
+ const children = this.results.getElementsByClassName('result');
+ const validateChild = children[e.detail.data.validate_index];
+ validateChild.querySelector('.curate-approve').toggleValidate();
+
+ const newResults = this.__getResults();
+
+ const curationStartEvent = new CustomEvent('save-curation', {
+ detail: {
+ type: 'validate',
+ data: {
+ url: document.location.href,
+ results: newResults,
+ curation: e.detail.data
+ }
+ }
+ });
+ globalBus.dispatch(curationStartEvent);
+ });
+
+ globalBus.on('begin-curating-results', (e) => {
+ // We might not be online, or logged in, so save the curation in local storage in case:
+ console.log("Begin curation event", e);
+ this.__beginCurating.bind(this)();
+ });
+
+ globalBus.on('curate-add-result', (e) => {
+ console.log("Add result", e);
+ this.__beginCurating();
+ const resultData = e.detail;
+ const resultHTML = /*html*/`
+
+ `;
+ this.results.insertAdjacentHTML('afterbegin', resultHTML);
+
+ const newResults = this.__getResults();
+
+ const curationSaveEvent = new CustomEvent('save-curation', {
+ detail: {
+ type: 'add',
+ data: {
+ url: document.location.href,
+ results: newResults,
+ curation: {
+ insert_index: 0,
+ url: e.detail.url
+ }
+ }
+ }
+ });
+ globalBus.dispatch(curationSaveEvent);
+
+ });
+ }
+
+ __sortableActivate(event, ui) {
+ console.log("Sortable activate", ui);
+ this.__beginCurating();
+ this.oldIndex = ui.item.index();
+ }
+
+ __beginCurating() {
+ if (!this.curating) {
+ const results = this.__getResults();
+ const curationStartEvent = new CustomEvent('save-curation', {
+ detail: {
+ type: 'begin',
+ data: {
+ url: document.location.href,
+ results: results
+ }
+ }
+ });
+ globalBus.dispatch(curationStartEvent);
+ this.curating = true;
+ }
+ }
+
+ __getResults() {
+ const resultsElements = document.querySelectorAll('.results .result:not(.ui-sortable-placeholder)');
+ const results = [];
+ for (let resultElement of resultsElements) {
+ const result = {
+ url: resultElement.querySelector('a').href,
+ title: resultElement.querySelector('.title').innerText,
+ extract: resultElement.querySelector('.extract').innerText,
+ curated: resultElement.querySelector('.curate-approve').isValidated()
+ }
+ results.push(result);
+ }
+ console.log("Results", results);
+ return results;
+ }
+
+ __sortableDeactivate(event, ui) {
+ const newIndex = ui.item.index();
+ console.log('Sortable deactivate', ui, this.oldIndex, newIndex);
+
+ const newResults = this.__getResults();
+
+ const curationMoveEvent = new CustomEvent('save-curation', {
+ detail: {
+ type: 'move',
+ data: {
+ url: document.location.href,
+ results: newResults,
+ curation: {
+ old_index: this.oldIndex,
+ new_index: newIndex,
+ }
+ }
+ }
+ });
+ globalBus.dispatch(curationMoveEvent);
}
});
\ No newline at end of file
diff --git a/front-end/src/components/organisms/save.js b/front-end/src/components/organisms/save.js
new file mode 100644
index 0000000..dc2f883
--- /dev/null
+++ b/front-end/src/components/organisms/save.js
@@ -0,0 +1,122 @@
+import define from '../../utils/define.js';
+import {globalBus} from "../../utils/events.js";
+import config from "../../../config.js";
+
+
+const CURATION_KEY_PREFIX = "curation-";
+const CURATION_URL = config.publicApiURL + "user/curation/";
+
+
+const template = () => /*html*/`
+ 🖫
+`;
+
+
+export default define('save', class extends HTMLLIElement {
+ constructor() {
+ super();
+ this.currentCurationId = null;
+ this.classList.add('save');
+ this.sendId = 0;
+ this.sending = false;
+ this.__setup();
+ }
+
+ __setup() {
+ this.innerHTML = template();
+ this.__events();
+ // TODO: figure out when to call __sendToApi()
+ // setInterval(this.__sendToApi.bind(this), 1000);
+ }
+
+ __events() {
+ globalBus.on('save-curation', (e) => {
+ // We might not be online, or logged in, so save the curation in local storage in case:
+ console.log("Curation event", e);
+ this.__setCuration(e.detail);
+ this.__sendToApi();
+ });
+ }
+
+ __setCuration(curation) {
+ this.sendId += 1;
+ const key = CURATION_KEY_PREFIX + this.sendId;
+ localStorage.setItem(key, JSON.stringify(curation));
+ }
+
+ __getOldestCurationKey() {
+ let oldestId = Number.MAX_SAFE_INTEGER;
+ let oldestKey = null;
+ for (let i=0; i row.startsWith('jwt='))
+ ?.split('=')[1];
+
+ if (!auth) {
+ console.log("No auth");
+ return;
+ }
+
+ if (localStorage.length > 0) {
+ const key = this.__getOldestCurationKey();
+ const value = JSON.parse(localStorage.getItem(key));
+ console.log("Value", value);
+ const url = CURATION_URL + value['type'];
+
+ let data = value['data'];
+ if (value.type !== 'begin') {
+ if (this.currentCurationId === null) {
+ throw ReferenceError("No current curation found");
+ }
+ data['curation_id'] = this.currentCurationId;
+ }
+ data['auth'] = auth;
+
+ console.log("Data", data);
+ const response = await fetch(url, {
+ method: 'POST',
+ cache: 'no-cache',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify(data),
+ });
+
+ console.log("Save curation API response", response);
+
+ if (response.status === 200) {
+ localStorage.removeItem(key);
+ } else {
+ console.log("Bad response, skipping");
+ return;
+ }
+
+ const responseData = await response.json();
+ console.log("Response data", responseData);
+ if (responseData["curation_id"]) {
+ this.currentCurationId = responseData["curation_id"];
+ }
+
+ // There may be more to send, wait a second and see
+ setTimeout(this.__sendToApi.bind(this), 1000);
+ }
+ this.sending = false;
+ }
+}, { extends: 'li' });
+
diff --git a/front-end/src/components/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js
index f700fda..5adf52b 100644
--- a/front-end/src/components/organisms/search-bar.js
+++ b/front-end/src/components/organisms/search-bar.js
@@ -144,8 +144,12 @@ export default define('search-bar', class extends HTMLElement {
// Focus search bar when pressing `ctrl + k` or `/`
document.addEventListener('keydown', (e) => {
- if ((e.key === 'k' && e.ctrlKey) || e.key === '/' || e.key === 'Escape') {
+ if ((e.key === 'k' && e.ctrlKey) || e.key === 'Escape') {
e.preventDefault();
+
+ // Remove the modal if it's visible
+ document.querySelector('.modal').style.display = 'none';
+
this.searchInput.focus();
}
});
diff --git a/front-end/src/components/register.js b/front-end/src/components/register.js
new file mode 100644
index 0000000..ff5bfc0
--- /dev/null
+++ b/front-end/src/components/register.js
@@ -0,0 +1,84 @@
+import define from '../utils/define.js';
+import config from "../../config.js";
+
+const template = () => /*html*/`
+
+`;
+
+export default define('register', class extends HTMLElement {
+ constructor() {
+ super();
+ this.registerForm = null;
+ this.emailInput = null;
+ this.usernameInput = null;
+ this.passwordInput = null;
+ this.passwordVerifyInput = null;
+ this.__setup();
+ this.__events();
+ }
+
+ __setup() {
+ this.innerHTML = template();
+ this.registerForm = this.querySelector('form');
+ this.emailInput = this.querySelector('#register-email');
+ this.usernameInput = this.querySelector('#register-username');
+ this.passwordInput = this.querySelector('#register-password');
+ this.passwordVerifyInput = this.querySelector('#register-password-verify');
+ }
+
+ __events() {
+ this.registerForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ this.__handleRegister(e);
+ });
+ }
+
+ __handleRegister = async () => {
+ const response = await fetch(`${config.publicApiURL}user/register`, {
+ method: 'POST',
+ cache: 'no-cache',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ "username": this.usernameInput.value,
+ "email": this.emailInput.value,
+ "password": this.passwordInput.value,
+ "password_verify": this.passwordVerifyInput.value,
+ })
+ });
+ if (response.status === 200) {
+ const registerData = await response.json();
+ console.log("Register data", registerData);
+ document.cookie = `jwt=${registerData["jwt"]}; SameSite=Strict`;
+ console.log("Register success");
+ } else {
+ console.log("Register error", response);
+ }
+ }
+});
\ No newline at end of file
diff --git a/front-end/src/index.html b/front-end/src/index.html
index 23c0b52..843bfc8 100644
--- a/front-end/src/index.html
+++ b/front-end/src/index.html
@@ -35,21 +35,28 @@
-
+
+
+
+
+
+
+
+