diff --git a/.gitignore b/.gitignore index e5651bb7..dbe81f32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -db.sqlite3 +*.sqlite3 *.log *.pyc *.mo diff --git a/antispam/__init__.py b/antispam/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/antispam/admin.py b/antispam/admin.py new file mode 100644 index 00000000..19dd817a --- /dev/null +++ b/antispam/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from antispam.models import ToxicDomain + + +@admin.register(ToxicDomain) +class ToxicDomainAdmin(admin.ModelAdmin): + list_display = ("domain", "is_externally_managed", "created") + search_fields = ("domain", "is_externally_managed", "created") + list_filter = ("is_externally_managed",) diff --git a/antispam/apps.py b/antispam/apps.py new file mode 100644 index 00000000..8a4857d1 --- /dev/null +++ b/antispam/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AntispamConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + verbose_name = "antispam" + name = "antispam" diff --git a/antispam/forms.py b/antispam/forms.py new file mode 100644 index 00000000..a2f97124 --- /dev/null +++ b/antispam/forms.py @@ -0,0 +1,18 @@ +import re + +from django import forms +from django.core.validators import EmailValidator +from django.utils.translation import gettext_lazy as _ + +from antispam.models import ToxicDomain + + +class AntiSpamEmailField(forms.EmailField): + """An email field that email addresses with a known toxic domain.""" + + def run_validators(self, value: str): + super().run_validators(value) + # Domain part should exist since email validation is guaranteed to run first + domain = re.search(EmailValidator.domain_regex, value) + if ToxicDomain.objects.filter(domain=domain[0]).exists(): + raise forms.ValidationError(_("Email domain is not allowed.")) diff --git a/antispam/management/commands/__init__.py b/antispam/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/antispam/management/commands/update_spam_database.py b/antispam/management/commands/update_spam_database.py new file mode 100644 index 00000000..4e9d0ec8 --- /dev/null +++ b/antispam/management/commands/update_spam_database.py @@ -0,0 +1,69 @@ +import requests +from django.conf import settings +from django.core.management import BaseCommand +from django.db.models import Max +from django.utils import timezone + +from antispam.models import ToxicDomain + + +class Command(BaseCommand): + """Update blocked ips/mails database""" + + help = "Update blocked ips/mails database" + + def add_arguments(self, parser): + parser.add_argument( + "--force", action="store_true", help="Force re-creation even if up to date" + ) + + def _should_update(self, *, force: bool = False) -> bool: + if force: + return True + oldest = ToxicDomain.objects.filter(is_externally_managed=True).aggregate( + res=Max("created") + )["res"] + return not (oldest and timezone.now() < (oldest + timezone.timedelta(days=1))) + + def _download_domains(self, providers: list[str]) -> set[str]: + domains = set() + for provider in providers: + res = requests.get(provider) + if not res.ok: + self.stderr.write( + f"Source {provider} responded with code {res.status_code}" + ) + continue + domains |= set(res.content.decode().splitlines()) + return domains + + def _update_domains(self, domains: set[str]): + # Cleanup database + ToxicDomain.objects.filter(is_externally_managed=True).delete() + + # Create database + ToxicDomain.objects.bulk_create( + [ + ToxicDomain(domain=domain, is_externally_managed=True) + for domain in domains + ], + ignore_conflicts=True, + ) + self.stdout.write("Domain database updated") + + def handle(self, *args, **options): + if not self._should_update(force=options["force"]): + self.stdout.write("Domain database is up to date") + return + self.stdout.write("Updating domain database") + + domains = self._download_domains(settings.TOXIC_DOMAINS_PROVIDERS) + + if not domains: + self.stderr.write( + "No domains could be fetched from settings.TOXIC_DOMAINS_PROVIDERS. " + "Please, have a look at your settings." + ) + return + + self._update_domains(domains) diff --git a/antispam/migrations/0001_initial.py b/antispam/migrations/0001_initial.py new file mode 100644 index 00000000..80aeaaac --- /dev/null +++ b/antispam/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.14 on 2024-08-03 23:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ToxicDomain", + fields=[ + ( + "domain", + models.URLField( + max_length=253, + primary_key=True, + serialize=False, + verbose_name="domain", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "is_externally_managed", + models.BooleanField( + default=False, + help_text="True if kept up-to-date using external toxic domain providers, else False", + verbose_name="is externally managed", + ), + ), + ], + ), + ] diff --git a/antispam/migrations/__init__.py b/antispam/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/antispam/models.py b/antispam/models.py new file mode 100644 index 00000000..44c81d1b --- /dev/null +++ b/antispam/models.py @@ -0,0 +1,19 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class ToxicDomain(models.Model): + """Domain marked as spam in public databases""" + + domain = models.URLField(_("domain"), max_length=253, primary_key=True) + created = models.DateTimeField(auto_now_add=True) + is_externally_managed = models.BooleanField( + _("is externally managed"), + default=False, + help_text=_( + "True if kept up-to-date using external toxic domain providers, else False" + ), + ) + + def __str__(self) -> str: + return self.domain diff --git a/core/apps.py b/core/apps.py index 50ea2cee..5f1b9fc3 100644 --- a/core/apps.py +++ b/core/apps.py @@ -20,8 +20,7 @@ # Place - Suite 330, Boston, MA 02111-1307, USA. # # - -import sys +import logging from django.apps import AppConfig from django.core.cache import cache @@ -41,7 +40,7 @@ class SithConfig(AppConfig): def clear_cached_memberships(**kwargs): Forum._club_memberships = {} - print("Connecting signals!", file=sys.stderr) + logging.getLogger("django").info("Connecting signals!") request_started.connect( clear_cached_memberships, weak=False, diff --git a/core/lookups.py b/core/lookups.py index ecd508a2..9a555f1b 100644 --- a/core/lookups.py +++ b/core/lookups.py @@ -21,19 +21,12 @@ from club.models import Club from core.models import Group, SithFile, User from core.views.site import search_user from counter.models import Counter, Customer, Product - - -def check_token(request): - return ( - "counter_token" in request.session.keys() - and request.session["counter_token"] - and Counter.objects.filter(token=request.session["counter_token"]).exists() - ) +from counter.utils import is_logged_in_counter class RightManagedLookupChannel(LookupChannel): def check_auth(self, request): - if not request.user.was_subscribed and not check_token(request): + if not request.user.was_subscribed and not is_logged_in_counter(request): raise PermissionDenied diff --git a/core/management/commands/install_xapian.py b/core/management/commands/install_xapian.py index e91491be..8fb9c5b3 100644 --- a/core/management/commands/install_xapian.py +++ b/core/management/commands/install_xapian.py @@ -48,20 +48,24 @@ class Command(BaseCommand): def handle(self, *args, force: bool, **options): if not os.environ.get("VIRTUAL_ENV", None): - print("No virtual environment detected, this command can't be used") + self.stdout.write( + "No virtual environment detected, this command can't be used" + ) return desired = self._desired_version() if desired == self._current_version(): if not force: - print( + self.stdout.write( f"Version {desired} is already installed, use --force to re-install" ) return - print(f"Version {desired} is already installed, re-installing") - print(f"Installing xapian version {desired} at {os.environ['VIRTUAL_ENV']}") + self.stdout.write(f"Version {desired} is already installed, re-installing") + self.stdout.write( + f"Installing xapian version {desired} at {os.environ['VIRTUAL_ENV']}" + ) subprocess.run( [str(Path(__file__).parent / "install_xapian.sh"), desired], env=dict(os.environ), ).check_returncode() - print("Installation success") + self.stdout.write("Installation success") diff --git a/core/management/commands/markdown.py b/core/management/commands/markdown.py index 8b43e5ac..f9379373 100644 --- a/core/management/commands/markdown.py +++ b/core/management/commands/markdown.py @@ -35,4 +35,4 @@ class Command(BaseCommand): root_path = settings.BASE_DIR with open(root_path / "core/fixtures/SYNTAX.md", "r") as md: result = markdown(md.read()) - print(result, end="") + self.stdout.write(result) diff --git a/core/models.py b/core/models.py index c9aceab6..1f5b2109 100644 --- a/core/models.py +++ b/core/models.py @@ -24,6 +24,7 @@ from __future__ import annotations import importlib +import logging import os import unicodedata from datetime import date, timedelta @@ -981,7 +982,7 @@ class SithFile(models.Model): return True if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): return True - return user.id == self.owner.id + return user.id == self.owner_id def can_be_viewed_by(self, user): if hasattr(self, "profile_of"): @@ -1085,19 +1086,15 @@ class SithFile(models.Model): # file storage parent_path = "." + self.parent.get_full_path() parent_full_path = settings.MEDIA_ROOT + parent_path - print("Parent full path: %s" % parent_full_path) os.makedirs(parent_full_path, exist_ok=True) old_path = self.file.name # Should be relative: "./users/skia/bleh.jpg" new_path = "." + self.get_full_path() - print("Old path: %s " % old_path) - print("New path: %s " % new_path) try: # Make this atomic, so that a FS problem rolls back the DB change with transaction.atomic(): # Set the new filesystem path self.file.name = new_path self.save() - print("New file path: %s " % self.file.path) # Really move at the FS level if os.path.exists(parent_full_path): os.rename( @@ -1108,25 +1105,22 @@ class SithFile(models.Model): # problem, and that can be solved with a simple shell # command: `find . -type d -empty -delete` except Exception as e: - print("This file likely had a problem. Here is the exception:") - print(repr(e)) - print("-" * 80) + logging.error(e) def _check_path_consistence(self): file_path = str(self.file) file_full_path = settings.MEDIA_ROOT + file_path db_path = ".%s" % self.get_full_path() if not os.path.exists(file_full_path): - print("%s: WARNING: real file does not exists!" % self.id) - print("file path: %s" % file_path, end="") - print(" db path: %s" % db_path) + print("%s: WARNING: real file does not exists!" % self.id) # noqa T201 + print("file path: %s" % file_path, end="") # noqa T201 + print(" db path: %s" % db_path) # noqa T201 return False if file_path != db_path: - print("%s: " % self.id, end="") - print("file path: %s" % file_path, end="") - print(" db path: %s" % db_path) + print("%s: " % self.id, end="") # noqa T201 + print("file path: %s" % file_path, end="") # noqa T201 + print(" db path: %s" % db_path) # noqa T201 return False - print("%s OK (%s)" % (self.id, file_path)) return True def _check_fs(self): @@ -1137,11 +1131,9 @@ class SithFile(models.Model): else: self._check_path_consistence() - def __getattribute__(self, attr): - if attr == "is_file": - return not self.is_folder - else: - return super().__getattribute__(attr) + @property + def is_file(self): + return not self.is_folder @cached_property def as_picture(self): diff --git a/core/static/core/js/alpinejs.min.js b/core/static/core/js/alpinejs.min.js index bd82e3c8..bc184f8a 100644 --- a/core/static/core/js/alpinejs.min.js +++ b/core/static/core/js/alpinejs.min.js @@ -1,5 +1,5 @@ -(()=>{var We=!1,Ge=!1,B=[];function $t(e){an(e)}function an(e){B.includes(e)||B.push(e),cn()}function he(e){let t=B.indexOf(e);t!==-1&&B.splice(t,1)}function cn(){!Ge&&!We&&(We=!0,queueMicrotask(ln))}function ln(){We=!1,Ge=!0;for(let e=0;ee.effect(t,{scheduler:r=>{Je?$t(r):r()}}),Ye=e.raw}function Ze(e){K=e}function Ft(e){let t=()=>{};return[n=>{let i=K(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),Y(i))},i},()=>{t()}]}var Bt=[],Kt=[],zt=[];function Vt(e){zt.push(e)}function _e(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Kt.push(t))}function Ht(e){Bt.push(e)}function qt(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Qe(e,t){!e._x_attributeCleanups||Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var et=new MutationObserver(Xe),tt=!1;function rt(){et.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),tt=!0}function fn(){un(),et.disconnect(),tt=!1}var te=[],nt=!1;function un(){te=te.concat(et.takeRecords()),te.length&&!nt&&(nt=!0,queueMicrotask(()=>{dn(),nt=!1}))}function dn(){Xe(te),te.length=0}function m(e){if(!tt)return e();fn();let t=e();return rt(),t}var it=!1,ge=[];function Ut(){it=!0}function Wt(){it=!1,Xe(ge),ge=[]}function Xe(e){if(it){ge=ge.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{Qe(s,o)}),n.forEach((o,s)=>{Bt.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(Kt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||!o.isConnected||(delete o._x_ignoreSelf,delete o._x_ignore,zt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function xe(e){return D(k(e))}function C(e,t,r){return e._x_dataStack=[t,...k(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function ot(e,t){let r=e._x_dataStack[0];Object.entries(t).forEach(([n,i])=>{r[n]=i})}function k(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?k(e.host):e.parentNode?k(e.parentNode):[]}function D(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,c=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...c,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function ye(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function be(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>pn(n,i),s=>st(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function st(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),st(e[t[0]],t.slice(1),r)}}var Gt={};function x(e,t){Gt[e]=t}function re(e,t){return Object.entries(Gt).forEach(([r,n])=>{Object.defineProperty(e,`$${r}`,{get(){let[i,o]=at(t);return i={interceptor:be,...i},_e(t,o),n(t,i)},enumerable:!1})}),e}function Yt(e,t,r,...n){try{return r(...n)}catch(i){J(i,e,t)}}function J(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} +(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;ee.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} ${r?'Expression: "'+r+`" -`:""}`,t),setTimeout(()=>{throw e},0)}var ve=!0;function Jt(e){let t=ve;ve=!1,e(),ve=t}function P(e,t,r={}){let n;return g(e,t)(i=>n=i,r),n}function g(...e){return Zt(...e)}var Zt=ct;function Qt(e){Zt=e}function ct(e,t){let r={};re(r,e);let n=[r,...k(e)];if(typeof t=="function")return mn(n,t);let i=hn(n,t,e);return Yt.bind(null,e,t,i)}function mn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(D([n,...e]),i);we(r,o)}}var lt={};function _n(e,t){if(lt[e])return lt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return J(s,t,e),Promise.resolve()}})();return lt[e]=o,o}function hn(e,t,r){let n=_n(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=D([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>J(l,r,t));n.finished?(we(i,n.result,a,s,r),n.result=void 0):c.then(l=>{we(i,l,a,s,r)}).catch(l=>J(l,r,t)).finally(()=>n.result=void 0)}}}function we(e,t,r,n,i){if(ve&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>we(e,s,r,n)).catch(s=>J(s,i,t)):e(o)}else e(t)}var ut="x-";function E(e=""){return ut+e}function Xt(e){ut=e}var er={};function d(e,t){er[e]=t}function ne(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=ft(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(tr((o,s)=>n[o]=s)).filter(rr).map(xn(n,r)).sort(yn).map(o=>gn(e,o))}function ft(e){return Array.from(e).map(tr()).filter(t=>!rr(t))}var dt=!1,ie=new Map,nr=Symbol();function ir(e){dt=!0;let t=Symbol();nr=t,ie.set(t,[]);let r=()=>{for(;ie.get(t).length;)ie.get(t).shift()();ie.delete(t)},n=()=>{dt=!1,r()};e(r),n()}function at(e){let t=[],r=a=>t.push(a),[n,i]=Ft(e);return t.push(i),[{Alpine:I,effect:n,cleanup:r,evaluateLater:g.bind(g,e),evaluate:P.bind(P,e)},()=>t.forEach(a=>a())]}function gn(e,t){let r=()=>{},n=er[t.type]||r,[i,o]=at(e);qt(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),dt?ie.get(nr).push(n):n())};return s.runCleanups=o,s}var Ee=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Se=e=>e;function tr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=or.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var or=[];function Z(e){or.push(e)}function rr({name:e}){return sr().test(e)}var sr=()=>new RegExp(`^${ut}([^:^.]+)\\b`);function xn(e,t){return({name:r,value:n})=>{let i=r.match(sr()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var pt="DEFAULT",Ae=["ignore","ref","data","id","radio","tabs","switch","disclosure","menu","listbox","list","item","combobox","bind","init","for","mask","model","modelable","transition","show","if",pt,"teleport"];function yn(e,t){let r=Ae.indexOf(e.type)===-1?pt:e.type,n=Ae.indexOf(t.type)===-1?pt:t.type;return Ae.indexOf(r)-Ae.indexOf(n)}function z(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}var mt=[],ht=!1;function Te(e=()=>{}){return queueMicrotask(()=>{ht||setTimeout(()=>{Oe()})}),new Promise(t=>{mt.push(()=>{e(),t()})})}function Oe(){for(ht=!1;mt.length;)mt.shift()()}function ar(){ht=!0}function R(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>R(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)R(n,t,!1),n=n.nextElementSibling}function O(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}function lr(){document.body||O("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` {% endblock %} diff --git a/core/templates/core/user_detail.jinja b/core/templates/core/user_detail.jinja index 9f5824fc..4eff73d3 100644 --- a/core/templates/core/user_detail.jinja +++ b/core/templates/core/user_detail.jinja @@ -9,10 +9,6 @@ {% trans user_name=profile.get_display_name() %}{{ user_name }}'s profile{% endtrans %} {% endblock %} -{% block additional_js %} - -{% endblock %} - {% block content %} {% endif %} @@ -104,8 +102,12 @@
{% for p in items %} -