Merge pull request #757 from ae-utbm/taiste

Taiste
This commit is contained in:
thomas girod 2024-08-04 16:51:36 +02:00 committed by GitHub
commit eb04e26b22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 475 additions and 235 deletions

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from club.models import Club, Membership
@ -32,4 +31,4 @@ class MembershipAdmin(admin.ModelAdmin):
"user__last_name",
"club__name",
)
form = make_ajax_form(Membership, {"user": "users"})
autocomplete_fields = ("user",)

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from haystack.admin import SearchModelAdmin
@ -23,19 +22,13 @@ from com.models import *
class NewsAdmin(SearchModelAdmin):
list_display = ("title", "type", "club", "author")
search_fields = ("title", "summary", "content")
form = make_ajax_form(
News,
{
"author": "users",
"moderator": "users",
},
)
autocomplete_fields = ("author", "moderator")
@admin.register(Poster)
class PosterAdmin(SearchModelAdmin):
list_display = ("name", "club", "date_begin", "date_end", "moderator")
form = make_ajax_form(Poster, {"moderator": "users"})
autocomplete_fields = ("moderator",)
@admin.register(Weekmail)

View File

@ -13,30 +13,30 @@
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from django.contrib.auth.models import Group as AuthGroup
from haystack.admin import SearchModelAdmin
from core.models import MetaGroup, Page, RealGroup, SithFile, User
from core.models import Group, Page, SithFile, User
admin.site.unregister(AuthGroup)
admin.site.register(MetaGroup)
admin.site.register(RealGroup)
@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
list_display = ("name", "description", "is_meta")
list_filter = ("is_meta",)
search_fields = ("name",)
@admin.register(User)
class UserAdmin(SearchModelAdmin):
class UserAdmin(admin.ModelAdmin):
list_display = ("first_name", "last_name", "username", "email", "nick_name")
form = make_ajax_form(
User,
{
"godfathers": "users",
"home": "files", # ManyToManyField
"profile_pict": "files", # ManyToManyField
"avatar_pict": "files", # ManyToManyField
"scrub_pict": "files", # ManyToManyField
},
autocomplete_fields = (
"godfathers",
"home",
"profile_pict",
"avatar_pict",
"scrub_pict",
)
search_fields = ["first_name", "last_name", "username"]
@ -44,25 +44,12 @@ class UserAdmin(SearchModelAdmin):
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
list_display = ("name", "_full_name", "owner_group")
form = make_ajax_form(
Page,
{
"lock_user": "users",
"owner_group": "groups",
"edit_groups": "groups",
"view_groups": "groups",
},
)
search_fields = ("name",)
autocomplete_fields = ("lock_user", "owner_group", "edit_groups", "view_groups")
@admin.register(SithFile)
class SithFileAdmin(admin.ModelAdmin):
list_display = ("name", "owner", "size", "date", "is_in_sas")
form = make_ajax_form(
SithFile,
{
"parent": "files",
"owner": "users",
"moderator": "users",
},
) # ManyToManyField
autocomplete_fields = ("parent", "owner", "moderator")
search_fields = ("name", "parent__name")

View File

@ -28,3 +28,5 @@ $twitblue: hsl(206, 82%, 63%);
$shadow-color: rgb(223, 223, 223);
$background-button-color: hsl(0, 0%, 95%);
$deepblue: #354a5f;

View File

@ -1,7 +1,8 @@
@import "colors";
$hovered-text-color: #c2c2c2;
$text-color: white;
$background-color: #354a5f;
$background-color-hovered: #283747;
$red-text-color: #eb2f06;
@ -9,7 +10,7 @@ $hovered-red-text-color: #ff4d4d;
.header {
box-sizing: border-box;
background-color: $background-color;
background-color: $deepblue;
box-shadow: 3px 3px 3px 0 #dfdfdf;
border-radius: 0;
width: 100%;
@ -98,7 +99,7 @@ $hovered-red-text-color: #ff4d4d;
border-radius: 0;
margin: 0;
box-sizing: border-box;
background-color: $background-color;
background-color: $deepblue;
width: 45px;
height: 25px;
padding: 0;
@ -213,7 +214,7 @@ $hovered-red-text-color: #ff4d4d;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
background-color: $background-color;
background-color: $deepblue;
}
>.options {

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.JSZipUtils=e():"undefined"!=typeof global?global.JSZipUtils=e():"undefined"!=typeof self&&(self.JSZipUtils=e())}(function(){return function o(i,f,u){function s(n,e){if(!f[n]){if(!i[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(a)return a(n,!0);throw new Error("Cannot find module '"+n+"'")}var r=f[n]={exports:{}};i[n][0].call(r.exports,function(e){var t=i[n][1][e];return s(t||e)},r,r.exports,o,i,f,u)}return f[n].exports}for(var a="function"==typeof require&&require,e=0;e<u.length;e++)s(u[e]);return s}({1:[function(e,t,n){"use strict";var u={};function r(){try{return new window.XMLHttpRequest}catch(e){}}u._getBinaryFromXHR=function(e){return e.response||e.responseText};var s="undefined"!=typeof window&&window.ActiveXObject?function(){return r()||function(){try{return new window.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}()}:r;u.getBinaryContent=function(t,n){var e,r,o,i;"function"==typeof(n=n||{})?(i=n,n={}):"function"==typeof n.callback&&(i=n.callback),i||"undefined"==typeof Promise?(r=function(e){i(null,e)},o=function(e){i(e,null)}):e=new Promise(function(e,t){r=e,o=t});try{var f=s();f.open("GET",t,!0),"responseType"in f&&(f.responseType="arraybuffer"),f.overrideMimeType&&f.overrideMimeType("text/plain; charset=x-user-defined"),f.onreadystatechange=function(e){if(4===f.readyState)if(200===f.status||0===f.status)try{r(u._getBinaryFromXHR(f))}catch(e){o(new Error(e))}else o(new Error("Ajax error for "+t+" : "+this.status+" "+this.statusText))},n.progress&&(f.onprogress=function(e){n.progress({path:t,originalEvent:e,percent:e.loaded/e.total*100,loaded:e.loaded,total:e.total})}),f.send()}catch(e){o(new Error(e),null)}return e},t.exports=u},{}]},{},[1])(1)});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2023, Gildas Lormeau
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,28 @@
# Built scripts of zip.js
**Warning**: These files are not compatible with ES modules, i.e. they cannot be imported with `import`. Instead, import `index.js` in the root folder of the project or one of the files prefixed with `zip-` in the [`/lib`](../lib) folder (e.g. `/lib/zip-no-worker-inflate.js`).
- for production (minified):
| | [`ZipReader`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipReader.html) API | [`ZipWriter`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipWriter.html) API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers | Usage |
|--------------------------------|-----------------|-----------------|--------------|-------------|----------------|-------------------------------------------------------|
| `zip.min.js` | x | x | | x | | compression/decompression with web workers |
| `zip-no-worker.min.js` | x | x | | | x | compression/decompression without web workers |
| `zip-no-worker-inflate.min.js` | x | | | | x | decompression without web workers |
| `zip-no-worker-deflate.min.js` | | x | | | x | compression without web workers |
| `zip-full.min.js` | x | x | | x | x | compression/decompression with or without web workers |
| `zip-fs.min.js` | x | x | x | x | | compression/decompression with web workers |
| `zip-fs-full.min.js` | x | x | x | x | x | compression/decompression with or without web workers |
- for development/debugging:
| | `zip` API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers |
|-----------------------|-----------|--------------|-------------|----------------|
| `zip.js` | x | | x | |
| `zip-full.js` | x | | x | x |
| `zip-fs.js` | x | x | x | |
| `zip-fs-full.js` | x | x | x | x |
- `z-worker.js` can be used as a web worker script if the [Content Security Policy](https://developer.mozilla.org/docs/Web/HTTP/CSP) blocks scripts loaded with a `blob:` scheme
- `z-worker-fflate.js` is the web worker script for using [fflate](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-fflate)
- `z-worker-pako.js` is the web worker script for using [pako](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-pako)

File diff suppressed because one or more lines are too long

View File

@ -105,7 +105,7 @@ a:not(.button) {
.collapse-header {
color: white;
background-color: #354a5f;
background-color: $deepblue;
padding: 5px 10px;
display: flex;
align-items: center;
@ -206,34 +206,36 @@ a:not(.button) {
width: 90%;
margin: 20px auto 0;
/*---------------------------------NAV---------------------------------*/
.btn {
font-size: 15px;
font-weight: normal;
color: white;
min-width: 60px;
padding: 5px 10px;
padding: 9px 13px;
border: none;
text-decoration: none;
&.btn-blue {
background-color: #354a5f;
background-color: $deepblue;
&:not(:disabled):hover {
background-color: darken($deepblue, 10%);
}
&.btn-blue:disabled {
&:disabled {
background-color: rgba(70, 90, 126, 0.4);
}
&.btn-blue.clickable:not(:disabled):hover {
background-color: #2c3646;
}
&.btn-grey {
background-color: grey;
&:not(:disabled):hover {
background-color: darken(gray, 15%);
}
&:disabled {
background-color: lighten(gray, 15%);
}
}
&.btn-grey.clickable:not(:disabled):hover {
background-color: hsl(210, 5%, 30%);
i {
margin-right: 4px;
}
}
@ -977,7 +979,7 @@ thead td {
}
thead {
background-color: #354a5f;
background-color: $deepblue;
color: white;
}

View File

@ -5,12 +5,12 @@
{%- endblock -%}
{% block additional_js %}
<script defer src="{{ static('core/js/jszip/jszip.min.js') }}"></script>
<script defer src="{{ static('core/js/jszip/jszip-utils.min.js') }}"></script>
<script defer type="module">
import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}";
window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */
</script>
<script defer type="text/javascript" src="{{ static('core/js/zipjs/zip-fs-full.min.js') }}"></script>
<script defer src="{{ static("core/js/alpinejs.min.js") }}"></script>
{% endblock %}
{% block title %}
@ -19,11 +19,22 @@
{% block content %}
<main>
{% if can_edit(profile, user) %}
<button disabled id="download" onclick="download('{{ url('api:pictures') }}?users_identified={{ object.id }}')">{% trans %}Download all my pictures{% endtrans %}</button>
{% if user.id == object.id and albums|length > 0 %}
<div x-data="picture_download" x-cloak>
<button
:disabled="in_progress"
class="btn btn-blue"
@click="download('{{ url("api:pictures") }}?users_identified={{ object.id }}')"
>
<i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %}
</button>
<progress x-ref="progress" x-show="in_progress"></progress>
</div>
{% endif %}
{% for album, pictures in albums|items %}
<h4>{{ album }}</h4>
<br />
<div class="photos">
{% for picture in pictures %}
{% if picture.can_be_viewed_by(user) %}
@ -51,52 +62,62 @@
</div>
<br>
{% endfor %}
</main>
{% endblock content %}
{% block script %}
{{ super() }}
{% if user.id == object.id %}
<script>
document.addEventListener("DOMContentLoaded", () => {
/* Enable button once everything is loaded and if JSZip is supported */
document.getElementById("download").disabled = !JSZip.support.blob;
});
async function download(url) {
/**
* @typedef Picture
* @property {number} id
* @property {string} name
* @property {number} size
* @property {string} date
* @property {Object} author
* @property {string} full_size_url
* @property {string} compressed_url
* @property {string} thumb_url
* @property {string} album
*/
let zip = new JSZip();
let size = 0;
let pictures = await (await fetch(url)).json();
pictures.forEach(async (picture) => {
size += picture.size;
zip.file(
"IMG_" + picture.date + picture.name.slice(picture.name.lastIndexOf(".")),
new Promise(function (resolve, reject) {
JSZipUtils.getBinaryContent(picture.full_size_url, (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data);
})
}),
{ binary: true }
);
});
document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({
in_progress: false,
let fileHandle = await window.showSaveFilePicker({
async download(url) {
this.in_progress = true;
const bar = this.$refs.progress;
bar.value = 0;
/** @type Picture[] */
const pictures = await (await fetch(url)).json();
bar.max = pictures.length;
const fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
let writeStream = await fileHandle.createWritable();
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
await zip.generateInternalStream({
type: "uint8array",
streamFiles: true,
compression: "DEFLATE",
compressionOptions: { level: 9 }
})
.on("data", (data) => writeStream.write(data))
.on("error", (err) => console.error(err))
.on("end", () => writeStream.close())
.resume();
await Promise.all(pictures.map(p => {
const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add(
img_name,
new zip.HttpReader(p.full_size_url),
{level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1}
);
}));
await zipWriter.close();
this.in_progress = false;
}
}))
});
</script>
</main>
{% endblock %}
{% endif %}
{% endblock script %}

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from haystack.admin import SearchModelAdmin
@ -41,7 +40,7 @@ class CustomerAdmin(SearchModelAdmin):
"user__first_name",
"user__last_name",
)
form = make_ajax_form(Customer, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(BillingInfo)
@ -52,18 +51,13 @@ class BillingInfoAdmin(admin.ModelAdmin):
@admin.register(Counter)
class CounterAdmin(admin.ModelAdmin):
list_display = ("name", "club", "type")
form = make_ajax_form(
Counter,
{
"products": "products",
"sellers": "users",
},
)
autocomplete_fields = ("products", "sellers")
@admin.register(Refilling)
class RefillingAdmin(SearchModelAdmin):
list_display = ("customer", "amount", "counter", "payment_method", "date")
autocomplete_fields = ("customer", "operator")
search_fields = (
"customer__user__username",
"customer__user__first_name",
@ -71,13 +65,6 @@ class RefillingAdmin(SearchModelAdmin):
"customer__account_id",
"counter__name",
)
form = make_ajax_form(
Refilling,
{
"customer": "customers",
"operator": "users",
},
)
@admin.register(Selling)
@ -90,13 +77,7 @@ class SellingAdmin(SearchModelAdmin):
"customer__account_id",
"counter__name",
)
form = make_ajax_form(
Selling,
{
"customer": "customers",
"seller": "users",
},
)
autocomplete_fields = ("customer", "seller")
@admin.register(Permanency)
@ -108,7 +89,7 @@ class PermanencyAdmin(SearchModelAdmin):
"user__last_name",
"counter__name",
)
form = make_ajax_form(Permanency, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(ProductType)
@ -125,7 +106,7 @@ class CashRegisterSummaryAdmin(SearchModelAdmin):
"user__last_name",
"counter__name",
)
form = make_ajax_form(CashRegisterSummary, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(Eticket)

View File

@ -16,12 +16,15 @@ import json
import re
import string
from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import timedelta
from model_bakery import baker
from club.models import Club
from club.models import Club, Membership
from core.baker_recipes import subscriber_user
from core.models import User
from counter.models import BillingInfo, Counter, Customer, Permanency, Product, Selling
from sith.settings import SITH_MAIN_CLUB
@ -911,3 +914,47 @@ class TestCustomerAccountId(TestCase):
assert created is False
assert account.account_id == "1111a"
assert account.amount == 10
class TestClubCounterClickAccess(TestCase):
@classmethod
def setUpTestData(cls):
cls.counter = baker.make(Counter, type="OFFICE")
cls.customer = subscriber_user.make()
cls.counter_url = reverse(
"counter:details", kwargs={"counter_id": cls.counter.id}
)
cls.click_url = reverse(
"counter:click",
kwargs={"counter_id": cls.counter.id, "user_id": cls.customer.id},
)
cls.user = subscriber_user.make()
def setUp(self):
cache.clear()
def test_anonymous(self):
res = self.client.get(self.click_url)
assert res.status_code == 403
def test_logged_in_without_rights(self):
self.client.force_login(self.user)
res = self.client.get(self.click_url)
assert res.status_code == 403
# being a member of the club, without being in the board, isn't enough
baker.make(Membership, club=self.counter.club, user=self.user, role=1)
res = self.client.get(self.click_url)
assert res.status_code == 403
def test_board_member(self):
baker.make(Membership, club=self.counter.club, user=self.user, role=3)
self.client.force_login(self.user)
res = self.client.get(self.click_url)
assert res.status_code == 200
def test_barman(self):
self.counter.sellers.add(self.user)
self.client.force_login(self.user)
res = self.client.get(self.click_url)
assert res.status_code == 200

View File

@ -329,7 +329,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
raise Http404
if obj.type != "BAR" and not request.user.is_authenticated:
raise PermissionDenied
if (
if obj.type == "BAR" and (
"counter_token" not in request.session
or request.session["counter_token"] != obj.token
or len(obj.barmen_list) == 0

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from eboutic.models import *
@ -21,7 +20,7 @@ from eboutic.models import *
@admin.register(Basket)
class BasketAdmin(admin.ModelAdmin):
list_display = ("user", "date", "get_total")
form = make_ajax_form(Basket, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(BasketItem)
@ -34,7 +33,7 @@ class BasketItemAdmin(admin.ModelAdmin):
class InvoiceAdmin(admin.ModelAdmin):
list_display = ("user", "date", "validated")
search_fields = ("user__username", "user__first_name", "user__last_name")
form = make_ajax_form(Invoice, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(InvoiceItem)

View File

@ -136,7 +136,7 @@
right: 5px;
padding: 5px;
border-radius: 50%;
box-shadow: 0px 0px 12px 2px rgb(0 0 0 / 14%);
box-shadow: 0 0 12px 2px rgb(0 0 0 / 14%);
background-color: white;
width: 20px;
height: 20px;
@ -186,29 +186,7 @@
}
#eboutic .catalog-buttons button {
font-size: 15px!important;
font-weight: normal;
color: white;
min-width: 60px;
padding: 10px 15px;
}
#eboutic .catalog-buttons .validate {
background-color: #354a5f;
}
#eboutic .catalog-buttons .clear {
background-color: gray;
}
#eboutic .catalog-buttons button i {
margin-right: 4px;
}
#eboutic .catalog-buttons button.validate:hover {
background-color: #2c3646;
}
#eboutic .catalog-buttons button.clear:hover {
background-color:hsl(210,5%,30%);
}
#eboutic .catalog-buttons form {
@ -252,7 +230,7 @@
}
#eboutic .product-image {
margin-bottom: 0px;
margin-bottom: 0;
max-width: 70px;
}
}

