rewrote participant status view as class based view

This commit is contained in:
Oskar Hahn 2012-08-10 19:19:41 +02:00
parent 90fddba63b
commit bb00eb3eb4
5 changed files with 284 additions and 285 deletions

View File

@ -9,17 +9,20 @@ $(function() {
$('.status_link').click(function(event) { $('.status_link').click(function(event) {
event.preventDefault(); event.preventDefault();
link = $(this); link = $(this);
group = $(this).parent();
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: link.attr('href'), url: link.attr('href'),
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
if (data.active) { if (data.active) {
link.addClass('active'); group.children('.status_link.deactivate').show();
group.children('.status_link.activate').hide();
} else { } else {
link.removeClass('active'); group.children('.status_link.deactivate').hide();
group.children('.status_link.activate').show();
} }
} }
}); });
}); });
}); });

View File

@ -4,14 +4,19 @@
* :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. * :copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
* :license: GNU GPL, see LICENSE for more details. * :license: GNU GPL, see LICENSE for more details.
*/ */
a.status_link span { a.status_link span {
background-image: url(../images/icons/off.png);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
display: inline-block; display: inline-block;
} }
a.status_link.active span {
a.status_link.deactivate span {
background-image: url(../images/icons/on.png); background-image: url(../images/icons/on.png);
} }
a.status_link.activate span {
background-image: url(../images/icons/off.png);
}

View File

@ -90,15 +90,18 @@
</td> </td>
<td> <td>
<span style="width: 1px; white-space: nowrap;"> <span style="width: 1px; white-space: nowrap;">
<a href="{% url user_edit user.id %}"> <a href="{% url user_edit user.id %}">
<img src="{% static 'images/icons/edit.png' %}" title="{% trans 'Edit participant' %}"> <img src="{% static 'images/icons/edit.png' %}" title="{% trans 'Edit participant' %}">
</a> </a>
<a href="{% url user_delete user.id %}"> <a href="{% url user_delete user.id %}">
<img src="{% static 'images/icons/delete.png' %}" title="{% trans 'Delete participant' %}"> <img src="{% static 'images/icons/delete.png' %}" title="{% trans 'Delete participant' %}">
</a> </a>
<a class="status_link{% if user.is_active %} active{% endif %}" href="{% url user_status user.id %}" title="{% trans 'Change status (active/inactive)' %}"> <a class="status_link deactivate" href="{% url user_status_deactivate user.id %}" title="{% trans 'Change status to inactive' %}"{% if not user.is_active %} style="display:none"{% endif %}>
<span></span> <span></span>
</a> </a>
<a class="status_link activate" href="{% url user_status_activate user.id %}" title="{% trans 'Change status to active' %}"{% if user.is_active %} style="display:none"{% endif %}>
<span></span>
</a>
</span> </span>
</td> </td>
{% endif %} {% endif %}

View File

