1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-20 17:41:55 +02:00

Merge branch 'master' into 'logging'

# Conflicts:
#   locale/de/LC_MESSAGES/django.po
#   locale/fr/LC_MESSAGES/django.po
This commit is contained in:
ynerant
2020-03-07 10:32:17 +01:00
358 changed files with 82864 additions and 156 deletions

View File

@ -15,8 +15,10 @@ urlpatterns = [
path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases', views.AliasView.as_view(), name="user_alias"),
path('user/aliases/delete/<int:pk>', views.DeleteAliasView.as_view(), name="user_alias_delete"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
# API for the user autocompleter
path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"),
]

View File

@ -1,19 +1,28 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView,DeleteView
from django.views.generic.edit import FormMixin
from django.contrib.auth.models import User
from django.contrib import messages
from django.urls import reverse_lazy
from django.http import HttpResponseRedirect
from django.db.models import Q
from django.core.exceptions import ValidationError
from django.conf import settings
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from dal import autocomplete
from PIL import Image
import io
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable
from note.tables import HistoryTable, AliasTable
from note.forms import AliasForm, ImageForm
from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
@ -51,39 +60,32 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User
fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html'
second_form = ProfileForm
context_object_name = 'user_object'
profile_form = ProfileForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_modified'] = context['user']
context['user'] = self.request.user
context["profile_form"] = self.second_form(
instance=context['user_modified'].profile)
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
context['title'] = _("Update Profile")
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
if 'username' not in form.data:
return form
new_username = form.data['username']
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
note = NoteUser.objects.filter(
alias__normalized_name=Alias.normalize(new_username))
if note.exists() and note.get().user != self.request.user:
if note.exists() and note.get().user != self.object:
form.add_error('username',
_("An alias with a similar name already exists."))
return form
def form_valid(self, form):
profile_form = ProfileForm(
data=self.request.POST,
instance=self.request.user.profile,
instance=self.object.profile,
)
if form.is_valid() and profile_form.is_valid():
new_username = form.data['username']
@ -155,7 +157,104 @@ class UserListView(LoginRequiredMixin, SingleTableView):
context["filter"] = self.filter
return context
class AliasView(LoginRequiredMixin,FormMixin,DetailView):
model = User
template_name = 'member/profile_alias.html'
context_object_name = 'user_object'
form_class = AliasForm
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
note = context['user_object'].note
context["aliases"] = AliasTable(note.alias_set.all())
return context
def get_success_url(self):
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id})
def post(self,request,*args,**kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
alias = form.save(commit=False)
alias.note = self.object.note
alias.save()
return super().form_valid(form)
class DeleteAliasView(LoginRequiredMixin, DeleteView):
model = Alias
def delete(self,request,*args,**kwargs):
try:
self.object = self.get_object()
self.object.delete()
except ValidationError as e:
# TODO: pass message to redirected view.
messages.error(self.request,str(e))
else:
messages.success(self.request,_("Alias successfully deleted"))
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
print(self.request)
return reverse_lazy('member:user_alias',kwargs={'pk':self.object.note.user.pk})
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
model = User
template_name = 'member/profile_picture_update.html'
context_object_name = 'user_object'
form_class = ImageForm
def get_context_data(self,*args,**kwargs):
context = super().get_context_data(*args,**kwargs)
context['form'] = self.form_class(self.request.POST,self.request.FILES)
return context
def get_success_url(self):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})
def post(self,request,*args,**kwargs):
form = self.get_form()
self.object = self.get_object()
if form.is_valid():
return self.form_valid(form)
else:
print('is_invalid')
print(form)
return self.form_invalid(form)
def form_valid(self,form):
image_field = form.cleaned_data['image']
x = form.cleaned_data['x']
y = form.cleaned_data['y']
w = form.cleaned_data['width']
h = form.cleaned_data['height']
# image crop and resize
image_file = io.BytesIO(image_field.read())
ext = image_field.name.split('.')[-1]
image = Image.open(image_file)
image = image.crop((x, y, x+w, y+h))
image_clean = image.resize((settings.PIC_WIDTH,
settings.PIC_RATIO*settings.PIC_WIDTH),
Image.ANTIALIAS)
image_file = io.BytesIO()
image_clean.save(image_file,ext)
image_field.file = image_file
# renaming
filename = "{}_pic.{}".format(self.object.note.pk, ext)
image_field.name = filename
self.object.note.display_image = image_field
self.object.note.save()
return super().form_valid(form)
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
"""
Affiche le jeton d'authentification, et permet de le regénérer

View File

@ -3,10 +3,39 @@
from dal import autocomplete
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
import os
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout, HTML
from .models import Transaction, TransactionTemplate, TemplateTransaction
from .models import Note, Alias
class AliasForm(forms.ModelForm):
class Meta:
model = Alias
fields = ("name",)
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.fields["name"].label = False
self.fields["name"].widget.attrs={"placeholder":_('New Alias')}
class ImageForm(forms.Form):
image = forms.ImageField(required = False,
label=_('select an image'),
help_text=_('Maximal size: 2MB'))
x = forms.FloatField(widget=forms.HiddenInput())
y = forms.FloatField(widget=forms.HiddenInput())
width = forms.FloatField(widget=forms.HiddenInput())
height = forms.FloatField(widget=forms.HiddenInput())
class TransactionTemplateForm(forms.ModelForm):
class Meta:
model = TransactionTemplate
@ -33,6 +62,23 @@ class TransactionForm(forms.ModelForm):
def save(self, commit=True):
super().save(commit)
def clean(self):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
Transactions between a note and the same note are not authorized.
"""
cleaned_data = super().clean()
if not "source" in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds"
cleaned_data["source"] = self.user.note
if cleaned_data["source"].pk == cleaned_data["destination"].pk:
self.add_error("destination", _("Source and destination must be different."))
return cleaned_data
class Meta:
model = Transaction
fields = (

View File

@ -43,7 +43,10 @@ class Note(PolymorphicModel):
display_image = models.ImageField(
verbose_name=_('display image'),
max_length=255,
blank=True,
blank=False,
null=False,
upload_to='pic/',
default='pic/default.png'
)
created_at = models.DateTimeField(
verbose_name=_('created at'),
@ -219,14 +222,6 @@ class Alias(models.Model):
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'})).casefold()
def save(self, *args, **kwargs):
"""
Handle normalized_name
"""
self.normalized_name = Alias.normalize(self.name)
if len(self.normalized_name) < 256:
super().save(*args, **kwargs)
def clean(self):
normalized_name = Alias.normalize(self.name)
if len(normalized_name) >= 255:
@ -235,12 +230,13 @@ class Alias(models.Model):
try:
sim_alias = Alias.objects.get(normalized_name=normalized_name)
if self != sim_alias:
raise ValidationError(_('An alias with a similar name already exists:'),
raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)),
code="same_alias"
)
except Alias.DoesNotExist:
pass
self.normalized_name = normalized_name
def delete(self, using=None, keep_parents=False):
if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."),

View File

@ -84,8 +84,6 @@ class Transaction(PolymorphicModel):
amount is store in centimes of currency, making it a positive integer
value. (from someone to someone else)
TODO: Ensure source != destination.
"""
source = models.ForeignKey(
@ -126,6 +124,11 @@ class Transaction(PolymorphicModel):
"""
When saving, also transfer money between two notes
"""
if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered
return
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:

View File

@ -3,9 +3,9 @@
import django_tables2 as tables
from django.db.models import F
from django_tables2.utils import A
from .models.transactions import Transaction
from .models.notes import Alias
class HistoryTable(tables.Table):
class Meta:
@ -24,3 +24,22 @@ class HistoryTable(tables.Table):
queryset = queryset.annotate(total=F('amount') * F('quantity')) \
.order_by(('-' if is_descending else '') + 'total')
return (queryset, True)
class AliasTable(tables.Table):
class Meta:
attrs = {
'class':
'table table condensed table-striped table-hover'
}
model = Alias
fields =('name',)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
name = tables.Column(attrs={'td':{'class':'text-center'}})
delete = tables.LinkColumn('member:user_alias_delete',
args=[A('pk')],
attrs={
'td': {'class':'col-sm-2'},
'a': {'class': 'btn btn-danger'} },
text='delete',accessor='pk')

View File

@ -41,17 +41,12 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
if False: # TODO: fix it with "if %user has no right to transfer funds"
del form.fields['source']
form.user = self.request.user
return form
def form_valid(self, form):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
"""
if False: # TODO: fix it with "if %user has no right to transfer funds"
form.instance.source = self.request.user.note
return super().form_valid(form)
def get_success_url(self):
return reverse('note:transfer')
class NoteAutocomplete(autocomplete.Select2QuerySetView):