View File

@ -62,13 +62,13 @@
</li>
</ul>
<div class="catalog-buttons">
<button @click="clear_basket()" class="clear">
<button @click="clear_basket()" class="btn btn-grey">
<i class="fa fa-trash"></i>
{% trans %}Clear{% endtrans %}
</button>
<form method="get" action="{{ url('eboutic:command') }}">
{% csrf_token %}
<button class="validate">
<button class="btn btn-blue">
<i class="fa fa-check"></i>
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
</button>

View File

@ -1,4 +1,3 @@
from ajax_select import make_ajax_form
from django.contrib import admin
from election.models import Candidature, Election, ElectionList, Role
@ -13,7 +12,7 @@ class ElectionAdmin(admin.ModelAdmin):
"is_vote_finished",
"archived",
)
form = make_ajax_form(Election, {"voters": "users"})
autocomplete_fields = ("voters",)
@admin.register(Role)
@ -31,7 +30,7 @@ class ElectionListAdmin(admin.ModelAdmin):
@admin.register(Candidature)
class CandidatureAdmin(admin.ModelAdmin):
list_display = ("user", "role", "election_list")
form = make_ajax_form(Candidature, {"user": "users"})
autocomplete_fields = ("user",)
# Votes must stay fully anonymous, so no ModelAdmin for Vote model