@ -15,7 +15,7 @@ from django.core.urlresolvers import reverse
from openslides.participant.views import ( from openslides.participant.views import (
ParticipantsListPDF, ParticipantsPasswordsPDF, Overview, UserCreateView, ParticipantsListPDF, ParticipantsPasswordsPDF, Overview, UserCreateView,
UserUpdateView, UserDeleteView) UserUpdateView, UserDeleteView, SetUserStatusView)
urlpatterns = patterns('openslides.participant.views', urlpatterns = patterns('openslides.participant.views',
url(r'^$', url(r'^$',
@ -43,9 +43,22 @@ urlpatterns = patterns('openslides.participant.views',
name='user_delete', name='user_delete',
), ),
url(r'^(?P<user_id>\d+)/status/$', url(r'^(?P<pk>\d+)/status/toggle/$',
'user_set_status', SetUserStatusView.as_view(),
name='user_status', {'action': 'toggle'},
name='user_status_toggle',
),
url(r'^(?P<pk>\d+)/status/activate/$',
SetUserStatusView.as_view(),
{'action': 'activate'},
name='user_status_activate',
),
url(r'^(?P<pk>\d+)/status/deactivate/$',
SetUserStatusView.as_view(),
{'action': 'deactivate'},
name='user_status_deactivate',
), ),
url(r'^import/$', url(r'^import/$',

View File

@ -44,7 +44,8 @@ from openslides.utils.utils import (
template, permission_required, gen_confirm_form, ajax_request, decodedict, template, permission_required, gen_confirm_form, ajax_request, decodedict,
encodedict, delete_default_permissions, html_strong) encodedict, delete_default_permissions, html_strong)
from openslides.utils.views import ( from openslides.utils.views import (
FormView, PDFView, TemplateView, CreateView, UpdateView, DeleteView) FormView, PDFView, TemplateView, CreateView, UpdateView, DeleteView,
RedirectView, SingleObjectMixin, ListView)
from openslides.config.models import config from openslides.config.models import config
@ -55,15 +56,15 @@ from openslides.participant.forms import (
UserImportForm, GroupForm, AdminPasswordChangeForm, ConfigForm) UserImportForm, GroupForm, AdminPasswordChangeForm, ConfigForm)
class Overview(TemplateView): class Overview(ListView):
""" """
Show all participants. Show all participants.
""" """
permission_required = 'participant.can_see_participant' permission_required = 'participant.can_see_participant'
template_name = 'participant/overview.html' template_name = 'participant/overview.html'
context_object_name = 'users'
def get_context_data(self, **kwargs): def get_queryset(self):
context = super(Overview, self).get_context_data(**kwargs)
try: try:
sortfilter = encodedict(parse_qs( sortfilter = encodedict(parse_qs(
self.request.COOKIES['participant_sortfilter'])) self.request.COOKIES['participant_sortfilter']))
@ -81,19 +82,15 @@ class Overview(TemplateView):
else: else:
sortfilter[value] = [self.request.REQUEST[value]] sortfilter[value] = [self.request.REQUEST[value]]
query = User.objects query = OpenSlidesUser.objects
if 'gender' in sortfilter: if 'gender' in sortfilter:
query = query.filter( query = query.filter(gender__iexact=sortfilter['gender'][0])
openslidesuser__gender__iexact=sortfilter['gender'][0])
if 'category' in sortfilter: if 'category' in sortfilter:
query = query.filter( query = query.filter(category__iexact=sortfilter['category'][0])
openslidesuser__category__iexact=sortfilter['category'][0])
if 'type' in sortfilter: if 'type' in sortfilter:
query = query.filter( query = query.filter(type__iexact=sortfilter['type'][0])
openslidesuser__type__iexact=sortfilter['type'][0])
if 'committee' in sortfilter: if 'committee' in sortfilter:
query = query.filter( query = query.filter(committee__iexact=sortfilter['committee'][0])
openslidesuser__committee__iexact=sortfilter['committee'][0])
if 'status' in sortfilter: if 'status' in sortfilter:
query = query.filter(is_active=sortfilter['status'][0]) query = query.filter(is_active=sortfilter['status'][0])
if 'sort' in sortfilter: if 'sort' in sortfilter:
@ -109,15 +106,18 @@ class Overview(TemplateView):
if 'reverse' in sortfilter: if 'reverse' in sortfilter:
query = query.reverse() query = query.reverse()
# list of filtered users self.sortfilter = sortfilter
users = query.all()
return query.all()
def get_context_data(self, **kwargs):
context = super(Overview, self).get_context_data(**kwargs)
# list of all existing users
all_users = User.objects.count() all_users = User.objects.count()
# quotient of selected users and all users # quotient of selected users and all users
if all_users > 0: if all_users > 0:
percent = float(len(users)) * 100 / float(all_users) percent = self.object_list.count() * 100 / float(all_users)
else: else:
percent = 0 percent = 0
@ -129,14 +129,13 @@ class Overview(TemplateView):
committees = [p['committee'] for p in OpenSlidesUser.objects.values('committee') committees = [p['committee'] for p in OpenSlidesUser.objects.values('committee')
.exclude(committee='').distinct()] .exclude(committee='').distinct()]
context.update({ context.update({
'users': users,
'allusers': all_users, 'allusers': all_users,
'percent': round(percent, 1), 'percent': round(percent, 1),
'categories': categories, 'categories': categories,
'committees': committees, 'committees': committees,
'cookie': ['participant_sortfilter', urlencode(decodedict(sortfilter), 'cookie': ['participant_sortfilter', urlencode(decodedict(self.sortfilter),
doseq=True)], doseq=True)],
'sortfilter': sortfilter}) 'sortfilter': self.sortfilter})
return context return context
@ -154,6 +153,8 @@ class UserCreateView(CreateView):
def manipulate_object(self, form): def manipulate_object(self, form):
self.object.username = gen_username(form.cleaned_data['first_name'], form.cleaned_data['last_name']) self.object.username = gen_username(form.cleaned_data['first_name'], form.cleaned_data['last_name'])
if not self.object.firstpassword:
self.object.firstpassword = gen_password()
class UserUpdateView(UpdateView): class UserUpdateView(UpdateView):
@ -178,164 +179,125 @@ class UserDeleteView(DeleteView):
url = 'user_overview' url = 'user_overview'
@permission_required('participant.can_manage_participant') class SetUserStatusView(RedirectView, SingleObjectMixin):
@template('confirm.html')
def user_delete(request, user_id):
""" """
Delete an user. Activate or deactivate an user.
""" """
user = User.objects.get(pk=user_id) permission_required = 'participant.can_manage_participant'
if request.method == 'POST': allow_ajax = True
user.delete() url = 'user_overview'
messages.success(request, model = OpenSlidesUser
_('Participant <b>%s</b> was successfully deleted.') % user)
else: def pre_redirect(self, request, *args, **kwargs):
gen_confirm_form(request, self.object = self.get_object()
_('Do you really want to delete <b>%s</b>?') % user, action = kwargs['action']
reverse('user_delete', args=[user_id])) if action == 'activate':
return redirect(reverse('user_overview')) self.object.is_active = True
elif action == 'deactivate':
self.object.is_active = False
elif action == 'toggle':
self.object.is_active = not self.object.is_active
self.object.save()
return super(SetUserStatusView, self).pre_redirect(request, *args, **kwargs)
def get_ajax_context(self, **kwargs):
context = super(SetUserStatusView, self).get_ajax_context(**kwargs)
context['active'] = self.object.is_active
return context
@permission_required('participant.can_manage_participant') class ParticipantsListPDF(PDFView):
@template('confirm.html')
def user_set_status(request, user_id):
""" """
Set the status of an user. Generate the userliste as PDF.
""" """
try: permission_required = 'participant.can_see_participant'
user = User.objects.get(pk=user_id) filename = ugettext_lazy("Participant-list")
if user.is_active: document_title = ugettext_lazy('List of Participants')
user.is_active = False
else:
user.is_active = True
user.save()
except User.DoesNotExist:
messages.error(request,
_('Participant ID %d does not exist.') % int(user_id))
return redirect(reverse('user_overview'))
if request.is_ajax(): def append_to_pdf(self, story):
return ajax_request({'active': user.is_active}) data = [['#', _('Last Name'), _('First Name'), _('Group'), _('Type'),
# set success messages for page reload only (= not ajax request) _('Committee')]]
if user.is_active: sort = 'last_name'
messages.success(request, _('<b>%s</b> is now <b>present</b>.') % user) counter = 0
else: for user in OpenSlidesUser.objects.all().order_by(sort):
messages.success(request, _('<b>%s</b> is now <b>absent</b>.') % user) counter += 1
return redirect(reverse('user_overview')) data.append([
counter,
Paragraph(user.last_name, stylesheet['Tablecell']),
Paragraph(user.first_name, stylesheet['Tablecell']),
Paragraph(user.category, stylesheet['Tablecell']),
Paragraph(user.type, stylesheet['Tablecell']),
Paragraph(user.committee, stylesheet['Tablecell'])
])
t = LongTable(data,
style=[
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LINEABOVE', (0, 0), (-1, 0), 2, colors.black),
('LINEABOVE', (0, 1), (-1, 1), 1, colors.black),
('LINEBELOW', (0, -1), (-1, -1), 2, colors.black),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
(colors.white, (.9, .9, .9)))])
t._argW[0] = 0.75 * cm
story.append(t)
@permission_required('participant.can_manage_participant') class ParticipantsPasswordsPDF(PDFView):
@template('participant/group_overview.html')
def get_group_overview(request):
""" """
Show all groups. Generate the Welcomepaper for the users.
""" """
if config['system_enable_anonymous']: permission_required = 'participant.can_manage_participant'
groups = Group.objects.all() filename = ugettext_lazy("Participant-passwords")
else: top_space = 0
groups = Group.objects.exclude(name='Anonymous')
return {
'groups': groups,
}
def get_template(self, buffer):
return SimpleDocTemplate(buffer, topMargin=-6, bottomMargin=-6,
leftMargin=0, rightMargin=0, showBoundary=False)
@permission_required('participant.can_manage_participant') def build_document(self, pdf_document, story):
@template('participant/group_edit.html') pdf_document.build(story)
def group_edit(request, group_id=None):
"""
Edit a group.
"""
if group_id is not None:
try:
group = Group.objects.get(id=group_id)
except Group.DoesNotExist:
# TODO: return a 404 Object
raise NameError("There is no group %d" % group_id)
else:
group = None
delete_default_permissions()
if request.method == 'POST': def append_to_pdf(self, story):
form = GroupForm(request.POST, instance=group) data = []
if form.is_valid(): participant_pdf_system_url = config["participant_pdf_system_url"]
# TODO: This can be done inside the form participant_pdf_welcometext = config["participant_pdf_welcometext"]
group_name = form.cleaned_data['name'].lower() for user in OpenSlidesUser.objects.all().order_by('last_name'):
cell = []
cell.append(Spacer(0, 0.8 * cm))
cell.append(Paragraph(_("Account for OpenSlides"),
stylesheet['Ballot_title']))
cell.append(Paragraph(_("for %s") % (user),
stylesheet['Ballot_subtitle']))
cell.append(Spacer(0, 0.5 * cm))
cell.append(Paragraph(_("User: %s") % (user.username),
stylesheet['Monotype']))
cell.append(Paragraph(_("Password: %s")
% (user.firstpassword), stylesheet['Monotype']))
cell.append(Spacer(0, 0.5 * cm))
cell.append(Paragraph(_("URL: %s")
% (participant_pdf_system_url),
stylesheet['Ballot_option']))
cell.append(Spacer(0, 0.5 * cm))
cell2 = []
cell2.append(Spacer(0, 0.8 * cm))
if participant_pdf_welcometext is not None:
cell2.append(Paragraph(
participant_pdf_welcometext.replace('\r\n', '<br/>'),
stylesheet['Ballot_subtitle']))
# TODO: Why is this code called on any request and not only, if the data.append([cell, cell2])
# anonymous_group is edited?
try:
anonymous_group = Group.objects.get(name='Anonymous')
except Group.DoesNotExist:
anonymous_group = None
# special handling for anonymous auth # add empty table line if no participants available
# TODO: This code should be a form validator. if not data:
if group is None and group_name.strip().lower() == 'anonymous': data.append(['', ''])
# don't allow to create this group # build table
messages.error(request, t = Table(data, 10.5 * cm, 7.42 * cm)
_('Group name "%s" is reserved for internal use.') t.setStyle(TableStyle([
% group_name) ('LINEBELOW', (0, 0), (-1, 0), 0.25, colors.grey),
return { ('LINEBELOW', (0, 1), (-1, 1), 0.25, colors.grey),
'form': form, ('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.grey),
'group': group ('VALIGN', (0, 0), (-1, -1), 'TOP'),
} ]))
story.append(t)
group = form.save()
try:
openslides_group = OpenSlidesGroup.objects.get(group=group)
except OpenSlidesGroup.DoesNotExist:
django_group = None
if form.cleaned_data['as_user'] and django_group is None:
OpenSlidesGroup(group=group).save()
elif not form.cleaned_data['as_user'] and django_group:
django_group.delete()
if anonymous_group is not None and \
anonymous_group.id == group.id:
# prevent name changes -
# XXX: I'm sure this could be done as *one* group.save()
group.name = 'Anonymous'
group.save()
if group_id is None:
messages.success(request,
_('New group was successfully created.'))
else:
messages.success(request, _('Group was successfully modified.'))
if not 'apply' in request.POST:
return redirect(reverse('user_group_overview'))
if group_id is None:
return redirect(reverse('user_group_edit', args=[group.id]))
else:
messages.error(request, _('Please check the form for errors.'))
else:
if group and OpenSlidesGroup.objects.filter(group=group).exists():
initial = {'as_user': True}
else:
initial = {'as_user': False}
form = GroupForm(instance=group, initial=initial)
return {
'form': form,
'group': group,
}
@permission_required('participant.can_manage_participant')
def group_delete(request, group_id):
"""
Delete a group.
"""
group = Group.objects.get(pk=group_id)
if request.method == 'POST':
group.delete()
messages.success(request,
_('Group <b>%s</b> was successfully deleted.') % group)
else:
gen_confirm_form(request,
_('Do you really want to delete <b>%s</b>?') % group,
reverse('user_group_delete', args=[group_id]))
return redirect(reverse('user_group_overview'))
@login_required @login_required
@ -582,121 +544,120 @@ def login(request):
return django_login(request, template_name='participant/login.html', extra_context=extra_content) return django_login(request, template_name='participant/login.html', extra_context=extra_content)
def register_tab(request):
"""
Register the participant tab.
"""
selected = request.path.startswith('/participant/')
return Tab(
title=_('Participants'),
url=reverse('user_overview'),
permission=request.user.has_perm('participant.can_see_participant')
or request.user.has_perm('participant.can_manage_participant'),
selected=selected,
)
class ParticipantsListPDF(PDFView): @permission_required('participant.can_manage_participant')
@template('participant/group_overview.html')
def get_group_overview(request):
""" """
Generate the userliste as PDF. Show all groups.
""" """
permission_required = 'participant.can_see_participant' if config['system_enable_anonymous']:
filename = ugettext_lazy("Participant-list") groups = Group.objects.all()
document_title = ugettext_lazy('List of Participants') else:
groups = Group.objects.exclude(name='Anonymous')
return {
'groups': groups,
}
def append_to_pdf(self, story):
data = [['#', _('Last Name'), _('First Name'), _('Group'), _('Type'), @permission_required('participant.can_manage_participant')
_('Committee')]] @template('participant/group_edit.html')
sort = 'last_name' def group_edit(request, group_id=None):
counter = 0 """
for user in User.objects.all().order_by(sort): Edit a group.
"""
if group_id is not None:
try:
group = Group.objects.get(id=group_id)
except Group.DoesNotExist:
# TODO: return a 404 Object
raise NameError("There is no group %d" % group_id)
else:
group = None
delete_default_permissions()
if request.method == 'POST':
form = GroupForm(request.POST, instance=group)
if form.is_valid():
# TODO: This can be done inside the form
group_name = form.cleaned_data['name'].lower()
# TODO: Why is this code called on any request and not only, if the
# anonymous_group is edited?
try: try:
counter += 1 anonymous_group = Group.objects.get(name='Anonymous')
user.get_profile() except Group.DoesNotExist:
data.append([ anonymous_group = None
counter,
Paragraph(user.last_name, stylesheet['Tablecell']),
Paragraph(user.first_name, stylesheet['Tablecell']),
Paragraph(user.profile.group, stylesheet['Tablecell']),
Paragraph(user.profile.get_type_display(),
stylesheet['Tablecell']),
Paragraph(user.profile.committee, stylesheet['Tablecell'])
])
except Profile.DoesNotExist:
counter -= 1
pass
t = LongTable(data,
style=[
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LINEABOVE', (0, 0), (-1, 0), 2, colors.black),
('LINEABOVE', (0, 1), (-1, 1), 1, colors.black),
('LINEBELOW', (0, -1), (-1, -1), 2, colors.black),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
(colors.white, (.9, .9, .9)))])
t._argW[0] = 0.75 * cm
story.append(t)
# special handling for anonymous auth
# TODO: This code should be a form validator.
if group is None and group_name.strip().lower() == 'anonymous':
# don't allow to create this group
messages.error(request,
_('Group name "%s" is reserved for internal use.')
% group_name)
return {
'form': form,
'group': group
}
class ParticipantsPasswordsPDF(PDFView): group = form.save()
"""
Generate the Welcomepaper for the users.
"""
permission_required = 'participant.can_manage_participant'
filename = ugettext_lazy("Participant-passwords")
top_space = 0
def get_template(self, buffer):
return SimpleDocTemplate(buffer, topMargin=-6, bottomMargin=-6,
leftMargin=0, rightMargin=0, showBoundary=False)
def build_document(self, pdf_document, story):
pdf_document.build(story)
def append_to_pdf(self, story):
data = []
participant_pdf_system_url = config["participant_pdf_system_url"]
participant_pdf_welcometext = config["participant_pdf_welcometext"]
for user in User.objects.all().order_by('last_name'):
try: try:
user.get_profile() openslides_group = OpenSlidesGroup.objects.get(group=group)
cell = [] except OpenSlidesGroup.DoesNotExist:
cell.append(Spacer(0, 0.8 * cm)) django_group = None
cell.append(Paragraph(_("Account for OpenSlides"), if form.cleaned_data['as_user'] and django_group is None:
stylesheet['Ballot_title'])) OpenSlidesGroup(group=group).save()
cell.append(Paragraph(_("for %s") % (user.profile), elif not form.cleaned_data['as_user'] and django_group:
stylesheet['Ballot_subtitle'])) django_group.delete()
cell.append(Spacer(0, 0.5 * cm))
cell.append(Paragraph(_("User: %s") % (user.username),
stylesheet['Monotype']))
cell.append(Paragraph(_("Password: %s")
% (user.profile.firstpassword), stylesheet['Monotype']))
cell.append(Spacer(0, 0.5 * cm))
cell.append(Paragraph(_("URL: %s")
% (participant_pdf_system_url),
stylesheet['Ballot_option']))
cell.append(Spacer(0, 0.5 * cm))
cell2 = []
cell2.append(Spacer(0, 0.8 * cm))
if participant_pdf_welcometext is not None:
cell2.append(Paragraph(
participant_pdf_welcometext.replace('\r\n', '<br/>'),
stylesheet['Ballot_subtitle']))
data.append([cell, cell2]) if anonymous_group is not None and \
except OpenSlidesUser.DoesNotExist: anonymous_group.id == group.id:
pass # prevent name changes -
# add empty table line if no participants available # XXX: I'm sure this could be done as *one* group.save()
if data == []: group.name = 'Anonymous'
data.append(['', '']) group.save()
# build table
t = Table(data, 10.5 * cm, 7.42 * cm) if group_id is None:
t.setStyle(TableStyle([ messages.success(request,
('LINEBELOW', (0, 0), (-1, 0), 0.25, colors.grey), _('New group was successfully created.'))
('LINEBELOW', (0, 1), (-1, 1), 0.25, colors.grey), else:
('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.grey), messages.success(request, _('Group was successfully modified.'))
('VALIGN', (0, 0), (-1, -1), 'TOP'), if not 'apply' in request.POST:
])) return redirect(reverse('user_group_overview'))
story.append(t) if group_id is None:
return redirect(reverse('user_group_edit', args=[group.id]))
else:
messages.error(request, _('Please check the form for errors.'))
else:
if group and OpenSlidesGroup.objects.filter(group=group).exists():
initial = {'as_user': True}
else:
initial = {'as_user': False}
form = GroupForm(instance=group, initial=initial)
return {
'form': form,
'group': group,
}
@permission_required('participant.can_manage_participant')
def group_delete(request, group_id):
"""
Delete a group.
"""
group = Group.objects.get(pk=group_id)
if request.method == 'POST':
group.delete()
messages.success(request,
_('Group <b>%s</b> was successfully deleted.') % group)
else:
gen_confirm_form(request,
_('Do you really want to delete <b>%s</b>?') % group,
reverse('user_group_delete', args=[group_id]))
return redirect(reverse('user_group_overview'))
class Config(FormView): class Config(FormView):
@ -721,3 +682,17 @@ class Config(FormView):
messages.success(self.request, messages.success(self.request,
_('Participants settings successfully saved.')) _('Participants settings successfully saved.'))
return super(Config, self).form_valid(form) return super(Config, self).form_valid(form)
def register_tab(request):
"""
Register the participant tab.
"""
selected = request.path.startswith('/participant/')
return Tab(
title=_('Participants'),
url=reverse('user_overview'),
permission=request.user.has_perm('participant.can_see_participant')
or request.user.has_perm('participant.can_manage_participant'),
selected=selected,
)