Browse Source

Initial commit

Maxime Raynal 7 years ago
commit
8c03f5480f
37 changed files with 3639 additions and 0 deletions
  1. 4 0
      .gitignore
  2. 1740 0
      data.yml
  3. 0 0
      speed_rack/backoffice/__init__.py
  4. 17 0
      speed_rack/backoffice/admin.py
  5. 5 0
      speed_rack/backoffice/apps.py
  6. 112 0
      speed_rack/backoffice/management/commands/import_references.py
  7. 52 0
      speed_rack/backoffice/migrations/0001_initial.py
  8. 42 0
      speed_rack/backoffice/migrations/0002_auto_20170816_1355.py
  9. 46 0
      speed_rack/backoffice/migrations/0003_auto_20170816_2143.py
  10. 20 0
      speed_rack/backoffice/migrations/0004_auto_20170816_2144.py
  11. 20 0
      speed_rack/backoffice/migrations/0005_auto_20170816_2201.py
  12. 20 0
      speed_rack/backoffice/migrations/0006_auto_20170816_2216.py
  13. 20 0
      speed_rack/backoffice/migrations/0007_auto_20170816_2235.py
  14. 20 0
      speed_rack/backoffice/migrations/0008_auto_20170816_2243.py
  15. 20 0
      speed_rack/backoffice/migrations/0009_auto_20170816_2248.py
  16. 21 0
      speed_rack/backoffice/migrations/0010_auto_20170816_2249.py
  17. 21 0
      speed_rack/backoffice/migrations/0011_auto_20170816_2249.py
  18. 20 0
      speed_rack/backoffice/migrations/0012_reference_disabled.py
  19. 37 0
      speed_rack/backoffice/migrations/0013_auto_20170827_1614.py
  20. 0 0
      speed_rack/backoffice/migrations/__init__.py
  21. 60 0
      speed_rack/backoffice/models.py
  22. 36 0
      speed_rack/backoffice/serializers.py
  23. 22 0
      speed_rack/manage.py
  24. 0 0
      speed_rack/speed_rack/__init__.py
  25. 123 0
      speed_rack/speed_rack/settings.py
  26. 8 0
      speed_rack/speed_rack/urls.py
  27. 16 0
      speed_rack/speed_rack/wsgi.py
  28. 0 0
      speed_rack/webapp/__init__.py
  29. 5 0
      speed_rack/webapp/apps.py
  30. 0 0
      speed_rack/webapp/migrations/__init__.py
  31. 409 0
      speed_rack/webapp/static/webapp/css/style.css
  32. 141 0
      speed_rack/webapp/static/webapp/js/http-request.js
  33. 149 0
      speed_rack/webapp/static/webapp/js/main.js
  34. 101 0
      speed_rack/webapp/static/webapp/js/template-rendering.js
  35. 210 0
      speed_rack/webapp/templates/webapp/index.html
  36. 10 0
      speed_rack/webapp/urls.py
  37. 112 0
      speed_rack/webapp/views.py

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+db.sqlite3
+__pycache__/
+*.py[cod]
+*$py.class

File diff suppressed because it is too large
+ 1740 - 0
data.yml


+ 0 - 0
speed_rack/backoffice/__init__.py


+ 17 - 0
speed_rack/backoffice/admin.py

@@ -0,0 +1,17 @@
+from django.contrib import admin
+
+from backoffice.models import Reference, Authority, ReferenceType
+
+
+class AuthorityAdmin(admin.ModelAdmin):
+    prepopulated_fields = {"slug": ("title",)}
+
+class ReferenceTypeAdmin(admin.ModelAdmin):
+    prepopulated_fields = {"slug": ("title",)}
+
+class ReferenceAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(Reference)
+admin.site.register(Authority)
+admin.site.register(ReferenceType)

+ 5 - 0
speed_rack/backoffice/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BackofficeConfig(AppConfig):
+    name = 'backoffice'

+ 112 - 0
speed_rack/backoffice/management/commands/import_references.py

@@ -0,0 +1,112 @@
+import yaml
+from datetime import datetime
+from pprint import pprint
+
+from django.core.management.base import BaseCommand
+from django.utils.text import slugify
+
+from backoffice.models import Reference, ReferenceType, Authority
+
+class Command(BaseCommand):
+
+    help = 'Importe dans la base les données du yml'
+
+    def add_arguments(self, parser):
+        pass
+
+    def handle(self, *args, **options):
+
+
+        print('Removing all entries...')
+        Reference.objects.all().delete()
+        ReferenceType.objects.all().delete()
+        Authority.objects.all().delete()
+        print('Done')
+
+        raw_data = None
+
+        with open('../data.yml', 'r', encoding='utf-8') as stream:
+            raw_data = yaml.load(stream)
+
+        ref_list = list(raw_data.values())[0]
+
+        for ref in ref_list:
+
+            print('Parsing %s' % ref['id'])
+
+            reference = Reference()
+
+            if 'authority' in ref:
+                authority_slug = slugify(ref['authority'])
+
+                print('Searching authority : %s' % authority_slug)
+
+                authority_object = Authority.objects.filter(slug=authority_slug).first()
+
+                if authority_object is None:
+
+                    print('Not found, creating')
+
+                    authority_object = Authority(name=ref['authority'], slug=authority_slug)
+                    authority_object.save()
+
+                reference.authority = authority_object
+
+            if 'type' in ref and ref['type'] is not None:
+                type_slug = ref['type']
+
+                type_object = ReferenceType.objects.filter(slug=type_slug).first()
+
+                if type_object is None:
+                    type_name = type_slug.replace('_', ' ').title()
+                    print('Creating %s' % type_name)
+                    type_object = ReferenceType(name=type_name, slug=type_slug)
+                    type_object.save()
+
+                reference.reference_type = type_object
+
+            if 'id' in ref and ref['id'] is not None:
+                reference.identifier = ref['id']
+
+            if 'section' in ref and ref['section'] is not None:
+                reference.section = ref['section'].replace('^e^', 'ieme')
+
+            if 'title' in ref:
+                reference.title = ref['title']
+
+            if 'title-short' in ref:
+                reference.title_short = ref['title-short']
+
+            if 'number' in ref and ref['number']  is not None:
+                reference.number = ref['number'].replace('^o^', '°')
+
+            if 'ECLI' in ref and ref['ECLI'] is not None:
+                reference.ecli = ref['ECLI']
+
+            if 'URL' in ref and ref['URL'] is not None:
+                reference.url = ref['URL']
+
+            if 'comments' in ref and ref['comments'] is not None:
+                reference.comments = ref['comments']
+
+            if 'issued' in ref and ref['issued']['year'] is not None:
+
+                year = ref['issued']['year']
+                month = ref['issued']['month']
+                day = ref['issued']['day']
+
+                if isinstance(day, str):
+                    day = int(day, base=10)
+
+                if isinstance(month, str):
+                    month = int(month, base=10)
+
+                if isinstance(year, str):
+                    year = int(year, base=10)
+
+                reference.issued_date = datetime(year, month, day)
+
+            reference.save()
+            print('---------')
+
+

