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:
Antoine Bartuccio 2024-10-13 23:26:18 +02:00
parent c7a8a1a91c
commit 3af5d96bf5
16 changed files with 429 additions and 249 deletions

View File

@ -0,0 +1 @@
window.htmx = require("htmx.org");

View File

@ -5,7 +5,6 @@
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/style.scss') }}">
<link rel="stylesheet" href="{{ static('core/markdown.scss') }}">
@ -23,6 +22,7 @@
<script src="{{ url('javascript-catalog') }}"></script>
<script src={{ static("webpack/core/components/include-index.ts") }}></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 -->
<script src="{{ static('webpack/jquery-index.js') }}"></script>
<!-- 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 -->
<!-- See this workaround https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true -->
{% csrf_token %}
<!-- BEGIN HEADER -->
{% block header %}
{% if not popup %}
<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') }}')">
&nbsp;
</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>
{% include "core/base/header.jinja" %}
{% 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) }}">
<div id="popupheader">{{ user.get_display_name() }}</div>
{% 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 %}
&nbsp;
{% 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 %}
<div id="info_boxes">
@ -195,64 +63,11 @@
{% endif %}
</div>
{% endblock %}
{% else %}
<div id="popupheader">{{ user.get_display_name() }}</div>
{% endif %}
{% endblock %}
<!-- END HEADER -->
{% block nav %}
{% if not popup %}
<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>
{% include "core/base/menu.jinja" %}
{% endif %}
{% endblock %}
@ -265,19 +80,16 @@
</ul>
<div id="content">
{% 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 %}
{% block tabs %}
{% include "core/base/tabs.jinja" %}
{% endblock %}
{% block errors%}
{% if error %}
{{ error }}
{% endif %}
{% endblock %}
{% block content %}
{% endblock %}
</div>

View 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') }}')">
&nbsp;
</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 %}
&nbsp;
{% 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>

View 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>

View 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 %}

View 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 %}

View File

@ -1,4 +1,8 @@
{% extends "core/base.jinja" %}
{% if is_fragment %}
{% extends "core/base_fragment.jinja" %}
{% else %}
{% extends "core/base.jinja" %}
{% endif %}
{% block title %}
{% if file %}
@ -21,7 +25,7 @@
{% endif %}
{% endmacro %}
{% block content %}
{% block tabs %}
{{ print_file_name(file) }}
<div class="tool_bar">
@ -44,6 +48,9 @@
</div>
</div>
<hr>
{% endblock %}
{% block content %}
{% if file %}
{% block file %}

View File

@ -4,15 +4,49 @@
{% trans %}Delete confirmation{% endtrans %}
{% endblock %}
{% if is_fragment %}
{# Don't display tabs and errors #}
{% block tabs %}
{% endblock %}
{% block errors %}
{% endblock %}
{% endif %}
{% block file %}
<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>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
</form>
<form method="GET" action="javascript:history.back();">
<input type="submit" name="cancel" value="{% trans %}Cancel{% endtrans %}" />
<button
{% if is_fragment %}
hx-post="{{ action }}"
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>
{% endblock %}

View File

@ -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 %}
{% trans %}File moderation{% endtrans %}
@ -7,8 +19,11 @@
{% block content %}
<h3>{% trans %}File moderation{% endtrans %}</h3>
<div>
{% for f in files %}
<div style="margin: 2px; padding: 2px; border: solid 1px red; text-align: center">
{% for f in object_list %}
<div
id="file-{{ loop.index }}"
style="margin: 2px; padding: 2px; border: solid 1px red; text-align: center"
>
{% if f.is_folder %}
<strong>Folder</strong>
{% else %}
@ -20,9 +35,19 @@
{% trans %}Owner: {% endtrans %}{{ f.owner.get_display_name() }}<br/>
{% trans %}Date: {% endtrans %}{{ f.date|date(DATE_FORMAT) }} {{ f.date|time(TIME_FORMAT) }}<br/>
</p>
<p><a href="{{ url('core:file_moderate', file_id=f.id) }}">{% trans %}Moderate{% endtrans %}</a> -
<a href="{{ url('core:file_delete', file_id=f.id) }}?next={{ url('core:file_moderation') }}">{% trans %}Delete{% endtrans %}</a></p>
<p><button
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>
{% endfor %}
{{ paginate_htmx(page_obj, paginator) }}
</div>
{% endblock %}

View File

@ -166,9 +166,37 @@
current_page (django.core.paginator.Page): the current page 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">
{% 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>
<i class="fa fa-caret-left"></i>
</button>
@ -182,14 +210,31 @@
{% elif i == paginator.ELLIPSIS %}
<strong>{{ paginator.ELLIPSIS }}</strong>
{% 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>
</a>
{% endif %}
{% endfor %}
{% if current_page.has_next() %}
<a href="?page={{ current_page.next_page_number() }}">
<button>
<a
{% 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>
</button>
</a>

View File

@ -77,7 +77,7 @@
{% set default_picture = this_picture.get_download_url()|tojson %}
{% set delete_url = (
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 %}
{%- else -%}
{% set default_picture = static('core/img/unknown.jpg')|tojson %}

View File

@ -142,6 +142,30 @@ class TestFileHandling(TestCase):
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
class TestUserProfilePicture:
"""Test interactions with user's profile picture."""

View File

@ -326,6 +326,12 @@ class DetailFormView(SingleObjectMixin, FormView):
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
# E402: putting those import at the top of the file would also be difficult
from .files import * # noqa: F403 E402

View File

@ -27,12 +27,13 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.http import http_date
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.edit import DeleteView, FormMixin, UpdateView
from core.models import Notification, RealGroup, SithFile
from core.models import Notification, RealGroup, SithFile, User
from core.views import (
AllowFragment,
CanEditMixin,
CanEditPropMixin,
CanViewMixin,
@ -352,7 +353,7 @@ class FileView(CanViewMixin, DetailView, FormMixin):
return kwargs
class FileDeleteView(CanEditPropMixin, DeleteView):
class FileDeleteView(AllowFragment, CanEditPropMixin, DeleteView):
model = SithFile
pk_url_kwarg = "file_id"
template_name = "core/file_delete_confirm.jinja"
@ -376,19 +377,24 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["popup"] = ""
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
kwargs["popup"] = "" if self.kwargs.get("popup") is None else "popup"
kwargs["next"] = self.request.GET.get("next", None)
kwargs["previous"] = self.request.GET.get("previous", None)
kwargs["current"] = self.request.path
return kwargs
class FileModerationView(TemplateView):
class FileModerationView(AllowFragment, ListView):
model = SithFile
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):
kwargs = super().get_context_data(**kwargs)
kwargs["files"] = SithFile.objects.filter(is_moderated=False)[:100]
return kwargs
def dispatch(self, request: HttpRequest, *args, **kwargs):
user: User = request.user
if user.is_root:
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied()
class FileModerateView(CanEditPropMixin, SingleObjectMixin):

6
package-lock.json generated
View File

@ -22,6 +22,7 @@
"d3-force-3d": "^3.0.5",
"easymde": "^2.18.0",
"glob": "^11.0.0",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0",
@ -4455,6 +4456,11 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"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": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",

View File

@ -54,6 +54,7 @@
"d3-force-3d": "^3.0.5",
"easymde": "^2.18.0",
"glob": "^11.0.0",
"htmx.org": "^2.0.3",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.0",
"jquery.shorten": "^1.0.0",