Compare commits

..

10 Commits

Author SHA1 Message Date
Yohann D'ANELLO ba067f050e Mails to be sent are added in a queue thanks to Django Mailer (todo: configure cron) 2020-08-03 20:09:16 +02:00
Yohann D'ANELLO 2a744a8610 Display invalid transactions but don't count on them in the total 2020-08-03 19:37:47 +02:00
Yohann D'ANELLO 0e8058ab0d Add script to send weekly report to all members 2020-08-03 19:35:25 +02:00
Yohann D'ANELLO 655390b265 A longer transaction history is better 2020-08-03 18:50:51 +02:00
Yohann D'ANELLO 985a5ca876 Add "search transactions page" 2020-08-03 18:49:15 +02:00
Yohann D'ANELLO 55580bc11e Merge remote-tracking branch 'origin/beta' into beta 2020-08-03 16:11:25 +02:00
Yohann D'ANELLO 5ea8d8f870 🎨 Update activity interface 2020-08-03 16:11:05 +02:00
Yohann D'ANELLO 0a2c9d9c87 🐛 Better entry page 2020-08-03 15:44:06 +02:00
Yohann D'ANELLO 208dc7f865 🎨 Use multiple checkboxes than multiple select widget 2020-08-03 13:33:25 +02:00
Yohann D'ANELLO fbf3a0bcf6 🐛 A new user can't take an existing alias as username 2020-08-03 12:35:51 +02:00
32 changed files with 1106 additions and 643 deletions

View File

@ -114,28 +114,31 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched = []
pattern = "^$"
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if not pattern:
pattern = "^$"
if pattern[0] != "^":
pattern = "^" + pattern
guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern)
| Q(inviter__alias__name__regex=pattern)
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))) \
.filter(activity=activity)\
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
.distinct()[:20]
.order_by('last_name', 'first_name').distinct()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if pattern[0] != "^":
pattern = "^" + pattern
guest_qs = guest_qs.filter(
Q(first_name__regex=pattern)
| Q(last_name__regex=pattern)
| Q(inviter__alias__name__regex=pattern)
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))
)
else:
pattern = None
guest_qs = guest_qs.none()
for guest in guest_qs:
guest.type = "Invité"
matched.append(guest)
@ -145,12 +148,18 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
username=F("note__noteuser__user__username"),
note_name=F("name"),
balance=F("note__balance"))\
.filter(Q(note__polymorphic_ctype__model="noteuser")
& (Q(note__noteuser__user__first_name__regex=pattern)
| Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern)))) \
.filter(note__polymorphic_ctype__model="noteuser")\
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if pattern:
note_qs = note_qs.filter(
Q(note__noteuser__user__first_name__regex=pattern)
| Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern))
)
else:
note_qs = note_qs.none()
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
note_qs = note_qs.distinct('note__pk')[:20]
else:
@ -158,6 +167,7 @@ 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]
for note in note_qs:
note.type = "Adhérent"
note.activity = activity
@ -172,8 +182,11 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request.user,
"activity.add_entry",
Entry(activity=a, note=self.request.user.note,))]
return context

View File

@ -18,7 +18,7 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
def get_queryset(self):
user = get_current_authenticated_user()
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
@ -32,4 +32,4 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
user = get_current_authenticated_user()
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()

View File

@ -4,6 +4,7 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
@ -151,6 +152,7 @@ class MembershipRolesForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(
queryset=Role.objects.filter(weirole=None).all(),
label=_("Roles"),
widget=CheckboxSelectMultiple(),
)
class Meta:

View File

@ -139,7 +139,7 @@ class Profile(models.Model):
'token': email_validation_token.make_token(self.user),
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)),
})
html = loader.render_to_string('registration/mails/email_validation_email.txt',
html = loader.render_to_string('registration/mails/email_validation_email.html',
{
'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"),

View File

@ -9,6 +9,8 @@ from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
@ -150,3 +152,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter]
search_fields = ['$reason', ]
def get_queryset(self):
user = get_current_authenticated_user()
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))

View File

