forked from DGNum/gestioCOF
Merge branch 'k-fet' into 'master'
K-Fêt - 17-01-07 ## K-Fêt - Général - Ajout: peuplement des modèles (tables BDD) pour le développement et les tests - Fix: incohérences d'arrondi d'UKF entre les différents composants - Fix: URL vers les informations détaillées des comptes dont le trigramme contient certains caractères spéciaux - Fix: freeze lors de la saisie de nouveaux transferts - Fix: il n'est plus possible de rentrer des commandes et transferts sur le compte utilisateur partagé K-Fêt - Fix: la balance de ``#13`` n'est plus comptée dans le négatif total des comptes - Fix: oubli déclaration du backend d'authentification au compte utilisateur partagé K-Fêt - Fix: warning pip sur la référence au package ``channels`` modifié ## K-Psul - Ajout d'un indicateur lors de la saisie d'un mot de passe si le verrouillage des majuscules est activé - Ajout d'indications visibles pour les raccourcis clavier - Modification des raccourcis clavier ``F2`` (reset du panier) et ``Shift + F2`` (reset du compte) - Ajout d'un affichage d'informations sur la dernière opération validée lors de la session en cours - Ajout d'un lien vers la liste des comptes depuis la zone de compte de K-Psul - Meilleur focus automatique après l'ajout d'une charge au panier See merge request !147
This commit is contained in:
commit
0e7fc99a09
17 changed files with 9170 additions and 1457 deletions
|
@ -173,7 +173,7 @@ Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande
|
|||
|
||||
Une base de donnée pré-remplie est disponible en lançant la commande :
|
||||
|
||||
python manage.py loaddata users root bda gestion sites
|
||||
python manage.py loaddata users root bda gestion sites accounts groups articles
|
||||
|
||||
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
||||
|
||||
|
|
|
@ -159,6 +159,7 @@ CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
|||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'gestioncof.shared.COFCASBackend',
|
||||
'kfet.backends.GenericTeamBackend',
|
||||
)
|
||||
|
||||
# EMAIL_HOST="nef.ens.fr"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -30,9 +30,17 @@ class GenericTeamBackend(object):
|
|||
def authenticate(self, username=None, token=None):
|
||||
valid_token = GenericTeamToken.objects.get(token=token)
|
||||
if username == 'kfet_genericteam' and valid_token:
|
||||
user, created = User.objects.get_or_create(username='kfet_genericteam')
|
||||
# Création du user s'il n'existe pas déjà
|
||||
user, _ = User.objects.get_or_create(username='kfet_genericteam')
|
||||
profile, _ = CofProfile.objects.get_or_create(user=user)
|
||||
account, _ = Account.objects.get_or_create(
|
||||
cofprofile=profile,
|
||||
trigramme='GNR')
|
||||
|
||||
# Ajoute la permission kfet.is_team à ce user
|
||||
perm_is_team = Permission.objects.get(codename='is_team')
|
||||
user.user_permissions.add(perm_is_team)
|
||||
|
||||
return user
|
||||
return None
|
||||
|
||||
|
|
1178
kfet/fixtures/accounts.json
Normal file
1178
kfet/fixtures/accounts.json
Normal file
File diff suppressed because it is too large
Load diff
2322
kfet/fixtures/articles.json
Normal file
2322
kfet/fixtures/articles.json
Normal file
File diff suppressed because it is too large
Load diff
98
kfet/fixtures/groups.json
Normal file
98
kfet/fixtures/groups.json
Normal file
|
@ -0,0 +1,98 @@
|
|||
[
|
||||
{
|
||||
"model": "auth.group",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "K-F\u00eat chef",
|
||||
"permissions": [
|
||||
115,
|
||||
116,
|
||||
117,
|
||||
118,
|
||||
119,
|
||||
120,
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
130,
|
||||
131,
|
||||
132,
|
||||
136,
|
||||
137,
|
||||
138,
|
||||
121,
|
||||
122,
|
||||
123,
|
||||
127,
|
||||
128,
|
||||
129,
|
||||
124,
|
||||
125,
|
||||
126,
|
||||
188,
|
||||
189,
|
||||
190,
|
||||
169,
|
||||
176,
|
||||
183,
|
||||
170,
|
||||
171,
|
||||
182,
|
||||
172,
|
||||
178,
|
||||
177,
|
||||
181,
|
||||
175,
|
||||
179,
|
||||
173,
|
||||
174,
|
||||
184,
|
||||
180,
|
||||
139,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
144,
|
||||
166,
|
||||
167,
|
||||
168,
|
||||
163,
|
||||
164,
|
||||
165,
|
||||
151,
|
||||
152,
|
||||
153,
|
||||
154,
|
||||
155,
|
||||
156,
|
||||
185,
|
||||
186,
|
||||
187,
|
||||
145,
|
||||
146,
|
||||
147,
|
||||
148,
|
||||
149,
|
||||
150,
|
||||
160,
|
||||
161,
|
||||
162,
|
||||
157,
|
||||
158,
|
||||
159
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "auth.group",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "K-F\u00eat girl",
|
||||
"permissions": [
|
||||
172,
|
||||
173
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
|
@ -280,12 +280,12 @@ class KPsulOperationGroupForm(forms.ModelForm):
|
|||
is_protected=False, valid_from__lte=timezone.now(),
|
||||
valid_to__gte=timezone.now()),
|
||||
widget = forms.HiddenInput())
|
||||
on_acc = forms.ModelChoiceField(
|
||||
queryset = Account.objects.exclude(trigramme='GNR'),
|
||||
widget = forms.HiddenInput())
|
||||
class Meta:
|
||||
model = OperationGroup
|
||||
fields = ['on_acc', 'checkout', 'comment']
|
||||
widgets = {
|
||||
'on_acc' : forms.HiddenInput(),
|
||||
}
|
||||
|
||||
class KPsulAccountForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -418,11 +418,11 @@ class TransferGroupForm(forms.ModelForm):
|
|||
|
||||
class TransferForm(forms.ModelForm):
|
||||
from_acc = forms.ModelChoiceField(
|
||||
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13']),
|
||||
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13', 'GNR']),
|
||||
widget = forms.HiddenInput()
|
||||
)
|
||||
to_acc = forms.ModelChoiceField(
|
||||
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13']),
|
||||
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13', 'GNR']),
|
||||
widget = forms.HiddenInput()
|
||||
)
|
||||
|
||||
|
|
|
@ -64,3 +64,22 @@
|
|||
color:#FFF !important;
|
||||
background:#C8102E !important;
|
||||
}
|
||||
|
||||
.jconfirm .capslock {
|
||||
position: relative ;
|
||||
}
|
||||
|
||||
.jconfirm .capslock .glyphicon {
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
right: 0px;
|
||||
top: 15px;
|
||||
font-size: 30px;
|
||||
display: none ;
|
||||
margin-left: 60px !important;
|
||||
}
|
||||
|
||||
.jconfirm .capslock input {
|
||||
padding-right: 50px;
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ input[type=number]::-webkit-outer-spin-button {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
#account, #checkout, input, #history, #basket, #basket_rel, #articles_data {
|
||||
#account, #checkout, input, #history, #basket, #basket_rel, #previous_op, #articles_data {
|
||||
background:#fff;
|
||||
}
|
||||
|
||||
|
@ -335,12 +335,13 @@ input[type=number]::-webkit-outer-spin-button {
|
|||
padding:0;
|
||||
}
|
||||
|
||||
#basket, #basket_rel {
|
||||
#basket, #basket_rel, #previous_op {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#basket_rel {
|
||||
#basket_rel, #previous_op {
|
||||
border-top:1px solid #C8102E;
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
#basket {
|
||||
|
@ -354,6 +355,11 @@ input[type=number]::-webkit-outer-spin-button {
|
|||
#basket_rel {
|
||||
border-top:0;
|
||||
margin-left:7px;
|
||||
margin-right:7px;
|
||||
}
|
||||
#previous_op {
|
||||
border-top:0;
|
||||
margin-left:7px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,6 +391,16 @@ input[type=number]::-webkit-outer-spin-button {
|
|||
|
||||
/* History */
|
||||
|
||||
#previous_op .trigramme {
|
||||
width:100%;
|
||||
background-color:rgba(200,16,46,0.85);
|
||||
color:#FFF;
|
||||
font-weight:bold;
|
||||
padding:3px;
|
||||
margin-left: -3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.kpsul_middle_right_col {
|
||||
overflow:auto;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ function amountDisplay(amount, is_cof=false, tri='') {
|
|||
|
||||
function amountToUKF(amount, is_cof=false) {
|
||||
var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1;
|
||||
return Math.round(amount * coef_cof * 10);
|
||||
return Math.floor(amount * coef_cof * 10);
|
||||
}
|
||||
|
||||
function isValidTrigramme(trigramme) {
|
||||
|
@ -88,7 +88,7 @@ function getErrorsHtml(data) {
|
|||
|
||||
function requestAuth(data, callback, focus_next = null) {
|
||||
var content = getErrorsHtml(data);
|
||||
content += '<input type="password" name="password" autofocus>',
|
||||
content += '<div class="capslock"><span class="glyphicon glyphicon-lock"></span><input type="password" name="password" autofocus><div>',
|
||||
$.confirm({
|
||||
title: 'Authentification requise',
|
||||
content: content,
|
||||
|
@ -102,14 +102,39 @@ function requestAuth(data, callback, focus_next = null) {
|
|||
},
|
||||
onOpen: function() {
|
||||
var that = this;
|
||||
var capslock = -1 ; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown
|
||||
this.$content.find('input').on('keypress', function(e) {
|
||||
if (e.keyCode == 13)
|
||||
that.$confirmButton.click();
|
||||
|
||||
var s = String.fromCharCode(e.which);
|
||||
if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off
|
||||
(s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on
|
||||
capslock = 1 ;
|
||||
} else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off
|
||||
(s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on
|
||||
capslock = 0 ;
|
||||
}
|
||||
if (capslock == 1)
|
||||
$('.capslock .glyphicon').show() ;
|
||||
else if (capslock == 0)
|
||||
$('.capslock .glyphicon').hide() ;
|
||||
});
|
||||
// Capslock key is not detected by keypress
|
||||
this.$content.find('input').on('keydown', function(e) {
|
||||
if (e.which == 20) {
|
||||
capslock = 1-capslock ;
|
||||
}
|
||||
if (capslock == 1)
|
||||
$('.capslock .glyphicon').show() ;
|
||||
else if (capslock == 0)
|
||||
$('.capslock .glyphicon').hide() ;
|
||||
});
|
||||
},
|
||||
onClose: function() {
|
||||
if (focus_next)
|
||||
this._lastFocused = focus_next;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -48,11 +48,11 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3"><b>F2</b></div>
|
||||
<div class="col-xs-9">Reset compte</div>
|
||||
<div class="col-xs-9">Reset panier</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3"><b>Shift + F2</b></div>
|
||||
<div class="col-xs-9">Reset panier</div>
|
||||
<div class="col-xs-9">Reset compte</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-3"><b>F9</b></div>
|
||||
|
@ -114,10 +114,10 @@
|
|||
<div class="kpsul_middle_left">
|
||||
<div class="kpsul_middle_left_top">
|
||||
<div id="special_operations">
|
||||
<button role="button" class="btn" id="operation_deposit">Charge</button>
|
||||
<button role="button" class="btn" id="operation_withdraw">Retrait</button>
|
||||
<button role="button" class="btn" id="cool_reset">RAZ</button>
|
||||
<button role="button" class="btn" id="ask_addcost">Major.</button>
|
||||
<button role="button" class="btn" id="operation_deposit">Charge (F3)</button>
|
||||
<button role="button" class="btn" id="operation_withdraw">Retrait (Shift+F3)</button>
|
||||
<button role="button" class="btn" id="cool_reset">RAZ (F1)</button>
|
||||
<button role="button" class="btn" id="ask_addcost">Major. (F9)</button>
|
||||
</div>
|
||||
<div id="article_selection">
|
||||
<input type="text" id="article_autocomplete" autocomplete="off">
|
||||
|
@ -138,10 +138,14 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-3">
|
||||
<div id="basket_rel">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div id="previous_op">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -245,13 +249,18 @@ $(document).ready(function() {
|
|||
if (account_data['id'] != 0) {
|
||||
var url_base = '{% url 'kfet.account.read' 'LIQ' %}';
|
||||
url_base = url_base.substr(0, url_base.length - 3);
|
||||
buttons += '<a href="'+ url_base + account_data['trigramme']+'" class="btn btn-primary" target="_blank" title="Modifier"><span class="glyphicon glyphicon-cog"></span></a>';
|
||||
trigramme = encodeURIComponent(account_data['trigramme']) ;
|
||||
buttons += '<a href="'+ url_base + trigramme +'" class="btn btn-primary" target="_blank" title="Modifier"><span class="glyphicon glyphicon-cog"></span></a>';
|
||||
}
|
||||
if (account_data['id'] == 0) {
|
||||
var trigramme = triInput.val().toUpperCase();
|
||||
var url_base = '{% url 'kfet.account.create' %}'
|
||||
if (isValidTrigramme(trigramme)) {
|
||||
var url_base = '{% url 'kfet.account.create' %}'
|
||||
trigramme = encodeURIComponent(trigramme);
|
||||
buttons += '<a href="'+url_base+'?trigramme='+trigramme+'" class="btn btn-primary" target="_blank" title="Créer"><span class="glyphicon glyphicon-plus"></span></a>';
|
||||
} else {
|
||||
var url_base = '{% url 'kfet.account' %}'
|
||||
buttons += '<a href="'+url_base+'" class="btn btn-primary" target="_blank" title="Rechercher"><span class="glyphicon glyphicon-search"></span></a>';
|
||||
}
|
||||
}
|
||||
account_container.find('.buttons').html(buttons);
|
||||
|
@ -471,6 +480,7 @@ $(document).ready(function() {
|
|||
},
|
||||
})
|
||||
.done(function(data) {
|
||||
updatePreviousOp();
|
||||
coolReset();
|
||||
lock = 0;
|
||||
})
|
||||
|
@ -924,7 +934,7 @@ $(document).ready(function() {
|
|||
}
|
||||
});
|
||||
},
|
||||
onClose: function() { this._lastFocused = articleSelect; }
|
||||
onClose: function() { this._lastFocused = (articleSelect.val() ? articleNb : articleSelect) ; }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1043,6 +1053,20 @@ $(document).ready(function() {
|
|||
});
|
||||
}
|
||||
|
||||
var previousop_container = $('#previous_op');
|
||||
|
||||
function updatePreviousOp() {
|
||||
var previousop_html = '';
|
||||
var trigramme = account_data['trigramme'];
|
||||
previousop_html += '<div class="trigramme">Trigramme : '+trigramme+'</div>';
|
||||
previousop_html += basketrel_container.html();
|
||||
previousop_container.html(previousop_html);
|
||||
}
|
||||
|
||||
function resetPreviousOp() {
|
||||
previousop_container.html('');
|
||||
}
|
||||
|
||||
// -----
|
||||
// Addcost
|
||||
// -----
|
||||
|
@ -1211,6 +1235,7 @@ $(document).ready(function() {
|
|||
coolReset(give_tri_focus);
|
||||
checkoutInput.trigger('change');
|
||||
resetArticles();
|
||||
resetPreviousOp();
|
||||
khistory.reset();
|
||||
resetSettings();
|
||||
getArticles();
|
||||
|
@ -1250,13 +1275,13 @@ $(document).ready(function() {
|
|||
return false;
|
||||
case 113:
|
||||
if (e.shiftKey) {
|
||||
// Shift+F2 - Basket reset
|
||||
resetBasket();
|
||||
articleSelect.focus();
|
||||
} else {
|
||||
// F2 - Account reset
|
||||
// Shift+F2 - Account reset
|
||||
resetAccount();
|
||||
triInput.focus();
|
||||
} else {
|
||||
// F2 - Basket reset
|
||||
resetBasket();
|
||||
articleSelect.focus();
|
||||
}
|
||||
return false;
|
||||
case 114:
|
||||
|
|
|
@ -34,12 +34,12 @@
|
|||
<tr class="transfer_form" id="{{ form.prefix }}">
|
||||
<td class="from_acc_data"></td>
|
||||
<td class="from_acc">
|
||||
<input type="text" name="from_acc" class="input_from_acc" autocomplete="off" spellcheck="false">
|
||||
<input type="text" name="from_acc" class="input_from_acc" maxlength="3" autocomplete="off" spellcheck="false">
|
||||
{{ form.from_acc }}
|
||||
</td>
|
||||
<td class="amount">{{ form.amount }}</td>
|
||||
<td class="to_acc">
|
||||
<input type="text" name="to_acc" class="input_to_acc" autocomplete="off" spellcheck="false">
|
||||
<input type="text" name="to_acc" class="input_to_acc" maxlength="3" autocomplete="off" spellcheck="false">
|
||||
{{ form.to_acc }}
|
||||
</td>
|
||||
<td class="to_acc_data"></td>
|
||||
|
@ -71,18 +71,21 @@ $(document).ready(function () {
|
|||
var $next = $form.next('.transfer_form').find('.from_acc input');
|
||||
}
|
||||
var $input_id = $input.next('input');
|
||||
if (isValidTrigramme(trigramme)) {
|
||||
getAccountData(trigramme, function(data) {
|
||||
$input_id.val(data.id);
|
||||
$data.text(data.name);
|
||||
$next.focus();
|
||||
});
|
||||
} else {
|
||||
$input_id.val('');
|
||||
$data.text('');
|
||||
}
|
||||
}
|
||||
|
||||
$('.input_from_acc, .input_to_acc').on('input', function() {
|
||||
var tri = $(this).val().toUpperCase();
|
||||
if (isValidTrigramme(tri)) {
|
||||
updateAccountData(tri, $(this));
|
||||
}
|
||||
});
|
||||
|
||||
$('#transfers_form').on('submit', function(e) {
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.utils.html import escape
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.core.cache import cache
|
||||
from kfet.models import Settings
|
||||
from math import floor
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
@ -37,5 +38,4 @@ def highlight_clipper(clipper, q):
|
|||
@register.filter()
|
||||
def ukf(balance, is_cof):
|
||||
grant = is_cof and (1 + Settings.SUBVENTION_COF() / 100) or 1
|
||||
# float nécessaire car sinon problème avec le round de future.builtins
|
||||
return round(float(balance * 10 * grant))
|
||||
return floor(balance * 10 * grant)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (absolute_import, division,
|
||||
print_function, unicode_literals)
|
||||
from builtins import *
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -42,16 +42,22 @@ def home(request):
|
|||
|
||||
@teamkfet_required
|
||||
def login_genericteam(request):
|
||||
# Check si besoin de déconnecter l'utilisateur de CAS
|
||||
profile, _ = CofProfile.objects.get_or_create(user=request.user)
|
||||
logout_cas = ''
|
||||
need_cas_logout = False
|
||||
if profile.login_clipper:
|
||||
need_cas_logout = True
|
||||
# Récupèration de la vue de déconnexion de CAS
|
||||
# Ici, car request sera modifié après
|
||||
logout_cas = django_cas_ng.views.logout(request)
|
||||
|
||||
# Authentification du compte générique
|
||||
token = GenericTeamToken.objects.create(token=get_random_string(50))
|
||||
user = authenticate(username="kfet_genericteam", token=token.token)
|
||||
login(request, user)
|
||||
|
||||
if logout_cas:
|
||||
if need_cas_logout:
|
||||
# Vue de déconnexion de CAS
|
||||
return logout_cas
|
||||
|
||||
return render(request, "kfet/login_genericteam.html")
|
||||
|
@ -484,7 +490,8 @@ class AccountGroupUpdate(UpdateView):
|
|||
|
||||
class AccountNegativeList(ListView):
|
||||
queryset = (AccountNegative.objects
|
||||
.select_related('account', 'account__cofprofile__user'))
|
||||
.select_related('account', 'account__cofprofile__user')
|
||||
.exclude(account__trigramme='#13'))
|
||||
template_name = 'kfet/account_negative.html'
|
||||
context_object_name = 'negatives'
|
||||
|
||||
|
@ -495,12 +502,13 @@ class AccountNegativeList(ListView):
|
|||
'overdraft_duration': Settings.OVERDRAFT_DURATION(),
|
||||
}
|
||||
negs_sum = (AccountNegative.objects
|
||||
.exclude(account__trigramme='#13')
|
||||
.aggregate(
|
||||
bal = Coalesce(Sum('account__balance'),0),
|
||||
offset = Coalesce(Sum('balance_offset'),0),
|
||||
)
|
||||
)
|
||||
context['negatives_sum'] = negs_sum['bal'] + negs_sum['offset']
|
||||
context['negatives_sum'] = negs_sum['bal'] - negs_sum['offset']
|
||||
return context
|
||||
|
||||
# -----
|
||||
|
|
|
@ -14,7 +14,7 @@ django-bootstrap-form==3.2.1
|
|||
asgiref==0.14.0
|
||||
daphne==0.14.3
|
||||
asgi-redis==0.14.0
|
||||
git+https://github.com/Aureplop/channels.git#egg=channel
|
||||
statistics==1.0.3.5
|
||||
future==0.15.2
|
||||
django-widget-tweaks==1.4.1
|
||||
git+https://github.com/Aureplop/channels.git#egg=channels
|
||||
|
|
Loading…
Reference in a new issue