Commit 5c702187 authored by ynerant's avatar ynerant

Merge branch 'beta' into 'master'

Corrections diverses

See merge request !123
parents 3191dba3 905d6537
Pipeline #4016 passed with stages
in 12 minutes and 38 seconds
......@@ -7,7 +7,7 @@ from threading import Thread
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
......@@ -123,6 +123,7 @@ class Activity(models.Model):
verbose_name=_('open'),
)
@transaction.atomic
def save(self, *args, **kwargs):
"""
Update the activity wiki page each time the activity is updated (validation, change description, ...)
......@@ -194,8 +195,8 @@ class Entry(models.Model):
else _("Entry for {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity))
@transaction.atomic
def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
......@@ -260,6 +261,7 @@ class Guest(models.Model):
except AttributeError:
return False
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
one_year = timedelta(days=365)
......
......@@ -7,6 +7,7 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
......@@ -44,6 +45,7 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_end=timezone.now(),
)
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
return super().form_valid(form)
......@@ -145,6 +147,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["inviter"].initial = self.request.user.note
return form
@transaction.atomic
def form_valid(self, form):
form.instance.activity = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
......
......@@ -8,6 +8,7 @@ from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.db import transaction
from django.forms import CheckboxSelectMultiple
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
......@@ -57,6 +58,7 @@ class ProfileForm(forms.ModelForm):
self.fields['address'].widget.attrs.update({"placeholder": "4 avenue des Sciences, 91190 GIF-SUR-YVETTE"})
self.fields['promotion'].widget.attrs.update({"max": timezone.now().year})
@transaction.atomic
def save(self, commit=True):
if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data):
......@@ -161,7 +163,7 @@ class MembershipForm(forms.ModelForm):
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
help_text=_("Check this case is the Société Générale paid the inscription."),
help_text=_("Check this case if the Société Générale paid the inscription."),
)
credit_type = forms.ModelChoiceField(
......
......@@ -7,7 +7,7 @@ import os
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.db import models, transaction
from django.db.models import Q
from django.template import loader
from django.urls import reverse, reverse_lazy
......@@ -271,6 +271,7 @@ class Club(models.Model):
self._force_save = True
self.save(force_update=True)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if not self.require_memberships:
......@@ -406,6 +407,7 @@ class Membership(models.Model):
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
@transaction.atomic
def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
......@@ -475,8 +477,13 @@ class Membership(models.Model):
# to treasurers.
transaction.valid = False
from treasury.models import SogeCredit
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0]
soge_credit.refresh_from_db()
if SogeCredit.objects.filter(user=self.user).exists():
soge_credit = SogeCredit.objects.get(user=self.user)
else:
soge_credit = SogeCredit(user=self.user)
soge_credit._force_save = True
soge_credit.save(force_insert=True)
soge_credit.refresh_from_db()
transaction.save(force_insert=True)
transaction.refresh_from_db()
soge_credit.transactions.add(transaction)
......
......@@ -38,7 +38,7 @@
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
{% if "note.view_note"|has_perm:user_object.note %}
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
......@@ -47,7 +47,7 @@
{% endif %}
</dl>
{% if user_object.pk == user_object.pk %}
{% if user_object.pk == user.pk %}
<div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
<i class="fa fa-cogs"></i>{% trans 'API token' %}
......
......@@ -38,6 +38,7 @@ class CustomLoginView(LoginView):
"""
form_class = CustomAuthenticationForm
@transaction.atomic
def form_valid(self, form):
logout(self.request)
_set_current_user_and_ip(form.get_user(), self.request.session, None)
......@@ -76,6 +77,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return context
@transaction.atomic
def form_valid(self, form):
"""
Check if ProfileForm is correct
......@@ -269,6 +271,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
self.object = self.get_object()
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
@transaction.atomic
def form_valid(self, form):
"""Save image to note"""
image = form.cleaned_data['image']
......@@ -650,6 +653,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return not error
@transaction.atomic
def form_valid(self, form):
"""
Create membership, check that all is good, make transactions
......
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.db.models import Q
from django.core.exceptions import ValidationError
......@@ -56,8 +57,9 @@ class AliasViewSet(ReadProtectedModelViewSet):
"""
queryset = Alias.objects.all()
serializer_class = AliasSerializer
filter_backends = [SearchFilter, OrderingFilter]
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note']
ordering_fields = ['name', 'normalized_name']
def get_serializer_class(self):
......@@ -106,8 +108,9 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects.all()
serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter]
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note']
ordering_fields = ['name', 'normalized_name']
def get_queryset(self):
......@@ -116,29 +119,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
:return: The filtered set of requested aliases
"""
queryset = super().get_queryset()
queryset = super().get_queryset().distinct()
# Sqlite doesn't support ORDER BY in subqueries
queryset = queryset.order_by("name") \
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
alias = self.request.query_params.get("alias", ".*")
alias = self.request.query_params.get("alias", None)
queryset = queryset.prefetch_related('note')
# We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias.
queryset = queryset.filter(
name__iregex="^" + alias
).union(
queryset.filter(
Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True).union(
queryset.filter(
Q(normalized_name__iregex="^" + alias.lower())
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True)
if alias:
# We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias.
queryset = queryset.filter(
name__iregex="^" + alias
).union(
queryset.filter(
Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True).union(
queryset.filter(
Q(normalized_name__iregex="^" + alias.lower())
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(name__iregex="^" + alias)
),
all=True)
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
else queryset.order_by("name")
......@@ -179,8 +184,11 @@ class TransactionViewSet(ReadProtectedModelViewSet):
"""
queryset = Transaction.objects.order_by("-created_at").all()
serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter]
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
"polymorphic_ctype", "amount", "created_at", ]
search_fields = ['$reason', ]
ordering_fields = ['created_at', 'amount']
def get_queryset(self):
user = self.request.user
......
......@@ -8,7 +8,7 @@ from django.conf.global_settings import DEFAULT_FROM_EMAIL
from django.core.exceptions import ValidationError
from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.db import models
from django.db import models, transaction
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
......@@ -93,6 +93,7 @@ class Note(PolymorphicModel):
delta = timezone.now() - self.last_negative
return "{:d} jours".format(delta.days)
@transaction.atomic
def save(self, *args, **kwargs):
"""
Save note with it's alias (called in polymorphic children)
......@@ -108,12 +109,16 @@ class Note(PolymorphicModel):
# Save alias
a.note = self
# Consider that if the name of the note could be changed, then the alias can be created.
# It does not mean that any alias can be created.
a._force_save = True
a.save(force_insert=True)
else:
# Check if the name of the note changed without changing the normalized form of the alias
alias = Alias.objects.get(normalized_name=Alias.normalize(str(self)))
if alias.name != str(self):
alias.name = str(self)
alias._force_save = True
alias.save()
def clean(self, *args, **kwargs):
......@@ -154,6 +159,7 @@ class NoteUser(Note):
def pretty(self):
return _("%(user)s's note") % {'user': str(self.user)}
@transaction.atomic
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteUser.objects.get(pk=self.pk)
......@@ -195,6 +201,7 @@ class NoteClub(Note):
def pretty(self):
return _("Note of %(club)s club") % {'club': str(self.club)}
@transaction.atomic
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteClub.objects.get(pk=self.pk)
......@@ -310,6 +317,7 @@ class Alias(models.Model):
pass
self.normalized_name = normalized_name
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
......
......@@ -170,19 +170,21 @@ class Transaction(PolymorphicModel):
previous_source_balance = self.source.balance
previous_dest_balance = self.destination.balance
source_balance = self.source.balance
dest_balance = self.destination.balance
source_balance = previous_source_balance
dest_balance = previous_dest_balance
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created and not self.valid and not hasattr(self, "_force_save"):
to_transfer = self.total
if not created:
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
# We make a select for update to avoid concurrency issues
old_transaction = Transaction.objects.select_for_update().get(pk=self.pk)
# Check that nothing important changed
for field_name in ["source_id", "destination_id", "quantity", "amount"]:
if getattr(self, field_name) != getattr(old_transaction, field_name):
raise ValidationError(_("You can't update the {field} on a Transaction. "
"Please invalidate it and create one other.").format(field=field_name))
if not hasattr(self, "_force_save"):
for field_name in ["source_id", "destination_id", "quantity", "amount"]:
if getattr(self, field_name) != getattr(old_transaction, field_name):
raise ValidationError(_("You can't update the {field} on a Transaction. "
"Please invalidate it and create one other.").format(field=field_name))
if old_transaction.valid == self.valid:
# Don't change anything
......@@ -215,10 +217,6 @@ class Transaction(PolymorphicModel):
# When source == destination, no money is transferred and no transaction is created
return
# We refresh the notes with the "select for update" tag to avoid concurrency issues
self.source = Note.objects.filter(pk=self.source_id).select_for_update().get()
self.destination = Note.objects.filter(pk=self.destination_id).select_for_update().get()
# Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate()
......@@ -237,9 +235,11 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs)
# Save notes
self.source.refresh_from_db()
self.source.balance += diff_source
self.source._force_save = True
self.source.save()
self.destination.refresh_from_db()
self.destination.balance += diff_dest
self.destination._force_save = True
self.destination.save()
......@@ -273,6 +273,7 @@ class RecurrentTransaction(Transaction):
_("The destination of this transaction must equal to the destination of the template."))
return super().clean()
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
return super().save(*args, **kwargs)
......@@ -323,6 +324,7 @@ class SpecialTransaction(Transaction):
raise(ValidationError(_("A special transaction is only possible between a"
" Note associated to a payment method and a User or a Club")))
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
......
......@@ -29,7 +29,6 @@ $(document).ready(function () {
// Switching in double consumptions mode should update the layout
$('#double_conso').change(function () {
$('#consos_list_div').removeClass('d-none')
$('#user_select_div').attr('class', 'col-xl-4')
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
const note_list_obj = $('#note_list')
......@@ -48,7 +47,6 @@ $(document).ready(function () {
$('#single_conso').change(function () {
$('#consos_list_div').addClass('d-none')
$('#user_select_div').attr('class', 'col-xl-7')
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
const consos_list_obj = $('#consos_list')
......
......@@ -10,22 +10,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block content %}
<div class="row mt-4">
<div class="col-sm-5 col-md-4" id="infos_div">
<div class="row">
<div class="row justify-content-center justify-content-md-end">
{# User details column #}
<div class="col">
<div class="card bg-light border-success mb-4 text-center">
<div class="col picture-col">
<div class="card bg-light mb-4 text-center">
<a id="profile_pic_link" href="#">
<img src="{% static "member/img/default_picture.png" %}"
id="profile_pic" alt="" class="card-img-top">
id="profile_pic" alt="" class="card-img-top d-none d-sm-block">
</a>
<div class="card-body text-center text-break">
<span id="user_note"></span>
<div class="card-body text-center text-break p-2">
<span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span>
</div>
</div>
</div>
{# User selection column #}
<div class="col-xl-7" id="user_select_div">
<div class="col-xl" id="user_select_div">
<div class="card bg-light border-success mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
......@@ -44,6 +44,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</div>
</div>
{# Summary of consumption and consume button #}
<div class="col-xl-5 d-none" id="consos_list_div">
<div class="card bg-light border-info mb-4">
......@@ -65,7 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</div>
{# Show last used buttons #}
<div class="card bg-light mb-4">
<div class="card-header">
......@@ -159,7 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript" src="{% static "js/consos.js" %}"></script>
<script type="text/javascript" src="{% static "note/js/consos.js" %}"></script>
<script type="text/javascript">
{% for button in highlighted %}
{% if button.display %}
......
......@@ -34,21 +34,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
<hr>
<div class="row">
<div class="row justify-content-center">
{# Preview note profile (picture, username and balance) #}
<div class="col-md-3" id="note_infos_div">
<div class="card bg-light border-success shadow mb-4 pt-4 text-center">
<div class="col-md picture-col" id="note_infos_div">
<div class="card bg-light mb-4 text-center">
<a id="profile_pic_link" href="#"><img src="{% static "member/img/default_picture.png" %}"
id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a>
<div class="card-body text-center">
<span id="user_note"></span>
<div class="card-body text-center p-2">
<span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span>
</div>
</div>
</div>
{# list of emitters #}
<div class="col-md-3" id="emitters_div">
<div class="card bg-light border-success shadow mb-4">
<div class="card bg-light mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
<label for="source_note" id="source_note_label">{% trans "Select emitters" %}</label>
......@@ -75,7 +75,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
{# list of receiver #}
<div class="col-md-3" id="dests_div">
<div class="card bg-light border-info shadow mb-4">
<div class="card bg-light mb-4">
<div class="card-header">
<p class="card-text font-weight-bold" id="dest_title">
<label for="dest_note" id="dest_note_label">{% trans "Select receivers" %}</label>
......@@ -97,8 +97,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
{# Information on transaction (amount, reason, name,...) #}
<div class="col-md-3" id="external_div">
<div class="card bg-light border-warning shadow mb-4">
<div class="col-md" id="external_div">
<div class="card bg-light mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Action" %}
......@@ -153,7 +153,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
{# transaction history #}
<div class="card shadow mb-4" id="history">
<div class="card mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Recent transactions history" %}
......@@ -176,5 +176,5 @@ SPDX-License-Identifier: GPL-2.0-or-later
select_receveirs_label = "{% trans "Select receivers" %}";
transfer_type_label = "{% trans "Transfer type" %}";
</script>
<script src="/static/js/transfer.js"></script>
<script src="{% static "note/js/transfer.js" %}"></script>
{% endblock %}
......@@ -144,7 +144,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
The Magic View that make people pay their beer and burgers.
(Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`)
(Most of the magic happens in the dark world of Javascript see `static/note/js/consos.js`)
"""
model = Transaction
template_name = "note/conso_form.html"
......
......@@ -4,7 +4,6 @@
from functools import lru_cache
from time import time
from django.conf import settings
from django.contrib.sessions.models import Session
from note_kfet.middlewares import get_current_session
......@@ -33,9 +32,9 @@ def memoize(f):
sess_funs = new_sess_funs
def func(*args, **kwargs):
if settings.DEBUG:
# Don't memoize in DEBUG mode
return f(*args, **kwargs)
# if settings.DEBUG:
# # Don't memoize in DEBUG mode
# return f(*args, **kwargs)
nonlocal last_collect
......
......@@ -2679,6 +2679,102 @@
"description": "Supprimer n'importe quel alias à une note non bloquée"
}
},
{
"model": "permission.permission",
"pk": 172,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les remises"
}
},
{
"model": "permission.permission",
"pk": 173,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter une remise"
}
},
{
"model": "permission.permission",
"pk": 174,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",