Add club view / update profile view

Profile view
- Let the user see his information.
- List the clubs whose he is a member.

Profile edition view
- Renamed from previous "profile" view
- User can now change "occupation" field.

Club detail view
- Informations about a club.
- Accessible by staff members and "respos" of the club.
- List members, with subscription fee (if applicable).

Club admin
- Change memberships of clubs added.
This commit is contained in:
Aurélien Delobelle 2017-08-07 18:56:56 +02:00
parent 7c0bd2a271
commit fe840f2003
12 changed files with 435 additions and 67 deletions

View file

@ -332,8 +332,8 @@ fieldset legend {
padding: 20px;
}
#main-content a,
#main-content a:hover {
#main-content a:not(.btn),
#main-content a:hover:not(.btn) {
color : #D81138;
}
@ -351,7 +351,7 @@ fieldset legend {
#main-content h3.horizontal-title {
display : block;
padding : 12px;
margin: -20px -20px 10px -20px ;
margin: 0 -20px 10px -20px ;
font-family: 'Dosis', sans-serif;
font-weight: 700;
color: white;
@ -365,11 +365,11 @@ fieldset legend {
border-width: 0px 0px 5px 0px;
}
#main-content h2 a {
#main-content h2 a:not(.btn) {
color : #C9E5E1;
}
#main-content h2 a:hover {
#main-content h2 a:hover:not(.btn) {
color : #ABB8B6;
}
@ -377,10 +377,46 @@ fieldset legend {
font-weight: 700;
font-size: large;
background-color:#EAA594;
border-width: 0px 0px 3px 0px;
margin-top: 10px;
}
#main-content > :first-child {
margin-top: -20px;
}
#main-content > div:first-of-type > h3.horizontal-title:first-child {
margin-top: -10px;
}
#main-content h2 .actions ,
#main-content h3 .actions {
float: right;
display: flex;
align-items: center;
margin: -12px;
padding-right: 10px;
}
#main-content h2 .actions {
min-height: 50px;
}
#main-content h3 .actions {
min-height: 44px;
}
#main-content h2 .actions > *,
#main-content h3 .actions > * {
height: 100%;
border-radius: 0;
vertical-align: middle;
}
#main-content h2 .actions > * + *,
#main-content h3 .actions > * + * {
margin-left: 10px;
}
/*
main-container {

View file

@ -15,7 +15,21 @@
Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span>
</a></span>
</div>
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
<h2 class="member-status">
<a href="{% url "gestion:profile" %}">
{% if user.first_name %}
{{ user.first_name }},
{% else %}
<tt>{{ user.username }}</tt>,
{% endif %}
</a>
{% if user.profile.is_cof %}
<tt class="user-is-cof">au COF</tt>
{% else %}
<tt class="user-is-not-cof">non-COF</tt>
{% endif %}
</h2>
</div><!-- /.container -->
</header>
{% block interm_content %}{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends "cof/base_header.html" %}
{% load i18n %}
{% block homelink %}
{% endblock %}
@ -57,7 +58,11 @@
<li><a href="{% url "calendar" %}">Calendrier dynamique</a></li>
{% if user.profile.cof.is_cof %}<li><a href="{% url "petits-cours-inscription" %}">Inscription pour donner des petits cours</a></li>{% endif %}
<li><a href="{% url "gestion:profile" %}">Éditer mon profil</a></li>
<li>
<a href="{% url "gestion:profile" %}">
{% trans "Voir/Éditer mon profil" %}
</a>
</li>
{% if not user.profile.login_clipper %}<li><a href="{% url "password_change" %}">Changer mon mot de passe</a></li>{% endif %}
</ul>
</div>

View file

@ -20,14 +20,21 @@ class UserProfileAdmin(UserAdmin):
]
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
# ---
# Clubs
# ---
class ClubUserInline(admin.TabularInline):
model = Club.members.through
@admin.register(Club)
class ClubAdmin(admin.ModelAdmin):
pass
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
inlines = [
ClubUserInline,
]
exclude = ('members',)

View file

@ -13,4 +13,4 @@ class UserForm(forms.ModelForm):
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["phone", "departement"]
fields = ["phone", "departement", "occupation"]

View file

@ -2,6 +2,7 @@ from django.contrib.auth.models import User, Group
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from cof.petits_cours_models import choices_length
@ -92,11 +93,22 @@ class Club(models.Model):
def __str__(self):
template = (
self.price and "{name} ({price}€ / {cotisation_frequency})"
or "{name}"
self.needs_cotiz and
"{name} ({price}€ / {cotisation_frequency})" or
"{name}"
)
return template.format(**vars(self))
def get_absolute_url(self):
return reverse('gestion:club_detail', args=[str(self.pk)])
@property
def needs_cotiz(self):
return bool(self.price)
def is_respo(self, user):
return self.clubuser_set.filter(user=user, is_respo=True).exists()
class ClubUser(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)

View file

@ -0,0 +1,82 @@
{% extends "base_title.html" %}
{% load i18n %}
{% block page_size %}col-sm-8{% endblock %}
{% block realcontent %}
<h2>
{% trans "Club" %} - {{ club.name|title }}
{% if user.is_staff %}
<div class="actions">
<a class="btn btn-primary" href="{% url "admin:gestion_club_change" club.pk %}">
{% trans "Voir dans l'admin" %}
</a>
</div>
{% endif %}
</h2>
<div class="row" style="margin: 10px 0;">
<p>
{% trans "Pas de description" as no_desc_str %}
{{ club.description|linebreaks|default:no_desc_str }}
</p>
<ul class="list-unstyled">
<li>
{% trans "Cotisation supplémentaire :" %}
{% if club.needs_cotiz %}
<b>{{ club.price }} € / {{ club.cotisation_frequency }}</b>
{% else %}
{% trans "Non" %}
{% endif %}
</li>
</ul>
<table class="table table-condensed">
<thead>
<tr>
<th>
{% trans "Total :" %} <b>{{ memberships|length }}</b>
{# Member name #}
</th>
<th>
{% if club.needs_cotiz %}
{% trans "Statut cotiz" %}
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for user, membership in memberships %}
{% ifchanged membership.is_respo %}
<tr style="background-color:#f9f9f9;">
<td colspan="2">
<b>
{% trans "Respo(s),Membre(s)" as title_respo_str %}
{{ membership.is_respo|yesno:"Respo(s),Membre(s)" }}
</b>
</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ user.get_full_name }}</td>
<td>
{% if club.needs_cotiz %}
<span class="glyphicon glyphicon-{{ membership.has_paid|yesno:"ok,remove" }}"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -1,37 +1,134 @@
{% extends "base_title.html" %}
{% load i18n %}
{% load bootstrap %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>Modifier mon profil</h2>
{% if success %}
<p class="success">Votre profil a été mis à jour avec succès !</p>
{% endif %}
<form id="profile form-horizontal" method="post" action="{% url 'gestion:profile' %}">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in user_form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
<fieldset"center-block">
{% for field in profile_form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
{% if user.profile.comments %}
<div class="row" style="margin: 0 15%;">
<h4>Commentaires</h4>
<p>
{{ user.profile.comments }}
</p>
</div>
{% endif %}
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
</div>
</form>
<h2>{% trans "Mon profil" %} - {{ user.get_full_name|title }}</h2>
<div class="row" style="margin: 10px 0;">
{# General #}
<h3 class="horizontal-title">
<div class="actions">
<a class="btn btn-primary" href="{% url "gestion:profile_edit" %}">
Modifier
</a>
</div>
{% trans "Informations générales" %}
</h3>
<h4>Contact</h4>
<ul class="list-unstyled">
<li>
{% trans "Mail :" %}
<b>{{ user.email|default:"n.c." }}</b>
</li>
<li>
{% trans "Téléphone :" %}
<b>{{ user.profile.phone|default:"n.c." }}</b>
</li>
</ul>
<h4>Situation</h4>
<ul class="list-unstyled">
<li>
{% trans "Département :" %}
<b>{{ user.profile.departement|default:"n.c." }}</b>
</li>
<li>
{% trans "Occupation :" %}
<b>{{ user.profile.occupation|default:"n.c." }}</b>
</li>
</ul>
{% if user.profile.comments %}
<h4>{% trans "Commentaires" %}</h4>
<p>
{{ user.profile.comments }}
</p>
{% endif %}
{# Clubs #}
<h3 class="horizontal-title">Clubs</h3>
{% if memberships %}
<table class="table table-striped">
<thead>
<tr>
<th>{# Actions #}</th>
<th>{# Club name #}</th>
<th>Statut cotiz</th>
</tr>
</thead>
<tbody>
{% for club, membership in memberships %}
<tr>
<td>
{% if membership.is_respo %}
<a href="{{ club.get_absolute_url }}" class="btn btn-sm btn-primary">
{% trans "Respo" %}
</a>
{% endif %}
</td>
<td>{{ club.name }}</td>
<td>
{% if club.needs_cotiz %}
<span class="glyphicon glyphicon-{{ membership.has_paid|yesno:"ok,remove" }}"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>
{% blocktrans %}
Pas d'inscription aux clubs pour le moment.
{% endblocktrans %}
</p>
{% endif %}
{# Petit cours #}
{% if perms.cof.member %}
<h3 class="horizontal-title">
<div class="actions">
<a class="btn btn-primary" href="{% url "petits-cours-inscription" %}">
Modifier
</a>
</div>
{% trans "Recevoir des petits cours :" %}
<span class="glyphicon glyphicon-{{ user.profile.cof.petit_cours_accept|yesno:"ok,remove" }}"></span>
</h3>
{% endif %}
{# K-Fêt #}
{% if user.profile.account_kfet %}
{% with trigramme=user.profile.account_kfet.trigramme %}
<h3 class="horizontal-title">
<div class="actions">
<a class="btn btn-primary" href="{% url "kfet.account.read" trigramme %}">
Consulter
</a>
</div>
{% trans "Compte K-Fêt :" %} {{ trigramme }}
</h3>
{% endwith %}
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,31 @@
{% extends "base_title.html" %}
{% load i18n %}
{% load bootstrap %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>{% trans "Modifier mon profil" %}</h2>
<form id="profile form-horizontal" method="post" action="{% url "gestion:profile_edit" %}">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
{% for field in forms.user %}
{{ field | bootstrap }}
{% endfor %}
{% for field in forms.profile %}
{{ field | bootstrap }}
{% endfor %}
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="{% trans "Enregistrer" %}">
</div>
</form>
{% endblock %}

View file

@ -7,8 +7,15 @@ from . import views
app_name = "gestion"
urlpatterns = [
# Profile edition
url(r"^profile/?$", views.profile, name="profile"),
# Profile
url(r'^profile/$', views.profile,
name='profile'),
url(r'^profile/edit/$', views.profile_edit,
name='profile_edit'),
# Clubs
url(r'^club/(?P<pk>\d+)/$', views.club_detail,
name='club_detail'),
# Authentication
url(r'^cof/denied$',

View file

@ -1,17 +1,26 @@
"""
The common views of the different organisations.
- Authentication
- Profile edition
- Profile
- Clubs
"""
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import (
login as django_login, logout as django_logout
)
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic import DetailView, TemplateView
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from multi_form_view import MultiModelFormView
from .forms import ProfileForm, UserForm
from .models import Club
def login(request):
@ -54,20 +63,87 @@ def logout(request):
return django_logout(request)
@login_required
def profile(request):
success = False
user = request.user
if request.method == "POST":
user_form = UserForm(request.POST, instance=user)
profile_form = ProfileForm(request.POST, instance=user.profile)
if (user_form.is_valid() and profile_form.is_valid()):
user_form.save()
profile_form.save()
success = True
else:
user_form = UserForm(instance=user)
profile_form = ProfileForm(instance=user.profile)
return render(request, "gestion/profile.html",
{"user_form": user_form, "profile_form": profile_form,
"success": success})
@method_decorator(login_required, name='dispatch')
class ProfileView(TemplateView):
template_name = 'gestion/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['memberships'] = self.get_memberships()
return context
def get_memberships(self):
"""Returns the clubs memberships of connected user.
A membership is an instance of 'ClubUser' model.
Returns:
List of (club, membership).
"""
qs = (
self.request.user.clubuser_set
.select_related('club')
.order_by('-is_respo', 'club__name')
)
return [(m.club, m) for m in qs]
profile = ProfileView.as_view()
@method_decorator(login_required, name='dispatch')
class EditProfileView(SuccessMessageMixin, MultiModelFormView):
template_name = 'gestion/profile_edit.html'
form_classes = {
'user': UserForm,
'profile': ProfileForm,
}
success_url = '/profile/'
success_message = _("Votre profil a été mis à jour avec succès !")
def get_objects(self):
user = self.request.user
return {
'user': user,
'profile': user.profile,
}
profile_edit = EditProfileView.as_view()
class ClubDetailView(AccessMixin, DetailView):
model = Club
def dispatch(self, request, *args, **kwargs):
if (request.user.is_authenticated and
self.get_object().is_respo(request.user)):
return super().dispatch(request, *args, **kwargs)
else:
return self.handle_no_permission()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['memberships'] = self.get_memberships()
return context
def get_memberships(self):
"""Returns the club' members with their membership.
A membership is an instance of 'ClubUser' model.
Returns:
List of (user, membership).
"""
qs = (
self.object.clubuser_set
.select_related('user')
.order_by('-is_respo', 'user__first_name')
)
print(qs)
return [(m.user, m) for m in qs]
club_detail = ClubDetailView.as_view()

View file

@ -3,13 +3,14 @@ Django==1.11.*
django-autocomplete-light==2.3.3
django-autoslug==1.9.3
django-cas-ng==3.5.6
git+https://github.com/TimBest/django-multi-form-view.git@3a72bba#egg=dango-multi-form-view
django-recaptcha==1.2.1
mysqlclient==1.3.10
Pillow
six
unicodecsv
icalendar
django-bootstrap-form==3.2.1
django-bootstrap-form==3.3
asgiref==1.0.0
daphne==1.0.3
asgi-redis==1.0.0