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" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from club.models import Club, Membership from club.models import Club, Membership
@ -32,4 +31,4 @@ class MembershipAdmin(admin.ModelAdmin):
"user__last_name", "user__last_name",
"club__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" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from haystack.admin import SearchModelAdmin from haystack.admin import SearchModelAdmin
@ -23,19 +22,13 @@ from com.models import *
class NewsAdmin(SearchModelAdmin): class NewsAdmin(SearchModelAdmin):
list_display = ("title", "type", "club", "author") list_display = ("title", "type", "club", "author")
search_fields = ("title", "summary", "content") search_fields = ("title", "summary", "content")
form = make_ajax_form( autocomplete_fields = ("author", "moderator")
News,
{
"author": "users",
"moderator": "users",
},
)
@admin.register(Poster) @admin.register(Poster)
class PosterAdmin(SearchModelAdmin): class PosterAdmin(SearchModelAdmin):
list_display = ("name", "club", "date_begin", "date_end", "moderator") list_display = ("name", "club", "date_begin", "date_end", "moderator")
form = make_ajax_form(Poster, {"moderator": "users"}) autocomplete_fields = ("moderator",)
@admin.register(Weekmail) @admin.register(Weekmail)

View File

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

View File

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

View File

@ -1,7 +1,8 @@
@import "colors";
$hovered-text-color: #c2c2c2; $hovered-text-color: #c2c2c2;
$text-color: white; $text-color: white;
$background-color: #354a5f;
$background-color-hovered: #283747; $background-color-hovered: #283747;
$red-text-color: #eb2f06; $red-text-color: #eb2f06;
@ -9,7 +10,7 @@ $hovered-red-text-color: #ff4d4d;
.header { .header {
box-sizing: border-box; box-sizing: border-box;
background-color: $background-color; background-color: $deepblue;
box-shadow: 3px 3px 3px 0 #dfdfdf; box-shadow: 3px 3px 3px 0 #dfdfdf;
border-radius: 0; border-radius: 0;
width: 100%; width: 100%;
@ -98,7 +99,7 @@ $hovered-red-text-color: #ff4d4d;
border-radius: 0; border-radius: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
background-color: $background-color; background-color: $deepblue;
width: 45px; width: 45px;
height: 25px; height: 25px;
padding: 0; padding: 0;
@ -213,7 +214,7 @@ $hovered-red-text-color: #ff4d4d;
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: $background-color; background-color: $deepblue;
} }
>.options { >.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 { .collapse-header {
color: white; color: white;
background-color: #354a5f; background-color: $deepblue;
padding: 5px 10px; padding: 5px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -206,34 +206,36 @@ a:not(.button) {
width: 90%; width: 90%;
margin: 20px auto 0; margin: 20px auto 0;
/*---------------------------------NAV---------------------------------*/ /*---------------------------------NAV---------------------------------*/
.btn { .btn {
font-size: 15px; font-size: 15px;
font-weight: normal; font-weight: normal;
color: white; color: white;
min-width: 60px; padding: 9px 13px;
padding: 5px 10px;
border: none; border: none;
text-decoration: none; text-decoration: none;
&.btn-blue { &.btn-blue {
background-color: #354a5f; background-color: $deepblue;
} &:not(:disabled):hover {
background-color: darken($deepblue, 10%);
&.btn-blue:disabled { }
background-color: rgba(70, 90, 126, 0.4); &:disabled {
} background-color: rgba(70, 90, 126, 0.4);
}
&.btn-blue.clickable:not(:disabled):hover {
background-color: #2c3646;
} }
&.btn-grey { &.btn-grey {
background-color: grey; background-color: grey;
&:not(:disabled):hover {
background-color: darken(gray, 15%);
}
&:disabled {
background-color: lighten(gray, 15%);
}
} }
&.btn-grey.clickable:not(:disabled):hover { i {
background-color: hsl(210, 5%, 30%); margin-right: 4px;
} }
} }
@ -977,7 +979,7 @@ thead td {
} }
thead { thead {
background-color: #354a5f; background-color: $deepblue;
color: white; color: white;
} }

View File

@ -5,12 +5,12 @@
{%- endblock -%} {%- endblock -%}
{% block additional_js %} {% 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"> <script defer type="module">
import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}"; import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}";
window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */ window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */
</script> </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 %} {% endblock %}
{% block title %} {% block title %}
@ -19,11 +19,22 @@
{% block content %} {% block content %}
<main> <main>
{% if can_edit(profile, user) %} {% if user.id == object.id and albums|length > 0 %}
<button disabled id="download" onclick="download('{{ url('api:pictures') }}?users_identified={{ object.id }}')">{% trans %}Download all my pictures{% endtrans %}</button> <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 %} {% endif %}
{% for album, pictures in albums|items %} {% for album, pictures in albums|items %}
<h4>{{ album }}</h4> <h4>{{ album }}</h4>
<br />
<div class="photos"> <div class="photos">
{% for picture in pictures %} {% for picture in pictures %}
{% if picture.can_be_viewed_by(user) %} {% if picture.can_be_viewed_by(user) %}
@ -51,52 +62,62 @@
</div> </div>
<br> <br>
{% endfor %} {% endfor %}
<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) {
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 }
);
});
let fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
let writeStream = 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();
}
</script>
</main> </main>
{% endblock %} {% endblock content %}
{% block script %}
{{ super() }}
{% if user.id == object.id %}
<script>
/**
* @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
*/
document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({
in_progress: false,
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,
})
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
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>
{% endif %}
{% endblock script %}