@ -3,10 +3,11 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput
from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
from .models import TransactionTemplate, NoteClub
from .models import TransactionTemplate, NoteClub, Alias
class ImageForm(forms.Form):
@ -38,3 +39,80 @@ class TransactionTemplateForm(forms.ModelForm):
),
'amount': AmountInput(),
}
class SearchTransactionForm(forms.Form):
source = forms.ModelChoiceField(
queryset=Alias.objects.all(),
label=_("Source"),
required=False,
widget=Autocomplete(
Alias,
resetable=True,
attrs={
'api_url': '/api/note/alias/',
'placeholder': 'Note ...',
},
),
)
destination = forms.ModelChoiceField(
queryset=Alias.objects.all(),
label=_("Destination"),
required=False,
widget=Autocomplete(
Alias,
resetable=True,
attrs={
'api_url': '/api/note/alias/',
'placeholder': 'Note ...',
},
),
)
type = forms.ModelMultipleChoiceField(
queryset=ContentType.objects.filter(app_label="note", model__endswith="transaction"),
initial=ContentType.objects.filter(app_label="note", model__endswith="transaction"),
label=_("Type"),
required=False,
widget=CheckboxSelectMultiple(),
)
reason = forms.CharField(
label=_("Reason"),
required=False,
)
valid = forms.BooleanField(
label=_("Valid"),
initial=False,
required=False,
)
amount_gte = forms.Field(
label=_("Total amount greater than"),
initial=0,
required=False,
widget=AmountInput(),
)
amount_lte = forms.Field(
initial=2 ** 31 - 1,
label=_("Total amount less than"),
required=False,
widget=AmountInput(),
)
created_after = forms.Field(
label=_("Created after"),
initial="2000-01-01 00:00",
required=False,
widget=DateTimePickerInput(),
)
created_before = forms.Field(
label=_("Created before"),
initial="2042-12-31 21:42",
required=False,
widget=DateTimePickerInput(),
)

View File

@ -243,6 +243,7 @@ class RecurrentTransaction(Transaction):
TransactionTemplate,
on_delete=models.PROTECT,
)
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
@ -252,6 +253,10 @@ class RecurrentTransaction(Transaction):
def type(self):
return _('Template')
class Meta:
verbose_name = _("recurrent transaction")
verbose_name_plural = _("recurrent transactions")
class SpecialTransaction(Transaction):
"""
@ -290,6 +295,10 @@ 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")))
class Meta:
verbose_name = _("Special transaction")
verbose_name_plural = _("Special transactions")
class MembershipTransaction(Transaction):
"""

View File

@ -12,4 +12,5 @@ urlpatterns = [
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
path('consos/', views.ConsoView.as_view(), name='consos'),
path('transactions/<int:pk>/', views.TransactionSearchView.as_view(), name='transactions'),
]

View File

@ -5,17 +5,19 @@ import json
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models import Q, F
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView
from django.views.generic import CreateView, UpdateView, DetailView
from django_tables2 import SingleTableView
from django.urls import reverse_lazy
from activity.models import Entry
from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import TransactionTemplateForm
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
from .forms import TransactionTemplateForm, SearchTransactionForm
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
from .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable
@ -52,9 +54,13 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
# Add a shortcut for entry page for open activities
if "activity" in settings.INSTALLED_APPS:
from activity.models import Activity
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request.user,
"activity.add_entry",
Entry(activity=a,
note=self.request.user.note, ))]
return context
@ -165,3 +171,51 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
context['no_cache'] = True
return context
class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Note
context_object_name = "note"
template_name = "note/search_transactions.html"
extra_context = {"title": _("Search transactions")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = SearchTransactionForm(data=self.request.GET if self.request.GET else None)
context["form"] = form
form.full_clean()
if form.is_valid():
data = form.cleaned_data
else:
data = {}
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
if "source" in data and data["source"]:
transactions = transactions.filter(source_id=data["source"].note_id)
if "destination" in data and data["destination"]:
transactions = transactions.filter(destination_id=data["destination"].note_id)
if "type" in data and data["type"]:
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
if "reason" in data and data["reason"]:
transactions = transactions.filter(reason__iregex=data["reason"])
if "valid" in data and data["valid"]:
transactions = transactions.filter(valid=data["valid"])
if "amount_gte" in data and data["amount_gte"]:
transactions = transactions.filter(total_amount__gte=data["amount_gte"])
if "amount_lte" in data and data["amount_lte"]:
transactions = transactions.filter(total_amount__lte=data["amount_lte"])
if "created_after" in data and data["created_after"]:
transactions = transactions.filter(created_at__gte=data["created_after"])
if "created_before" in data and data["created_before"]:
transactions = transactions.filter(created_at__lte=data["created_before"])
table = HistoryTable(transactions)
table.paginate(per_page=100)
context["table"] = table
return context

View File

@ -2311,6 +2311,38 @@
"description": "Ajouter un membre à n'importe quel club"
}
},
{
"model": "permission.permission",
"pk": 148,
"fields": {
"model": [
"activity",
"activity"
],
"query": "{\"valid\": false}",
"type": "change",
"mask": 2,
"field": "",
"permanent": false,
"description": "Modifier une activité non validée"
}
},
{
"model": "permission.permission",
"pk": 149,
"fields": {
"model": [
"activity",
"activity"
],
"query": "{\"valid\": false}",
"type": "delete",
"mask": 2,
"field": "",
"permanent": false,
"description": "Supprimer une activité non validée"
}
},
{
"model": "permission.role",
"pk": 1,
@ -2643,7 +2675,9 @@
144,
145,
146,
147
147,
148,
149
]
}
},
@ -2690,7 +2724,9 @@
43,
44,
45,
46
46,
148,
149
]
}
},