+ 52 - 0
speed_rack/backoffice/migrations/0001_initial.py

@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-15 00:36
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Authority',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=200)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Reference',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('identifier', models.CharField(max_length=200)),
+                ('number', models.CharField(max_length=200)),
+                ('title', models.TextField()),
+                ('title_short', models.TextField()),
+                ('section', models.CharField(max_length=200)),
+                ('ecli', models.CharField(max_length=200)),
+                ('url', models.URLField()),
+                ('comments', models.TextField()),
+                ('issued_date', models.DateTimeField(verbose_name='date published')),
+                ('authority', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backoffice.Authority')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ReferenceType',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=200)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='reference',
+            name='reference_type',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backoffice.ReferenceType'),
+        ),
+    ]

+ 42 - 0
speed_rack/backoffice/migrations/0002_auto_20170816_1355.py

@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 11:55
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='authority',
+            name='slug',
+            field=models.SlugField(default=' '),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='referencetype',
+            name='slug',
+            field=models.SlugField(default=' '),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='authority',
+            name='name',
+            field=models.CharField(max_length=200, verbose_name="L'instance qui à produit cette référence"),
+        ),
+        migrations.AlterField(
+            model_name='reference',
+            name='title_short',
+            field=models.TextField(verbose_name='Titre affiché dans la liste'),
+        ),
+        migrations.AlterField(
+            model_name='referencetype',
+            name='name',
+            field=models.CharField(max_length=200, verbose_name='Typê de référence'),
+        ),
+    ]

+ 46 - 0
speed_rack/backoffice/migrations/0003_auto_20170816_2143.py

@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 19:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0002_auto_20170816_1355'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='authority',
+            name='name',
+            field=models.CharField(max_length=200),
+        ),
+        migrations.AlterField(
+            model_name='authority',
+            name='slug',
+            field=models.SlugField(unique=True),
+        ),
+        migrations.AlterField(
+            model_name='reference',
+            name='authority',
+            field=models.ForeignKey(help_text="L'instance qui à produit cette référence", on_delete=django.db.models.deletion.CASCADE, to='backoffice.Authority'),
+        ),
+        migrations.AlterField(
+            model_name='reference',
+            name='title_short',
+            field=models.TextField(help_text='Titre court'),
+        ),
+        migrations.AlterField(
+            model_name='referencetype',
+            name='name',
+            field=models.CharField(max_length=200, verbose_name='Type de référence'),
+        ),
+        migrations.AlterField(
+            model_name='referencetype',
+            name='slug',
+            field=models.SlugField(unique=True),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0004_auto_20170816_2144.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 19:44
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0003_auto_20170816_2143'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='ecli',
+            field=models.CharField(blank=True, max_length=200),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0005_auto_20170816_2201.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:01
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0004_auto_20170816_2144'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0006_auto_20170816_2216.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:16
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0005_auto_20170816_2201'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='url',
+            field=models.URLField(blank=True),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0007_auto_20170816_2235.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:35
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0006_auto_20170816_2216'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='identifier',
+            field=models.CharField(blank=True, max_length=200),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0008_auto_20170816_2243.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0007_auto_20170816_2235'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='issued_date',
+            field=models.DateTimeField(blank=True, verbose_name='date published'),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0009_auto_20170816_2248.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:48
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0008_auto_20170816_2243'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='issued_date',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='date published'),
+        ),
+    ]

+ 21 - 0
speed_rack/backoffice/migrations/0010_auto_20170816_2249.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0009_auto_20170816_2248'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='authority',
+            field=models.ForeignKey(blank=True, help_text="L'instance qui à produit cette référence", on_delete=django.db.models.deletion.CASCADE, to='backoffice.Authority'),
+        ),
+    ]

+ 21 - 0
speed_rack/backoffice/migrations/0011_auto_20170816_2249.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-08-16 20:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0010_auto_20170816_2249'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='reference',
+            name='authority',
+            field=models.ForeignKey(blank=True, help_text="L'instance qui à produit cette référence", null=True, on_delete=django.db.models.deletion.CASCADE, to='backoffice.Authority'),
+        ),
+    ]

+ 20 - 0
speed_rack/backoffice/migrations/0012_reference_disabled.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-08-17 06:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0011_auto_20170816_2249'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='reference',
+            name='disabled',
+            field=models.BooleanField(default=False),
+        ),
+    ]

+ 37 - 0
speed_rack/backoffice/migrations/0013_auto_20170827_1614.py

@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2017-08-27 14:14
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('backoffice', '0012_reference_disabled'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='authority',
+            options={'ordering': ['name'], 'verbose_name': 'Authorité'},
+        ),
+        migrations.AlterModelOptions(
+            name='reference',
+            options={'ordering': ['issued_date'], 'verbose_name': 'Référence'},
+        ),
+        migrations.AlterModelOptions(
+            name='referencetype',
+            options={'ordering': ['name'], 'verbose_name': 'Type de référence'},
+        ),
+        migrations.AlterField(
+            model_name='reference',
+            name='issued_date',
+            field=models.DateField(blank=True, null=True, verbose_name='date published'),
+        ),
+        migrations.AlterField(
+            model_name='reference',
+            name='number',
+            field=models.CharField(blank=True, max_length=200),
+        ),
+    ]

