Merge branch 'documents' into beta

This commit is contained in:
Pierre-antoine Comby 2020-08-19 13:18:12 +02:00
commit 6ea92cdcde
13 changed files with 143 additions and 88 deletions

View File

@ -54,6 +54,15 @@ class ActivityForm(forms.ModelForm):
class GuestForm(forms.ModelForm):
def clean(self):
"""
Someone can be invited as a Guest to an Activity if:
- the activity has not already started.
- the activity is validated.
- the Guest has not already been invited more than 5 times.
- the Guest is already invited.
- the inviter already invited 3 peoples.
"""
cleaned_data = super().clean()
if timezone.now() > timezone.localtime(self.activity.date_start):

View File

@ -11,6 +11,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, TemplateView, UpdateView
from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -21,6 +22,9 @@ from .tables import ActivityTable, EntryTable, GuestTable
class ActivityCreateView(ProtectedCreateView):
"""
View to create a new Activity
"""
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Create new activity")}
@ -47,6 +51,9 @@ class ActivityCreateView(ProtectedCreateView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
table_class = ActivityTable
ordering = ('-date_start',)
@ -73,6 +80,9 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Shows details about one activity. Add guest to context
"""
model = Activity
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
@ -90,6 +100,9 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Updates one Activity
"""
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Update activity")}
@ -99,11 +112,15 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`
"""
model = Guest
form_class = GuestForm
template_name = "activity/activity_invite.html"
def get_sample_object(self):
""" Creates a standart Guest binds to the Activity"""
activity = Activity.objects.get(pk=self.kwargs["pk"])
return Guest(
activity=activity,
@ -134,6 +151,9 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
class ActivityEntryView(LoginRequiredMixin, TemplateView):
"""
Manages entry to an activity
"""
template_name = "activity/activity_entry.html"
def dispatch(self, request, *args, **kwargs):
@ -154,14 +174,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
raise PermissionDenied(_("This activity is closed."))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched = []
def get_invited_guest(self,activity):
"""
Retrieves all Guests to the activity
"""
guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
@ -182,11 +198,13 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
else:
pattern = None
guest_qs = guest_qs.none()
return guest_qs
for guest in guest_qs:
guest.type = "Invité"
matched.append(guest)
def get_invited_note(self,activity):
"""
Retrieves all Note that can attend the activity,
they need to have an up-to-date membership in the attendees_club.
"""
note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"),
first_name=F("note__noteuser__user__first_name"),
username=F("note__noteuser__user__username"),
@ -223,8 +241,25 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
# In production mode, please use PostgreSQL.
note_qs = note_qs.distinct()[:20]
return note_qs
for note in note_qs:
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched=[]
for guest in get_invited_guest(self,activity):
guest.type = "Invité"
matched.append(guest)
for note in get_invited_note(self,activity):
note.type = "Adhérent"
note.activity = activity
matched.append(note)

View File

@ -65,6 +65,18 @@ class ProfileForm(forms.ModelForm):
exclude = ('user', 'email_confirmed', 'registration_valid', )
class ImageForm(forms.Form):
"""
Form used for the js interface for profile picture
"""
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 ClubForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()

View File

@ -3,8 +3,8 @@
import io
from datetime import timedelta, date
from PIL import Image
from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
@ -18,8 +18,8 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from note.forms import ImageForm
from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable
@ -28,7 +28,8 @@ from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\
CustomAuthenticationForm, MembershipRolesForm,
from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
@ -49,6 +50,7 @@ class CustomLoginView(LoginView):
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update the user information.
On this view both `:models:member.User` and `:models:member.Profile` are updated through forms
"""
model = User
form_class = UserForm
@ -77,14 +79,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return context
def form_valid(self, 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.object:
form.add_error('username',
_("An alias with a similar name already exists."))
return super().form_invalid(form)
"""
Check if ProfileForm is correct
then check if username is not already taken by someone else or by the user,
then check if email has changed, and if so ask for new validation.
"""
profile_form = ProfileForm(
data=self.request.POST,
@ -93,31 +92,35 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
profile_form.full_clean()
if not profile_form.is_valid():
return super().form_invalid(form)
new_username = form.data['username']
# Check if the new username is not already taken as an alias of someone else.
note = NoteUser.objects.filter(
alias__normalized_name=Alias.normalize(new_username))
if note.exists() and note.get().user != self.object:
form.add_error('username',
_("An alias with a similar name already exists."))
return super().form_invalid(form)
# Check if the username is one of user's aliases.
alias = Alias.objects.filter(name=new_username)
# Si le nouveau pseudo n'est pas un de nos alias,
# on supprime éventuellement un alias similaire pour le remplacer
if not alias.exists():
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
if similar.exists():
similar.delete()
olduser = User.objects.get(pk=form.instance.pk)
user = form.save(commit=False)
profile = profile_form.save(commit=False)
profile.user = user
profile.save()
user.save()
if olduser.email != user.email:
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
user.profile.email_confirmed = False
user.profile.save()
user.profile.send_email_validation_link()
profile = profile_form.save(commit=False)
profile.user = user
profile.save()
user.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
@ -127,7 +130,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs...
Display all information about a user.
"""
model = User
context_object_name = "user_object"
@ -141,6 +144,9 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return super().get_queryset().filter(profile__registration_valid=True)
def get_context_data(self, **kwargs):
"""
Add history of transaction and list of membership of user.
"""
context = super().get_context_data(**kwargs)
user = context['user_object']
history_list = \
@ -356,22 +362,26 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
extra_context = {"title": _("Club detail")}
def get_context_data(self, **kwargs):
"""
Add list of managers (peoples with Permission/Roles in this club), history of transactions and members list
"""
context = super().get_context_data(**kwargs)
club = context["club"]
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
# managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\
.order_by('user__last_name').all()
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.order_by('-created_at')
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table
# member list
club_member = Membership.objects.filter(
club=club,
date_end__gte=date.today(),
@ -469,15 +479,19 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
)
def get_context_data(self, **kwargs):
"""
Membership can be created, or renewed
In case of creation the url is /club/<club_pk>/add_member
For a renewal it will be `club/renew_membership/<pk>`
"""
context = super().get_context_data(**kwargs)
form = context['form']
if "club_pk" in self.kwargs:
# We create a new membership.
if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid
# Ensure that the user is member of the parent club and all its the family tree.
c = club
clubs_renewal = []
additional_fee_renewal = 0
@ -498,8 +512,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid
context["total_fee"] = "{:.02f}".format(fee / 100, )
else:
# This is a renewal. Fields can be pre-completed.
else: # This is a renewal. Fields can be pre-completed.
context["renewal"] = True
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
@ -511,6 +524,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
additional_fee_renewal = 0
while c.parent_club is not None:
c = c.parent_club
# check if a valid membership exists for the parent club
if c.membership_start and not Membership.objects.filter(
club=c,
user=user,
@ -529,7 +543,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields['last_name'].initial = user.last_name
form.fields['first_name'].initial = user.first_name
# If this is a renewal of a BDE membership, Société générale can pays, if it is not yet done
# If this is a renewal of a BDE membership, Société générale can pays, if it has not been already done.
if (club.name != "BDE" and club.name != "Kfet") or user.profile.soge:
del form.fields['soge']
else:
@ -559,11 +573,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
Create membership, check that all is good, make transactions
"""
# Get the club that is concerned by the membership
if "club_pk" in self.kwargs:
if "club_pk" in self.kwargs: # get from url of new membership
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
else:
else: # get from url for renewal
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
user = old_membership.user
@ -572,6 +586,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# Get form data
credit_type = form.cleaned_data["credit_type"]
# but with this way users can customize their section as they want.
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
@ -589,6 +604,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
fee = 0
c = club
# collect the fees required to be paid
while c is not None and c.membership_start:
if not Membership.objects.filter(
club=c,
@ -632,9 +648,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# Now, all is fine, the membership can be created.
if club.name == "BDE" or club.name == "Kfet":
# When we renew the BDE membership, we update the profile section.
# We could automate that and remove the section field from the Profile model,
# but with this way users can customize their section as they want.
# When we renew the BDE membership, we update the profile section
# that should happens at least once a year.
user.profile.section = user.profile.section_generated
user.profile.save()

View File

@ -10,15 +10,6 @@ from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
from .models import TransactionTemplate, NoteClub, 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:

View File

@ -1,7 +1,9 @@
{# Select amount to transfert in € #}
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" {% if not widget.attrs.negative %}min="0"{% endif %} step="0.01"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}"
{# Other attributes are loaded #}
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>

View File

@ -45,7 +45,7 @@
</div>
</div>
</div>
{# Summary of consumption and consume button #}
<div class="col-xl-5 d-none" id="consos_list_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
@ -91,7 +91,6 @@
</div>
{# Regroup buttons under categories #}
{# {% regroup transaction_templates by category as categories %} #}
<div class="card border-primary text-center shadow mb-4">
{# Tabs for button categories #}
@ -148,7 +147,7 @@
</div>
</div>
</div>
{# history of transaction #}
<div class="card shadow mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% load i18n static django_tables2 perms %}
{% block content %}
{# bandeau transfert/crédit/débit/activité #}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
@ -34,8 +34,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
</div>
<div class="row">
{# Preview note profile (picture, username and balance) #}
<div class="col-md-3" id="note_infos_div">
<div class="card border-success shadow mb-4">
<a id="profile_pic_link" href="#"><img src="/media/pic/default.png"
@ -45,7 +45,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
</div>
{# list of emitters #}
<div class="col-md-3" id="emitters_div">
<div class="card border-success shadow mb-4">
<div class="card-header">
@ -66,7 +66,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
</div>
{# list of receiver #}
<div class="col-md-3" id="dests_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
@ -83,7 +83,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
</div>
{# Information on transaction (amount, reason, name,...) #}
<div class="col-md-3" id="external_div">
<div class="card border-warning shadow mb-4">
<div class="card-header">
@ -108,7 +108,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<p id="reason-required" class="invalid-feedback"></p>
</div>
</div>
{# in case of special transaction add identity information #}
<div class="d-none" id="special_transaction_div">
<div class="form-row">
<div class="col-md-12">
@ -149,7 +149,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div>
</div>
</div>
{# transaction history #}
<div class="card shadow mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">

View File

@ -5,6 +5,7 @@
{% block content %}
<div class="row justify-content-center mb-4">
<div class="col-md-10 text-center">
{# Search field , see js #}
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}" value="{{ request.GET.search }}">
<hr>
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}" data-turbolinks="false">{% trans "New button" %}</a>

View File

@ -29,21 +29,19 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
"""
template_name = "note/transaction_form.html"
# SingleTableView creates `context["table"]` we will load it with transaction history
model = Transaction
# Transaction history table
table_class = HistoryTable
extra_context = {"title": _("Transfer money")}
def get_queryset(self, **kwargs):
# retrieves only Transaction that user has the right to see.
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-created_at").all()[:20]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
@ -146,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 consos.js)
(Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`)
"""
model = Transaction
template_name = "note/conso_form.html"
@ -168,29 +166,30 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
"""
restrict to the transaction history the user can see.
"""
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-created_at").all()[:20]
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
categories = TemplateCategory.objects.order_by('name').all()
# for each category, find which transaction templates the user can see.
for category in categories:
category.templates_filtered = category.templates.filter(
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).filter(display=True).order_by('name').all()
context['categories'] = [cat for cat in categories if cat.templates_filtered]
# some transactiontemplate are put forward to find them easily
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
).order_by('name').all()
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
# select2 compatibility
context['no_cache'] = True
return context

View File

@ -29,7 +29,7 @@ from .tokens import email_validation_token
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profil
A view to create a User and add a Profile
"""
form_class = SignUpForm

View File

@ -57,7 +57,6 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
form_set = ProductFormSet(instance=form.instance)
context['formset'] = form_set
context['helper'] = ProductFormSetHelper()
context['no_cache'] = True
return context
@ -125,7 +124,6 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form_set = ProductFormSet(instance=self.object)
context['formset'] = form_set
context['helper'] = ProductFormSetHelper()
context['no_cache'] = True
if self.object.locked:
for field_name in form.fields:

View File

@ -22,12 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}">
<meta name="theme-color" content="#ffffff">
{# Disable turbolink cache for some pages #}
{% if no_cache %}
<meta name="turbolinks-cache-control" content="no-cache">
{% endif %}
{# Bootstrap CSS #}
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"