View File

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

View File

@ -16,12 +16,15 @@ import json
import re import re
import string import string
from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import timedelta 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 core.models import User
from counter.models import BillingInfo, Counter, Customer, Permanency, Product, Selling from counter.models import BillingInfo, Counter, Customer, Permanency, Product, Selling
from sith.settings import SITH_MAIN_CLUB from sith.settings import SITH_MAIN_CLUB
@ -911,3 +914,47 @@ class TestCustomerAccountId(TestCase):
assert created is False assert created is False
assert account.account_id == "1111a" assert account.account_id == "1111a"
assert account.amount == 10 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 raise Http404
if obj.type != "BAR" and not request.user.is_authenticated: if obj.type != "BAR" and not request.user.is_authenticated:
raise PermissionDenied raise PermissionDenied
if ( if obj.type == "BAR" and (
"counter_token" not in request.session "counter_token" not in request.session
or request.session["counter_token"] != obj.token or request.session["counter_token"] != obj.token
or len(obj.barmen_list) == 0 or len(obj.barmen_list) == 0

View File

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

View File

@ -136,7 +136,7 @@
right: 5px; right: 5px;
padding: 5px; padding: 5px;
border-radius: 50%; 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; background-color: white;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -186,29 +186,7 @@
} }
#eboutic .catalog-buttons button { #eboutic .catalog-buttons button {
font-size: 15px!important;
font-weight: normal;
color: white;
min-width: 60px; 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 { #eboutic .catalog-buttons form {
@ -252,7 +230,7 @@
} }
#eboutic .product-image { #eboutic .product-image {
margin-bottom: 0px; margin-bottom: 0;
max-width: 70px; max-width: 70px;
} }
} }

View File

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

View File

@ -1,4 +1,3 @@
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from election.models import Candidature, Election, ElectionList, Role from election.models import Candidature, Election, ElectionList, Role
@ -13,7 +12,7 @@ class ElectionAdmin(admin.ModelAdmin):
"is_vote_finished", "is_vote_finished",
"archived", "archived",
) )
form = make_ajax_form(Election, {"voters": "users"}) autocomplete_fields = ("voters",)
@admin.register(Role) @admin.register(Role)
@ -31,7 +30,7 @@ class ElectionListAdmin(admin.ModelAdmin):
@admin.register(Candidature) @admin.register(Candidature)
class CandidatureAdmin(admin.ModelAdmin): class CandidatureAdmin(admin.ModelAdmin):
list_display = ("user", "role", "election_list") 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 # 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 * from forum.models import *
@admin.register(Forum)
class ForumAdmin(SearchModelAdmin): class ForumAdmin(SearchModelAdmin):
search_fields = ["name", "description"] search_fields = ["name", "description"]
@admin.register(ForumTopic)
class ForumTopicAdmin(SearchModelAdmin): class ForumTopicAdmin(SearchModelAdmin):
search_fields = ["_title", "description"] search_fields = ["_title", "description"]
@admin.register(ForumMessage)
class ForumMessageAdmin(SearchModelAdmin): class ForumMessageAdmin(SearchModelAdmin):
search_fields = ["title", "message"] 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" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from launderette.models import * from launderette.models import *
@ -31,10 +30,10 @@ class MachineAdmin(admin.ModelAdmin):
@admin.register(Token) @admin.register(Token)
class TokenAdmin(admin.ModelAdmin): class TokenAdmin(admin.ModelAdmin):
list_display = ("name", "launderette", "type", "user") list_display = ("name", "launderette", "type", "user")
form = make_ajax_form(Token, {"user": "users"}) autocomplete_fields = ("user",)
@admin.register(Slot) @admin.register(Slot)
class SlotAdmin(admin.ModelAdmin): class SlotAdmin(admin.ModelAdmin):
list_display = ("machine", "user", "start_date") 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. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from haystack.admin import SearchModelAdmin from haystack.admin import SearchModelAdmin
@ -31,7 +30,7 @@ from pedagogy.models import UV, UVComment, UVCommentReport
class UVAdmin(admin.ModelAdmin): class UVAdmin(admin.ModelAdmin):
list_display = ("code", "title", "credit_type", "credits", "department") list_display = ("code", "title", "credit_type", "credits", "department")
search_fields = ("code", "title", "department") search_fields = ("code", "title", "department")
form = make_ajax_form(UV, {"author": "users"}) autocomplete_fields = ("author",)
@admin.register(UVComment) @admin.register(UVComment)
@ -43,7 +42,7 @@ class UVCommentAdmin(admin.ModelAdmin):
"author__last_name", "author__last_name",
"uv__code", "uv__code",
) )
form = make_ajax_form(UVComment, {"author": "users"}) autocomplete_fields = ("author",)
@admin.register(UVCommentReport) @admin.register(UVCommentReport)
@ -55,4 +54,4 @@ class UVCommentReportAdmin(SearchModelAdmin):
"reporter__last_name", "reporter__last_name",
"comment__uv__code", "comment__uv__code",
) )
form = make_ajax_form(UVCommentReport, {"reporter": "users"}) autocomplete_fields = ("reporter",)

