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) context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ 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 context["activity"] = activity
matched = [] 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\ guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern) .filter(activity=activity)\
| Q(inviter__alias__name__regex=pattern)
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\ .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: for guest in guest_qs:
guest.type = "Invité" guest.type = "Invité"
matched.append(guest) matched.append(guest)
@ -145,12 +148,18 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
username=F("note__noteuser__user__username"), username=F("note__noteuser__user__username"),
note_name=F("name"), note_name=F("name"),
balance=F("note__balance"))\ balance=F("note__balance"))\
.filter(Q(note__polymorphic_ctype__model="noteuser") .filter(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(PermissionBackend.filter_queryset(self.request.user, Alias, "view")) .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': if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
note_qs = note_qs.distinct('note__pk')[:20] note_qs = note_qs.distinct('note__pk')[:20]
else: 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. # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
# In production mode, please use PostgreSQL. # In production mode, please use PostgreSQL.
note_qs = note_qs.distinct()[:20] note_qs = note_qs.distinct()[:20]
for note in note_qs: for note in note_qs:
note.type = "Adhérent" note.type = "Adhérent"
note.activity = activity note.activity = activity
@ -172,8 +182,11 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = Activity.objects.filter(open=True).filter( activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter( PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).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 return context

View File

@ -18,7 +18,7 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
user = get_current_authenticated_user() 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): class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
@ -32,4 +32,4 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
user = get_current_authenticated_user() 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 import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
@ -151,6 +152,7 @@ class MembershipRolesForm(forms.ModelForm):
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=Role.objects.filter(weirole=None).all(), queryset=Role.objects.filter(weirole=None).all(),
label=_("Roles"), label=_("Roles"),
widget=CheckboxSelectMultiple(),
) )
class Meta: class Meta:

View File

@ -139,7 +139,7 @@ class Profile(models.Model):
'token': email_validation_token.make_token(self.user), 'token': email_validation_token.make_token(self.user),
'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), '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, 'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"), '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.response import Response
from rest_framework import status from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet 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,\ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
@ -150,3 +152,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
serializer_class = TransactionPolymorphicSerializer serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter] filter_backends = [SearchFilter]
search_fields = ['$reason', ] 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 import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _ 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): class ImageForm(forms.Form):
@ -38,3 +39,80 @@ class TransactionTemplateForm(forms.ModelForm):
), ),
'amount': AmountInput(), '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, TransactionTemplate,
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
category = models.ForeignKey( category = models.ForeignKey(
TemplateCategory, TemplateCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -252,6 +253,10 @@ class RecurrentTransaction(Transaction):
def type(self): def type(self):
return _('Template') return _('Template')
class Meta:
verbose_name = _("recurrent transaction")
verbose_name_plural = _("recurrent transactions")
class SpecialTransaction(Transaction): class SpecialTransaction(Transaction):
""" """
@ -290,6 +295,10 @@ class SpecialTransaction(Transaction):
raise(ValidationError(_("A special transaction is only possible between a" raise(ValidationError(_("A special transaction is only possible between a"
" Note associated to a payment method and a User or a Club"))) " 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): class MembershipTransaction(Transaction):
""" """

View File

@ -12,4 +12,5 @@ urlpatterns = [
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'), path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
path('consos/', views.ConsoView.as_view(), name='consos'), 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.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType 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.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_tables2 import SingleTableView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from activity.models import Entry
from note_kfet.inputs import AmountInput from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from .forms import TransactionTemplateForm from .forms import TransactionTemplateForm, SearchTransactionForm
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
from .models.transactions import SpecialTransaction from .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable from .tables import HistoryTable, ButtonTable
@ -52,9 +54,13 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
# Add a shortcut for entry page for open activities # Add a shortcut for entry page for open activities
if "activity" in settings.INSTALLED_APPS: if "activity" in settings.INSTALLED_APPS:
from activity.models import Activity from activity.models import Activity
context["activities_open"] = Activity.objects.filter(open=True).filter( activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter( PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).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 return context
@ -165,3 +171,51 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
context['no_cache'] = True context['no_cache'] = True
return context 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" "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", "model": "permission.role",
"pk": 1, "pk": 1,
@ -2643,7 +2675,9 @@
144, 144,
145, 145,
146, 146,
147 147,
148,
149
] ]
} }
}, },
@ -2690,7 +2724,9 @@
43, 43,
44, 44,
45, 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.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _ 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 from note_kfet.inputs import AmountInput
@ -22,6 +22,12 @@ class SignUpForm(UserCreationForm):
self.fields['email'].required = True self.fields['email'].required = True
self.fields['email'].help_text = _("This address must be valid.") 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: class Meta:
model = User model = User
fields = ('first_name', 'last_name', 'username', 'email', ) 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 import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.forms import CheckboxSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
@ -47,6 +48,7 @@ class WEIChooseBusForm(forms.Form):
label=_("bus"), label=_("bus"),
help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team," 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."), + " in particular if you are a free eletron."),
widget=CheckboxSelectMultiple(),
) )
team = forms.ModelMultipleChoiceField( team = forms.ModelMultipleChoiceField(
@ -54,17 +56,24 @@ class WEIChooseBusForm(forms.Form):
label=_("Team"), label=_("Team"),
required=False, required=False,
help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"), help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"),
widget=CheckboxSelectMultiple(),
) )
roles = forms.ModelMultipleChoiceField( roles = forms.ModelMultipleChoiceField(
queryset=WEIRole.objects.filter(~Q(name="1A")), queryset=WEIRole.objects.filter(~Q(name="1A")),
label=_("WEI Roles"), label=_("WEI Roles"),
help_text=_("Select the roles that you are interested in."), 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): 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): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()