View File

@ -5,7 +5,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
from note.models import NoteSpecial, Alias
from note_kfet.inputs import AmountInput
@ -22,6 +22,12 @@ class SignUpForm(UserCreationForm):
self.fields['email'].required = True
self.fields['email'].help_text = _("This address must be valid.")
def clean_username(self):
value = self.cleaned_data["username"]
if Alias.objects.filter(normalized_name=Alias.normalize(value)).exists():
self.add_error("username", _("An alias with a similar name already exists."))
return value
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email', )

@ -1 +1 @@
Subproject commit f41a5a32f7417a874b497640373ea3911eb1e133
Subproject commit 034d8c43b663ac3a33f6e3d06bcdcbbeea0bc517

View File

@ -4,6 +4,7 @@
from django import forms
from django.contrib.auth.models import User
from django.db.models import Q
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
@ -47,6 +48,7 @@ class WEIChooseBusForm(forms.Form):
label=_("bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team,"
+ " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(),
)
team = forms.ModelMultipleChoiceField(
@ -54,17 +56,24 @@ class WEIChooseBusForm(forms.Form):
label=_("Team"),
required=False,
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
widget=CheckboxSelectMultiple(),
)
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."),
initial=WEIRole.objects.filter(name="Adhérent WEI").all(),
widget=CheckboxSelectMultiple(),
)
class WEIMembershipForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects, label=_("WEI Roles"))
roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects,
label=_("WEI Roles"),
widget=CheckboxSelectMultiple(),
)
def clean(self):
cleaned_data = super().clean()

View File

@ -130,7 +130,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["my_registration"] = my_registration
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
.filter(wei=self.object).annotate(count=Count("memberships"))
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
bus_table = BusTable(data=buses, prefix="bus-")
context['buses'] = bus_table
@ -527,8 +527,9 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
context["form"].fields["user"].disabled = True
choose_bus_form = WEIChooseBusForm()
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"])
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]).order_by('name')
choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\
.order_by('bus__name', 'name')
context['membership_form'] = choose_bus_form
return context
@ -588,9 +589,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
form_class = WEIRegistrationForm
extra_context = {"title": _("Update WEI Registration")}
def get_queryset(self, **kwargs):
return WEIRegistration.objects
def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei
today = date.today()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -23,10 +23,11 @@ class AmountInput(NumberInput):
class Autocomplete(TextInput):
template_name = "member/autocomplete_model.html"
def __init__(self, model, attrs=None):
def __init__(self, model, resetable=False, attrs=None):
super().__init__(attrs)
self.model = model
self.resetable = resetable
self.model_pk = None
class Media:
@ -34,6 +35,11 @@ class Autocomplete(TextInput):
js = ('js/autocomplete_model.js', )
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['resetable'] = self.resetable
return context
def format_value(self, value):
if value:
self.attrs["model_pk"] = int(value)

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
# 'theme',
# External apps
'mailer',
'polymorphic',
'crispy_forms',
'django_tables2',