View File

@ -19,19 +19,16 @@ from haystack.admin import SearchModelAdmin
from forum.models import *
@admin.register(Forum)
class ForumAdmin(SearchModelAdmin):
search_fields = ["name", "description"]
@admin.register(ForumTopic)
class ForumTopicAdmin(SearchModelAdmin):
search_fields = ["_title", "description"]
@admin.register(ForumMessage)
class ForumMessageAdmin(SearchModelAdmin):
search_fields = ["title", "message"]
admin.site.register(Forum, ForumAdmin)
admin.site.register(ForumTopic, ForumTopicAdmin)
admin.site.register(ForumMessage, ForumMessageAdmin)
admin.site.register(ForumUserInfo)

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from launderette.models import *
@ -31,10 +30,10 @@ class MachineAdmin(admin.ModelAdmin):
@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_display = ("name", "launderette", "type", "user")
form = make_ajax_form(Token, {"user": "users"})
autocomplete_fields = ("user",)
@admin.register(Slot)
class SlotAdmin(admin.ModelAdmin):
list_display = ("machine", "user", "start_date")
form = make_ajax_form(Slot, {"user": "users"})
autocomplete_fields = ("user",)

View File

@ -20,7 +20,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from haystack.admin import SearchModelAdmin
@ -31,7 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
class UVAdmin(admin.ModelAdmin):
list_display = ("code", "title", "credit_type", "credits", "department")
search_fields = ("code", "title", "department")
form = make_ajax_form(UV, {"author": "users"})
autocomplete_fields = ("author",)
@admin.register(UVComment)
@ -43,7 +42,7 @@ class UVCommentAdmin(admin.ModelAdmin):
"author__last_name",
"uv__code",
)
form = make_ajax_form(UVComment, {"author": "users"})
autocomplete_fields = ("author",)
@admin.register(UVCommentReport)
@ -55,4 +54,4 @@ class UVCommentReportAdmin(SearchModelAdmin):
"reporter__last_name",
"comment__uv__code",
)
form = make_ajax_form(UVCommentReport, {"reporter": "users"})
autocomplete_fields = ("reporter",)

