Skip to content
Snippets Groups Projects
Unverified Commit 34e7474c authored by Florimond Manca's avatar Florimond Manca Committed by GitHub
Browse files

Merge pull request #4 from oser-cs/dev

Release version ready to welcome first users
parents 122c68c9 f0927315
No related branches found
No related tags found
No related merge requests found
Showing
with 80 additions and 388 deletions
...@@ -8,7 +8,10 @@ python: ...@@ -8,7 +8,10 @@ python:
services: services:
- postgresql - postgresql
- redis-server
addons:
# Django 2.1+ requires PostgreSQL 9.4+
postgresql: '9.4'
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
...@@ -17,9 +20,6 @@ install: ...@@ -17,9 +20,6 @@ install:
- pip install git+https://github.com/Supervisor/supervisor.git - pip install git+https://github.com/Supervisor/supervisor.git
before_script: before_script:
# Start Celery using the supervisor config
- supervisord
# Create local PostgreSQL database # Create local PostgreSQL database
# NOTE: the database name (here 'oser_backend_db') must match the name # NOTE: the database name (here 'oser_backend_db') must match the name
# in one of these DATABASE_URL setting: # in one of these DATABASE_URL setting:
......
web: gunicorn oser_backend.wsgi:application --bind 0.0.0.0:$PORT web: gunicorn oser_backend.wsgi:application --bind 0.0.0.0:$PORT
# Toggle next line to start Celery in a worker process
# worker: celery -A oser_backend worker --beat -l info
...@@ -50,18 +50,6 @@ Le site utilise une base de données SQL. Plusieurs technologies existent mais o ...@@ -50,18 +50,6 @@ Le site utilise une base de données SQL. Plusieurs technologies existent mais o
Après avoir installé PostgreSQL, démarrez le serveur en ouvrant pgAdmin, l'interface graphique qui sera installée en même temps que Postgres. Après avoir installé PostgreSQL, démarrez le serveur en ouvrant pgAdmin, l'interface graphique qui sera installée en même temps que Postgres.
#### Optionnel : Redis, supervisord
Le backend Django est relié à [Celery](http://www.celeryproject.org), une librairie Python permet d'effectuer des traitements ou opérations en tâche de fond.
> NOTE : Pour l'instant, Celery n'est utilisé que pour effectuer un nettoyage périodique des fichiers de médias inutilisés, opération qui peut de toute façon être déclenchée par `$ python manage.py cleanmedia`. Il n'est donc **pas obligatoire d'installer ce qui suit en développement.**
Celery a besoin d'un système de *messaging* pour fonctionner, on utilise donc ici [Redis](https://redis.io).
Enfin, [supervisord](http://supervisord.org) est un gestionnaire de processus qui nous permet de lancer Redis et Celery en une seule commande.
Le plus simple est de se référer aux sites de chaque logiciel/librairie pour leur installation. :wink:
### Installation du projet ### Installation du projet
- (Recommandé) Créez un environnement virtuel (ici appelé `env`) puis activez-le : - (Recommandé) Créez un environnement virtuel (ici appelé `env`) puis activez-le :
...@@ -162,29 +150,6 @@ $ curl -X GET "localhost:8000/api/articles/" -H "Authorization: Token b6302cebe7 ...@@ -162,29 +150,6 @@ $ curl -X GET "localhost:8000/api/articles/" -H "Authorization: Token b6302cebe7
[{"id": 39, "content": ...}, ...] [{"id": 39, "content": ...}, ...]
``` ```
### Tâches de fond
Le daemon Celery gère le calendrier des tâches de fond (nettoyage des fichiers de médias non-utilisés ou autres tâches définies dans le `settings.py`). Pour fonctionner, Celery nécessite un serveur de messages, on utilise ici Redis.
Les opérations nécessaires pour lancer Celery ainsi que la configuration avec Redis sont rassemblées dans le fichier `supervisord.conf`. Assurez-vous donc d'avoir installé Redis et Supervisor puis démarrez Supervisor au même niveau que le fichier `supervisord.conf` :
```
# Supervisor ne supporte toujours pas officiellement Python 3,
# mais la dernière version de développement oui.
$ pip install git+https://github.com/Supervisor/supervisor.git
$ supervisord
```
Pour accéder aux derniers logs de Celery ou de Redis, utilisez `supervisorctl tail (celery|redis)`:
```
$ supervisorctl tail celery
[2018-04-29 10:59:31,550: INFO/MainProcess] Connected to redis://localhost:6379//
[2018-04-29 10:59:31,566: INFO/MainProcess] mingle: searching for neighbors
[2018-04-29 10:59:32,601: INFO/MainProcess] mingle: all alone
[2018-04-29 10:59:32,657: INFO/MainProcess] celery@MacBook-Pro-de-Florimond-2.local ready.
```
### Envoi de mails ### Envoi de mails
Le backend utilise le plan gratuit de [SendGrid](https://sendgrid.com) (jusqu'à 100 emails par jour) pour envoyer des emails et notifications aux utilisateurs. Le backend utilise le plan gratuit de [SendGrid](https://sendgrid.com) (jusqu'à 100 emails par jour) pour envoyer des emails et notifications aux utilisateurs.
......
...@@ -6,7 +6,6 @@ from api.auth import obtain_auth_token ...@@ -6,7 +6,6 @@ from api.auth import obtain_auth_token
from core import views as core_views from core import views as core_views
from profiles import views as profiles_views from profiles import views as profiles_views
from register import views as register_views from register import views as register_views
from tutoring import views as tutoring_views
from users import views as users_views from users import views as users_views
from visits import views as visits_views from visits import views as visits_views
import projects.views import projects.views
...@@ -29,13 +28,6 @@ router.register('users', users_views.UserViewSet) ...@@ -29,13 +28,6 @@ router.register('users', users_views.UserViewSet)
router.register('tutors', profiles_views.TutorViewSet) router.register('tutors', profiles_views.TutorViewSet)
router.register('students', profiles_views.StudentViewSet) router.register('students', profiles_views.StudentViewSet)
# Tutoring views
router.register('schools', tutoring_views.SchoolViewSet)
router.register('groups', tutoring_views.TutoringGroupViewSet,
base_name='tutoring_group')
router.register('sessions', tutoring_views.TutoringSessionViewSet,
base_name='tutoring_session')
# Register views # Register views
router.register('registrations', register_views.RegistrationViewSet) router.register('registrations', register_views.RegistrationViewSet)
......
# Generated by Django 2.0.3 on 2018-03-17 15:20 # Generated by Django 2.0.7 on 2018-09-11 17:38
from django.db import migrations, models from django.db import migrations, models
import django_countries.fields
import markdownx.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
...@@ -12,14 +14,29 @@ class Migration(migrations.Migration): ...@@ -12,14 +14,29 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Link', name='Address',
fields=[ fields=[
('slug', models.SlugField(help_text="Un identifiant unique pour ce lien. Privilégiez 'ce-format-de-slug'.", primary_key=True, serialize=False, unique=True)), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(verbose_name='URL')), ('line1', models.CharField(help_text='Numéro, voie, rue…', max_length=300, verbose_name='ligne 1')),
('description', models.TextField(help_text='Précisez ce que contient ce lien et comment il est utilisé.')), ('line2', models.CharField(blank=True, default='', help_text='Résidence, appartement, lieu-dit…', max_length=300, verbose_name='ligne 2')),
('post_code', models.CharField(help_text="Code postal. Note : le format n'est pas vérifié.", max_length=20, verbose_name='code postal')),
('city', models.CharField(help_text='Ville', max_length=100, verbose_name='ville')),
('country', django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2, verbose_name='pays')),
], ],
options={ options={
'verbose_name': 'lien', 'verbose_name': 'adresse',
},
),
migrations.CreateModel(
name='Document',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Titre du document', max_length=300, verbose_name='titre')),
('slug', models.SlugField(help_text='Un court identifiant généré après la création du document.', max_length=100, unique=True)),
('content', markdownx.models.MarkdownxField(help_text='Contenu du document (Markdown est supporté).', verbose_name='contenu')),
],
options={
'ordering': ('title',),
}, },
), ),
] ]
# Generated by Django 2.0.3 on 2018-03-19 21:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Link',
),
]
# Generated by Django 2.0.3 on 2018-03-24 18:37
from django.db import migrations, models
import markdownx.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0002_delete_link'),
]
operations = [
migrations.CreateModel(
name='Document',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Titre du document', max_length=300, verbose_name='titre')),
('slug', models.SlugField(help_text='Un court identifiant généré après la création du document.', max_length=100, unique=True)),
('content', markdownx.models.MarkdownxField(help_text='Contenu du document (Markdown est supporté).', verbose_name='contenu')),
],
options={
'ordering': ('title',),
},
),
]
# Generated by Django 2.0.3 on 2018-04-08 13:18
from django.db import migrations, models
import django_countries.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0003_document'),
]
operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('line1', models.CharField(help_text='Numéro, voie, rue…', max_length=300, verbose_name='ligne 1')),
('line2', models.CharField(blank=True, default='', help_text='Résidence, appartement, lieu-dit…', max_length=300, verbose_name='ligne 2')),
('post_code', models.CharField(help_text="Code postal. Note : le format n'est pas vérifié.", max_length=20, verbose_name='code postal')),
('city', models.CharField(help_text='Ville', max_length=100, verbose_name='ville')),
('country', django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2)),
],
),
]
# Generated by Django 2.0.3 on 2018-04-08 13:25
from django.db import migrations
import django_countries.fields
class Migration(migrations.Migration):
dependencies = [
('core', '0004_address'),
]
operations = [
migrations.AlterModelOptions(
name='address',
options={'verbose_name': 'adresse'},
),
migrations.AlterField(
model_name='address',
name='country',
field=django_countries.fields.CountryField(default='FR', help_text='Pays (FR par défaut).', max_length=2, verbose_name='pays'),
),
]
"""Core Celery tasks."""
from oser_backend.celery import app
from django.core.management import call_command
@app.task
def cleanmedia():
"""Clean unused media files."""
call_command('cleanmedia')
# Generated by Django 2.0.6 on 2018-06-15 21:24 # Generated by Django 2.0.7 on 2018-09-11 17:38
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import dynamicforms.utils
class Migration(migrations.Migration): class Migration(migrations.Migration):
...@@ -18,15 +19,32 @@ class Migration(migrations.Migration): ...@@ -18,15 +19,32 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('answer', models.TextField(blank=True, null=True, verbose_name='réponse')), ('answer', models.TextField(blank=True, null=True, verbose_name='réponse')),
], ],
options={
'verbose_name': 'réponse',
},
),
migrations.CreateModel(
name='File',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=300, verbose_name='nom')),
('file', models.FileField(upload_to=dynamicforms.utils.file_upload_to, verbose_name='fichier')),
],
options={
'verbose_name': 'fichier',
'verbose_name_plural': 'fichiers',
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Form', name='Form',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=300, verbose_name='titre')), ('title', models.CharField(max_length=300, verbose_name='titre')),
('slug', models.SlugField(blank=True, default='', max_length=100)),
('created', models.DateTimeField(auto_now_add=True, verbose_name='créé le')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='créé le')),
], ],
options={ options={
'verbose_name': 'formulaire',
'ordering': ('-created',), 'ordering': ('-created',),
}, },
), ),
...@@ -35,9 +53,11 @@ class Migration(migrations.Migration): ...@@ -35,9 +53,11 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted', models.DateTimeField(auto_now_add=True, help_text="Date et heure de soumission de l'entrée.", verbose_name='soumis le')), ('submitted', models.DateTimeField(auto_now_add=True, help_text="Date et heure de soumission de l'entrée.", verbose_name='soumis le')),
('form', models.ForeignKey(help_text="Formulaire associé à l'entrée.", on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='dynamicforms.Form')), ('form', models.ForeignKey(help_text="Formulaire associé à l'entrée.", on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='dynamicforms.Form', verbose_name='formulaire')),
], ],
options={ options={
'verbose_name': 'entrée de formulaire',
'verbose_name_plural': 'entrées de formulaire',
'ordering': ('-submitted',), 'ordering': ('-submitted',),
}, },
), ),
...@@ -45,16 +65,42 @@ class Migration(migrations.Migration): ...@@ -45,16 +65,42 @@ class Migration(migrations.Migration):
name='Question', name='Question',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_type', models.CharField(max_length=100, verbose_name='type de question')), ('text', models.CharField(help_text='intitulé de la question', max_length=300, verbose_name='intitulé')),
('text', models.TextField(help_text='intitulé de la question', verbose_name='intitulé')), ('type', models.CharField(choices=[('text-small', 'Texte court'), ('text-long', 'Texte long'), ('yes-no', 'Oui/Non'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question')),
('required', models.BooleanField(verbose_name='requis')), ('help_text', models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=300, verbose_name='aide')),
('form', models.ForeignKey(help_text='Formulaire associé à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Form')), ('required', models.BooleanField(default=True, verbose_name='requis')),
('order', models.PositiveIntegerField(default=0, verbose_name='position')),
],
options={
'ordering': ('order',),
},
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='titre')),
('order', models.PositiveIntegerField(default=0, verbose_name='position')),
('form', models.ForeignKey(help_text='Formulaire associé à la section.', on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='dynamicforms.Form', verbose_name='formulaire')),
], ],
options={
'ordering': ('order',),
},
),
migrations.AddField(
model_name='question',
name='section',
field=models.ForeignKey(help_text='Section de formulaire associée à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Section', verbose_name='section'),
),
migrations.AddField(
model_name='file',
name='form',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='dynamicforms.Form', verbose_name='formulaire'),
), ),
migrations.AddField( migrations.AddField(
model_name='answer', model_name='answer',
name='entry', name='entry',
field=models.ForeignKey(help_text='Entrée associée à la réponse.', on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry'), field=models.ForeignKey(help_text='Entrée associée à la réponse.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry', verbose_name='entrée'),
), ),
migrations.AddField( migrations.AddField(
model_name='answer', model_name='answer',
......
# Generated by Django 2.0.6 on 2018-06-15 21:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='answer',
options={'verbose_name': 'réponse'},
),
migrations.AlterModelOptions(
name='form',
options={'ordering': ('-created',), 'verbose_name': 'formulaire'},
),
migrations.AlterModelOptions(
name='formentry',
options={'ordering': ('-submitted',), 'verbose_name': 'entrée de formulaire', 'verbose_name_plural': 'entrées de formulaire'},
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0002_auto_20180615_2325'),
]
operations = [
migrations.AlterField(
model_name='answer',
name='entry',
field=models.ForeignKey(help_text='Entrée associée à la réponse.', on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='dynamicforms.FormEntry', verbose_name='entrée'),
),
migrations.AlterField(
model_name='formentry',
name='form',
field=models.ForeignKey(help_text="Formulaire associé à l'entrée.", on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='dynamicforms.Form', verbose_name='formulaire'),
),
migrations.AlterField(
model_name='question',
name='form',
field=models.ForeignKey(help_text='Formulaire associé à la question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Form', verbose_name='formulaire'),
),
migrations.AlterField(
model_name='question',
name='required',
field=models.BooleanField(default=True, verbose_name='requis'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0003_auto_20180615_2338'),
]
operations = [
migrations.RenameField(
model_name='question',
old_name='question_type',
new_name='type',
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0004_auto_20180615_2339'),
]
operations = [
migrations.AddField(
model_name='question',
name='help_text',
field=models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=500, verbose_name='aide'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 21:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0005_question_help_text'),
]
operations = [
migrations.AlterField(
model_name='question',
name='help_text',
field=models.CharField(blank=True, default='', help_text='Apporte des précisions sur la question', max_length=300, verbose_name='aide'),
),
migrations.AlterField(
model_name='question',
name='text',
field=models.CharField(help_text='intitulé de la question', max_length=300, verbose_name='intitulé'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 22:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0006_auto_20180615_2357'),
]
operations = [
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('text-small', 'Texte court'), ('text-small', 'Texte long'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 22:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0007_auto_20180616_0000'),
]
operations = [
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='titre')),
('form', models.ForeignKey(help_text='Formulaire associé à la section.', on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='dynamicforms.Form', verbose_name='formulaire')),
],
),
migrations.RemoveField(
model_name='question',
name='form',
),
migrations.AddField(
model_name='question',
name='section',
field=models.ForeignKey(help_text='Section de formulaire associée à la question.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='dynamicforms.Section', verbose_name='section'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 23:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0008_auto_20180616_0007'),
]
operations = [
migrations.CreateModel(
name='File',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=300, verbose_name='nom')),
('file', models.FileField(upload_to='forms/attachments/', verbose_name='fichier')),
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='dynamicforms.Form', verbose_name='formulaire')),
],
options={
'verbose_name': 'fichier',
'verbose_name_plural': 'fichiers',
},
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(choices=[('text-small', 'Texte court'), ('text-small', 'Texte long'), ('yes-no', 'Oui/Non'), ('date', 'Date'), ('sex', 'Sexe')], max_length=100, verbose_name='type de question'),
),
]
# Generated by Django 2.0.6 on 2018-06-15 23:21
from django.db import migrations, models
import dynamicforms.utils
class Migration(migrations.Migration):
dependencies = [
('dynamicforms', '0009_auto_20180616_0105'),
]
operations = [
migrations.AddField(
model_name='form',
name='slug',
field=models.SlugField(default='noname', max_length=100),
),
migrations.AlterField(
model_name='file',
name='file',
field=models.FileField(upload_to=dynamicforms.utils.file_upload_to, verbose_name='fichier'),
),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment