Implémentation 3DSv2 + résolution bugs eboutic + amélioration pages admin (#558)

Eboutic :
- Implémentation de la norme 3DSecure v2 pour les paiement par carte bancaire
- Amélioration générale de l'interface utilisateur
- Résolution du problème avec les caractères spéciaux dans le panier sur Safari
- Réparation du cookie du panier de l'eboutic qui n'était pas fonctionnel

Autre :
- Mise à jour de la documentation
- Mise à jour des dépendances Javascript
- Suppression du code inutilisé dans `subscription/models.py`
- Amélioration des pages administrateur (back-office Django)

Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: Julien Constant <julienconstant190@gmail.com>
This commit is contained in:
thomas girod
2023-01-09 20:53:12 +01:00
committed by GitHub
parent 310f1a2283
commit 73305c0b28
53 changed files with 3461 additions and 2248 deletions

View File

@ -24,17 +24,19 @@
from django.contrib import admin
from ajax_select import make_ajax_form
from core.models import User, Page, RealGroup, SithFile
from core.models import User, Page, RealGroup, MetaGroup, SithFile
from django.contrib.auth.models import Group as AuthGroup
from haystack.admin import SearchModelAdmin
admin.site.unregister(AuthGroup)
admin.site.register(MetaGroup)
admin.site.register(RealGroup)
@admin.register(User)
class UserAdmin(SearchModelAdmin):
list_display = ["first_name", "last_name", "username", "email", "nick_name"]
list_display = ("first_name", "last_name", "username", "email", "nick_name")
form = make_ajax_form(
User,
{
@ -48,11 +50,9 @@ class UserAdmin(SearchModelAdmin):
search_fields = ["first_name", "last_name", "username"]
admin.site.register(User, UserAdmin)
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
list_display = ("name", "_full_name", "owner_group")
form = make_ajax_form(
Page,
{
@ -66,4 +66,12 @@ class PageAdmin(admin.ModelAdmin):
@admin.register(SithFile)
class SithFileAdmin(admin.ModelAdmin):
form = make_ajax_form(SithFile, {"parent": "files"}) # ManyToManyField
list_display = ("name", "owner", "size", "date", "is_in_sas")
form = make_ajax_form(
SithFile,
{
"parent": "files",
"owner": "users",
"moderator": "users",
},
) # ManyToManyField

View File

@ -28,8 +28,9 @@ from ajax_select import register, LookupChannel
from core.views.site import search_user
from core.models import User, Group, SithFile
from club.models import Club
from counter.models import Product, Counter
from counter.models import Product, Counter, Customer
from accounting.models import ClubAccount, Company
from eboutic.models import BasketItem
def check_token(request):
@ -60,6 +61,21 @@ class UsersLookup(RightManagedLookupChannel):
return item.get_display_name()
@register("customers")
class CustomerLookup(RightManagedLookupChannel):
model = Customer
def get_query(self, q, request):
users = search_user(q)
return [user.customer for user in users]
def format_match(self, obj):
return obj.user.get_mini_item()
def format_item_display(self, obj):
return f"{obj.user.get_display_name()} ({obj.account_id})"
@register("groups")
class GroupsLookup(RightManagedLookupChannel):
model = Group

View File

@ -26,6 +26,7 @@ import os
from datetime import date, datetime, timedelta
from io import StringIO, BytesIO
from django.contrib.auth.models import Permission
from django.core.management.base import BaseCommand
from django.core.management import call_command
from django.conf import settings
@ -73,7 +74,7 @@ class Command(BaseCommand):
root_path = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
Group(name="Root").save()
root_group, _ = Group.objects.get_or_create(name="Root")
Group(name="Public").save()
Group(name="Subscribers").save()
Group(name="Old subscribers").save()
@ -87,6 +88,11 @@ class Command(BaseCommand):
Group(name="Forum admin").save()
Group(name="Pedagogy admin").save()
self.reset_index("core", "auth")
change_billing = Permission.objects.get(codename="change_billinginfo")
add_billing = Permission.objects.get(codename="add_billinginfo")
root_group.permissions.add(change_billing, add_billing)
root = User(
id=0,
username="root",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -49,36 +49,27 @@ body {
font-family: sans-serif;
}
input[type=button], input[type=submit], input[type=reset],input[type=file] {
button, input[type=button], input[type=submit], input[type=reset],input[type=file] {
border: none;
text-decoration: none;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-weight: bold;
font-size: 1.2em;
border-radius: 5px;
cursor: pointer;
box-shadow: $shadow-color 0px 0px 1px;
&:hover {
background: hsl(0, 0%, 83%);
}
}
&:hover {
background: hsl(0, 0%, 83%);
}
input[type=button], input[type=submit], input[type=reset],input[type=file] {
font-weight: bold;
}
button{
border: none;
text-decoration: none;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: $shadow-color 0px 0px 1px;
button:not(:disabled), input[type=button]:not(:disabled), input[type=submit]:not(:disabled), input[type=reset]:not(:disabled),input[type=file]:not(:disabled) {
cursor: pointer;
&:hover {
background: hsl(0, 0%, 83%);
}
}
input,textarea[type=text],[type=number]{
border: none;
text-decoration: none;
@ -123,6 +114,38 @@ a {
margin: 1px;
}
.collapse {
border-radius: 5px;
overflow: hidden;
.collapse-header {
color: white;
background-color: #354a5f;
padding: 5px 10px;
display: flex;
align-items: center;
gap: 10px;
.collapse-header-text {
flex: 2;
}
.collapse-header-icon {
transition: all ease-in-out 150ms;
&.reverse {
transform: rotate(180deg);
}
}
}
.collapse-body {
padding: 10px;
}
}
.shadow {
box-shadow: rgba(60, 64, 67, .3) 0 1px 3px 0, rgba(60, 64, 67, .15) 0 4px 8px 3px;
}
.w_big {
width: 75%;
}
@ -135,10 +158,12 @@ a {
width: 23%;
}
.clickable:hover {
.clickable:not(:disabled):hover {
cursor: pointer;
}
[x-cloak] { display: none !important; }
/*--------------------------------HEADER-------------------------------*/
#header_language_chooser {
@ -170,21 +195,11 @@ header {
background-color: $primary-neutral-dark-color;
border-radius: 0px 0px 10px 10px;
// PINKTOBER
// background-color: $pinktober;
// border-bottom: 5px solid $pinktober-secondary;
// margin-bottom: -5px;
// border-radius: 0 0 5px 7px;
#header_logo {
background-color: $white-color;
padding: 0.2em;
border-radius: 0px 0px 0px 9px;
//PINKTOBER
// border-bottom: 5px solid $shadow-color;
// border-radius: 0px 0px 0px 5px;
// margin-bottom: -5px;
border-radius: 0 0 0 9px;
a {
display: flex;
@ -211,14 +226,8 @@ header {
width: 100%;
label {
display: inline;
// PINKTOBER
// color: $pinktober-primary-text;
}
}
a {
display: button;
}
}
#header_bar {
@ -243,16 +252,6 @@ header {
flex: initial;
list-style-type: none;
margin: 0.2em 0.2em;
/*
PINKTOBER
& .fa.fa-times {
color: $pinktober-bar-closed !important;
}
& .fa.fa-check {
color: $pinktober-bar-opened !important;
}*/
}
#header_search {
@ -445,6 +444,36 @@ header {
}
}
.btn {
font-size: 15px;
font-weight: normal;
color: white;
min-width: 60px;
padding: 5px 10px;
border: none;
text-decoration: none;
&.btn-blue {
background-color: #354a5f;
}
&.btn-blue:disabled {
background-color: rgba(70, 90, 126, 0.4);
}
&.btn-blue.clickable:not(:disabled):hover {
background-color: #2c3646;
}
&.btn-grey {
background-color: grey;
}
&.btn-grey.clickable:not(:disabled):hover {
background-color:hsl(210,5%,30%);
}
}
/*--------------------------------CONTENT------------------------------*/
#quick_notif {
width: 100%;
@ -466,10 +495,7 @@ header {
.alert {
margin: 10px;
border: #fc8181 1px solid;
background-color: rgb(255,245,245);
border-radius: 4px;
color: #c53030;
padding: 12px 16px;
display: flex;
gap: 16px;
@ -477,6 +503,18 @@ header {
align-items: center;
text-align: justify;
&.alert-green {
background-color: rgb(245, 255, 245);
color: rgb(3, 84, 63);
border: rgb(14, 159, 110) 1px solid;
}
&.alert-red {
background-color: rgb(255,245,245);
color: #c53030;
border: #fc8181 1px solid;
}
.alert-main {
flex: 2;
}
@ -1497,7 +1535,7 @@ textarea {
margin: 10px 0;
display: flex;
flex-wrap: wrap;
height: 20p;
height: 20px;
align-items: center;
}
.search_check {

View File

@ -1,191 +0,0 @@
#eboutic {
display: flex;
flex-direction: row-reverse;
align-items: flex-start;
column-gap: 20px;
margin: 0 20px 20px;
}
#eboutic-title {
margin-left: 20px;
}
#eboutic h3 {
margin-left: 0;
margin-right: 0;
}
#basket {
min-width: 300px;
border-radius: 8px;
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
padding: 10px;
}
#basket h3 {
margin-top: 0;
}
@media screen and (max-width: 765px) {
#eboutic {
flex-direction: column;
align-items: center;
margin: 10px;
row-gap: 20px;
}
#eboutic-title {
margin-bottom: 20px;
margin-top: 4px;
}
#basket {
}
}
#eboutic #basket .error-message {
margin-top: 5px;
background-color: #f8d7da;
border: #f5c6cb 1px solid;
border-radius: 4px;
padding: 10px;
display: flex;
flex-direction: column;
row-gap: 7px;
}
#eboutic #basket .error-message p {
margin: 0;
}
#eboutic .item-list {
margin-left: 0;
list-style: none;
}
#eboutic .item-list li {
display: flex;
align-items: center;
margin-bottom: 10px
}
#eboutic .item-name {
flex: 1;
}
#eboutic .item-price, #eboutic .item-quantity {
width: 65px;
}
#eboutic .item-quantity {
text-align: center;
}
#eboutic .fa-plus, #eboutic .fa-minus {
cursor: pointer;
}
#eboutic .item-price {
text-align: right;
}
/* CSS du catalogue */
#eboutic #catalog {
display: flex;
flex-grow: 1;
flex-direction: column;
row-gap: 30px;
}
#eboutic .category-header {
margin-bottom: 15px;
}
#eboutic .product-group {
display: flex;
flex-wrap: wrap;
column-gap: 15px;
row-gap: 15px;
}
@media screen and (max-width: 765px) {
#eboutic #catalog {
row-gap: 15px;
}
#eboutic section {
text-align: center;
}
#eboutic .product-group {
justify-content: space-around;
}
}
#eboutic .product-button {
position: relative;
box-sizing: border-box;
min-width: 120px;
max-width: 150px;
padding: 10px;
overflow: hidden;
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
display: flex;
flex-direction: column;
align-items: center;
row-gap: 5px;
}
#eboutic .product-button:active {
box-shadow: none;
}
#eboutic .product-button img, #eboutic .product-button .fa {
margin: 0;
border-radius: 4px;
height: 40px;
line-height: 40px;
}
#eboutic .product-button p {
font-size: 13px;
margin: 0;
}
#eboutic .catalog-buttons {
display: flex;
justify-content: center;
column-gap: 30px;
margin: 30px 0 0;
}
#eboutic input {
all: unset;
}
#eboutic .catalog-buttons button {
font-size: 15px;
font-weight: normal;
color: white;
min-width: 60px;
padding: 5px 10px;
}
#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 {
margin: 0;
}