View File

@ -1,6 +1,8 @@
from typing import Literal
from django.db.models import Q
from django.utils import html
from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
from pydantic.alias_generators import to_camel
@ -120,6 +122,27 @@ class UvFilterSchema(FilterSchema):
language: str = "FR"
department: set[str] | None = Field(None, q="department__in")
def filter_search(self, value: str | None) -> Q:
"""Special filter for the search text.
It does a full text search if available.
"""
if not value:
return Q()
if len(value) < 3 or (len(value) < 5 and any(c.isdigit() for c in value)):
# Likely to be an UV code
return Q(code__istartswith=value)
qs = list(
SearchQuerySet()
.models(UV)
.autocomplete(auto=html.escape(value))
.values_list("pk", flat=True)
)
return Q(id__in=qs)
def filter_semester(self, value: set[str] | None) -> Q:
"""Special filter for the semester.

View File

@ -2,6 +2,7 @@ import json
from django.conf import settings
from django.test import TestCase
from django.test.testcases import call_command
from django.urls import reverse
from model_bakery import baker
from model_bakery.recipe import Recipe
@ -21,16 +22,31 @@ class TestUVSearch(TestCase):
uv_recipe = Recipe(UV, author=cls.root)
uvs = [
uv_recipe.prepare(
code="AP4A", credit_type="CS", semester="AUTUMN", department="GI"
code="AP4A",
credit_type="CS",
semester="AUTUMN",
department="GI",
manager="francky",
title="Programmation Orientée Objet: Concepts fondamentaux et mise en pratique avec le langage C++",
),
uv_recipe.prepare(
code="MT01", credit_type="CS", semester="AUTUMN", department="TC"
code="MT01",
credit_type="CS",
semester="AUTUMN",
department="TC",
manager="ben",
title="Intégration1. Algèbre linéaire - Fonctions de deux variables",
),
uv_recipe.prepare(
code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC"
),
uv_recipe.prepare(
code="TNEV", credit_type="TM", semester="SPRING", department="TC"
code="TNEV",
credit_type="TM",
semester="SPRING",
department="TC",
manager="moss",
title="tnetennba",
),
uv_recipe.prepare(
code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI"
@ -40,9 +56,11 @@ class TestUVSearch(TestCase):
credit_type="TM",
semester="AUTUMN_AND_SPRING",
department="GI",
manager="francky",
),
]
UV.objects.bulk_create(uvs)
call_command("update_index")
def test_permissions(self):
# Test with anonymous user
@ -92,14 +110,22 @@ class TestUVSearch(TestCase):
],
}
def test_search_by_code(self):
def test_search_by_text(self):
self.client.force_login(self.root)
res = self.client.get(self.url + "?search=MT")
for query, expected in (
# UV code search case insensitive
("m", {"MT01", "MT10"}),
("M", {"MT01", "MT10"}),
("mt", {"MT01", "MT10"}),
("MT", {"MT01", "MT10"}),
("algèbre", {"MT01"}), # Title search case insensitive
# Manager search
("moss", {"TNEV"}),
("francky", {"DA50", "AP4A"}),
):
res = self.client.get(self.url + f"?search={query}")
assert res.status_code == 200
assert {uv["code"] for uv in json.loads(res.content)["results"]} == {
"MT01",
"MT10",
}
assert {uv["code"] for uv in json.loads(res.content)["results"]} == expected
def test_search_by_credit_type(self):
self.client.force_login(self.root)

152
poetry.lock generated
View File

@ -57,6 +57,17 @@ six = ">=1.12.0"
astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"]
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "babel"
version = "2.15.0"
@ -499,18 +510,14 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-ajax-selects"
version = "3.0.2"
version = "2.2.1"
description = "Edit ForeignKey, ManyToManyField and CharField in Django Admin using jQuery UI AutoComplete."
optional = false
python-versions = ">=3.10,<4.0"
python-versions = "*"
files = [
{file = "django_ajax_selects-3.0.2-py3-none-any.whl", hash = "sha256:83da065b3fe6bdee5996662734eb18ee70fee510171c179a02f1ce45dcaa2870"},
{file = "django_ajax_selects-3.0.2.tar.gz", hash = "sha256:8554659f5c7da50cfe1f0d0e14c6f360d0f2ab2d94b24e3203cc4fe974bd945a"},
{file = "django-ajax-selects-2.2.1.tar.gz", hash = "sha256:996ffb38dff1a621b358613afdf2681dbf261e5976da3c30a75e9b08fd81a887"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-countries"
version = "7.6.1"
@ -821,6 +828,109 @@ files = [
backports-strenum = {version = ">=1.3", markers = "python_version < \"3.11\""}
colorama = ">=0.4"
[[package]]
name = "hiredis"
version = "3.0.0"
description = "Python wrapper for hiredis"
optional = false
python-versions = ">=3.8"
files = [
{file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"},
{file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"},
{file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"},
{file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"},
{file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"},
{file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"},
{file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"},
{file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"},
{file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"},
{file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"},
{file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"},
{file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"},
{file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"},
{file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"},
{file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"},
{file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"},
{file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"},
{file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"},
{file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"},
{file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"},
{file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"},
{file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"},
{file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"},
{file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"},
{file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"},
{file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"},
{file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"},
{file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"},
{file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"},
{file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"},
{file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"},
{file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"},
{file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"},
{file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"},
{file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"},
{file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"},
{file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"},
{file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"},
{file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"},
{file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"},
{file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"},
{file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"},
{file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"},
{file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"},
{file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"},
{file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"},
{file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"},
{file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"},
{file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"},
{file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"},
{file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"},
{file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"},
{file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"},
{file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"},
{file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"},
{file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"},
{file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"},
{file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"},
{file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"},
{file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"},
{file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"},
{file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"},
{file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"},
{file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"},
{file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"},
{file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"},
{file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"},
{file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"},
{file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"},
{file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"},
{file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"},
{file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"},
{file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"},
{file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"},
{file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"},
{file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"},
{file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"},
{file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"},
{file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"},
]
[[package]]
name = "identify"
version = "2.6.0"
@ -1483,13 +1593,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.7.1"
version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
{file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
]
[package.dependencies]
@ -1892,6 +2002,7 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1940,6 +2051,25 @@ files = [
[package.dependencies]
pyyaml = "*"
[[package]]
name = "redis"
version = "5.0.8"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
files = [
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
]
[package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""}
hiredis = {version = ">1.0.0", optional = true, markers = "extra == \"hiredis\""}
[package.extras]
hiredis = ["hiredis (>1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
name = "regex"
version = "2024.7.24"
@ -2502,4 +2632,4 @@ filelock = ">=3.4"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "5e90eff0d3e11e48c1467165d121f1cff094cf83834b5df517708e72bc82425f"
content-hash = "a9573a584420b00b0bd5bb85a0fb2daedb365bd1ff604b94ec23c187bc4dd991"

View File

@ -30,7 +30,7 @@ django-jinja = "^2.11"
cryptography = "^43.0.0"
django-phonenumber-field = "^8.0.0"
phonenumbers = "^8.12"
django-ajax-selects = "^3.0.2"
django-ajax-selects = "^2.2.1"
reportlab = "^4.2"
django-haystack = "^3.2.1"
xapian-haystack = "^3.0.1"
@ -50,6 +50,7 @@ django-honeypot = "^1.2.0"
[tool.poetry.group.prod.dependencies]
# deps used in prod, but unnecessary for development
psycopg2-binary = "^2.9"
redis = {extras = ["hiredis"], version = "^5.0.8"}
[tool.poetry.group.prod]
optional = true
@ -58,7 +59,7 @@ optional = true
# deps used for development purposes, but unneeded in prod
django-debug-toolbar = "^4.4.6"
ipython = "^8.26.0"
pre-commit = "^3.7.1"
pre-commit = "^3.8.0"
ruff = "^0.5.5" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
djhtml = "^3.0.6"
faker = "^26.0.0"

View File

@ -15,8 +15,20 @@
from django.contrib import admin
from sas.models import *
from sas.models import Album, PeoplePictureRelation, Picture
@admin.register(Picture)
class PictureAdmin(admin.ModelAdmin):
list_display = ("name", "parent", "date", "size", "is_moderated")
search_fields = ("name",)
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups", "moderator")
@admin.register(PeoplePictureRelation)
class PeoplePictureRelationAdmin(admin.ModelAdmin):
list_display = ("picture", "user")
autocomplete_fields = ("picture", "user")
admin.site.register(Album)
# admin.site.register(Picture)
admin.site.register(PeoplePictureRelation)

View File

@ -1,4 +1,5 @@
from django.conf import settings
from django.db.models import F
from ninja import Query
from ninja_extra import ControllerBase, api_controller, route
from ninja_extra.exceptions import PermissionDenied
@ -47,6 +48,7 @@ class PicturesController(ControllerBase):
)
.distinct()
.order_by("-date")
.annotate(album=F("parent__name"))
)
for picture in pictures:
picture.full_size_url = picture.get_download_url()

View File

@ -23,6 +23,7 @@ class PictureSchema(ModelSchema):
full_size_url: str
compressed_url: str
thumb_url: str
album: str
class PictureCreateRelationSchema(Schema):

View File

@ -203,8 +203,6 @@ SASS_PRECISION = 8
WSGI_APPLICATION = "sith.wsgi.application"
REST_FRAMEWORK = {}
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
@ -215,6 +213,8 @@ DATABASES = {
}
}
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
# Logging
LOGGING = {
"version": 1,
@ -280,7 +280,7 @@ LOGOUT_URL = "/logout"
LOGIN_REDIRECT_URL = "/"
DEFAULT_FROM_EMAIL = "bibou@git.an"
SITH_COM_EMAIL = "bibou_com@git.an"
REST_FRAMEWORK["UNAUTHENTICATED_USER"] = "core.models.AnonymousUser"
# Those values are to be changed in production to be more effective
HONEYPOT_FIELD_NAME = "body2"
HONEYPOT_VALUE = "content"
@ -724,8 +724,7 @@ if SENTRY_DSN:
)
SITH_FRONT_DEP_VERSIONS = {
"https://github.com/Stuk/jszip-utils": "0.1.0",
"https://github.com/Stuk/jszip": "3.10.1",
"https://github.com/gildas-lormeau/zip.js": "2.7.47",
"https://github.com/jimmywarting/native-file-system-adapter": "3.0.1",
"https://github.com/chartjs/Chart.js/": "2.6.0",
"https://github.com/Ionaru/easy-markdown-editor/": "2.18.0",

View File

@ -12,7 +12,6 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from ajax_select import make_ajax_form
from django.contrib import admin
from subscription.models import Subscription
@ -33,4 +32,4 @@ class SubscriptionAdmin(admin.ModelAdmin):
"subscription_end",
"subscription_type",
)
form = make_ajax_form(Subscription, {"member": "users"})
autocomplete_fields = ("member",)