View File

@ -33,7 +33,8 @@ ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')]
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
# Emails
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = 'mailer.backend.DbBackend' # Mailer place emails in a queue before sending them to avoid spam
MAILER_EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = False
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org')
EMAIL_PORT = os.getenv('EMAIL_PORT', 465)

View File

@ -6,6 +6,7 @@ django-allauth==0.39.1
django-crispy-forms==1.7.2
django-extensions==2.1.9
django-filter==2.2.0
django-mailer==2.0.1
django-polymorphic==2.0.3
django-tables2==2.1.0
docutils==0.14

View File

@ -10,6 +10,7 @@ $(document).ready(function () {
if (!name_field)
name_field = "name";
let input = target.val();
$("#" + prefix + "_reset").removeClass("d-none");
$.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) {
let html = "";
@ -39,4 +40,12 @@ $(document).ready(function () {
}
});
});
$(".autocomplete-reset").click(function() {
let name = $(this).attr("id").replace("_reset", "");
$("#" + name + "_pk").val("");
$("#" + name).val("");
$("#" + name + "_list").html("");
$(this).addClass("d-none");
});
});

View File

@ -1,3 +1,5 @@
{% load i18n %}
<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
<input type="text"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
@ -5,5 +7,8 @@
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
{% if widget.resetable %}
<a id="{{ widget.attrs.id }}_reset" class="btn btn-light autocomplete-reset{% if not widget.value %} d-none{% endif %}">{% trans "Reset" %}</a>
{% endif %}
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul>

View File

@ -1,5 +1,7 @@
{% load render_table from django_tables2 %}
{% load i18n %}
{% load perms %}
{% if managers.data %}
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
@ -29,7 +31,7 @@
{% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="font-weight-bold">
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %} href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>

View File

@ -2,22 +2,22 @@
<div class="card bg-light shadow">
<div class="card-header text-center" >
<h4> {% trans "Account #" %} {{ object.pk }}</h4>
<h4> {% trans "Account #" %} {{ user.pk }}</h4>
</div>
<div class="card-top text-center">
<a href="{% url 'member:user_update_pic' object.pk %}">
<img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" >
<a href="{% url 'member:user_update_pic' user.pk %}">
<img src="{{ user.note.display_image.url }}" class="img-thumbnail mt-2" >
</a>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
<dd class="col-xl-6">{{ user.last_name }} {{ user.first_name }}</dd>
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.username }}</dd>
<dd class="col-xl-6">{{ user.username }}</dd>
{% if object.pk == user.pk %}
{% if user.pk == user.pk %}
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="small" href="{% url 'password_change' %}">
@ -27,25 +27,25 @@
{% endif %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.section }}</dd>
<dd class="col-xl-6">{{ user.profile.section }}</dd>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.profile.address }}</dd>
<dd class="col-xl-6">{{ user.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.note.balance | pretty_money }}</dd>
<dd class="col-xl-6">{{ user.note.balance | pretty_money }}</dd>
<dt class="col-xl-6"> <a href="{% url 'member:user_alias' object.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
<dt class="col-xl-6"> <a href="{% url 'member:user_alias' user.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-6 text-truncate">{{ user.note.alias_set.all|join:", " }}</dd>
</dl>
{% if object.pk == user.pk %}
{% if user.pk == user.pk %}
<a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a>
{% endif %}
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
{% url 'member:user_detail' object.pk as user_profile_url %}
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' user.pk %}">{% trans 'Update Profile' %}</a>
{% url 'member:user_detail' user.pk as user_profile_url %}
{%if request.path_info != user_profile_url %}
<a class="btn btn-primary btn-sm" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a>
{% endif %}

View File

@ -2,10 +2,10 @@
{% load i18n %}
{% load perms %}
{% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %}
{% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:user.profile %}
<div class="alert alert-warning">
{% trans "This user doesn't have confirmed his/her e-mail address." %}
<a href="{% url "registration:email_validation_resend" pk=object.pk %}">{% trans "Click here to resend a validation link." %}</a>
<a href="{% url "registration:email_validation_resend" pk=user.pk %}">{% trans "Click here to resend a validation link." %}</a>
</div>
{% endif %}
@ -22,9 +22,7 @@
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="true" aria-controls="historyListCollapse">
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:user.note %} href="{% url 'note:transactions' pk=user.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>

