mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-25 18:44:23 +00:00
Introduce htmx in sith files
* Convert FileModerationView into ListView and add pagination with htmx * Don't allow sas moderation in file moderation view * Split up base.jinja and introduce base_fragment.jinja * Improve FileModerationView performances and make it root only * Add permissions tests for file modération
This commit is contained in:
parent
c7a8a1a91c
commit
3af5d96bf5
1
core/static/webpack/htmx-index.js
Normal file
1
core/static/webpack/htmx-index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
window.htmx = require("htmx.org");
|
@ -5,7 +5,6 @@
|
|||||||
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ static('user/user_stats.scss') }}">
|
|
||||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/style.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
|
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
|
||||||
@ -23,6 +22,7 @@
|
|||||||
<script src="{{ url('javascript-catalog') }}"></script>
|
<script src="{{ url('javascript-catalog') }}"></script>
|
||||||
<script src={{ static("webpack/core/components/include-index.ts") }}></script>
|
<script src={{ static("webpack/core/components/include-index.ts") }}></script>
|
||||||
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
|
<script src="{{ static('webpack/alpine-index.js') }}" defer></script>
|
||||||
|
<script src="{{ static('webpack/htmx-index.js') }}" defer></script>
|
||||||
<!-- Jquery declared here to be accessible in every django widgets -->
|
<!-- Jquery declared here to be accessible in every django widgets -->
|
||||||
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
<script src="{{ static('webpack/jquery-index.js') }}"></script>
|
||||||
<!-- Put here to always have access to those functions on django widgets -->
|
<!-- Put here to always have access to those functions on django widgets -->
|
||||||
@ -40,145 +40,13 @@
|
|||||||
<!-- The token is always passed here to be accessible from the dom -->
|
<!-- The token is always passed here to be accessible from the dom -->
|
||||||
<!-- See this workaround https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true -->
|
<!-- See this workaround https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true -->
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<!-- BEGIN HEADER -->
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if not popup %}
|
{% if not popup %}
|
||||||
<header class="header">
|
{% include "core/base/header.jinja" %}
|
||||||
<div class="header-logo">
|
|
||||||
<a class="header-logo-picture" href="{{ url('core:index') }}" style="background-image: url('{{ static('core/img/logo_no_text.png') }}')">
|
|
||||||
|
|
||||||
</a>
|
|
||||||
<a class="header-logo-text" href="{{ url('core:index') }}">
|
|
||||||
<span>Association des Étudiants</span>
|
|
||||||
<span>de l'Université de Technologie de Belfort-Montbéliard</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% if not user.is_authenticated %}
|
|
||||||
<div class="header-disconnected">
|
|
||||||
<a class="button" href="{{ url('core:login') }}">{% trans %}Login{% endtrans %}</a>
|
|
||||||
<a class="button" href="{{ url('core:register') }}">{% trans %}Register{% endtrans %}</a>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="header-connected">
|
<div id="popupheader">{{ user.get_display_name() }}</div>
|
||||||
<div class="left">
|
|
||||||
<form class="search" action="{{ url('core:search') }}" method="GET" id="header_search">
|
|
||||||
<input class="header-input" type="text" placeholder="{% trans %}Search{% endtrans %}" name="query" id="search" />
|
|
||||||
<input type="submit" value="{% trans %}Search{% endtrans %}" style="display: none;" />
|
|
||||||
</form>
|
|
||||||
<ul class="bars">
|
|
||||||
{% cache 100 "counters_activity" %}
|
|
||||||
{# The sith has no periodic tasks manager
|
|
||||||
and using cron jobs would be way too overkill here.
|
|
||||||
Thus the barmen timeout is handled in the only place that
|
|
||||||
is loaded on every page : the header bar.
|
|
||||||
However, let's be clear : this has nothing to do here.
|
|
||||||
It's' merely a contrived workaround that should
|
|
||||||
replaced by a proper task manager as soon as possible. #}
|
|
||||||
{% set _ = Counter.objects.filter(type="BAR").handle_timeout() %}
|
|
||||||
{% endcache %}
|
|
||||||
{% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %}
|
|
||||||
<li>
|
|
||||||
{# If the user is a barman, we redirect him directly to the barman page
|
|
||||||
else we redirect him to the activity page #}
|
|
||||||
{% if bar.has_annotated_barman %}
|
|
||||||
<a href="{{ url('counter:details', counter_id=bar.id) }}">
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url('counter:activity', counter_id=bar.id) }}">
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if bar.is_open %}
|
|
||||||
<i class="fa fa-check" style="color: #2ecc71"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fa fa-times" style="color: #eb2f06"></i>
|
|
||||||
{% endif %}
|
|
||||||
<span>{{ bar }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<div class="user">
|
|
||||||
<div class="options">
|
|
||||||
<div class="username">
|
|
||||||
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="links">
|
|
||||||
<a href="{{ url('core:user_tools') }}">{% trans %}Tools{% endtrans %}</a>
|
|
||||||
<a href="{{ url('core:logout') }}">{% trans %}Logout{% endtrans %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="{{ url('core:user_profile', user_id=user.id) }}"
|
|
||||||
{% if user.profile_pict %}
|
|
||||||
style="background-image: url('{{ user.profile_pict.get_download_url() }}')"
|
|
||||||
{% else %}
|
|
||||||
style="background-image: url('{{ static('core/img/unknown.jpg') }}')"
|
|
||||||
{% endif %}
|
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
<div class="notification">
|
|
||||||
<a href="#" onclick="displayNotif()">
|
|
||||||
<i class="fa-regular fa-bell"></i>
|
|
||||||
{% set notification_count = user.notifications.filter(viewed=False).count() %}
|
|
||||||
|
|
||||||
{% if notification_count > 0 %}
|
|
||||||
<span>
|
|
||||||
{% if notification_count < 100 %}
|
|
||||||
{{ notification_count }}
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
<div id="header_notif">
|
|
||||||
<ul>
|
|
||||||
{% if user.notifications.filter(viewed=False).count() > 0 %}
|
|
||||||
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url("core:notification", notif_id=n.id) }}">
|
|
||||||
<div class="datetime">
|
|
||||||
<span class="header_notif_date">
|
|
||||||
{{ n.date|localtime|date(DATE_FORMAT) }}
|
|
||||||
</span>
|
|
||||||
<span class="header_notif_time">
|
|
||||||
{{ n.date|localtime|time(DATETIME_FORMAT) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="reason">
|
|
||||||
{{ n }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<li class="empty-notification">{% trans %}You do not have any unread notification{% endtrans %}</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
<div class="options">
|
|
||||||
<a href="{{ url('core:notification_list') }}">
|
|
||||||
{% trans %}View more{% endtrans %}
|
|
||||||
</a>
|
|
||||||
<a href="{{ url('core:notification_list') }}?see_all">
|
|
||||||
{% trans %}Mark all as read{% endtrans %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="header-lang">
|
|
||||||
{% for language in LANGUAGES %}
|
|
||||||
<form action="{{ url('set_language') }}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input name="next" value="{{ request.path }}" type="hidden" />
|
|
||||||
<input name="language" value="{{ language[0] }}" type="hidden" />
|
|
||||||
<input type="submit" value="{% if language[0] == 'en' %}🇬🇧{% else %}🇫🇷{% endif %}" />
|
|
||||||
</form>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% block info_boxes %}
|
{% block info_boxes %}
|
||||||
<div id="info_boxes">
|
<div id="info_boxes">
|
||||||
@ -195,64 +63,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<div id="popupheader">{{ user.get_display_name() }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<!-- END HEADER -->
|
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
{% if not popup %}
|
{% if not popup %}
|
||||||
<nav class="navbar">
|
{% include "core/base/menu.jinja" %}
|
||||||
<button class="expand-button" onclick="showMenu()"><i class="fa fa-bars"></i></button>
|
|
||||||
<div id="navbar-content" class="content" style="display: none;">
|
|
||||||
<a class="link" href="{{ url('core:index') }}">{% trans %}Main{% endtrans %}</a>
|
|
||||||
<div class="menu">
|
|
||||||
<span class="head">{% trans %}Associations & Clubs{% endtrans %}</span>
|
|
||||||
<ul class="content">
|
|
||||||
<li><a href="{{ url('core:page', page_name='ae') }}">{% trans %}AE{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', page_name='clubs') }}">{% trans %}AE's clubs{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', page_name='utbm-associations') }}">{% trans %}Others UTBM's Associations{% endtrans %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="menu">
|
|
||||||
<span class="head">{% trans %}Events{% endtrans %}</span>
|
|
||||||
<ul class="content">
|
|
||||||
<li><a href="{{ url('election:list') }}">{% trans %}Elections{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<a class="link" href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
|
|
||||||
<a class="link" href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
|
|
||||||
<a class="link" href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
|
|
||||||
<div class="menu">
|
|
||||||
<span class="head">{% trans %}Services{% endtrans %}</span>
|
|
||||||
<ul class="content">
|
|
||||||
<li><a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
|
|
||||||
<li><a href="/launderette">{% trans %}Launderette{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="menu">
|
|
||||||
<span class="head">{% trans %}My Benefits{% endtrans %}</span>
|
|
||||||
<ul class="content">
|
|
||||||
<li><a href="{{ url('core:page', page_name='partenaires')}}">{% trans %}Sponsors{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', page_name='avantages') }}">{% trans %}Subscriber benefits{% endtrans %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="menu">
|
|
||||||
<span class="head">{% trans %}Help{% endtrans %}</span>
|
|
||||||
<ul class="content">
|
|
||||||
<li><a href="{{ url('core:page', page_name='FAQ') }}">{% trans %}FAQ{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', 'contacts') }}">{% trans %}Contacts{% endtrans %}</a></li>
|
|
||||||
<li><a href="{{ url('core:page', page_name='Index') }}">{% trans %}Wiki{% endtrans %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -265,19 +80,16 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
{% if list_of_tabs %}
|
{% block tabs %}
|
||||||
<div class="tool_bar">
|
{% include "core/base/tabs.jinja" %}
|
||||||
<div class="tools">
|
{% endblock %}
|
||||||
{% for t in list_of_tabs -%}
|
|
||||||
<a href="{{ t.url }}" {%- if current_tab==t.slug %} class="selected_tab" {%- endif -%}>{{ t.name }}</a>
|
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
{% block errors%}
|
||||||
{% if error %}
|
{% if error %}
|
||||||
{{ error }}
|
{{ error }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
136
core/templates/core/base/header.jinja
Normal file
136
core/templates/core/base/header.jinja
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<header class="header">
|
||||||
|
<div class="header-logo">
|
||||||
|
<a class="header-logo-picture" href="{{ url('core:index') }}" style="background-image: url('{{ static('core/img/logo_no_text.png') }}')">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<a class="header-logo-text" href="{{ url('core:index') }}">
|
||||||
|
<span>Association des Étudiants</span>
|
||||||
|
<span>de l'Université de Technologie de Belfort-Montbéliard</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
<div class="header-disconnected">
|
||||||
|
<a class="button" href="{{ url('core:login') }}">{% trans %}Login{% endtrans %}</a>
|
||||||
|
<a class="button" href="{{ url('core:register') }}">{% trans %}Register{% endtrans %}</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="header-connected">
|
||||||
|
<div class="left">
|
||||||
|
<form class="search" action="{{ url('core:search') }}" method="GET" id="header_search">
|
||||||
|
<input class="header-input" type="text" placeholder="{% trans %}Search{% endtrans %}" name="query" id="search" />
|
||||||
|
<input type="submit" value="{% trans %}Search{% endtrans %}" style="display: none;" />
|
||||||
|
</form>
|
||||||
|
<ul class="bars">
|
||||||
|
{% cache 100 "counters_activity" %}
|
||||||
|
{# The sith has no periodic tasks manager
|
||||||
|
and using cron jobs would be way too overkill here.
|
||||||
|
Thus the barmen timeout is handled in the only place that
|
||||||
|
is loaded on every page : the header bar.
|
||||||
|
However, let's be clear : this has nothing to do here.
|
||||||
|
It's' merely a contrived workaround that should
|
||||||
|
replaced by a proper task manager as soon as possible. #}
|
||||||
|
{% set _ = Counter.objects.filter(type="BAR").handle_timeout() %}
|
||||||
|
{% endcache %}
|
||||||
|
{% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %}
|
||||||
|
<li>
|
||||||
|
{# If the user is a barman, we redirect him directly to the barman page
|
||||||
|
else we redirect him to the activity page #}
|
||||||
|
{% if bar.has_annotated_barman %}
|
||||||
|
<a href="{{ url('counter:details', counter_id=bar.id) }}">
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url('counter:activity', counter_id=bar.id) }}">
|
||||||
|
{% endif %}
|
||||||
|
{% if bar.is_open %}
|
||||||
|
<i class="fa fa-check" style="color: #2ecc71"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fa fa-times" style="color: #eb2f06"></i>
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ bar }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="user">
|
||||||
|
<div class="options">
|
||||||
|
<div class="username">
|
||||||
|
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="links">
|
||||||
|
<a href="{{ url('core:user_tools') }}">{% trans %}Tools{% endtrans %}</a>
|
||||||
|
<a href="{{ url('core:logout') }}">{% trans %}Logout{% endtrans %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="{{ url('core:user_profile', user_id=user.id) }}"
|
||||||
|
{% if user.profile_pict %}
|
||||||
|
style="background-image: url('{{ user.profile_pict.get_download_url() }}')"
|
||||||
|
{% else %}
|
||||||
|
style="background-image: url('{{ static('core/img/unknown.jpg') }}')"
|
||||||
|
{% endif %}
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
<div class="notification">
|
||||||
|
<a href="#" onclick="displayNotif()">
|
||||||
|
<i class="fa-regular fa-bell"></i>
|
||||||
|
{% set notification_count = user.notifications.filter(viewed=False).count() %}
|
||||||
|
|
||||||
|
{% if notification_count > 0 %}
|
||||||
|
<span>
|
||||||
|
{% if notification_count < 100 %}
|
||||||
|
{{ notification_count }}
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<div id="header_notif">
|
||||||
|
<ul>
|
||||||
|
{% if user.notifications.filter(viewed=False).count() > 0 %}
|
||||||
|
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url("core:notification", notif_id=n.id) }}">
|
||||||
|
<div class="datetime">
|
||||||
|
<span class="header_notif_date">
|
||||||
|
{{ n.date|localtime|date(DATE_FORMAT) }}
|
||||||
|
</span>
|
||||||
|
<span class="header_notif_time">
|
||||||
|
{{ n.date|localtime|time(DATETIME_FORMAT) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="reason">
|
||||||
|
{{ n }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li class="empty-notification">{% trans %}You do not have any unread notification{% endtrans %}</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div class="options">
|
||||||
|
<a href="{{ url('core:notification_list') }}">
|
||||||
|
{% trans %}View more{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a href="{{ url('core:notification_list') }}?see_all">
|
||||||
|
{% trans %}Mark all as read{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="header-lang">
|
||||||
|
{% for language in LANGUAGES %}
|
||||||
|
<form action="{{ url('set_language') }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input name="next" value="{{ request.path }}" type="hidden" />
|
||||||
|
<input name="language" value="{{ language[0] }}" type="hidden" />
|
||||||
|
<input type="submit" value="{% if language[0] == 'en' %}🇬🇧{% else %}🇫🇷{% endif %}" />
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</header>
|
48
core/templates/core/base/menu.jinja
Normal file
48
core/templates/core/base/menu.jinja
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<nav class="navbar">
|
||||||
|
<button class="expand-button" onclick="showMenu()"><i class="fa fa-bars"></i></button>
|
||||||
|
<div id="navbar-content" class="content" style="display: none;">
|
||||||
|
<a class="link" href="{{ url('core:index') }}">{% trans %}Main{% endtrans %}</a>
|
||||||
|
<div class="menu">
|
||||||
|
<span class="head">{% trans %}Associations & Clubs{% endtrans %}</span>
|
||||||
|
<ul class="content">
|
||||||
|
<li><a href="{{ url('core:page', page_name='ae') }}">{% trans %}AE{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', page_name='clubs') }}">{% trans %}AE's clubs{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', page_name='utbm-associations') }}">{% trans %}Others UTBM's Associations{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<span class="head">{% trans %}Events{% endtrans %}</span>
|
||||||
|
<ul class="content">
|
||||||
|
<li><a href="{{ url('election:list') }}">{% trans %}Elections{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a class="link" href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
|
||||||
|
<a class="link" href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
|
||||||
|
<a class="link" href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
|
||||||
|
<div class="menu">
|
||||||
|
<span class="head">{% trans %}Services{% endtrans %}</span>
|
||||||
|
<ul class="content">
|
||||||
|
<li><a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a></li>
|
||||||
|
<li><a href="/launderette">{% trans %}Launderette{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<span class="head">{% trans %}My Benefits{% endtrans %}</span>
|
||||||
|
<ul class="content">
|
||||||
|
<li><a href="{{ url('core:page', page_name='partenaires')}}">{% trans %}Sponsors{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', page_name='avantages') }}">{% trans %}Subscriber benefits{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<span class="head">{% trans %}Help{% endtrans %}</span>
|
||||||
|
<ul class="content">
|
||||||
|
<li><a href="{{ url('core:page', page_name='FAQ') }}">{% trans %}FAQ{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', 'contacts') }}">{% trans %}Contacts{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('core:page', page_name='Index') }}">{% trans %}Wiki{% endtrans %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
9
core/templates/core/base/tabs.jinja
Normal file
9
core/templates/core/base/tabs.jinja
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% if list_of_tabs %}
|
||||||
|
<div class="tool_bar">
|
||||||
|
<div class="tools">
|
||||||
|
{% for t in list_of_tabs -%}
|
||||||
|
<a href="{{ t.url }}" {%- if current_tab==t.slug %} class="selected_tab" {%- endif -%}>{{ t.name }}</a>
|
||||||
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
20
core/templates/core/base_fragment.jinja
Normal file
20
core/templates/core/base_fragment.jinja
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% block additional_css %}{% endblock %}
|
||||||
|
{% block additional_js %}{% endblock %}
|
||||||
|
|
||||||
|
<div id="fragment-content">
|
||||||
|
{% block tabs %}
|
||||||
|
{% include "core/base/tabs.jinja" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block errors%}
|
||||||
|
{% if error %}
|
||||||
|
{{ error }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{% endblock %}
|
@ -1,4 +1,8 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% if is_fragment %}
|
||||||
|
{% extends "core/base_fragment.jinja" %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "core/base.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% if file %}
|
{% if file %}
|
||||||
@ -21,7 +25,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block tabs %}
|
||||||
{{ print_file_name(file) }}
|
{{ print_file_name(file) }}
|
||||||
|
|
||||||
<div class="tool_bar">
|
<div class="tool_bar">
|
||||||
@ -44,6 +48,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
{% if file %}
|
{% if file %}
|
||||||
{% block file %}
|
{% block file %}
|
||||||
|
@ -4,15 +4,49 @@
|
|||||||
{% trans %}Delete confirmation{% endtrans %}
|
{% trans %}Delete confirmation{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% if is_fragment %}
|
||||||
|
|
||||||
|
{# Don't display tabs and errors #}
|
||||||
|
{% block tabs %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block errors %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block file %}
|
{% block file %}
|
||||||
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
||||||
<form action="" method="post">{% csrf_token %}
|
|
||||||
|
{% if next %}
|
||||||
|
{% set action = current + "?next=" + next %}
|
||||||
|
{% else %}
|
||||||
|
{% set action = current %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{{ action }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
|
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
|
||||||
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
|
<button
|
||||||
</form>
|
{% if is_fragment %}
|
||||||
<form method="GET" action="javascript:history.back();">
|
hx-post="{{ action }}"
|
||||||
<input type="submit" name="cancel" value="{% trans %}Cancel{% endtrans %}" />
|
hx-target="#content"
|
||||||
|
hx-swap="outerHtml"
|
||||||
|
{% endif %}
|
||||||
|
>{% trans %}Confirm{% endtrans %}</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{% if is_fragment %}
|
||||||
|
hx-get="{{ previous }}"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-swap="outerHtml"
|
||||||
|
{% else %}
|
||||||
|
action="window.history.back()"
|
||||||
|
{% endif %}
|
||||||
|
>{% trans %}Cancel{% endtrans %}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% if is_fragment %}
|
||||||
|
{% extends "core/base_fragment.jinja" %}
|
||||||
|
|
||||||
|
{# Don't display tabs and errors #}
|
||||||
|
{% block tabs %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block errors %}
|
||||||
|
{% endblock %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "core/base.jinja" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% from "core/macros.jinja" import paginate_htmx %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}File moderation{% endtrans %}
|
{% trans %}File moderation{% endtrans %}
|
||||||
@ -7,8 +19,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}File moderation{% endtrans %}</h3>
|
<h3>{% trans %}File moderation{% endtrans %}</h3>
|
||||||
<div>
|
<div>
|
||||||
{% for f in files %}
|
{% for f in object_list %}
|
||||||
<div style="margin: 2px; padding: 2px; border: solid 1px red; text-align: center">
|
<div
|
||||||
|
id="file-{{ loop.index }}"
|
||||||
|
style="margin: 2px; padding: 2px; border: solid 1px red; text-align: center"
|
||||||
|
>
|
||||||
{% if f.is_folder %}
|
{% if f.is_folder %}
|
||||||
<strong>Folder</strong>
|
<strong>Folder</strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -20,9 +35,19 @@
|
|||||||
{% trans %}Owner: {% endtrans %}{{ f.owner.get_display_name() }}<br/>
|
{% trans %}Owner: {% endtrans %}{{ f.owner.get_display_name() }}<br/>
|
||||||
{% trans %}Date: {% endtrans %}{{ f.date|date(DATE_FORMAT) }} {{ f.date|time(TIME_FORMAT) }}<br/>
|
{% trans %}Date: {% endtrans %}{{ f.date|date(DATE_FORMAT) }} {{ f.date|time(TIME_FORMAT) }}<br/>
|
||||||
</p>
|
</p>
|
||||||
<p><a href="{{ url('core:file_moderate', file_id=f.id) }}">{% trans %}Moderate{% endtrans %}</a> -
|
<p><button
|
||||||
<a href="{{ url('core:file_delete', file_id=f.id) }}?next={{ url('core:file_moderation') }}">{% trans %}Delete{% endtrans %}</a></p>
|
hx-get="{{ url('core:file_moderate', file_id=f.id) }}"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-swap="outerHtml"
|
||||||
|
>{% trans %}Moderate{% endtrans %}</button> -
|
||||||
|
{% set current_page = url('core:file_moderation') + "?page=" + page_obj.number | string %}
|
||||||
|
<button
|
||||||
|
hx-get="{{ url('core:file_delete', file_id=f.id) }}?next={{ current_page | urlencode }}&previous={{ current_page | urlencode }}"
|
||||||
|
hx-target="#file-{{ loop.index }}"
|
||||||
|
hx-swap="outerHtml"
|
||||||
|
>{% trans %}Delete{% endtrans %}</button></p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{{ paginate_htmx(page_obj, paginator) }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -166,9 +166,37 @@
|
|||||||
current_page (django.core.paginator.Page): the current page object
|
current_page (django.core.paginator.Page): the current page object
|
||||||
paginator (django.core.paginator.Paginator): the paginator object
|
paginator (django.core.paginator.Paginator): the paginator object
|
||||||
#}
|
#}
|
||||||
|
{{ paginate_server_side(current_page, paginator, False) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro paginate_htmx(current_page, paginator) %}
|
||||||
|
{# Add pagination buttons for pages without Alpine but supporting framgents.
|
||||||
|
|
||||||
|
This must be coupled with a view that handles pagination
|
||||||
|
with the Django Paginator object and supports framgents.
|
||||||
|
|
||||||
|
The relpaced fragment will be #content so make sure you are calling this macro inside your content block.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
current_page (django.core.paginator.Page): the current page object
|
||||||
|
paginator (django.core.paginator.Paginator): the paginator object
|
||||||
|
#}
|
||||||
|
{{ paginate_server_side(current_page, paginator, True) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro paginate_server_side(current_page, paginator, use_htmx) %}
|
||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
{% if current_page.has_previous() %}
|
{% if current_page.has_previous() %}
|
||||||
<a href="?page={{ current_page.previous_page_number() }}">
|
<a
|
||||||
|
{% if use_htmx -%}
|
||||||
|
hx-get="?page={{ current_page.previous_page_number() }}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-push-url="true"
|
||||||
|
{%- else -%}
|
||||||
|
href="?page={{ current_page.previous_page_number() }}"
|
||||||
|
{%- endif -%}
|
||||||
|
>
|
||||||
<button>
|
<button>
|
||||||
<i class="fa fa-caret-left"></i>
|
<i class="fa fa-caret-left"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -182,14 +210,31 @@
|
|||||||
{% elif i == paginator.ELLIPSIS %}
|
{% elif i == paginator.ELLIPSIS %}
|
||||||
<strong>{{ paginator.ELLIPSIS }}</strong>
|
<strong>{{ paginator.ELLIPSIS }}</strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="?page={{ i }}">
|
<a
|
||||||
|
{% if use_htmx -%}
|
||||||
|
hx-get="?page={{ i }}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-push-url="true"
|
||||||
|
{%- else -%}
|
||||||
|
href="?page={{ i }}"
|
||||||
|
{%- endif -%}
|
||||||
|
>
|
||||||
<button>{{ i }}</button>
|
<button>{{ i }}</button>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if current_page.has_next() %}
|
{% if current_page.has_next() %}
|
||||||
<a href="?page={{ current_page.next_page_number() }}">
|
<a
|
||||||
<button>
|
{% if use_htmx -%}
|
||||||
|
hx-get="?page={{ current_page.next_page_number() }}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-push-url="true"
|
||||||
|
{%- else -%}
|
||||||
|
href="?page={{ current_page.next_page_number() }}"
|
||||||
|
{%- endif -%}
|
||||||
|
><button>
|
||||||
<i class="fa fa-caret-right"></i>
|
<i class="fa fa-caret-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
{% set default_picture = this_picture.get_download_url()|tojson %}
|
{% set default_picture = this_picture.get_download_url()|tojson %}
|
||||||
{% set delete_url = (
|
{% set delete_url = (
|
||||||
url('core:file_delete', file_id=this_picture.id, popup='')
|
url('core:file_delete', file_id=this_picture.id, popup='')
|
||||||
+"?next=" + profile.get_absolute_url()
|
+ "?next=" + url('core:user_edit', user_id=profile.id)
|
||||||
)|tojson %}
|
)|tojson %}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% set default_picture = static('core/img/unknown.jpg')|tojson %}
|
{% set default_picture = static('core/img/unknown.jpg')|tojson %}
|
||||||
|
@ -142,6 +142,30 @@ class TestFileHandling(TestCase):
|
|||||||
assert "ls</a>" in str(response.content)
|
assert "ls</a>" in str(response.content)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestFileModerationView:
|
||||||
|
"""Test access to file moderation view"""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("user_factory", "status_code"),
|
||||||
|
[
|
||||||
|
(lambda: None, 403), # Anonymous user
|
||||||
|
(lambda: baker.make(User, is_superuser=True), 200),
|
||||||
|
(lambda: baker.make(User), 403),
|
||||||
|
(lambda: subscriber_user.make(), 403),
|
||||||
|
(lambda: old_subscriber_user.make(), 403),
|
||||||
|
(lambda: board_user.make(), 403),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_view_access(
|
||||||
|
self, client: Client, user_factory: Callable[[], User | None], status_code: int
|
||||||
|
):
|
||||||
|
user = user_factory()
|
||||||
|
if user: # if None, then it's an anonymous user
|
||||||
|
client.force_login(user_factory())
|
||||||
|
assert client.get(reverse("core:file_moderation")).status_code == status_code
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestUserProfilePicture:
|
class TestUserProfilePicture:
|
||||||
"""Test interactions with user's profile picture."""
|
"""Test interactions with user's profile picture."""
|
||||||
|
@ -326,6 +326,12 @@ class DetailFormView(SingleObjectMixin, FormView):
|
|||||||
return super().get_object()
|
return super().get_object()
|
||||||
|
|
||||||
|
|
||||||
|
class AllowFragment:
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs["is_fragment"] = self.request.headers.get("HX-Request", False)
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
# F403: those star-imports would be hellish to refactor
|
# F403: those star-imports would be hellish to refactor
|
||||||
# E402: putting those import at the top of the file would also be difficult
|
# E402: putting those import at the top of the file would also be difficult
|
||||||
from .files import * # noqa: F403 E402
|
from .files import * # noqa: F403 E402
|
||||||
|
@ -27,12 +27,13 @@ from django.shortcuts import get_object_or_404, redirect
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import http_date
|
from django.utils.http import http_date
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, ListView, TemplateView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.views.generic.edit import DeleteView, FormMixin, UpdateView
|
from django.views.generic.edit import DeleteView, FormMixin, UpdateView
|
||||||
|
|
||||||
from core.models import Notification, RealGroup, SithFile
|
from core.models import Notification, RealGroup, SithFile, User
|
||||||
from core.views import (
|
from core.views import (
|
||||||
|
AllowFragment,
|
||||||
CanEditMixin,
|
CanEditMixin,
|
||||||
CanEditPropMixin,
|
CanEditPropMixin,
|
||||||
CanViewMixin,
|
CanViewMixin,
|
||||||
@ -352,7 +353,7 @@ class FileView(CanViewMixin, DetailView, FormMixin):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class FileDeleteView(CanEditPropMixin, DeleteView):
|
class FileDeleteView(AllowFragment, CanEditPropMixin, DeleteView):
|
||||||
model = SithFile
|
model = SithFile
|
||||||
pk_url_kwarg = "file_id"
|
pk_url_kwarg = "file_id"
|
||||||
template_name = "core/file_delete_confirm.jinja"
|
template_name = "core/file_delete_confirm.jinja"
|
||||||
@ -376,19 +377,24 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["popup"] = ""
|
kwargs["popup"] = "" if self.kwargs.get("popup") is None else "popup"
|
||||||
if self.kwargs.get("popup") is not None:
|
kwargs["next"] = self.request.GET.get("next", None)
|
||||||
kwargs["popup"] = "popup"
|
kwargs["previous"] = self.request.GET.get("previous", None)
|
||||||
|
kwargs["current"] = self.request.path
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class FileModerationView(TemplateView):
|
class FileModerationView(AllowFragment, ListView):
|
||||||
|
model = SithFile
|
||||||
template_name = "core/file_moderation.jinja"
|
template_name = "core/file_moderation.jinja"
|
||||||
|
queryset = SithFile.objects.filter(is_moderated=False, is_in_sas=False)
|
||||||
|
paginate_by = 100
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def dispatch(self, request: HttpRequest, *args, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
user: User = request.user
|
||||||
kwargs["files"] = SithFile.objects.filter(is_moderated=False)[:100]
|
if user.is_root:
|
||||||
return kwargs
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
class FileModerateView(CanEditPropMixin, SingleObjectMixin):
|
class FileModerateView(CanEditPropMixin, SingleObjectMixin):
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"d3-force-3d": "^3.0.5",
|
"d3-force-3d": "^3.0.5",
|
||||||
"easymde": "^2.18.0",
|
"easymde": "^2.18.0",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
|
"htmx.org": "^2.0.3",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-ui": "^1.14.0",
|
"jquery-ui": "^1.14.0",
|
||||||
"jquery.shorten": "^1.0.0",
|
"jquery.shorten": "^1.0.0",
|
||||||
@ -4455,6 +4456,11 @@
|
|||||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/htmx.org": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA=="
|
||||||
|
},
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
"d3-force-3d": "^3.0.5",
|
"d3-force-3d": "^3.0.5",
|
||||||
"easymde": "^2.18.0",
|
"easymde": "^2.18.0",
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
|
"htmx.org": "^2.0.3",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jquery-ui": "^1.14.0",
|
"jquery-ui": "^1.14.0",
|
||||||
"jquery.shorten": "^1.0.0",
|
"jquery.shorten": "^1.0.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user