mirror of https://gitlab.crans.org/bde/nk20
Custom auto-complete fields, remove DAL requirement
This commit is contained in:
parent
823bcfe781
commit
f09364d3d8
|
@ -3,7 +3,8 @@
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from activity.models import Activity
|
from activity.models import Activity
|
||||||
from note_kfet.inputs import DateTimePickerInput
|
from member.models import Club
|
||||||
|
from note_kfet.inputs import DateTimePickerInput, AutocompleteModelSelect
|
||||||
|
|
||||||
|
|
||||||
class ActivityForm(forms.ModelForm):
|
class ActivityForm(forms.ModelForm):
|
||||||
|
@ -11,6 +12,14 @@ class ActivityForm(forms.ModelForm):
|
||||||
model = Activity
|
model = Activity
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
widgets = {
|
widgets = {
|
||||||
|
"organizer": AutocompleteModelSelect(
|
||||||
|
model=Club,
|
||||||
|
attrs={"api_url": "/api/members/club/"},
|
||||||
|
),
|
||||||
|
"attendees_club": AutocompleteModelSelect(
|
||||||
|
model=Club,
|
||||||
|
attrs={"api_url": "/api/members/club/"},
|
||||||
|
),
|
||||||
"date_start": DateTimePickerInput(),
|
"date_start": DateTimePickerInput(),
|
||||||
"date_end": DateTimePickerInput(),
|
"date_end": DateTimePickerInput(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
|
@ -13,6 +14,7 @@ from .models import Activity
|
||||||
class ActivityCreateView(LoginRequiredMixin, CreateView):
|
class ActivityCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
success_url = reverse_lazy('activity:activity_list')
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(LoginRequiredMixin, SingleTableView):
|
class ActivityListView(LoginRequiredMixin, SingleTableView):
|
||||||
|
@ -33,6 +35,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
|
||||||
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
success_url = reverse_lazy('activity:activity_list')
|
||||||
|
|
||||||
|
|
||||||
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
|
|
|
@ -4,10 +4,11 @@
|
||||||
from crispy_forms.bootstrap import Div
|
from crispy_forms.bootstrap import Div
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout
|
from crispy_forms.layout import Layout
|
||||||
from dal import autocomplete
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from note_kfet.inputs import AutocompleteModelSelect
|
||||||
from permission.models import PermissionMask
|
from permission.models import PermissionMask
|
||||||
|
|
||||||
from .models import Profile, Club, Membership
|
from .models import Profile, Club, Membership
|
||||||
|
@ -63,11 +64,12 @@ class MembershipForm(forms.ModelForm):
|
||||||
# et récupère les noms d'utilisateur valides
|
# et récupère les noms d'utilisateur valides
|
||||||
widgets = {
|
widgets = {
|
||||||
'user':
|
'user':
|
||||||
autocomplete.ModelSelect2(
|
AutocompleteModelSelect(
|
||||||
url='member:user_autocomplete',
|
User,
|
||||||
attrs={
|
attrs={
|
||||||
'data-placeholder': 'Nom ...',
|
'api_url': '/api/user/',
|
||||||
'data-minimum-input-length': 1,
|
'name_field': 'username',
|
||||||
|
'placeholder': 'Nom ...',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,4 @@ urlpatterns = [
|
||||||
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||||
path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
|
path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||||
# API for the user autocompleter
|
|
||||||
path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from dal import autocomplete
|
|
||||||
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.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -253,28 +252,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UserAutocomplete(autocomplete.Select2QuerySetView):
|
|
||||||
"""
|
|
||||||
Auto complete users by usernames
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""
|
|
||||||
Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion.
|
|
||||||
Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos.
|
|
||||||
"""
|
|
||||||
# Un utilisateur non connecté n'a accès à aucune information
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return User.objects.none()
|
|
||||||
|
|
||||||
qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all()
|
|
||||||
|
|
||||||
if self.q:
|
|
||||||
qs = qs.filter(username__regex="^" + self.q)
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
# ******************************* #
|
# ******************************* #
|
||||||
# CLUB #
|
# CLUB #
|
||||||
# ******************************* #
|
# ******************************* #
|
||||||
|
|
|
@ -24,7 +24,8 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Note.objects.all()
|
queryset = Note.objects.all()
|
||||||
serializer_class = NotePolymorphicSerializer
|
serializer_class = NotePolymorphicSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
|
filterset_fields = ['polymorphic_ctype', 'is_active', ]
|
||||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
|
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
|
||||||
ordering_fields = ['alias__name', 'alias__normalized_name']
|
ordering_fields = ['alias__name', 'alias__normalized_name']
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from dal import autocomplete
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note_kfet.inputs import AutocompleteModelSelect
|
||||||
|
|
||||||
from .models import TransactionTemplate
|
from .models import TransactionTemplate, NoteClub
|
||||||
|
|
||||||
|
|
||||||
class ImageForm(forms.Form):
|
class ImageForm(forms.Form):
|
||||||
|
@ -30,11 +31,12 @@ class TransactionTemplateForm(forms.ModelForm):
|
||||||
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
|
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
|
||||||
widgets = {
|
widgets = {
|
||||||
'destination':
|
'destination':
|
||||||
autocomplete.ModelSelect2(
|
AutocompleteModelSelect(
|
||||||
url='note:note_autocomplete',
|
NoteClub,
|
||||||
attrs={
|
attrs={
|
||||||
'data-placeholder': 'Note ...',
|
'api_url': '/api/note/note/',
|
||||||
'data-minimum-input-length': 1,
|
'api_url_suffix': '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk),
|
||||||
|
'placeholder': 'Note ...',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,4 @@ 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'),
|
||||||
|
|
||||||
# API for the note autocompleter
|
|
||||||
path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from dal import autocomplete
|
|
||||||
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.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
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
|
@ -13,7 +11,7 @@ from note_kfet.inputs import AmountInput
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .forms import TransactionTemplateForm
|
from .forms import TransactionTemplateForm
|
||||||
from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
|
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
||||||
from .models.transactions import SpecialTransaction
|
from .models.transactions import SpecialTransaction
|
||||||
from .tables import HistoryTable, ButtonTable
|
from .tables import HistoryTable, ButtonTable
|
||||||
|
|
||||||
|
@ -49,62 +47,6 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
|
||||||
"""
|
|
||||||
Auto complete note by aliases. Used in every search field for note
|
|
||||||
ex: :view:`ConsoView`, :view:`TransactionCreateView`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""
|
|
||||||
When someone look for an :models:`note.Alias`, a query is sent to the dedicated API.
|
|
||||||
This function handles the result and return a filtered list of aliases.
|
|
||||||
"""
|
|
||||||
# Un utilisateur non connecté n'a accès à aucune information
|
|
||||||
if not self.request.user.is_authenticated:
|
|
||||||
return Alias.objects.none()
|
|
||||||
|
|
||||||
qs = Alias.objects.all()
|
|
||||||
|
|
||||||
# self.q est le paramètre de la recherche
|
|
||||||
if self.q:
|
|
||||||
qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \
|
|
||||||
.order_by('normalized_name').distinct()
|
|
||||||
|
|
||||||
# Filtrage par type de note (user, club, special)
|
|
||||||
note_type = self.forwarded.get("note_type", None)
|
|
||||||
if note_type:
|
|
||||||
types = str(note_type).lower()
|
|
||||||
if "user" in types:
|
|
||||||
qs = qs.filter(note__polymorphic_ctype__model="noteuser")
|
|
||||||
elif "club" in types:
|
|
||||||
qs = qs.filter(note__polymorphic_ctype__model="noteclub")
|
|
||||||
elif "special" in types:
|
|
||||||
qs = qs.filter(note__polymorphic_ctype__model="notespecial")
|
|
||||||
else:
|
|
||||||
qs = qs.none()
|
|
||||||
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def get_result_label(self, result):
|
|
||||||
"""
|
|
||||||
Show the selected alias and the username associated
|
|
||||||
<Alias> (aka. <Username> )
|
|
||||||
"""
|
|
||||||
# Gère l'affichage de l'alias dans la recherche
|
|
||||||
res = result.name
|
|
||||||
note_name = str(result.note)
|
|
||||||
if res != note_name:
|
|
||||||
res += " (aka. " + note_name + ")"
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_result_value(self, result):
|
|
||||||
"""
|
|
||||||
The value used for the transactions will be the id of the Note.
|
|
||||||
"""
|
|
||||||
return str(result.note.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
|
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create TransactionTemplate
|
Create TransactionTemplate
|
||||||
|
|
|
@ -1,19 +1,9 @@
|
||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
"""
|
|
||||||
This file comes from the project `django-bootstrap-datepicker-plus` available on Github:
|
|
||||||
https://github.com/monim67/django-bootstrap-datepicker-plus
|
|
||||||
This is distributed under Apache License 2.0.
|
|
||||||
|
|
||||||
This adds datetime pickers with bootstrap.
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""Contains Base Date-Picker input class for widgets of this package."""
|
|
||||||
|
|
||||||
from json import dumps as json_dumps
|
from json import dumps as json_dumps
|
||||||
|
|
||||||
from django.forms.widgets import DateTimeBaseInput, NumberInput
|
from django.forms.widgets import DateTimeBaseInput, NumberInput, Select
|
||||||
|
|
||||||
|
|
||||||
class AmountInput(NumberInput):
|
class AmountInput(NumberInput):
|
||||||
|
@ -30,6 +20,44 @@ class AmountInput(NumberInput):
|
||||||
return str(int(100 * float(val))) if val else val
|
return str(int(100 * float(val))) if val else val
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteModelSelect(Select):
|
||||||
|
template_name = "member/autocomplete_model.html"
|
||||||
|
|
||||||
|
def __init__(self, model, attrs=None, choices=()):
|
||||||
|
super().__init__(attrs, choices)
|
||||||
|
|
||||||
|
self.model = model
|
||||||
|
self.model_pk = None
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
"""JS/CSS resources needed to render the date-picker calendar."""
|
||||||
|
|
||||||
|
js = ('js/autocomplete_model.js', )
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
if value:
|
||||||
|
self.attrs["model_pk"] = int(value)
|
||||||
|
return str(self.model.objects.get(pk=int(value)))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
val = super().value_from_datadict(data, files, name)
|
||||||
|
print(data)
|
||||||
|
print(self.attrs)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github:
|
||||||
|
https://github.com/monim67/django-bootstrap-datepicker-plus
|
||||||
|
This is distributed under Apache License 2.0.
|
||||||
|
|
||||||
|
This adds datetime pickers with bootstrap.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Contains Base Date-Picker input class for widgets of this package."""
|
||||||
|
|
||||||
|
|
||||||
class DatePickerDictionary:
|
class DatePickerDictionary:
|
||||||
"""Keeps track of all date-picker input classes."""
|
"""Keeps track of all date-picker input classes."""
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,6 @@ INSTALLED_APPS = [
|
||||||
# API
|
# API
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
# Autocomplete
|
|
||||||
'dal',
|
|
||||||
'dal_select2',
|
|
||||||
|
|
||||||
# Note apps
|
# Note apps
|
||||||
'activity',
|
'activity',
|
||||||
|
|
|
@ -3,7 +3,6 @@ chardet==3.0.4
|
||||||
defusedxml==0.6.0
|
defusedxml==0.6.0
|
||||||
Django~=2.2
|
Django~=2.2
|
||||||
django-allauth==0.39.1
|
django-allauth==0.39.1
|
||||||
django-autocomplete-light==3.5.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
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
$(document).ready(function () {
|
||||||
|
$(".autocomplete").keyup(function(e) {
|
||||||
|
let target = $("#" + e.target.id);
|
||||||
|
let prefix = target.attr("id");
|
||||||
|
let api_url = target.attr("api_url");
|
||||||
|
let api_url_suffix = target.attr("api_url_suffix");
|
||||||
|
if (!api_url_suffix)
|
||||||
|
api_url_suffix = "";
|
||||||
|
let name_field = target.attr("name_field");
|
||||||
|
if (!name_field)
|
||||||
|
name_field = "name";
|
||||||
|
let input = target.val();
|
||||||
|
|
||||||
|
$.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) {
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
objects.results.forEach(function (obj) {
|
||||||
|
html += li(prefix + "_" + obj.id, obj[name_field]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#" + prefix + "_list").html(html);
|
||||||
|
|
||||||
|
objects.results.forEach(function (obj) {
|
||||||
|
$("#" + prefix + "_" + obj.id).click(function() {
|
||||||
|
target.val(obj[name_field]);
|
||||||
|
$("#" + prefix + "_pk").val(obj.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input === obj[name_field])
|
||||||
|
$("#" + prefix + "_pk").val(obj.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
|
||||||
|
<input class="form-control mx-auto d-block autocomplete" type="text"
|
||||||
|
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
|
||||||
|
name="{{ widget.name }}_name" autocomplete="off"
|
||||||
|
{% for name, value in widget.attrs.items %}
|
||||||
|
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||||
|
{% endfor %}>
|
||||||
|
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
|
||||||
|
</ul>
|
|
@ -13,8 +13,10 @@
|
||||||
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ club.name}}</dd>
|
<dd class="col-xl-6">{{ club.name}}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
|
{% if club.parent_club %}
|
||||||
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
|
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
|
||||||
|
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ club.membership_start }}</dd>
|
<dd class="col-xl-6">{{ club.membership_start }}</dd>
|
||||||
|
|
Loading…
Reference in New Issue