View File

@ -0,0 +1,41 @@
{% load pretty_money %}
{% load render_table from django_tables2 %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>[Note Kfet] Rapport de la Note Kfet</title>
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
</head>
<body>
<p>
Rapport d'activité de {{ user.first_name }} {{ user.last_name }} (note : {{ user }})
depuis le {{ last_week }} jusqu'au {{ now }}.
</p>
<p>
Dépenses totales : {{ outcoming|pretty_money }}<br>
Apports totaux : {{ incoming|pretty_money }}<br>
Différentiel : {{ diff|pretty_money }}<br>
Nouveau solde : {{ user.note.balance|pretty_money }}
</p>
<h4>Rapport détaillé</h4>
{% render_table table %}
--
<p>
Le BDE<br>
Mail généré par la Note Kfet le {% now "j F Y à H:i:s" %}
</p>
</body>
</html>

View File

@ -0,0 +1,57 @@
{% extends "member/noteowner_detail.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
{% block profile_info %}
{% if note.club.weiclub %}
{% with club=note.club.weiclub %}
{% include "wei/weiclub_info.html" %}
{% endwith %}
{% elif note.club %}
{% with club=note.club %}
{% include "member/club_info.html" %}
{% endwith %}
{% elif note.user %}
{% with user=note.user %}
{% include "member/profile_info.html" %}
{% endwith %}
{% endif %}
{% endblock %}
{% block profile_content %}
{% crispy form %}
<div id="table">
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'note:transactions' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'note:transactions' pk=object.pk %} #profile_infos");
}
function refreshFilters() {
let filters = "";
filters += "source=" + $("#id_source_pk").val();
filters += "&destination=" + $("#id_destination_pk").val();
filters += $("input[name='type']:checked").map(function() {
return "&type=" + $(this).val();
}).toArray().join("");
filters += "&reason=" + $("#id_reason").val();
filters += "&valid=" + ($("#id_valid").is(":checked") ? "1" : "");
filters += "&amount_gte=" + $("#id_amount_gte").val();
filters += "&amount_lte=" + $("#id_amount_lte").val();
filters += "&created_after=" + $("#id_created_after").val();
filters += "&created_before=" + $("#id_created_before").val();
console.log(filters.replace(" ", "%20"));
$("#table").load(location.pathname + "?" + filters.replaceAll(" ", "%20") + " #table");
}
$(document).ready(function() {
$("input").change(refreshFilters);
$("input").keyup(refreshFilters);
});
</script>
{% endblock %}

View File

@ -1,7 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h2>Account Activation</h2>
<h2>{% trans "Account activation" %}</h2>
An email has been sent. Please click on the link to activate your account.
<p>
{% trans "An email has been sent. Please click on the link to activate your account." %}
</p>
<p>
{% trans "You must also go to the Kfet to pay your membership. The WEI registration includes the BDE membership." %}
</p>
{% endblock %}

View File

@ -27,7 +27,7 @@
</p>
<p>
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %}
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet. Note that the WEI registration includes the Kfet membership." %}
</p>
<p>

View File

@ -8,7 +8,7 @@ https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=toke
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %}
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet. Note that the WEI registration includes the Kfet membership." %}
{% trans "Thanks" %},

View File

