From 60b27d639e5dc4f1dc479ebfc91d28229ac60247 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 14:08:56 +0000 Subject: [PATCH 01/20] Missing chart.js dependency --- front-end/package-lock.json | 32 ++++++++++++++++++++++++++++++++ front-end/package.json | 3 +++ 2 files changed, 35 insertions(+) diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 277dfb4..af0e754 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "front-end", + "dependencies": { + "chart.js": "^4.4.0" + }, "devDependencies": { "@vitejs/plugin-legacy": "^2.3.1", "terser": "^5.16.1", @@ -110,6 +113,11 @@ "@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", @@ -148,6 +156,17 @@ "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", @@ -855,6 +874,11 @@ "@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", @@ -880,6 +904,14 @@ "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", diff --git a/front-end/package.json b/front-end/package.json index 76e8d53..53a57e0 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -11,5 +11,8 @@ "@vitejs/plugin-legacy": "^2.3.1", "terser": "^5.16.1", "vite": "^3.2.3" + }, + "dependencies": { + "chart.js": "^4.4.0" } } From 932579ead3c15272cf5c9059332d31b2f01c74e7 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 18:51:55 +0000 Subject: [PATCH 02/20] Remove more unused code --- .gitignore | 1 + front-end/src/components/login.js | 69 ------- .../src/components/organisms/search-bar.js | 184 ------------------ front-end/src/components/register.js | 84 -------- front-end/src/index.html | 6 +- front-end/src/index.js | 3 - front-end/vite.config.js | 3 +- 7 files changed, 6 insertions(+), 344 deletions(-) delete mode 100644 front-end/src/components/login.js delete mode 100644 front-end/src/components/organisms/search-bar.js delete mode 100644 front-end/src/components/register.js diff --git a/.gitignore b/.gitignore index 451cbea..ead122e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ __pycache__/ build/ develop-eggs/ dist/ +front-end/dist/ downloads/ eggs/ .eggs/ diff --git a/front-end/src/components/login.js b/front-end/src/components/login.js deleted file mode 100644 index 9546695..0000000 --- a/front-end/src/components/login.js +++ /dev/null @@ -1,69 +0,0 @@ -import define from '../utils/define.js'; -import config from "../../config.js"; - -const template = () => /*html*/` -
-
Login
-
- -
- -
-
-
- -
- - -
-
-
- -
-
-`; - -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/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js deleted file mode 100644 index 5adf52b..0000000 --- a/front-end/src/components/organisms/search-bar.js +++ /dev/null @@ -1,184 +0,0 @@ -import define from '../../utils/define.js'; -import config from '../../../config.js'; -import { globalBus } from '../../utils/events.js'; -import debounce from '../../utils/debounce.js' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion)').matches; - -const template = () => /*html*/` - -`; - -export default define('search-bar', class extends HTMLElement { - constructor() { - super(); - this.searchInput = null; - this.searchForm = null; - this.abortController = new AbortController(); - this.__setup(); - } - - __setup() { - this.innerHTML = template(); - this.searchInput = this.querySelector('input'); - this.searchForm = this.querySelector('form'); - this.__events(); - } - - __dispatchSearch({ results = null, error = null }) { - const searchEvent = new CustomEvent('search', { - detail: { - results, - error, - }, - }); - globalBus.dispatch(searchEvent) - } - - /** - * Updates the overall layout of the page. - * - * `home` centers the search bar on the page. - * `compact` raises it to the top and makes room for displaying results. - * - * @param {'compact' | 'home'} mode - * @return {void} - */ - __setDisplayMode(mode) { - switch (mode) { - case 'compact': { - document.body.style.paddingTop = '25px'; - document.querySelector('.search-menu').classList.add('compact'); - break; - } - case 'home': { - document.body.style.paddingTop = '30vh'; - document.querySelector('.search-menu').classList.remove('compact'); - break; - } - } - } - - async __executeSearch() { - this.abortController.abort(); - this.abortController = new AbortController(); - // Get response from API - const response = await fetch(`${config.publicApiURL}search?s=${encodeURIComponent(this.searchInput.value)}`, { - signal: this.abortController.signal - }); - // Getting results from API - const search = await (response).json(); - return search; - } - - __handleSearch = async () => { - // Update page title - document.title = `MWMBL - ${this.searchInput.value || "Search"}`; - - // Update query params - const queryParams = new URLSearchParams(document.location.search); - // Sets query param if search value is not empty - if (this.searchInput.value) queryParams.set(config.searchQueryParam, this.searchInput.value); - else queryParams.delete(config.searchQueryParam); - // New URL with query params - const newURL = - document.location.protocol - + "//" - + document.location.host - + document.location.pathname - + (this.searchInput.value ? '?' : '') - + queryParams.toString(); - // Replace history state - window.history.replaceState({ path: newURL }, '', newURL); - - if (this.searchInput.value) { - this.__setDisplayMode('compact') - - try { - const search = await this.__executeSearch() - // This is a guess at an explanation - // Check the searcInput.value before setting the results to prevent - // race condition where the user has cleared the search input after - // submitting an original search but before the search results have - // come back from the API - this.__dispatchSearch({ results: this.searchInput.value ? search : null }); - } - catch(error) { - this.__dispatchSearch({ error }) - } - } - else { - this.__setDisplayMode('home') - this.__dispatchSearch({ results: null }); - } - } - - __events() { - /** - * Always add the submit event, it makes things feel faster if - * someone does not prefer reduced motion and reflexively hits - * return once they've finished typing. - */ - this.searchForm.addEventListener('submit', (e) => { - e.preventDefault(); - this.__handleSearch(e); - }); - - /** - * Only add the "real time" search behavior when the client does - * not prefer reduced motion; this prevents the page from changing - * while the user is still typing their query. - */ - if (!prefersReducedMotion) { - this.searchInput.addEventListener('input', debounce(this.__handleSearch, 500)) - } - - // Focus search bar when pressing `ctrl + k` or `/` - document.addEventListener('keydown', (e) => { - 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(); - } - }); - - // Focus first result when pressing down arrow - this.addEventListener('keydown', (e) => { - if (e.key === 'ArrowDown' && this.searchInput.value) { - e.preventDefault(); - const focusResultEvent = new CustomEvent('focus-result'); - globalBus.dispatch(focusResultEvent); - } - }); - - globalBus.on('focus-search', (e) => { - this.searchInput.focus(); - }); - } - - connectedCallback() { - // Focus search input when component is connected - this.searchInput.focus(); - - const searchQuery = new URLSearchParams(document.location.search).get(config.searchQueryParam); - this.searchInput.value = searchQuery; - /** - * Trigger search handling to coordinate the value pulled from the query string - * across the rest of the UI and to actually retrieve the results if the search - * value is now non-empty. - */ - this.__handleSearch(); - } -}); diff --git a/front-end/src/components/register.js b/front-end/src/components/register.js deleted file mode 100644 index ff5bfc0..0000000 --- a/front-end/src/components/register.js +++ /dev/null @@ -1,84 +0,0 @@ -import define from '../utils/define.js'; -import config from "../../config.js"; - -const template = () => /*html*/` -
-
Register
-
- -
- -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-`; - -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 a08e557..f0ae9a4 100644 --- a/front-end/src/index.html +++ b/front-end/src/index.html @@ -53,7 +53,7 @@
mwmbl logo

- Welcome to mwmbl, the free, open-source and non-profit search engine. + Welcome to Mwmbl, the free, open-source and non-profit search engine.

This website requires you to support/enable scripts.

@@ -72,10 +72,10 @@

-
    +
    mwmbl logo MWMBL diff --git a/front-end/src/index.js b/front-end/src/index.js index 34cd8cb..04c3087 100644 --- a/front-end/src/index.js +++ b/front-end/src/index.js @@ -14,9 +14,6 @@ if (!redirected) { // Load components only after redirects are checked. - import('./components/login.js'); - import('./components/register.js'); - import("./components/organisms/search-bar.js"); import("./components/organisms/results.js"); import("./components/organisms/footer.js"); import("./components/organisms/save.js"); diff --git a/front-end/vite.config.js b/front-end/vite.config.js index 6855f71..e50fc19 100644 --- a/front-end/vite.config.js +++ b/front-end/vite.config.js @@ -5,7 +5,8 @@ export default { base: '/static', publicDir: '../assets', build: { - outDir: '../dist' + outDir: '../dist', + minify: false }, plugins: [ legacy({ From 5dbe792579764d7f06f440847581b00e4fddc847 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 21:20:48 +0000 Subject: [PATCH 03/20] Update title in response --- front-end/src/components/organisms/results.js | 6 - .../src/components/organisms/search-bar.js | 180 ++++++++++++++++++ mwmbl/templates/results.html | 1 + mwmbl/views.py | 7 +- 4 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 front-end/src/components/organisms/search-bar.js diff --git a/front-end/src/components/organisms/results.js b/front-end/src/components/organisms/results.js index 7a0e6b4..f699f3f 100644 --- a/front-end/src/components/organisms/results.js +++ b/front-end/src/components/organisms/results.js @@ -1,11 +1,5 @@ import {globalBus} from '../../utils/events.js'; - -document.body.addEventListener('htmx:load', function(evt) { - -}); - - class ResultsHandler { constructor() { this.results = null; diff --git a/front-end/src/components/organisms/search-bar.js b/front-end/src/components/organisms/search-bar.js new file mode 100644 index 0000000..f700fda --- /dev/null +++ b/front-end/src/components/organisms/search-bar.js @@ -0,0 +1,180 @@ +import define from '../../utils/define.js'; +import config from '../../../config.js'; +import { globalBus } from '../../utils/events.js'; +import debounce from '../../utils/debounce.js' + +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion)').matches; + +const template = () => /*html*/` + +`; + +export default define('search-bar', class extends HTMLElement { + constructor() { + super(); + this.searchInput = null; + this.searchForm = null; + this.abortController = new AbortController(); + this.__setup(); + } + + __setup() { + this.innerHTML = template(); + this.searchInput = this.querySelector('input'); + this.searchForm = this.querySelector('form'); + this.__events(); + } + + __dispatchSearch({ results = null, error = null }) { + const searchEvent = new CustomEvent('search', { + detail: { + results, + error, + }, + }); + globalBus.dispatch(searchEvent) + } + + /** + * Updates the overall layout of the page. + * + * `home` centers the search bar on the page. + * `compact` raises it to the top and makes room for displaying results. + * + * @param {'compact' | 'home'} mode + * @return {void} + */ + __setDisplayMode(mode) { + switch (mode) { + case 'compact': { + document.body.style.paddingTop = '25px'; + document.querySelector('.search-menu').classList.add('compact'); + break; + } + case 'home': { + document.body.style.paddingTop = '30vh'; + document.querySelector('.search-menu').classList.remove('compact'); + break; + } + } + } + + async __executeSearch() { + this.abortController.abort(); + this.abortController = new AbortController(); + // Get response from API + const response = await fetch(`${config.publicApiURL}search?s=${encodeURIComponent(this.searchInput.value)}`, { + signal: this.abortController.signal + }); + // Getting results from API + const search = await (response).json(); + return search; + } + + __handleSearch = async () => { + // Update page title + document.title = `MWMBL - ${this.searchInput.value || "Search"}`; + + // Update query params + const queryParams = new URLSearchParams(document.location.search); + // Sets query param if search value is not empty + if (this.searchInput.value) queryParams.set(config.searchQueryParam, this.searchInput.value); + else queryParams.delete(config.searchQueryParam); + // New URL with query params + const newURL = + document.location.protocol + + "//" + + document.location.host + + document.location.pathname + + (this.searchInput.value ? '?' : '') + + queryParams.toString(); + // Replace history state + window.history.replaceState({ path: newURL }, '', newURL); + + if (this.searchInput.value) { + this.__setDisplayMode('compact') + + try { + const search = await this.__executeSearch() + // This is a guess at an explanation + // Check the searcInput.value before setting the results to prevent + // race condition where the user has cleared the search input after + // submitting an original search but before the search results have + // come back from the API + this.__dispatchSearch({ results: this.searchInput.value ? search : null }); + } + catch(error) { + this.__dispatchSearch({ error }) + } + } + else { + this.__setDisplayMode('home') + this.__dispatchSearch({ results: null }); + } + } + + __events() { + /** + * Always add the submit event, it makes things feel faster if + * someone does not prefer reduced motion and reflexively hits + * return once they've finished typing. + */ + this.searchForm.addEventListener('submit', (e) => { + e.preventDefault(); + this.__handleSearch(e); + }); + + /** + * Only add the "real time" search behavior when the client does + * not prefer reduced motion; this prevents the page from changing + * while the user is still typing their query. + */ + if (!prefersReducedMotion) { + this.searchInput.addEventListener('input', debounce(this.__handleSearch, 500)) + } + + // Focus search bar when pressing `ctrl + k` or `/` + document.addEventListener('keydown', (e) => { + if ((e.key === 'k' && e.ctrlKey) || e.key === '/' || e.key === 'Escape') { + e.preventDefault(); + this.searchInput.focus(); + } + }); + + // Focus first result when pressing down arrow + this.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown' && this.searchInput.value) { + e.preventDefault(); + const focusResultEvent = new CustomEvent('focus-result'); + globalBus.dispatch(focusResultEvent); + } + }); + + globalBus.on('focus-search', (e) => { + this.searchInput.focus(); + }); + } + + connectedCallback() { + // Focus search input when component is connected + this.searchInput.focus(); + + const searchQuery = new URLSearchParams(document.location.search).get(config.searchQueryParam); + this.searchInput.value = searchQuery; + /** + * Trigger search handling to coordinate the value pulled from the query string + * across the rest of the UI and to actually retrieve the results if the search + * value is now non-empty. + */ + this.__handleSearch(); + } +}); diff --git a/mwmbl/templates/results.html b/mwmbl/templates/results.html index 11fa224..869db07 100644 --- a/mwmbl/templates/results.html +++ b/mwmbl/templates/results.html @@ -1,4 +1,5 @@ {% load result_filters %} +Mwmbl - {{ query }} {% for result in results %}
  • diff --git a/mwmbl/views.py b/mwmbl/views.py index a3fb215..0958053 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -48,7 +48,7 @@ def profile(request): def search_results(request): query = request.GET["query"] results = ranker.search(query) - return render(request, "results.html", {"results": results}) + return render(request, "results.html", {"results": results, "query": query}) def fetch_url(request): @@ -63,4 +63,7 @@ def fetch_url(request): extract = extract[:NUM_EXTRACT_CHARS - 1] + '…' result = Document(title=title, url=url, extract=extract, score=0.0) - return render(request, "results.html", {"results": [format_result(result, query)]}) + return render(request, "results.html", { + "results": [format_result(result, query)], + "query": query, + }) From 8293a7afa4012f2a7ad040fed924dda93030d0f6 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 21:45:13 +0000 Subject: [PATCH 04/20] Update query string --- mwmbl/settings_common.py | 3 +- mwmbl/views.py | 9 ++- poetry.lock | 121 ++++++++------------------------------- pyproject.toml | 1 + 4 files changed, 36 insertions(+), 98 deletions(-) diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py index a71ac6d..e75ac86 100644 --- a/mwmbl/settings_common.py +++ b/mwmbl/settings_common.py @@ -30,7 +30,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'mwmbl', - + "django_htmx", 'allauth', 'allauth.account', 'allauth.socialaccount', @@ -45,6 +45,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django_htmx.middleware.HtmxMiddleware", "allauth.account.middleware.AccountMiddleware", ] diff --git a/mwmbl/views.py b/mwmbl/views.py index 0958053..0f1e5a8 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -2,6 +2,7 @@ import justext import requests from django.contrib.auth.decorators import login_required from django.shortcuts import render +from django_htmx.http import push_url from mwmbl.format import format_result from mwmbl.search_setup import ranker @@ -48,7 +49,13 @@ def profile(request): def search_results(request): query = request.GET["query"] results = ranker.search(query) - return render(request, "results.html", {"results": results, "query": query}) + rendered = render(request, "results.html", {"results": results, "query": query}) + current_url = request.htmx.current_url + # Replace query string with new query + stripped_url = current_url[:current_url.index("?")] if "?" in current_url else current_url + query_string = "?q=" + query if len(query) > 0 else "" + new_url = stripped_url + query_string + return push_url(rendered, new_url) def fetch_url(request): diff --git a/poetry.lock b/poetry.lock index 0808bb5..26b5ed3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "anyio" version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -26,7 +25,6 @@ trio = ["trio (<0.22)"] name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -44,7 +42,6 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -56,7 +53,6 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -75,7 +71,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "beautifulsoup4" version = "4.10.0" description = "Screen-scraping library" -category = "main" optional = true python-versions = ">3.0.0" files = [ @@ -94,7 +89,6 @@ lxml = ["lxml"] name = "blis" version = "0.7.11" description = "The Blis BLAS-like linear algebra library, as a self-contained C-extension." -category = "main" optional = false python-versions = "*" files = [ @@ -141,7 +135,6 @@ numpy = {version = ">=1.19.0", markers = "python_version >= \"3.9\""} name = "boto3" version = "1.28.62" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -161,7 +154,6 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "botocore" version = "1.31.62" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -181,7 +173,6 @@ crt = ["awscrt (==0.16.26)"] name = "catalogue" version = "2.0.10" description = "Super lightweight function registries for your library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -193,7 +184,6 @@ files = [ name = "cattrs" version = "23.1.2" description = "Composable complex class support for attrs and dataclasses." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -219,7 +209,6 @@ ujson = ["ujson (>=5.4.0,<6.0.0)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -231,7 +220,6 @@ files = [ name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -296,7 +284,6 @@ pycparser = "*" name = "charset-normalizer" version = "3.3.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -396,7 +383,6 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -411,7 +397,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -423,7 +408,6 @@ files = [ 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 = [ @@ -469,7 +453,6 @@ test-randomorder = ["pytest-randomly"] name = "cymem" version = "2.0.8" description = "Manage calls to calloc/free through Cython" -category = "main" optional = false python-versions = "*" files = [ @@ -512,7 +495,6 @@ files = [ 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 = [ @@ -524,7 +506,6 @@ files = [ name = "dj-database-url" version = "2.1.0" description = "Use Database URLs in your Django Application." -category = "main" optional = false python-versions = "*" files = [ @@ -540,7 +521,6 @@ typing-extensions = ">=3.10.0.0" name = "django" version = "4.2.6" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -561,7 +541,6 @@ bcrypt = ["bcrypt"] 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 = [ @@ -579,11 +558,24 @@ requests-oauthlib = ">=0.3.0" mfa = ["qrcode (>=7.0.0)"] saml = ["python3-saml (>=1.15.0,<2.0.0)"] +[[package]] +name = "django-htmx" +version = "1.17.0" +description = "Extensions for using Django with htmx." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_htmx-1.17.0-py3-none-any.whl", hash = "sha256:070a37092b88a42cd7af26c1b65f63c4529bae276710fd16137dc934938b44f2"}, + {file = "django_htmx-1.17.0.tar.gz", hash = "sha256:2ef0d19db41c6152881e782673cd2cd1755a7fd6784f8b4f2279fb18dc03d2c2"}, +] + +[package.dependencies] +Django = ">=3.2" + [[package]] name = "django-ninja" version = "0.22.2" description = "Django Ninja - Fast Django REST framework" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -604,7 +596,6 @@ test = ["black", "django-stubs", "flake8", "isort", "mypy (==0.931)", "psycopg2- name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -619,7 +610,6 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.70.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -641,7 +631,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==21.9b0)", "databases[sqlite] ( name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -653,7 +642,6 @@ files = [ name = "hiredis" version = "2.2.3" description = "Python wrapper for hiredis" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -752,7 +740,6 @@ files = [ name = "idna" version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -764,7 +751,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -776,7 +762,6 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -794,7 +779,6 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -806,7 +790,6 @@ files = [ name = "joblib" version = "1.3.2" description = "Lightweight pipelining with Python functions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -818,7 +801,6 @@ files = [ name = "justext" version = "3.0.0" description = "Heuristic based boilerplate removal tool" -category = "main" optional = false python-versions = "*" files = [ @@ -833,7 +815,6 @@ lxml = ">=4.4.2" name = "langcodes" version = "3.3.0" description = "Tools for labeling human languages with IETF language tags" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -848,7 +829,6 @@ data = ["language-data (>=1.1,<2.0)"] name = "langdetect" version = "1.0.9" description = "Language detection library ported from Google's language-detection." -category = "main" optional = true python-versions = "*" files = [ @@ -863,7 +843,6 @@ six = "*" name = "levenshtein" version = "0.16.0" description = "Python extension for computing string edit distances and similarities." -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -928,7 +907,6 @@ rapidfuzz = ">=1.8.2,<1.9" name = "lxml" version = "4.6.4" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -1004,7 +982,6 @@ source = ["Cython (>=0.29.7)"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1074,7 +1051,6 @@ files = [ name = "mmh3" version = "3.1.0" description = "Python wrapper for MurmurHash (MurmurHash3), a set of fast and robust hash functions." -category = "main" optional = false python-versions = "*" files = [ @@ -1119,7 +1095,6 @@ files = [ name = "murmurhash" version = "1.0.10" description = "Cython bindings for MurmurHash" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1162,7 +1137,6 @@ files = [ name = "numpy" version = "1.26.0" description = "Fundamental package for array computing in Python" -category = "main" optional = false python-versions = "<3.13,>=3.9" files = [ @@ -1204,7 +1178,6 @@ files = [ 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 = [ @@ -1221,7 +1194,6 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1233,7 +1205,6 @@ files = [ name = "pandas" version = "1.5.3" description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1278,7 +1249,6 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] name = "pathy" version = "0.10.2" description = "pathlib.Path subclasses for local and cloud bucket storage" -category = "main" optional = false python-versions = ">= 3.6" files = [ @@ -1301,7 +1271,6 @@ test = ["mock", "pytest", "pytest-coverage", "typer-cli"] name = "platformdirs" version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1317,7 +1286,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1333,7 +1301,6 @@ testing = ["pytest", "pytest-benchmark"] name = "preshed" version = "3.0.9" description = "Cython hash table that trusts the keys are pre-hashed" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1380,7 +1347,6 @@ murmurhash = ">=0.28.0,<1.1.0" name = "psycopg2-binary" version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1410,6 +1376,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -1418,6 +1385,8 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -1459,7 +1428,6 @@ files = [ name = "py4j" version = "0.10.9.2" description = "Enables Python programs to dynamically access arbitrary Java objects" -category = "main" optional = true python-versions = "*" files = [ @@ -1471,7 +1439,6 @@ files = [ name = "pyarrow" version = "6.0.0" description = "Python library for Apache Arrow" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1520,7 +1487,6 @@ numpy = ">=1.16.6" name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1532,7 +1498,6 @@ files = [ name = "pydantic" version = "1.8.2" description = "Data validation and settings management using python 3.6 type hinting" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -1571,7 +1536,6 @@ email = ["email-validator (>=1.0.3)"] name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1592,7 +1556,6 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyspark" version = "3.2.0" description = "Apache Spark Python API" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1612,7 +1575,6 @@ sql = ["pandas (>=0.23.2)", "pyarrow (>=1.0.0)"] name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1635,7 +1597,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1653,7 +1614,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1668,7 +1628,6 @@ six = ">=1.5" name = "python3-openid" version = "3.2.0" description = "OpenID support for modern servers and consumers." -category = "main" optional = false python-versions = "*" files = [ @@ -1687,7 +1646,6 @@ postgresql = ["psycopg2"] name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -1699,7 +1657,6 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1749,7 +1706,6 @@ files = [ name = "rapidfuzz" version = "1.8.3" description = "rapid fuzzy string matching" -category = "main" optional = true python-versions = ">=2.7" files = [ @@ -1816,7 +1772,6 @@ full = ["numpy"] name = "redis" version = "5.0.1" description = "Python client for Redis database and key-value store" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1836,7 +1791,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1858,7 +1812,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-cache" version = "1.1.0" description = "A persistent cache for python requests" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1889,7 +1842,6 @@ yaml = ["pyyaml (>=5.4)"] 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 = [ @@ -1908,7 +1860,6 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] name = "s3transfer" version = "0.7.0" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -1926,7 +1877,6 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] name = "scikit-learn" version = "1.3.1" description = "A set of python modules for machine learning and data mining" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1941,6 +1891,11 @@ files = [ {file = "scikit_learn-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f66eddfda9d45dd6cadcd706b65669ce1df84b8549875691b1f403730bdef217"}, {file = "scikit_learn-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6448c37741145b241eeac617028ba6ec2119e1339b1385c9720dae31367f2be"}, {file = "scikit_learn-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c413c2c850241998168bbb3bd1bb59ff03b1195a53864f0b80ab092071af6028"}, + {file = "scikit_learn-1.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ef540e09873e31569bc8b02c8a9f745ee04d8e1263255a15c9969f6f5caa627f"}, + {file = "scikit_learn-1.3.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9147a3a4df4d401e618713880be023e36109c85d8569b3bf5377e6cd3fecdeac"}, + {file = "scikit_learn-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2cd3634695ad192bf71645702b3df498bd1e246fc2d529effdb45a06ab028b4"}, + {file = "scikit_learn-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c275a06c5190c5ce00af0acbb61c06374087949f643ef32d355ece12c4db043"}, + {file = "scikit_learn-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:0e1aa8f206d0de814b81b41d60c1ce31f7f2c7354597af38fae46d9c47c45122"}, {file = "scikit_learn-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:52b77cc08bd555969ec5150788ed50276f5ef83abb72e6f469c5b91a0009bbca"}, {file = "scikit_learn-1.3.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a683394bc3f80b7c312c27f9b14ebea7766b1f0a34faf1a2e9158d80e860ec26"}, {file = "scikit_learn-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15d964d9eb181c79c190d3dbc2fff7338786bf017e9039571418a1d53dab236"}, @@ -1969,7 +1924,6 @@ tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc ( name = "scipy" version = "1.11.3" description = "Fundamental algorithms for scientific computing in Python" -category = "main" optional = false python-versions = "<3.13,>=3.9" files = [ @@ -2012,7 +1966,6 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo name = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2029,7 +1982,6 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2041,7 +1993,6 @@ files = [ name = "smart-open" version = "6.4.0" description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" -category = "main" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -2063,7 +2014,6 @@ webhdfs = ["requests"] name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2075,7 +2025,6 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = true python-versions = ">=3.8" files = [ @@ -2087,7 +2036,6 @@ files = [ name = "spacy" version = "3.2.1" description = "Industrial-strength Natural Language Processing (NLP) in Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2157,7 +2105,6 @@ transformers = ["spacy-transformers (>=1.1.2,<1.2.0)"] name = "spacy-legacy" version = "3.0.12" description = "Legacy registered functions for spaCy backwards compatibility" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2169,7 +2116,6 @@ files = [ name = "spacy-loggers" version = "1.0.5" description = "Logging utilities for SpaCy" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2181,7 +2127,6 @@ files = [ name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2198,7 +2143,6 @@ test = ["pytest", "pytest-cov"] name = "srsly" version = "2.4.8" description = "Modern high-performance serialization utilities for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2245,7 +2189,6 @@ catalogue = ">=2.0.3,<2.1.0" name = "starlette" version = "0.16.0" description = "The little ASGI library that shines." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2263,7 +2206,6 @@ full = ["graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "req name = "thinc" version = "8.0.17" description = "A refreshing functional take on deep learning, compatible with your favorite libraries" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2329,7 +2271,6 @@ torch = ["torch (>=1.6.0)"] name = "threadpoolctl" version = "3.2.0" description = "threadpoolctl" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2341,7 +2282,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2353,7 +2293,6 @@ files = [ name = "tqdm" version = "4.66.1" description = "Fast, Extensible Progress Meter" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2374,7 +2313,6 @@ telegram = ["requests"] name = "typer" version = "0.4.2" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2395,7 +2333,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6. name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2407,7 +2344,6 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ @@ -2419,7 +2355,6 @@ files = [ name = "ujson" version = "4.3.0" description = "Ultra fast JSON encoder and decoder for Python" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -2473,7 +2408,6 @@ files = [ name = "url-normalize" version = "1.4.3" description = "URL normalization for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -2488,7 +2422,6 @@ six = "*" name = "urllib3" version = "2.0.6" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2506,7 +2439,6 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.16.0" description = "The lightning-fast ASGI server." -category = "main" optional = false python-versions = "*" files = [ @@ -2526,7 +2458,6 @@ standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.2.0,<0.4.0)", name = "warcio" version = "1.7.4" description = "Streaming WARC (and ARC) IO library" -category = "main" optional = true python-versions = "*" files = [ @@ -2541,7 +2472,6 @@ six = "*" name = "wasabi" version = "0.10.1" description = "A lightweight console printing and formatting toolkit" -category = "main" optional = false python-versions = "*" files = [ @@ -2553,7 +2483,6 @@ files = [ name = "zstandard" version = "0.16.0" description = "Zstandard bindings for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2610,9 +2539,9 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -indexer = ["ujson", "warcio", "idna", "beautifulsoup4", "lxml", "langdetect", "pyarrow", "pyspark", "Levenshtein"] +indexer = ["Levenshtein", "beautifulsoup4", "idna", "langdetect", "lxml", "pyarrow", "pyspark", "ujson", "warcio"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "37c79d582b976c81d731ea9bac38911f8cf578ae72fe715e23ab7d1236712f81" +content-hash = "88ebe68ab5d0445ed67c07723156e622ebe10dbc50381b4982741f1404c0e8ce" diff --git a/pyproject.toml b/pyproject.toml index 548dacf..0594d23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ requests-cache = "^1.1.0" redis = {extras = ["hiredis"], version = "^5.0.1"} django-allauth = "^0.57.0" dj-database-url = "^2.1.0" +django-htmx = "^1.17.0" [tool.poetry.extras] indexer = [ From e3371233b5e56c38264442c8559ffe6eeba616d3 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Sun, 5 Nov 2023 21:52:09 +0000 Subject: [PATCH 05/20] Use replace header instead of push --- mwmbl/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mwmbl/views.py b/mwmbl/views.py index 0f1e5a8..f030c0c 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -49,13 +49,15 @@ def profile(request): def search_results(request): query = request.GET["query"] results = ranker.search(query) - rendered = render(request, "results.html", {"results": results, "query": query}) + response = render(request, "results.html", {"results": results, "query": query}) current_url = request.htmx.current_url # Replace query string with new query stripped_url = current_url[:current_url.index("?")] if "?" in current_url else current_url query_string = "?q=" + query if len(query) > 0 else "" new_url = stripped_url + query_string - return push_url(rendered, new_url) + # Set the htmx replace header + response["HX-Replace-Url"] = new_url + return response def fetch_url(request): From 9933a529c3965cedc8307002986dec3c05960388 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Mon, 6 Nov 2023 18:09:54 +0000 Subject: [PATCH 06/20] Work without JS --- {front-end/src => mwmbl/templates}/index.html | 54 ++++++++----------- mwmbl/urls.py | 7 +-- mwmbl/views.py | 8 ++- 3 files changed, 32 insertions(+), 37 deletions(-) rename {front-end/src => mwmbl/templates}/index.html (61%) diff --git a/front-end/src/index.html b/mwmbl/templates/index.html similarity index 61% rename from front-end/src/index.html rename to mwmbl/templates/index.html index f0ae9a4..8c68729 100644 --- a/front-end/src/index.html +++ b/mwmbl/templates/index.html @@ -8,34 +8,38 @@ - MWMBL - Search + {% if query %} + Mwmbl - {{ query }} + {% else %} + Mwmbl - Search + {% endif %} - + - + - - - + + + - + - + @@ -47,30 +51,11 @@ - - + -
    - +
    - - - + {##}
    {##} -
    +
    -
    - mwmbl logo - MWMBL -
    + +
    + mwmbl logo + Mwmbl +
    +
    +
  • + {% endfor %} + {% else %} +
  • +

    + No results found for "{{query}}". +

  • - {% endfor %} + {% endif %} {% else %}
  • From dc2bd082cf51f8fe75ea17ca282f06afc45af348 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Wed, 8 Nov 2023 09:59:29 +0000 Subject: [PATCH 18/20] Show activity list when there are no queries --- mwmbl/settings_common.py | 1 + mwmbl/templates/home.html | 13 +++++---- mwmbl/views.py | 55 +++++++++++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py index 38a890e..223330e 100644 --- a/mwmbl/settings_common.py +++ b/mwmbl/settings_common.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.humanize', 'mwmbl', 'django_htmx', 'django_vite', diff --git a/mwmbl/templates/home.html b/mwmbl/templates/home.html index c78d05b..b15e047 100644 --- a/mwmbl/templates/home.html +++ b/mwmbl/templates/home.html @@ -1,4 +1,5 @@ {% load result_filters %} +{% load humanize %} {% include "title.html" %} {% if query %} {% if results %} @@ -28,9 +29,11 @@

  • {% endif %} {% else %} -
  • -

    - Welcome to Mwmbl, the free, open-source and non-profit search engine. -

    -
  • + {% for item in activity %} +
  • +

    + {{ item.user }} made {{ item.num_curations | apnumber }} changes to {{ item.query }} {{ item.timestamp | naturaltime }}. +

    +
  • + {% endfor %} {% endif %} diff --git a/mwmbl/views.py b/mwmbl/views.py index 6139441..f555b95 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -1,3 +1,8 @@ +from dataclasses import dataclass +from datetime import datetime +from itertools import groupby +from urllib.parse import urlparse, parse_qs + import justext import requests from django.contrib.auth.decorators import login_required @@ -5,6 +10,7 @@ from django.shortcuts import render from django_htmx.http import push_url from mwmbl.format import format_result +from mwmbl.models import UserCuration, MwmblUser from mwmbl.search_setup import ranker from justext.core import html_to_dom, ParagraphMaker, classify_paragraphs, revise_paragraph_classification, \ @@ -42,19 +48,22 @@ def justext_with_dom(html_text, stoplist, length_low=LENGTH_LOW_DEFAULT, def index(request): - query = request.GET.get("q") - results = ranker.search(query) if query else None + activity, query, results = _get_results_and_activity(request) return render(request, "index.html", { "results": results, "query": query, "user": request.user, + "activity": activity, }) def home_fragment(request): - query = request.GET["q"] - results = ranker.search(query) - response = render(request, "home.html", {"results": results, "query": query}) + activity, query, results = _get_results_and_activity(request) + response = render(request, "home.html", { + "results": results, + "query": query, + "activity": activity, + }) current_url = request.htmx.current_url # Replace query string with new query stripped_url = current_url[:current_url.index("?")] if "?" in current_url else current_url @@ -65,6 +74,42 @@ def home_fragment(request): return response +@dataclass +class Activity: + user: MwmblUser + num_curations: int + timestamp: datetime + query: str + url: str + + +def _get_results_and_activity(request): + query = request.GET.get("q") + if query: + results = ranker.search(query) + activity = None + else: + results = None + curations = UserCuration.objects.order_by("-timestamp")[:100] + sorted_curations = sorted(curations, key=lambda x: x.user.username) + groups = groupby(sorted_curations, key=lambda x: (x.user.username, x.url)) + activity = [] + for (user, url), group in groups: + parsed_url_query = parse_qs(urlparse(url).query) + activity_query = parsed_url_query.get("q", [""])[0] + group = list(group) + activity.append(Activity( + user=user, + num_curations=len(group), + timestamp=max([i.timestamp for i in group]), + query=activity_query, + url=url, + )) + + print("Activity", activity) + return activity, query, results + + def fetch_url(request): url = request.GET["url"] query = request.GET["query"] From c0605e6bf72ab07e71aeef8403a0d5b9d614a5f5 Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Wed, 8 Nov 2023 12:49:17 +0000 Subject: [PATCH 19/20] Redo footer --- front-end/assets/css/global.css | 2 +- front-end/assets/fonts/phosphor/icons.css | 18 +++++++++- front-end/assets/fonts/phosphor/unused.css | 16 --------- front-end/config.js | 12 ------- front-end/src/components/organisms/footer.js | 36 -------------------- front-end/src/index.js | 1 - mwmbl/settings_common.py | 30 ++++++++++++++++ mwmbl/templates/index.html | 13 ++++++- mwmbl/views.py | 2 ++ 9 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 front-end/src/components/organisms/footer.js diff --git a/front-end/assets/css/global.css b/front-end/assets/css/global.css index f682477..9d817a2 100644 --- a/front-end/assets/css/global.css +++ b/front-end/assets/css/global.css @@ -159,7 +159,7 @@ mwmbl-results, footer { font-weight: var(--bold-font-weight); } -footer { +.footer { position: sticky; top: 100vh; margin-bottom: 25px; diff --git a/front-end/assets/fonts/phosphor/icons.css b/front-end/assets/fonts/phosphor/icons.css index b9c4e82..42398b9 100644 --- a/front-end/assets/fonts/phosphor/icons.css +++ b/front-end/assets/fonts/phosphor/icons.css @@ -103,4 +103,20 @@ Phosphor Web Font .ph-info-bold::before { content: "\f88f"; -} \ No newline at end of file +} + +.ph-book-bold::before { + content: "\f6fb"; +} + +.ph-browser-bold::before { + content: "\f70d"; +} + +.ph-youtube-logo-bold::before { + content: "\fa5d"; +} + +.ph-chat-circle-text-bold::before { + content: "\f74c"; +} diff --git a/front-end/assets/fonts/phosphor/unused.css b/front-end/assets/fonts/phosphor/unused.css index 16da7b4..024afd1 100644 --- a/front-end/assets/fonts/phosphor/unused.css +++ b/front-end/assets/fonts/phosphor/unused.css @@ -13290,10 +13290,6 @@ content: "\f6fa"; } -.ph-book-bold::before { - content: "\f6fb"; -} - .ph-book-bookmark-bold::before { content: "\f6fc"; } @@ -13362,10 +13358,6 @@ content: "\f70c"; } -.ph-browser-bold::before { - content: "\f70d"; -} - .ph-browsers-bold::before { content: "\f70e"; } @@ -13614,10 +13606,6 @@ content: "\f74b"; } -.ph-chat-circle-text-bold::before { - content: "\f74c"; -} - .ph-chat-dots-bold::before { content: "\f74d"; } @@ -16750,10 +16738,6 @@ content: "\fa5c"; } -.ph-youtube-logo-bold::before { - content: "\fa5d"; -} - .ph-activity-fill::before { content: "\fa5e"; } diff --git a/front-end/config.js b/front-end/config.js index e7abff4..0769191 100644 --- a/front-end/config.js +++ b/front-end/config.js @@ -11,18 +11,6 @@ export default { publicApiURL: '/api/v1/', // publicApiURL: 'http://localhost:5000/', searchQueryParam: 'q', - footerLinks: [ - { - name: 'Github', - icon: 'ph-github-logo-bold', - href: 'https://github.com/mwmbl/mwmbl' - }, - { - name: 'Wiki', - icon: 'ph-info-bold', - href: 'https://github.com/mwmbl/mwmbl/wiki' - } - ], commands: { 'go: ': 'https://', 'search: google.com ': 'https://www.google.com/search?q=', diff --git a/front-end/src/components/organisms/footer.js b/front-end/src/components/organisms/footer.js deleted file mode 100644 index 4b8d62f..0000000 --- a/front-end/src/components/organisms/footer.js +++ /dev/null @@ -1,36 +0,0 @@ -import define from '../../utils/define.js'; -import config from '../../../config.js'; - -const template = ({ data }) => /*html*/` - - -`; - -export default define('footer', class extends HTMLElement { - constructor() { - super(); - this.__setup(); - } - - __setup() { - this.innerHTML = template({ - data: { - links: config.footerLinks - } - }); - this.__events(); - } - - __events() { - - } -}, { extends: 'footer' }); \ No newline at end of file diff --git a/front-end/src/index.js b/front-end/src/index.js index e982c79..027f34c 100644 --- a/front-end/src/index.js +++ b/front-end/src/index.js @@ -16,7 +16,6 @@ import 'vite/modulepreload-polyfill'; if (!redirected) { // Load components only after redirects are checked. import("./components/organisms/results.js"); - import("./components/organisms/footer.js"); import("./components/organisms/save.js"); import("./components/molecules/add-button.js"); import("./components/molecules/add-result.js"); diff --git a/mwmbl/settings_common.py b/mwmbl/settings_common.py index 223330e..c0a7436 100644 --- a/mwmbl/settings_common.py +++ b/mwmbl/settings_common.py @@ -134,3 +134,33 @@ ACCOUNT_EMAIL_VERIFICATION = "mandatory" DEFAULT_FROM_EMAIL = "admin@mwmbl.org" LOGIN_REDIRECT_URL = "/" + +FOOTER_LINKS = [ + { + "name": "Matrix", + "icon": "ph-chat-circle-text-bold", + "href": "https://matrix.to/#/#mwmbl:matrix.org", + }, + { + "name": "Book", + "icon": "ph-book-bold", + "href": "https://book.mwmbl.org", + }, + { + "name": "Blog", + "icon": "ph-browser-bold", + "href": "https://blog.mwmbl.org", + }, + { + "name": "GitHub", + "icon": "ph-github-logo-bold", + "href": "https://github.com/mwmbl/mwmbl", + }, + { + "name": "YouTube", + "icon": "ph-youtube-logo-bold", + "href": "https://www.youtube.com/channel/UCFLbqrH63-icAHxQ1eFfAvA", + }, + + +] diff --git a/mwmbl/templates/index.html b/mwmbl/templates/index.html index 9aa3b57..b098809 100644 --- a/mwmbl/templates/index.html +++ b/mwmbl/templates/index.html @@ -86,7 +86,18 @@
-
+ {% vite_asset 'index.js' %} {% vite_legacy_polyfills %} {% vite_legacy_asset 'index-legacy.js' %} diff --git a/mwmbl/views.py b/mwmbl/views.py index f555b95..0b3daba 100644 --- a/mwmbl/views.py +++ b/mwmbl/views.py @@ -19,6 +19,7 @@ from justext.core import html_to_dom, ParagraphMaker, classify_paragraphs, revis from mwmbl.settings import NUM_EXTRACT_CHARS from mwmbl.tinysearchengine.indexer import Document +from django.conf import settings def justext_with_dom(html_text, stoplist, length_low=LENGTH_LOW_DEFAULT, @@ -54,6 +55,7 @@ def index(request): "query": query, "user": request.user, "activity": activity, + "footer_links": settings.FOOTER_LINKS, }) From 463a1178d042fc8c19786fb5239505c4eb491a0b Mon Sep 17 00:00:00 2001 From: Daoud Clarke Date: Wed, 8 Nov 2023 13:08:22 +0000 Subject: [PATCH 20/20] Root now served by django --- nginx.conf.sigil | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nginx.conf.sigil b/nginx.conf.sigil index 89fa102..48a4ca7 100644 --- a/nginx.conf.sigil +++ b/nginx.conf.sigil @@ -103,12 +103,7 @@ server { alias /var/lib/dokku/data/storage/mwmbl-beta/; } - ## Root and stats served statically - location = / { - root /var/lib/dokku/data/storage/mwmbl-beta; - try_files /index.html =404; - } - + ## Stats served statically location ~ ^\/stats\/?$ { root /var/lib/dokku/data/storage/mwmbl-beta; try_files /stats/index.html =404;