View File

@ -1,104 +0,0 @@
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function get_starting_items() {
const cookie = getCookie("basket_items")
try {
// django cookie backend does an utter mess on non-trivial data types
// so we must perform a conversion of our own
const biscuit = JSON.parse(JSON.parse(cookie.replace(/\\054/g, ',')));
if (Array.isArray(biscuit)) {
return biscuit;
}
return [];
} catch (e) {
return [];
}
}
document.addEventListener('alpine:init', () => {
Alpine.data('basket', () => ({
items: get_starting_items(),
get_total() {
let total = 0;
for (const item of this.items) {
total += item["quantity"] * item["unit_price"];
}
return total;
},
add(item) {
item.quantity++;
this.edit_cookies()
},
remove(item_id) {
const index = this.items.findIndex(e => e.id === item_id);
if (index < 0) return;
this.items[index].quantity -= 1;
if (this.items[index].quantity === 0) {
this.items = this.items.filter((e) => e.id !== this.items[index].id);
}
this.edit_cookies();
},
clear_basket() {
this.items = []
this.edit_cookies();
},
edit_cookies() {
// a cookie survives an hour
document.cookie = "basket_items=" + JSON.stringify(this.items) + ";Max-Age=3600";
},
/**
* Create an item in the basket if it was not already in
* @param id : int the id of the product to add
* @param name : String the name of the product
* @param price : number the unit price of the product
*/
create_item(id, name, price) {
let new_item = {
id: id,
name: name,
quantity: 0,
unit_price: price
};
this.items.push(new_item);
this.add(new_item);
},
/**
* add an item to the basket.
* This is called when the user click
* on a button in the catalog (left side of the page)
* @param id : int the id of the product to add
* @param name : String the name of the product
* @param price : number the unit price of the product
*/
add_from_catalog(id, name, price) {
const item = this.items.find(e => e.id === id)
if (item === undefined) {
this.create_item(id, name, price);
} else {
// the user clicked on an item which is already in the basket
this.add(item);
}
},
}))
})

View File

@ -18,7 +18,7 @@
<script defer href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
<!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
<script src="{{ static('core/js/jquery-3.6.2.min.js') }}"></script>
<!-- Put here to always have acces to those functions on django widgets -->
<script src="{{ static('core/js/script.js') }}"></script>
{% block additional_css %}{% endblock %}

View File

@ -24,7 +24,7 @@
<div id="progress_bar"></div>
</div>
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
<script src="{{ static('core/js/jquery-3.6.2.min.js') }}"></script>
<script src="{{ static('com/js/slideshow.js') }}"></script>
</body>
</html>

View File

@ -67,7 +67,7 @@ from core.views.forms import (
)
from core.models import User, SithFile, Preferences, Gift
from subscription.models import Subscription
from counter.views import StudentCardForm
from counter.forms import StudentCardForm
from trombi.views import UserTrombiForm