View File

@ -1,6 +1,8 @@
from typing import Literal from typing import Literal
from django.db.models import Q from django.db.models import Q
from django.utils import html
from haystack.query import SearchQuerySet
from ninja import FilterSchema, ModelSchema, Schema from ninja import FilterSchema, ModelSchema, Schema
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
@ -120,6 +122,27 @@ class UvFilterSchema(FilterSchema):
language: str = "FR" language: str = "FR"
department: set[str] | None = Field(None, q="department__in") 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: def filter_semester(self, value: set[str] | None) -> Q:
"""Special filter for the semester. """Special filter for the semester.

View File

@ -2,6 +2,7 @@ import json
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.testcases import call_command
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from model_bakery.recipe import Recipe from model_bakery.recipe import Recipe
@ -21,16 +22,31 @@ class TestUVSearch(TestCase):
uv_recipe = Recipe(UV, author=cls.root) uv_recipe = Recipe(UV, author=cls.root)
uvs = [ uvs = [
uv_recipe.prepare( 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( 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( uv_recipe.prepare(
code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC" code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC"
), ),
uv_recipe.prepare( 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( uv_recipe.prepare(
code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI" code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI"
@ -40,9 +56,11 @@ class TestUVSearch(TestCase):
credit_type="TM", credit_type="TM",
semester="AUTUMN_AND_SPRING", semester="AUTUMN_AND_SPRING",
department="GI", department="GI",
manager="francky",
), ),
] ]
UV.objects.bulk_create(uvs) UV.objects.bulk_create(uvs)
call_command("update_index")
def test_permissions(self): def test_permissions(self):
# Test with anonymous user # 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) self.client.force_login(self.root)
res = self.client.get(self.url + "?search=MT") for query, expected in (
assert res.status_code == 200 # UV code search case insensitive
assert {uv["code"] for uv in json.loads(res.content)["results"]} == { ("m", {"MT01", "MT10"}),
"MT01", ("M", {"MT01", "MT10"}),
"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"]} == expected
def test_search_by_credit_type(self): def test_search_by_credit_type(self):
self.client.force_login(self.root) 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)"] astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"]
test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] 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]] [[package]]
name = "babel" name = "babel"
version = "2.15.0" version = "2.15.0"
@ -499,18 +510,14 @@ bcrypt = ["bcrypt"]
[[package]] [[package]]
name = "django-ajax-selects" 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." description = "Edit ForeignKey, ManyToManyField and CharField in Django Admin using jQuery UI AutoComplete."
optional = false optional = false
python-versions = ">=3.10,<4.0" python-versions = "*"
files = [ files = [
{file = "django_ajax_selects-3.0.2-py3-none-any.whl", hash = "sha256:83da065b3fe6bdee5996662734eb18ee70fee510171c179a02f1ce45dcaa2870"}, {file = "django-ajax-selects-2.2.1.tar.gz", hash = "sha256:996ffb38dff1a621b358613afdf2681dbf261e5976da3c30a75e9b08fd81a887"},
{file = "django_ajax_selects-3.0.2.tar.gz", hash = "sha256:8554659f5c7da50cfe1f0d0e14c6f360d0f2ab2d94b24e3203cc4fe974bd945a"},
] ]
[package.dependencies]
Django = ">=3.2"
[[package]] [[package]]
name = "django-countries" name = "django-countries"
version = "7.6.1" version = "7.6.1"
@ -821,6 +828,109 @@ files = [
backports-strenum = {version = ">=1.3", markers = "python_version < \"3.11\""} backports-strenum = {version = ">=1.3", markers = "python_version < \"3.11\""}
colorama = ">=0.4" 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]] [[package]]
name = "identify" name = "identify"
version = "2.6.0" version = "2.6.0"
@ -1483,13 +1593,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.7.1" version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
] ]
[package.dependencies] [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-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_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-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-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-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1940,6 +2051,25 @@ files = [
[package.dependencies] [package.dependencies]
pyyaml = "*" 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]] [[package]]
name = "regex" name = "regex"
version = "2024.7.24" version = "2024.7.24"
@ -2502,4 +2632,4 @@ filelock = ">=3.4"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "5e90eff0d3e11e48c1467165d121f1cff094cf83834b5df517708e72bc82425f" content-hash = "a9573a584420b00b0bd5bb85a0fb2daedb365bd1ff604b94ec23c187bc4dd991"

View File

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

View File

@ -15,8 +15,20 @@
from django.contrib import admin 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(Album)
# admin.site.register(Picture)
admin.site.register(PeoplePictureRelation)

View File

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

View File

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

View File

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

View File

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