+ 0 - 0
speed_rack/backoffice/migrations/__init__.py


+ 60 - 0
speed_rack/backoffice/models.py

@@ -0,0 +1,60 @@
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
+from django.db import models
+
+
+class AdminURLMixin(object):
+    def get_admin_url(self):
+        content_type = ContentType.objects.get_for_model(self.__class__)
+        return reverse("admin:%s_%s_change" % (
+            content_type.app_label,
+            content_type.model),
+            args=(self.id,))
+
+class Authority(models.Model, AdminURLMixin):
+
+    class Meta:
+        ordering = ['name']
+        verbose_name = 'Authorité'
+
+    name = models.CharField(max_length=200)
+    slug = models.SlugField(unique=True)
+
+    def __str__(self):
+        return self.name
+
+class ReferenceType(models.Model, AdminURLMixin):
+
+    class Meta:
+        ordering = ['name']
+        verbose_name = 'Type de référence'
+
+    name = models.CharField(max_length=200, verbose_name='Type de référence')
+    slug = models.SlugField(unique=True)
+
+    def __str__(self):
+        return self.name
+
+class Reference(models.Model, AdminURLMixin):
+
+    class Meta:
+        ordering = ['issued_date']
+        verbose_name = 'Référence'
+
+    identifier = models.CharField(max_length=200, blank=True, verbose_name='Identifiant')
+    number = models.CharField(max_length=200, blank=True, verbose_name='Numéro')
+    authority = models.ForeignKey('Authority', null=True, help_text='L\'instance qui à produit cette référence', blank=True)
+    reference_type = models.ForeignKey('ReferenceType', verbose_name='Type')
+    title = models.TextField(verbose_name='Titre')
+    title_short = models.TextField(verbose_name='Titre court')
+    section = models.CharField(max_length=200, verbose_name='Section')
+    ecli = models.CharField(max_length=200, blank=True, verbose_name='E.C.L.I.')
+    url = models.URLField(max_length=200, blank=True)
+    comments = models.TextField(blank=True, verbose_name='Commentaires')
+    issued_date = models.DateField(null=True, blank=True, verbose_name='Date de publication')
+    disabled = models.BooleanField(default=False, verbose_name='Désactivé', help_text='Masque cette entrée lors de recherches')
+
+    def __str__(self):
+        return self.identifier + ' > ' + self.title_short
+
+

+ 36 - 0
speed_rack/backoffice/serializers.py

@@ -0,0 +1,36 @@
+from rest_framework import serializers
+
+from backoffice.models import Reference, Authority, ReferenceType
+
+class ReferenceSerializer(serializers.ModelSerializer):
+
+    admin_link = serializers.SerializerMethodField()
+
+    def get_admin_link(self, reference):
+        return reference.get_admin_url()
+
+    class Meta:
+        model = Reference
+        exclude = ('disabled',)
+        depth = 1
+
+class LightReferenceSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = Reference
+        fields = ('id', 'identifier', 'number', 'authority', 'reference_type',
+                  'title', 'title_short', 'ecli', 'section', 'issued_date')
+        depth = 1
+
+
+class AuthoritySerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = Authority
+        fields = '__all__'
+
+class ReferenceTypeSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = ReferenceType
+        fields = '__all__'

+ 22 - 0
speed_rack/manage.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "speed_rack.settings")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError:
+        # The above import may fail for some other reason. Ensure that the
+        # issue is really that Django is missing to avoid masking other
+        # exceptions on Python 2.
+        try:
+            import django
+        except ImportError:
+            raise ImportError(
+                "Couldn't import Django. Are you sure it's installed and "
+                "available on your PYTHONPATH environment variable? Did you "
+                "forget to activate a virtual environment?"
+            )
+        raise
+    execute_from_command_line(sys.argv)

+ 0 - 0
speed_rack/speed_rack/__init__.py


+ 123 - 0
speed_rack/speed_rack/settings.py