View File

@ -130,7 +130,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["my_registration"] = my_registration context["my_registration"] = my_registration
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \ 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-") bus_table = BusTable(data=buses, prefix="bus-")
context['buses'] = bus_table context['buses'] = bus_table
@ -527,8 +527,9 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
context["form"].fields["user"].disabled = True context["form"].fields["user"].disabled = True
choose_bus_form = WEIChooseBusForm() choose_bus_form = WEIChooseBusForm()
choose_bus_form.fields["bus"].queryset = Bus.objects.filter(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"]) choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"])\
.order_by('bus__name', 'name')
context['membership_form'] = choose_bus_form context['membership_form'] = choose_bus_form
return context return context
@ -588,9 +589,6 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
form_class = WEIRegistrationForm form_class = WEIRegistrationForm
extra_context = {"title": _("Update WEI Registration")} extra_context = {"title": _("Update WEI Registration")}
def get_queryset(self, **kwargs):
return WEIRegistration.objects
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
wei = self.get_object().wei wei = self.get_object().wei
today = date.today() 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): class Autocomplete(TextInput):
template_name = "member/autocomplete_model.html" template_name = "member/autocomplete_model.html"
def __init__(self, model, attrs=None): def __init__(self, model, resetable=False, attrs=None):
super().__init__(attrs) super().__init__(attrs)
self.model = model self.model = model
self.resetable = resetable
self.model_pk = None self.model_pk = None
class Media: class Media:
@ -34,6 +35,11 @@ class Autocomplete(TextInput):
js = ('js/autocomplete_model.js', ) 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): def format_value(self, value):
if value: if value:
self.attrs["model_pk"] = int(value) self.attrs["model_pk"] = int(value)

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
# 'theme', # 'theme',
# External apps # External apps
'mailer',
'polymorphic', 'polymorphic',
'crispy_forms', 'crispy_forms',
'django_tables2', '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') SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
# Emails # 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_USE_SSL = False
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org') EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org')
EMAIL_PORT = os.getenv('EMAIL_PORT', 465) 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-crispy-forms==1.7.2
django-extensions==2.1.9 django-extensions==2.1.9
django-filter==2.2.0 django-filter==2.2.0
django-mailer==2.0.1
django-polymorphic==2.0.3 django-polymorphic==2.0.3
django-tables2==2.1.0 django-tables2==2.1.0
docutils==0.14 docutils==0.14

View File

@ -10,6 +10,7 @@ $(document).ready(function () {
if (!name_field) if (!name_field)
name_field = "name"; name_field = "name";
let input = target.val(); let input = target.val();
$("#" + prefix + "_reset").removeClass("d-none");
$.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) { $.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) {
let html = ""; 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="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
<input type="text" <input type="text"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %} {% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
@ -5,5 +7,8 @@
{% for name, value in widget.attrs.items %} {% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %} {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}> {% 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 class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul> </ul>

View File

@ -1,5 +1,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %} {% load i18n %}
{% load perms %}
{% if managers.data %} {% if managers.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
@ -29,7 +31,7 @@
{% if history_list.data %} {% if history_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <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" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a> </a>
</div> </div>

View File

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

View File

@ -2,10 +2,10 @@
{% load i18n %} {% load i18n %}
{% load perms %} {% 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"> <div class="alert alert-warning">
{% trans "This user doesn't have confirmed his/her e-mail address." %} {% 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> </div>
{% endif %} {% endif %}
@ -22,9 +22,7 @@
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
<a class="collapsed font-weight-bold" <a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:user.note %} href="{% url 'note:transactions' pk=user.note.pk %}" {% endif %}>
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="true" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> {% trans "Transaction history" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a> </a>
</div> </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" %} {% extends "base.html" %}
{% block content %} {% 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 %} {% endblock %}

View File

@ -27,7 +27,7 @@
</p> </p>
<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>
<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 "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" %}, {% trans "Thanks" %},

View File

@ -1,59 +1,12 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %} {% load i18n %}
{% load perms %}
<div class="card"> <div class="card">
<div class="card-header text-center"> <div class="card-header text-center">
<h4>WEI</h4> <h4>WEI</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<p>LE WEI, c'est cool !</p> <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> </div>
{% if club.is_current_wei %} {% if club.is_current_wei %}
@ -101,7 +54,7 @@
{% if history_list.data %} {% if history_list.data %}
<div class="card"> <div class="card">
<div class="card-header position-relative" id="historyListHeading"> <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" %} <i class="fa fa-euro"></i> {% trans "Transaction history" %}
</a> </a>
</div> </div>

View File

@ -14,3 +14,29 @@
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form> </form>
{% endblock %} {% 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 %}