Merge branch 'ju' into 'master'

Recherche basique, visualisation d'une fiche et édition se sa fiche

Closes #3 and #2

See merge request klub-dev-ens/annuaire!2
This commit is contained in:
Martin Pepin 2020-02-08 11:17:21 +01:00
commit c6f134c31d
19 changed files with 758 additions and 20 deletions

6
.gitignore vendored
View file

@ -4,11 +4,7 @@
*~ *~
*# *#
/media/archives/* /media/picture
/media/images/*
/media/documents/*
/media/original_images/*
/media/admin
/recensements/ /recensements/
/venv/ /venv/

View file

@ -86,7 +86,8 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
@ -103,7 +104,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/dev/topics/i18n/ # https://docs.djangoproject.com/en/dev/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
@ -118,3 +119,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/dev/howto/static-files/ # https://docs.djangoproject.com/en/dev/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

View file

@ -13,9 +13,17 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from django.conf.urls.static import static
from fiches.views import home
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('fiche/', include('fiches.urls')),
path('', home, name='home')
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -3,9 +3,13 @@ from fiches.models import Profile
from fiches.models import Department from fiches.models import Department
from fiches.models import Phone from fiches.models import Phone
from fiches.models import Social from fiches.models import Social
from fiches.models import Mail
from fiches.models import Address
# Register your models here.
admin.site.register(Profile) admin.site.register(Profile)
admin.site.register(Department) admin.site.register(Department)
admin.site.register(Phone) admin.site.register(Phone)
admin.site.register(Social) admin.site.register(Social)
admin.site.register(Mail)
admin.site.register(Address)

30
fiches/forms.py Normal file
View file

@ -0,0 +1,30 @@
from django import forms
from fiches.models import Profile, Department
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = [
"full_name",
"nickname",
"picture",
"department",
"promotion",
"birth_date",
"thurne",
"text_field",
"printing",
"keep_me"
]
class SearchForm(forms.Form):
name = forms.CharField(label='Nom/Surnom', max_length=1023, required=False)
year = forms.IntegerField(label='Promotion', required=False)
department = forms.ModelMultipleChoiceField(queryset=Department.objects.all(), required=False)
def clean(self):
cleaned_data = super().clean()
if (not cleaned_data['name'] and not cleaned_data['year'] and not cleaned_data['department']):
raise forms.ValidationError(('Tous les champs sont vides'), code='invalid')

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-04-08 19:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('fiches', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='phone',
name='profile',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fiches.Profile', verbose_name='profil'),
),
migrations.AlterField(
model_name='social',
name='profile',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fiches.Profile', verbose_name='profil'),
),
]

View file

@ -0,0 +1,47 @@
# Generated by Django 2.2.9 on 2020-01-08 23:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('fiches', '0002_auto_20190408_1929'),
]
operations = [
migrations.AlterField(
model_name='department',
name='name',
field=models.CharField(max_length=255, verbose_name='nom du département'),
),
migrations.AlterField(
model_name='phone',
name='name',
field=models.CharField(max_length=255, verbose_name='type'),
),
migrations.AlterField(
model_name='social',
name='name',
field=models.CharField(max_length=255, verbose_name='type'),
),
migrations.CreateModel(
name='Mail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='type')),
('mail', models.CharField(max_length=1023, verbose_name='adresse mail')),
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fiches.Profile', verbose_name='profil')),
],
),
migrations.CreateModel(
name='Address',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='type')),
('content', models.CharField(max_length=1023, verbose_name='adresse')),
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fiches.Profile', verbose_name='profil')),
],
),
]

View file

@ -6,7 +6,10 @@ from django.contrib.auth.models import User
class Profile(models.Model): class Profile(models.Model):
user = models.OneToOneField( user = models.OneToOneField(
User, on_delete=models.CASCADE, verbose_name=_("utilisateur") User,
on_delete=models.CASCADE,
verbose_name=_("utilisateur"),
related_name="profile",
) )
full_name = models.CharField(max_length=1023, verbose_name=_("nom")) full_name = models.CharField(max_length=1023, verbose_name=_("nom"))
nickname = models.CharField(blank=True, max_length=1023, verbose_name=_("surnom")) nickname = models.CharField(blank=True, max_length=1023, verbose_name=_("surnom"))
@ -27,22 +30,56 @@ class Profile(models.Model):
default=False, verbose_name=_("conserver la fiche annuaire ?") default=False, verbose_name=_("conserver la fiche annuaire ?")
) )
def __str__(self):
return self.full_name
class Department(models.Model): class Department(models.Model):
name = models.CharField(max_length=1023, verbose_name=_("nom du département")) name = models.CharField(max_length=255, verbose_name=_("nom du département"))
def __str__(self):
return self.name
class Phone(models.Model): class Phone(models.Model):
profile = models.OneToOneField( profile = models.ForeignKey(
Profile, on_delete=models.CASCADE, verbose_name=_("profil") Profile, on_delete=models.CASCADE, verbose_name=_("profil")
) )
name = models.CharField(max_length=1023, verbose_name=_("type")) name = models.CharField(max_length=255, verbose_name=_("type"))
number = models.CharField(max_length=1023, verbose_name=_("numéro")) number = models.CharField(max_length=1023, verbose_name=_("numéro"))
def __str__(self):
return "{} : {}".format(self.name, self.number)
class Social(models.Model): class Social(models.Model):
profile = models.OneToOneField( profile = models.ForeignKey(
Profile, on_delete=models.CASCADE, verbose_name=_("profil") Profile, on_delete=models.CASCADE, verbose_name=_("profil")
) )
name = models.CharField(max_length=1023, verbose_name=_("type")) name = models.CharField(max_length=255, verbose_name=_("type"))
content = models.CharField(max_length=1023, verbose_name=_("contenu")) content = models.CharField(max_length=1023, verbose_name=_("contenu"))
def __str__(self):
return "{} : {}".format(self.name, self.content)
class Mail(models.Model):
profile = models.ForeignKey(
Profile, on_delete=models.CASCADE, verbose_name=_("profil")
)
name = models.CharField(max_length=255, verbose_name=_("type"))
mail = models.CharField(max_length=1023, verbose_name=_("adresse mail"))
def __str__(self):
return "{} : {}".format(self.name, self.mail)
class Address(models.Model):
profile = models.ForeignKey(
Profile, on_delete=models.CASCADE, verbose_name=_("profil")
)
name = models.CharField(max_length=255, verbose_name=_("type"))
content = models.CharField(max_length=1023, verbose_name=_("adresse"))
def __str__(self):
return "{} : {}".format(self.name, self.content)

View file

@ -0,0 +1,351 @@
html {
background-color: black;
color: #FFF;
font-family: Verdana, Verdana, Geneva, sans-serif;
}
body {
margin: 0 0 0.8em 0;
min-height: 538px;
}
img {
border: 0;
}
a:link {
background-color: inherit;
color: #7978DA;
text-decoration: none;
}
a:visited {
background-color: inherit;
color: #9CA6FF;
text-decoration: none;
}
a:hover {
background-color: inherit;
color: #006CE3;
text-decoration: underline;
}
p {
margin-top: 0.6em;
margin-bottom: 0.6em;
}
.ens {
color: #89C4FF;
background-color: inherit;
}
.tiny {
font-size: 0.8em;
}
.verytiny {
font-size: 0.7em;
}
.dark {
color: #666;
background-color: inherit;
}
.center {
text-align: center;
}
.right {
float: right;
}
.spacer {
width: 100%;
clear: both;
}
.error {
font-weight: bold;
color: #FB0000;
background-color: inherit;
}
.warning {
font-weight: bold;
color: #FF8A00;
background-color: inherit;
}
.mainspacer {
width: 100%;
height: 0.8em;
}
.success {
font-weight: bold;
color: #00E000;
background-color: inherit;
}
.success a:link {
background-color: inherit;
color: #008800;
text-decoration: none;
}
.success a:visited {
background-color: inherit;
color: #008800;
text-decoration: none;
}
.success a:hover {
background-color: inherit;
color: #4FFF4F;
text-decoration: underline;
}
.block {
background-color: #333;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
text-align: justify;
padding: 0.5em 0.5em;
margin: 0 1em 1em 1em;
}
#main {
padding-top: 0.5em;
margin-left: 1em;
background-color: transparent;
color: #FFF;
width: 40em;
float: left;
}
#header {
margin: 0 1em 0 1em;
float: left;
text-align: center;
}
#header h1 {
margin-top: 0;
font-family: Georgia, sans-serif;
}
#header h1 a:link, #header h1 a:visited {
color: #FFF;
background-color: inherit;
text-decoration: none;
}
#header h1 a:hover {
text-decoration: underline;
}
#language_switch {
margin-top: 0.5em;
float: left;
}
#language_switch form {
float: left;
padding: 0;
margin-left: 0.5em;
}
#content {
}
#content h3 {
margin-top: 0;
margin-bottom: 0.2em;
}
#content h4 {
margin-top: 0.5em;
margin-bottom: 0.2em;
}
#footer {
padding: 0.2em 0;
margin: 1em 5em 0 5em;
clear: both;
background-color: #333;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
text-align: center;
}
#contact {
margin: 0.5em 8em 0 8em;
}
#search {
margin-bottom: 1em;
}
.errorlist {
display: inline;
margin-top: 0;
margin-bottom: 0;
padding-left: 0;
}
.errorlist li {
display: inline;
font-weight: bold;
color: #E00000;
background-color: inherit;
}
#advanced_search {
border: 1px solid #CCC;
padding: 0.5em;
margin-bottom: 1em;
}
.advanced_filter {
width: 50%;
margin-top: 1px;
margin-bottom: 1px;
float: left;
}
.advanced_filter input {
max-width: 60%;
}
.submit_button {
text-align: center;
}
.add_delete_button {
text-align: right;
margin: 0.3em;
}
#pdflink {
text-align: right;
font-size: 0.8em;
}
#picture {
width: 8em;
float: left;
}
#picture img {
margin-top: 0.3em;
max-width: 8em;
}
#infos {
padding-left: 1em;
float: left;
width: 31em;
}
#ficheedit {
margin-top: 0;
padding-top: 0;
}
.editfield {
padding-top: 0.2em;
padding-bottom: 0.2em;
}
.editfield input {
}
.editfield textarea {
margin-left: auto;
margin-right: auto;
margin-top:0.2em;
display: block;
width: 98%;
height: 3em;
}
.message {
margin-top: 0.3em;
margin-bottom: 0.3em;
text-align: center;
}
.long_message {
margin-top: 0.6em;
margin-bottom: 0.6em;
text-align: justify;
}
fieldset.address .editfield textarea, fieldset.quote .editfield textarea {
height: 3em;
}
fieldset.address, fieldset.quote {
padding-bottom: 0.2em;
}
#main-login-container {
width: 500px;
margin: 3.5em auto;
}
#main-login-container h3.error {
text-align: center;
width: 100%;
margin-bottom: 20px;
font-size: 1.3em;
font-family: 'Droid Serif', serif;
}
#main-login {
width: 500px;
border: 15px solid #333;
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
border-radius: 20px;
}
#main-login.login_block {
padding: 2em;
box-shadow: 0 0 100px #AAA inset;
}
a#login_clipper, a#login_outsider {
float: left;
display: block;
width: 250px;
height: 200px;
text-align: center;
font-family: 'Droid Serif', serif;
font-size: 1.3em;
font-weight: bold;
line-height: 190px;
text-decoration: none;
color: #FFF;
}
a#login_clipper {
background-color: #123E96;
box-shadow: 0 0 100px #040C78 inset;
}
a#login_clipper:hover {
background-color: #164BB6;
}
a#login_outsider {
background-color: #961221;
box-shadow: 0 0 100px #780411 inset;
}
a#login_outsider:hover {
background-color: #B31729;
}

View file

@ -0,0 +1,66 @@
{% load i18n %}
{% load staticfiles %}
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>
{% block title_onglet %}{% trans "Annuaire des élèves de l'ENS" %}{% endblock %}
</title>
<link rel="stylesheet" type="text/css" href="{% static "fiches/css/style.css" %}" />
{% block extrahead %}{% endblock %}
{# Pour pouvoir ajouter des trucs dans le head sur une autre page #}
</head>
<body>
<div id="header">
<div id="language_switch">
</div>
<div class="spacer"></div>
<h1>
{% block title %} <a href='{% url "home" %}'>{% trans "Annuaire des élèves de l'ENS" %}</a>{% endblock %}
</h1>
{% if user.is_authenticated %}
{% blocktrans %}Connecté en tant que <tt>{{ user }}</tt>{% endblocktrans %}<br />
{% endif %}
<nav>
<a href='{% url "home" %}'> Accueil </a>
<a href='{% url "fiche_modif" %}'> Modifier sa fiche d'annuaire </a>
<a href='{% url "fiche" request.user.profile.id %}'> Consulter sa fiche d'annuaire </a>
<a href=''> Anniversaires à venir </a>
</nav>
</div>
<div id="main">
<div id="content">
{% block contentspacer %}{% endblock %}
{% block content %}{% endblock %}
</div>
<div id="footer">
{% block footer %}
[<a href="http://www.ens.fr">ENS</a>]
[<a href="http://www.eleves.ens.fr/">{% trans "Page des élèves" %}</a>]
{% endblock %}
</div>
<div id="contact" class="verytiny">
{% block contact %}
<a href="mailto:klub-dev@ens.fr">
{% trans "Contacter l'équipe annuaire" %}
</a>
<span class="right dark" title="annuaireweb v{{ VERSION }}">
powered by Django, KDENS
</span>
<div class="spacer"></div>
{% endblock %}
</div>
</div>
<div class="spacer"></div>
</body>
</html>

View file

@ -0,0 +1,66 @@
{% extends "fiches/base.html" %}
{% block content %}
<div>
<div style="width:200px;height:200px;overflow:hidden;" >
<img src="{{ profile.picture.url }}" width="200px" height="auto"/>
</div>
<div>
<h3>{{ profile.full_name }}
({{ profile.promotion }})
{% if profile.nickname %}
<em>alias:</em>{{ profile.nickname }})</h3>
{% endif %}
<p>
{% if profile.department.exists %}
<em>Département{{ profile.department.count|pluralize}} :</em>
{% endif %}
{% for dep in profile.department.all %}
{{ dep }}
{% if not forloop.last %}
,
{% endif %}
{% endfor %}
{% if profile.birth_date %}
</p>
<p>
{% if profile.phone_set.exists %}
<em>Téléphone{{ profile.phone_set.count|pluralize}} :</em>
{% endif %}
{% for ph in profile.phone_set.all %}
{{ ph }}
{% if not forloop.last %}
,<br/>
{% endif %}
{% endfor %}
</p>
<p>
{% if profile.social_set.exists %}
<em>{{ profile.social_set.count|pluralize:"Réseau social,Réseaux sociaux"}} :</em>
{% endif %}
{% for ph in profile.social_set.all %}
{{ ph }}
{% if not forloop.last %}
,<br/>
{% endif %}
{% endfor %}
</p>
<p>
<em>Date de naissance :</em> {{ profile.birth_date }}
</p>
{% endif %}
{% if profile.thurne %}
<p>
<em>Thurne :</em> {{ profile.thurne }}
</p>
{% endif %}
</div>
</div>
<div>
{% if profile.text_field %}
<p>
<b>Champ libre :</b> {{ profile.text_field }}
</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends "fiches/base.html" %}
{% block content %}
<h2>Modifier ma page d'annuaire</h2>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "fiches/base.html" %}
{% block content %}
<h2> Chercher quelqu'un.e dans l'annuaire </h2>
<form method='post' action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Recherche">
</form>
<div>
<ul>
{% for profile in result %}
<li><a href="{% url 'fiche' profile.id %}">{{profile.full_name}} {{profile.departement}}
</a> </li>
{% endfor %}
</ul>
</div>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "fiches/base.html" %}
{% block content %}
<h2> Chercher quelqu'un.e dans l'annuaire </h2>
<form method='post' action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Recherche">
</form>
<div>
<ul>
{% for profile in result %}
<li><a href="{% url 'fiche' profile.id %}">{{profile.full_name}} {{profile.departement}}
</a> </li>
{% endfor %}
</ul>
</div>
{% endblock %}

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

7
fiches/urls.py Normal file
View file

@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path('<int:id>',views.fiche, name='fiche'),
path('edit',views.fiche_modif, name='fiche_modif')
]

View file

@ -1,3 +1,41 @@
from django.shortcuts import render from django.shortcuts import render
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from fiches.models import Profile
from fiches.forms import ProfileForm, SearchForm
from django.urls import reverse
from django.db.models import Q
# Create your views here.
@login_required
def fiche(request, id):
profile = get_object_or_404(Profile, id=id)
return render(request, 'fiches/fiche.html', {"profile": profile})
@login_required
def fiche_modif(request):
profile = request.user.profile
if request.method == 'POST':
form = ProfileForm(request.POST, instance=profile)
if form.is_valid():
form.save()
return redirect(reverse('fiche', args=(profile.id,)))
else:
form = ProfileForm(instance=profile)
return render(request, 'fiches/fiches_modif.html', {"form": form})
@login_required
def home(request):
if request.method == 'POST':
form = SearchForm(request.POST)
if form.is_valid():
result = Profile.objects.filter(Q(full_name__icontains=form.cleaned_data['name']) | Q(nickname__icontains=form.cleaned_data['name']))
return render(request,'fiches/home.html',{"form":form, "result":result})
else:
form = SearchForm()
return render(request,'fiches/home.html',{"form":form})

6
requirements-dev.txt Normal file
View file

@ -0,0 +1,6 @@
-r requirements.txt
django-debug-toolbar
ipython
black
flake8
isort

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
django==2.2.*
Pillow