@ -1,59 +1,12 @@
{% load render_table from django_tables2 %}
{% load i18n %}
{% load perms %}
<div class="card">
<div class="card-header text-center">
<h4>WEI</h4>
</div>
<div class="card-body">
<p>LE WEI, c'est cool !</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Dapibus ultrices in iaculis nunc sed augue. In hendrerit gravida rutrum quisque non tellus orci
ac. Massa vitae tortor condimentum lacinia quis vel eros. Elit ut aliquam purus sit amet. Aliquam faucibus
purus in massa tempor. Quisque id diam vel quam elementum pulvinar etiam non. Condimentum id venenatis a
condimentum vitae sapien pellentesque habitant. Egestas congue quisque egestas diam in. Vestibulum rhoncus
est pellentesque elit ullamcorper. Massa sed elementum tempus egestas sed sed. Sapien pellentesque habitant
morbi tristique. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Sed
adipiscing diam donec adipiscing. Leo integer malesuada nunc vel risus commodo viverra maecenas.
</p>
<p>
Fusce id velit ut tortor pretium viverra suspendisse. Urna condimentum mattis pellentesque id nibh tortor id
aliquet. Vel facilisis volutpat est velit egestas dui. Turpis egestas sed tempus urna et pharetra pharetra
massa massa. Eget nunc scelerisque viverra mauris in. Etiam dignissim diam quis enim. Urna cursus eget nunc
scelerisque viverra mauris in aliquam sem. Amet porttitor eget dolor morbi non arcu risus quis. Ullamcorper
sit amet risus nullam eget felis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Enim nulla
aliquet porttitor lacus luctus accumsan tortor. Urna condimentum mattis pellentesque id nibh tortor id.
Feugiat in fermentum posuere urna nec. Risus nec feugiat in fermentum posuere urna nec tincidunt. Porttitor
massa id neque aliquam vestibulum morbi. Diam quis enim lobortis scelerisque. Ornare massa eget egestas
purus. Ut tortor pretium viverra suspendisse. Purus in mollis nunc sed. Tristique magna sit amet purus
gravida.
</p>
<p>
Ut porttitor leo a diam sollicitudin tempor. Viverra nam libero justo laoreet sit amet cursus sit amet.
Lectus arcu bibendum at varius vel pharetra vel turpis nunc. Vivamus arcu felis bibendum ut tristique et
egestas quis ipsum. Parturient montes nascetur ridiculus mus mauris. A cras semper auctor neque vitae
tempus quam pellentesque. Netus et malesuada fames ac. Mauris in aliquam sem fringilla ut. Sapien
pellentesque habitant morbi tristique. Mauris sit amet massa vitae tortor condimentum. Sagittis
aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc. Amet consectetur adipiscing elit
duis tristique sollicitudin nibh sit. Nunc mattis enim ut tellus elementum. Sapien eget mi proin sed libero
enim. Pulvinar sapien et ligula ullamcorper. Nibh mauris cursus mattis molestie a iaculis at erat
pellentesque. Molestie at elementum eu facilisis. Velit sed ullamcorper morbi tincidunt. Quam vulputate
dignissim suspendisse in est ante.
</p>
<p>
Id cursus metus aliquam eleifend mi. Eu turpis egestas pretium aenean pharetra magna ac. Faucibus ornare
suspendisse sed nisi lacus sed viverra tellus. Sed vulputate mi sit amet mauris commodo. Lacus laoreet non
curabitur gravida arcu ac. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget. Fusce ut
placerat orci nulla pellentesque dignissim. Quis blandit turpis cursus in hac habitasse platea dictumst
quisque. Tellus id interdum velit laoreet id donec ultrices. Risus feugiat in ante metus dictum. Velit ut
tortor pretium viverra suspendisse. Lacus vel facilisis volutpat est velit egestas dui id. Nunc eget lorem
dolor sed viverra ipsum nunc aliquet bibendum. Varius quam quisque id diam vel quam. Orci dapibus ultrices
in iaculis. Neque gravida in fermentum et sollicitudin ac orci.
</p>
</div>
{% if club.is_current_wei %}
@ -101,7 +54,7 @@
{% if history_list.data %}
<div class="card">
<div class="card-header position-relative" id="historyListHeading">
<a class="font-weight-bold">
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %} href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a>
</div>

View File

@ -14,3 +14,29 @@
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}
{% block extrajavascript %}
<script>
$(document).ready(function() {
function refreshTeams() {
let buses = [];
$("input[name='bus']:checked").each(function(ignored) {
buses.push($(this).parent().text().trim());
});
console.log(buses);
$("input[name='team']").each(function() {
let label = $(this).parent();
$(this).parent().addClass('d-none');
buses.forEach(function(bus) {
if (label.text().includes(bus))
label.removeClass('d-none');
});
});
}
$("input[name='bus']").change(refreshTeams);
refreshTeams();
});
</script>
{% endblock %}