@@ -0,0 +1,123 @@
+"""
+Django settings for speed_rack project.
+
+Generated by 'django-admin startproject' using Django 1.11.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.11/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '-lqwwip#2#)_k)bmg^9%!mfb6kqtfn@r%mu1q8ceoc@ecj653#'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'rest_framework',
+    'backoffice.apps.BackofficeConfig',
+    'webapp.apps.WebappConfig',
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'speed_rack.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'speed_rack.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.11/topics/i18n/
+
+LANGUAGE_CODE = 'fr-fr'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.11/howto/static-files/
+
+STATIC_URL = '/static/'

+ 8 - 0
speed_rack/speed_rack/urls.py

@@ -0,0 +1,8 @@
+
+from django.conf.urls import url, include
+from django.contrib import admin
+
+urlpatterns = [
+    url(r'^admin/', admin.site.urls),
+    url(r'', include('webapp.urls')),
+]

+ 16 - 0
speed_rack/speed_rack/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for speed_rack project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "speed_rack.settings")
+
+application = get_wsgi_application()

+ 0 - 0
speed_rack/webapp/__init__.py


+ 5 - 0
speed_rack/webapp/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class WebappConfig(AppConfig):
+    name = 'webapp'

+ 0 - 0
speed_rack/webapp/migrations/__init__.py


+ 409 - 0
speed_rack/webapp/static/webapp/css/style.css

@@ -0,0 +1,409 @@
+
+* {
+    box-sizing: border-box;
+    font-family: Roboto;
+}
+
+html,
+body {
+    width: 100%;
+    height: 100%;
+    background-color: #F9F9F9;
+    color: rgba(0, 0, 0, .87);
+    font-size: 12px;
+    font-family: Roboto;
+    margin: 0;
+    padding: 0;
+    overflow-y: hidden;
+}
+
+
+header {
+    height: 64px;
+    border: 0px solid rgba(0, 0, 0, .12);
+    border-bottom-width: 1px;
+    padding-left: 32px;
+    line-height: 64px;
+    font-size: 28px;
+}
+
+footer {
+    height: 32px;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    text-align: right;
+    padding-right: 16px;
+    line-height: 32px;
+    background-color: #FFFFFF;
+    box-shadow: 0px -4px 17px 0px rgba(0,0,0,.12);
+}
+
+#search-form {
+    height: 170px;
+    width: 100%;
+    padding: 8px 16px;
+
+    display: flex;
+    justify-content: flex-start;
+    align-content: flex-start;
+    align-items: flex-start;
+}
+
+#result-block {
+    border: 0px solid rgba(0, 0, 0, .12);
+    border-top-width: 1px;
+    width: 100%;
+    height: calc(100% - (64px + 128px + 32px));
+    padding-bottom: 32px;
+    overflow-y: scroll;
+}
+
+#result-table {
+    width: 100%;
+    font-size: 13px;
+    border-collapse: collapse;
+    table-layout: fixed;
+}
+
+#result-table th,
+#result-table td {
+    vertical-align: middle;
+    padding-left: 12px;
+    padding-right: 12px;
+    text-align: left;
+    padding-top: 0;
+    padding-bottom: 0;
+}
+
+#result-table th.numeric-value,
+#result-table td.numeric-value {
+    text-align: right;
+}
+
+#result-table tr {
+    border: 0px solid rgba(0, 0, 0, .12);
+    border-bottom-width: 1px;
+}
+
+#result-table thead {
+    font-size: 12px;
+    color: rgba(0, 0, 0, .54);
+}
+
+#result-table thead tr {
+    height: 64px;
+    cursor: default;
+    border-bottom: solid 1px #8bc34a;
+}
+
+#result-table tbody tr {
+    height: 48px;
+    max-height: 48px;
+    transition: background-color .2s ease-out;
+    cursor: pointer;
+    font-size: 13px;
+}
+
+#result-table tbody tr:hover {
+    background-color: rgba(0, 0, 0, .12)
+}
+
+.input-block {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    align-content: flex-start;
+    align-items: flex-start;
+    width: 280px;
+    height: 100%;
+    padding-bottom: 8px;
+}
+
+.input-block .block-label {
+    width: 50%;
+}
+
+.input-block label {
+    margin-top: 4px;
+    width: 100px;
+    font-size: 11px;
+    color: rgba(0, 0, 0, .54);
+    margin-right: 32px;
+}
+
+.input-block input {
+    margin-top: 4px;
+    margin-bottom: 8px;
+    padding-bottom: 8px;
+    border: none;
+    border-bottom: 1px solid rgba(0, 0, 0, .42);
+    background: none;
+    width: 100px;
+    font-size: 13px;
+    color: rgba(0, 0, 0, .87);
+    transition: border-color .2s ease-out;
+}
+
+.input-block input:focus {
+    border-color: #8bc34a;
+}
+
+.authority-block {
+    width: 400px;
+}
+
+.type-block {
+    width: 300px;
+}
+
+.authority-block,
+.type-block {
+    padding-bottom: 8px;
+
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    align-items: center;
+    align-content: center;
+}
+
+.authority-block label,
+.type-block label {
+    width: 100px;
+    margin-bottom: 8px;
+    margin-right: 8px;
+}
+
+.authority-block input,
+.type-block input {
+    margin-bottom: 8px;
+    margin-right: 8px;
+}
+
+.authority-block input:hover,
+.type-block input:hover {
+    border-color: #8bc34a;
+    color: #8bc34a;
+}
+
+.block-label {
+    font-weight: bold;
+    width: 100%;
+    margin-bottom: 8px;
+}
+
+.dialog-ref {
+    position: fixed;
+    margin-left: auto;
+    margin-right: auto;
+    width: 700px;
+    top: 50%;
+    left: 50%;
+    transform: perspective(1px) translateY(-50%) translateX(-50%);
+    background-color: #FFF;
+
+    box-shadow: 0 4px 5px 7px rgba(0, 0, 0, .25);
+
+    border-radius: 2px;
+    padding: 24px;
+}
+
+.dialog-ref .close-button  {
+    cursor: pointer;
+}
+
+.dialog-ref .dialog-header {
+    padding-bottom: 20px;
+    font-size: 20px;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    justify-content: flex-start;
+    align-content: center;
+    align-items: center;
+}
+
+.dialog-ref .ref-title {
+    line-height: 28px;
+    font-weight: bold;
+    padding-left: 20px;
+}
+
+.dialog-ref .edit-ref-btn {
+    margin-left: auto;
+    margin-right: 0;
+
+    color: rgba(0, 0, 0, .87);
+    text-decoration: none;
+    cursor: pointer;
+    font-weight: normal;
+
+    transition: color .2s ease-out;
+}
+
+.dialog-ref .edit-ref-btn:hover {
+    color: #8bc34a;
+}
+
+.dialog-ref .ref-field {
+    height: 36px;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    justify-content: flex-start;
+    align-content: center;
+    align-items: baseline;
+
+    margin-top: 8px;
+    margin-bottom: 8px;
+}
+
+.dialog-ref .ref-field label {
+    width: 68px;
+    padding-bottom: 8px;
+    text-align: right;
+    font-size: 13px;
+    font-weight: bold;
+    margin-right: 8px;
+}
+
+.dialog-ref .ref-field input[type="text"] {
+    margin-top: 4px;
+    margin-bottom: 8px;
+    border: none;
+    border-bottom: 1px solid rgba(0, 0, 0, .42);
+    background: none;
+    color: rgba(0, 0, 0, .87);
+    width: 234px;
+}
+
+#ref-title {
+    width: 440px;
+    margin-right: auto;
+}
+
+#ref-issued_date {
+    width: 75px;
+    text-align: center;
+    margin-right: 0;
+}
+
+#ref-label-issued_date {
+    width: 32px;
+}
+
+#ref-label-issued_date {
+    align-self: flex-start;
+}
+
+#ref-identifier,
+#ref-number {
+    margin-right: 32px;
+}
+
+.ref-comment-block {
+    border: solid 1px rgba(0, 0, 0, .42);
+    border-radius: 4px;
+
+    padding: 8px;
+}
+
+.ref-comment-block textarea {
+    background: none;
+    border: none;
+    padding: 0;
+    margin-top: 4px;
+    width: 100%;
+    resize: none;
+}
+
+.ref-field .link-icon {
+    align-items: flex-start;
+    margin-right: 8px;
+}
+
+.ref-field.link-field {
+    height: 36px;
+    align-items: center;
+}
+
+.link-field a {
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    line-height: 36px;
+    height: 36px;
+}
+
+.id-col, .number-col, .ecli-col, .ref-col,
+.auth-col, .section-col, .date-col, .id-col {
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow-x: hidden;
+}
+
+.id-col {
+    width: 170px;
+}
+
+.number-col {
+    width: 150px;
+}
+
+.ecli-col {
+    width: 200px;
+}
+
+.ref-col {
+    width: 100px;
+}
+
+.auth-col {
+    width: 150px;
+}
+
+.section-col {
+    width: 100px;
+}
+
+.date-col {
+    width: 100px;
+}
+
+#export-button {
+
+    line-height: 36px;
+    height: 36px;
+    margin: 8px;
+
+    text-transform: uppercase;
+
+    background: none;
+
+    border: none;
+    border-radius: 2px;
+
+    font-size: 14px;
+    padding-left: 8px;
+    padding-right: 8px;
+
+
+    box-shadow: none;
+
+    transition: box-shadow .2s ease-out,
+                color .2s ease-out;
+
+    cursor: pointer;
+}
+
+#export-button:hover {
+    color: #8bc34a;
+    /*box-shadow: 0 2px 4px 3px rgba(0, 0, 0, .25);*/
+}
+
+#export-button:active {
+    /*box-shadow: 0 3px 7px 3px rgba(0, 0, 0, .25);*/
+}

+ 141 - 0
speed_rack/webapp/static/webapp/js/http-request.js

@@ -0,0 +1,141 @@
+'use strict';
+
+function HttpRequest(config) {
+
+    /**
+     * L'objet XMLHttpRequest
+     */
+    this.xhr = new XMLHttpRequest();
+
+
+    var defaultConfig = {method: 'GET',
+                          debug: false,
+                           data: {},
+                          async: true,
+                   cacheProtect: false};
+
+    for (var attr in config) {
+        defaultConfig[attr] = config[attr];
+    }
+
+    this.config = defaultConfig;
+
+    this.doneCallBack = [];
+    this.errorCallBack = [];
+
+    this.headersList = {};
+
+    /**
+     * Success callback
+     */
+    this.done = function (callback) {
+        this.doneCallBack.push(callback);
+        return this;
+    };
+
+    /**
+     * Fail callback
+     */
+    this.error = function(callback) {
+        this.errorCallBack.push(callback);
+        return this;
+    };
+
+    /**
+     * Permet la définition des headers
+     */
+    this.headers = function (h) {
+        this.headersList = h;
+        return this;
+    };
+
+    this.data = function (data) {
+        this.config.data = data;
+    }
+
+    /**
+     * Fonction d'envoi de la requête
+     */
+    this.send = function () {
+
+        this.config.method = this.config.method.toUpperCase();
+
+        if (this.config.cacheProtect) {
+            var preChar = '?';
+            if (this.config.url.indexOf('?') !== -1) {
+                preChar = '&';
+            }
+
+            this.config.url += preChar + 'cacheProtect=' + Date.now;
+        }
+
+        this.xhr.open(this.config.method, this.config.url, this.config.async);
+
+        for (var h in this.headersList) {
+            this.xhr.setRequestHeader(h, this.headersList[h]);
+        }
+
+        this.xhr.onreadystatechange = this.onReadyStateChangeCallback;
+
+        if (this.config.method != 'GET') {
+
+            var encapsuledData = new FormData();
+
+            for (var prop in config.data) {
+                encapsuledData.append(prop, config.data[prop]);
+            }
+
+            this.xhr.send(encapsuledData);
+        } else {
+            this.xhr.send();
+        }
+    };
+
+    /**
+     * Traitement des status sup à 400
+     */
+    this.errorProcess = (function () {
+
+        if (this.config.debug) {
+            var win = window.open('', this.xhr.statusText);
+            var target = win.document;
+            target.open();
+            target.write(this.xhr.responseText);
+            target.close();
+        }
+
+        var data = this.xhr.responseText;
+
+        this.errorCallBack.forEach(function (elmnt) {
+            elmnt(data, this.xhr.status, this.xhr);
+        }.bind(this));
+    }).bind(this);
+
+    /**
+     * Traitement des status entre 200 et 400
+     */
+    this.doneProcess = (function () {
+
+        var data = this.xhr.responseText;
+
+        if (this.xhr.getResponseHeader('Content-Type').indexOf('application/json') !== -1) {
+            data = JSON.parse(data);
+        }
+        var self = this;
+        this.doneCallBack.forEach(function (elmnt) {
+            elmnt(data, self.xhr.status, self.xhr);
+        });
+    }).bind(this);
+
+    this.onReadyStateChangeCallback = (function () {
+        if (this.xhr.readyState == 4) {
+            if (this.xhr.status < 400) {
+                this.doneProcess();
+            } else if (this.xhr.status >= 400) {
+                this.errorProcess();
+            }
+        }
+    }).bind(this);
+
+    return this;
+}

+ 149 - 0
speed_rack/webapp/static/webapp/js/main.js

@@ -0,0 +1,149 @@
+'use strict';
+
+document.addEventListener('DOMContentLoaded', function() {
+
+    var input_list = document.querySelectorAll('.search-field');
+
+    for (var elem of input_list) {
+        if (elem.getAttribute('type') == 'checkbox') {
+            elem.addEventListener('change', searchReference);
+        } else {
+            elem.addEventListener('input', searchReference);
+        }
+    }
+
+    var refs_list = document.querySelectorAll('.ref-line');
+
+    for (var elem of refs_list) {
+        elem.addEventListener('click', clickEventRef);
+    }
+ });
+
+
+function searchReference() {
+    var param = extractQueryParam();
+
+    var search_json = JSON.stringify(param);
+
+    document.getElementById('export-payload').value = search_json;
+
+    var request = new HttpRequest({
+        'url': '/search',
+        'method': 'POST',
+        'data': {
+            'payload': JSON.stringify(param)
+        }
+    });
+
+    request.done(updateResultData).send();
+}
+
+function updateResultData(data) {
+
+    var resultTable = document.querySelector('#result-table tbody');
+
+    while (resultTable.firstChild) {
+        resultTable.removeChild(resultTable.firstChild);
+    }
+
+    data.forEach(function (ref) {
+
+        if (ref.authority !== null) {
+            ref.authority = ref.authority.name;
+        }
+
+        if (ref.reference_type !== null) {
+            ref.reference_type = ref.reference_type.name;
+        }
+
+
+        ref.issued_date = formatDate(ref.issued_date)
+
+        var newNode = Template.render('ref-line-template', ref);
+        resultTable.appendChild(newNode);
+        newNode.addEventListener('click', clickEventRef);
+    });
+}
+
+/**
+ * Extrai du DOM les param pour la recherche
+ * @return {[type]} [description]
+ */
+function extractQueryParam() {
+
+    var param = {};
+
+    param.numero = document.getElementById('numero_id').value;
+    param.title = document.getElementById('titre').value;
+    param.dateAfter = document.getElementById('date-after').value;
+    param.dateBefore = document.getElementById('date-before').value;
+
+    param.authorities = [];
+
+    var authorities_list = document.querySelectorAll('.authority-block input');
+
+    for (var elem of authorities_list) {
+        if (elem.checked) {
+            param.authorities.push(elem.getAttribute('data-authority-slug'))
+        }
+    }
+
+    param.types = [];
+
+    var types_list = document.querySelectorAll('.type-block input');
+
+    for (var elem of types_list) {
+        if (elem.checked) {
+            param.types.push(elem.getAttribute('data-type-slug'))
+        }
+    }
+
+    return param;
+}
+
+function clickEventRef(evnt) {
+    var node = evnt.currentTarget;
+
+    var ref_id = node.getAttribute('data-ref-id');
+
+    var request = new HttpRequest({
+        'url': '/find-' + ref_id,
+        'method': 'GET',
+    });
+
+    request.done(displayRefDialog).send();
+}
+
+function displayRefDialog(data) {
+
+    // On supprime l'ancienne
+    removeDialog();
+
+    data.issued_date = formatDate(data.issued_date)
+
+    // Puis on créé la nouvelle
+    var newNode = Template.render('ref-dialog-template', data);
+    document.getElementsByTagName('body')[0].appendChild(newNode);
+
+    newNode.querySelector('.close-button').addEventListener('click', removeDialog);
+}
+
+
+function removeDialog() {
+    var dialogNode = document.querySelector('.dialog-ref');
+
+    if (dialogNode != null) {
+        dialogNode.parentNode.removeChild(dialogNode);
+    }
+}
+
+
+function formatDate(date_str) {
+    if (typeof date_str == 'undefined' || date_str == null || date_str == 'null') {
+        return '';
+    }
+
+    var date_array = date_str.split('-');
+
+    return date_array.reverse().join('/');
+}

+ 101 - 0
speed_rack/webapp/static/webapp/js/template-rendering.js

@@ -0,0 +1,101 @@
+'use strict';
+
+/**
+ * "Moteur" de template minimaliste le but est de simplifié la créatoin
+ * de "composant" pour par exemple coller à des données récupéré en ajax
+ * Pas fait pour gérer des gros templates,
+ * pas de compilation, pas de fonction.
+ * Dans un premier temps les variables et ensuite du code libre, gestion d'une
+ * valeur en cas d'abscence
+ */
+
+/**
+ * Minimalistic "template engine".
+ * The goal is to simplify "composant" creation, for exemple when use
+ * ajax data to create DOM Element.
+ * Not for big templates
+ * No compilation, no functions.
+ * Only access to passed variables and not found value.
+ */
+function Template() {
+
+}
+
+Template.config = {
+    notFoundReplacment: '',
+    htmlTrim: true
+
+};
+
+Template.config.templatesSelector = function() {
+    return document.querySelectorAll('template');
+};
+
+Template.variableRegex = /{{\s*([a-zA-Z][\w-\.]*)\s*}}/g;
+
+Template.render = function (template, datas) {
+
+    // Accept a Node or a Node Id
+    if (typeof template === 'string') {
+        template = document.getElementById(template);
+    }
+
+    if (template === null) {
+        return null;
+    }
+
+    var result = template.innerHTML;
+
+    // Remove useless withespace who can make style issues
+    if (Template.config.htmlTrim) {
+        result = result.trim().replace(/>(\s+)</g, '><');
+        result = result.trim().replace(/>(\s+){/g, '>{');
+        result = result.trim().replace(/}(\s+)</g, '}<');
+    }
+
+    var variable;
+    while ((variable = Template.variableRegex.exec(template.innerHTML)) !== null) {
+        var property = '';
+        // Eval make eaysier the access to sub property
+        eval('property = datas.' + variable[1]);
+        if ( property === undefined  &&
+                    Template.config.notFoundReplacment !== null) {
+            result = result.replace(variable[0],
+                                         Template.config.notFoundReplacment);
+        } else {
+            result = result.replace(variable[0], property);
+        }
+    }
+
+    return Template.strToDOM(result);
+};
+
+// Not the worst way to turn a string in DOM Element
+Template.strToDOM = function (s) {
+
+    var parentElement = 'div';
+
+    if (s.startsWith('<tr')) {
+        parentElement = 'table'
+    }
+
+    var  d=document
+        ,i
+        ,a=d.createElement(parentElement)
+        ,b=d.createElement(parentElement);
+    a.innerHTML=s;
+    while(i=a.firstChild)b.appendChild(i);
+
+    if (b.tagName.toLowerCase() == 'table') {
+        return b.firstChild.firstChild;
+    }
+
+    return b.firstChild;
+};
+
+document.addEventListener('DOMContentLoaded', function() {
+    // Hide know templates
+    for (var template of Template.config.templatesSelector()) {
+        //template.setAttribute('hidden', '');
+    }
+});

+ 210 - 0
speed_rack/webapp/templates/webapp/index.html

@@ -0,0 +1,210 @@
+{% load static %}
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Speed Rack</title>
+    <meta charset="utf-8">
+    <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
+    <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      rel="stylesheet">
+    <link rel="stylesheet" type="text/css" href="{% static 'webapp/css/style.css' %}">
+</head>
+<body>
+    <header>
+        Speed Rack
+    </header>
+    <div id="search-form">
+        <div class="input-block">
+            <span class="block-label">Publication</span>
+            <label for="numero_id">Numéro / Id / ECLI :</label>
+            <input type="text" class="search-field" name="numero_id" id="numero_id" placeholder="Recherche de référence">
+            <label for="titre">Titre :</label>
+            <input type="text" class="search-field" name="titre" id="titre" placeholder="Titre contenant...">
+            <span class="block-label">&nbsp;</span>
+            <label for="date-after">Depuis le :</label>
+            <input type="date" class="search-field" name="date-after" id="date-after" placeholder="jj/mm/aaaa">
+            <label for="date-before">Avant le :</label>
+            <input type="date" class="search-field" name="date-before" id="date-before" placeholder="jj/mm/aaaa">
+        </div>
+        <div class="authority-block">
+            <span class="block-label">Authorité</span>
+            {% for authority in authorities %}
+                <input type="checkbox" class="search-field" name="{{ authority.slug }}-authority" id="{{ authority.slug }}-authority" data-authority-slug="{{ authority.slug }}" checked="checked">
+                <label for="{{ authority.slug }}-authority">
+                    {{ authority.name }}
+                </label>
+            {% endfor %}
+        </div>
+        <div class="type-block">
+            <span class="block-label">Type de référence</span>
+            {% for type in ref_types %}
+                <input type="checkbox" class="search-field" name="{{ type.slug }}-type" id="{{ type.slug }}-type" data-type-slug="{{ type.slug }}" checked="checked">
+                <label for="{{ type.slug }}-type">
+                    {{ type.name }}
+                </label>
+            {% endfor %}
+        </div>
+        <div class="export-block">
+            <form method="POST" action="/export">
+                <button id="export-button" title="Exporte la selection au format YML">
+                    EXPORTER
+                </button>
+                <input type="hidden" name="payload" id="export-payload">
+            </form>
+        </div>
+    </div>
+    <div id="result-block">
+        <table id="result-table">
+            <thead>
+                <tr>
+                    <th class="id-col">
+                        Id
+                    </th>
+                    <th class="number-col">
+                        Numéro
+                    </th>
+                    <th title="Identifiant europeen de la jurisprudence" class="ecli-col">
+                        ECLI
+                    </th>
+                    <th class="ref-col">
+                        Type
+                    </th>
+                    <th class="auth-col">
+                        Autoritée
+                    </th>
+                    <th class="section-col">
+                        Section
+                    </th>
+                    <th class="date-col">
+                        Date
+                    </th>
+                    <th>
+                        Titre
+                    </th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for ref in references %}
+                    <tr class="ref-line" data-ref-id="{{ ref.id }}">
+                        <td class="id-col">
+                            {{ ref.identifier }}
+                        </td>
+                        <td class="number-col" title="{{ ref.number }}">
+                            {{ ref.number }}
+                        </td>
+                        <td class="ecli-col" title="{{ ref.ecli }}">
+                            {{ ref.ecli }}
+                        </td>
+                        <td class="ref-col">
+                            {{ ref.reference_type.name }}
+                        </td>
+                        <td class="auth-col">
+                            {{ ref.authority.name }}
+                        </td>
+                        <td class="section-col">
+                            {{ ref.section }}
+                        </td>
+                        <td class="date-col">
+                            {{ ref.issued_date|date:"d/m/Y" }}
+                        </td>
+                        <td title="{{ ref.title }}">
+                            {{ ref.title_short }}
+                        </td>
+                    </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+    <footer>
+        &copy; Les exégètes amateurs - French Data Network (FDN), La Quadrature du Net (LQDN) et la fédération des fournisseurs d'accès à Internet associatifs (Fédération FDN)
+    </footer>
+    {% verbatim %}
+        <template id="ref-line-template">
+            <tr class="ref-line" data-ref-id="{{ id }}">
+                <td class="id-col">
+                    {{ identifier }}
+                </td>
+                <td class="number-col" title="{{ number }}">
+                    {{ number }}
+                </td>
+                <td class="ecli-col" title="{{ ecli }}">
+                    {{ ecli }}
+                </td>
+                <td  class="ref-col">
+                    {{ reference_type }}
+                </td>
+                <td class="auth-col">
+                    {{ authority }}
+                </td>
+                <td class="section-col">
+                    {{ section }}
+                </td>
+                <td class="date-col">
+                    {{ issued_date }}
+                </td>
+                <td title="{{ title }}">
+                    {{ title_short }}
+                </td>
+            </tr>
+        </template>
+        <template id="ref-dialog-template">
+            <div class="dialog-ref">
+                <div class="dialog-header">
+                    <i class="material-icons close-button">close</i>
+                    <span class="ref-title">
+                        {{ title_short }}
+                    </span>
+                    <a class="edit-ref-btn" href="{{ admin_link }}" target="_blank">
+                        EDITER
+                    </a>
+                </div>
+                <div class="ref-field">
+                    <label for="ref-title">
+                        Titre :
+                    </label>
+                    <input type="text" id="ref-title" disabled="disabled" value="{{ title }}">
+                    <label for="ref-issued_date" id="ref-label-issued_date">
+                        <i class="material-icons">event</i>
+                    </label>
+                    <input type="text" id="ref-issued_date" disabled="disabled" value="{{ issued_date }}">
+                </div>
+                <div class="ref-field">
+                    <label for="ref-identifier">
+                        Identifiant :
+                    </label>
+                    <input type="text" id="ref-identifier" disabled="disabled" value="{{ identifier }}">
+                    <label for="ref-ecli">
+                        ECLI :
+                    </label>
+                    <input type="text" id="ref-ecli" disabled="disabled" value="{{ ecli }}">
+                </div>
+                <div class="ref-field">
+                    <label for="ref-number">
+                        Numéro :
+                    </label>
+                    <input type="text" id="ref-number" disabled="disabled" value="{{ number }}">
+                    <label for="ref-number">
+                        Section :
+                    </label>
+                    <input type="text" id="ref-section" disabled="disabled" value="{{ section }}">
+                </div>
+                <div class="ref-field link-field">
+                    <i class="material-icons link-icon">link</i>
+                    <a href="{{ url }}" alt="Lien vers la référence" target="_blank">
+                        {{ url }}
+                    </a>
+                </div>
+                <div class="ref-comment-block">
+                    <label>
+                        Commentaire :
+                    </label>
+                    <textarea disabled="disabled">{{ comments }}</textarea>
+                </div>
+            </div>
+        </template>
+    {% endverbatim %}
+    <script type="text/javascript" src="{% static 'webapp/js/http-request.js' %}"></script>
+    <script type="text/javascript" src="{% static 'webapp/js/template-rendering.js' %}"></script>
+    <script type="text/javascript" src="{% static 'webapp/js/main.js' %}"></script>
+</body>
+</html>

+ 10 - 0
speed_rack/webapp/urls.py

@@ -0,0 +1,10 @@
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(r'^search$', views.search, name='search'),
+    url(r'^find-(?P<id>[0-9]+)$', views.find, name='find'),
+    url(r'^export$', views.export, name='export'),
+    url(r'^$', views.index, name='index'),
+]

+ 112 - 0
speed_rack/webapp/views.py

@@ -0,0 +1,112 @@
+import json
+import yaml
+from datetime import datetime
+
+from django.shortcuts import render
+from django.db.models import Q
+from django.http import JsonResponse, HttpResponse
+from django.views.decorators.csrf import csrf_exempt
+
+from backoffice.models import Authority, ReferenceType, Reference
+from backoffice.serializers import ReferenceSerializer, LightReferenceSerializer
+
+
+def index(request):
+    authorities = Authority.objects.all()
+    types = ReferenceType.objects.all()
+
+    references = Reference.objects.all()
+
+    context = {
+        'authorities': authorities,
+        'ref_types': types,
+        'references': references
+    }
+
+    return render(request, 'webapp/index.html', context)
+
+@csrf_exempt
+def search(request):
+
+    search_param = json.loads(request.POST['payload'])
+
+    query_set = generate_search_querybuilder(search_param)
+
+    serializer = LightReferenceSerializer(query_set, many=True)
+
+    return JsonResponse(serializer.data, safe=False)
+
+@csrf_exempt
+def find(request, id):
+
+    ref = Reference.objects.get(pk=id)
+    serializer = ReferenceSerializer(ref)
+
+    return JsonResponse(serializer.data, safe=False)
+
+
+@csrf_exempt
+def export(request):
+
+    search_param = json.loads(request.POST['payload'])
+
+    query_set = generate_search_querybuilder(search_param)
+
+    formated_data = list()
+
+    for ref in query_set:
+        ref_data = dict()
+        ref_data['type'] = ref.reference_type.name
+        ref_data['id'] = ref.identifier
+        ref_data['authority'] = ref.authority.name
+        if ref.issued_date is not None:
+            ref_data['issued'] = dict()
+            ref_data['issued']['year'] = ref.issued_date.year
+            ref_data['issued']['month'] = ref.issued_date.month
+            ref_data['issued']['day'] = ref.issued_date.day
+        ref_data['title'] = ref.title
+        ref_data['title-short'] = ref.title_short
+        ref_data['number'] = ref.number
+        ref_data['ECLI'] = ref.ecli
+        ref_data['URL'] = ref.url
+        ref_data['comments'] = ref.comments
+
+        formated_data.append(ref_data)
+
+
+    yml_data = yaml.dump({'references': formated_data}, explicit_start=True, encoding='utf8')
+
+    response = HttpResponse(yml_data, content_type='text/yaml')
+    response['Content-Disposition'] = 'filename=export.yml'
+    return response
+
+def empty(dic, key):
+    return key not in dic or not dic[key]
+
+def generate_search_querybuilder(search_param):
+    query_set = Reference.objects.all().filter(disabled=False)
+
+    if not empty(search_param, 'title'):
+        query_set = query_set.filter(title_short__icontains=search_param['title'])
+
+    if not empty(search_param, 'numero'):
+        number = search_param['numero']
+        query_set = query_set.filter(Q(identifier__icontains=number) | Q(number__icontains=number) | Q(ecli__icontains=number))
+
+    if not empty(search_param, 'dateAfter'):
+        date_after = datetime.strptime(search_param['dateAfter'], '%d/%m/%Y')
+        query_set = query_set.filter(issued_date__gte=date_after)
+
+    if not empty(search_param, 'dateBefore'):
+        date_before = datetime.strptime(search_param['dateBefore'], '%d/%m/%Y')
+        query_set = query_set.filter(issued_date__lte=date_before)
+
+
+    query_set = query_set.filter(authority__slug__in=search_param['authorities'])
+
+    query_set = query_set.filter(reference_type__slug__in=search_param['types'])
+
+    return query_set
+
+
+