MASSOT Olivier преди 7 години
родител
ревизия
27dec18048
променени са 37 файла, в които са добавени 1479 реда и са изтрити 390 реда
  1. 1 1
      backlog/settings.py
  2. BIN
      db.sqlite3
  3. BIN
      db_.sqlite3
  4. 3 2
      main/admin.py
  5. 19 4
      main/forms.py
  6. 51 14
      main/migrations/0001_initial.py
  7. 0 51
      main/migrations/0002_auto_20180928_1106.py
  8. 0 97
      main/migrations/0003_auto_20181001_1721.py
  9. 0 17
      main/migrations/0004_auto_20181002_0939.py
  10. 103 13
      main/models.py
  11. 160 4
      main/static/css/custom.css
  12. 149 0
      main/static/images/weight_base.svg
  13. 25 4
      main/static/js/custom.js
  14. 25 21
      main/templates/_layout.html
  15. 75 0
      main/templates/backlog_editor.html
  16. 33 26
      main/templates/epic_details.html
  17. 8 0
      main/templates/epic_form.html
  18. 22 21
      main/templates/index.html
  19. 4 0
      main/templates/registration/change_password.html
  20. 4 0
      main/templates/registration/logged_out.html
  21. 4 0
      main/templates/registration/login.html
  22. 4 0
      main/templates/registration/register.html
  23. 0 0
      main/templates/reports/__init__.py
  24. 24 0
      main/templates/reports/report_index.html
  25. 52 0
      main/templates/reports/report_projects.html
  26. 50 0
      main/templates/reports/report_sprints.html
  27. 33 0
      main/templates/search_results.html
  28. 47 4
      main/templates/story_details.html
  29. 76 12
      main/templates/story_form.html
  30. 61 70
      main/templates/story_index.html
  31. 45 0
      main/templates/story_li.html
  32. 150 0
      main/templates/weight_svg.html
  33. 12 0
      main/urls.py
  34. 130 29
      main/views.py
  35. 72 0
      migration.py
  36. 1 0
      runserver.cmd
  37. 36 0
      todo.txt

+ 1 - 1
backlog/settings.py

@@ -27,7 +27,6 @@ DEBUG = True
 
 ALLOWED_HOSTS = []
 
-
 # Application definition
 
 INSTALLED_APPS = [
@@ -78,6 +77,7 @@ WSGI_APPLICATION = 'backlog.wsgi.application'
 DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.sqlite3',
+#         'NAME': r'\\p-app-4\E\Backlog\backlog_db.sqlite3',
         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
     }
 }

BIN
db.sqlite3


BIN
db_.sqlite3


+ 3 - 2
main/admin.py

@@ -1,9 +1,10 @@
 from django.contrib import admin
 
-from main.models import Epic, Story, Project, Sprint
+from main.models import Epic, Story, Project, Sprint, Comment
 
 
 admin.site.register(Project)
 admin.site.register(Epic)
 admin.site.register(Sprint)
-admin.site.register(Story)
+admin.site.register(Story)
+admin.site.register(Comment)

+ 19 - 4
main/forms.py

@@ -7,7 +7,7 @@ from django.contrib.auth.forms import UserCreationForm
 from django.contrib.auth.models import User
 from martor.fields import MartorFormField
 
-from main.models import Story, Epic
+from main.models import Story, Epic, Comment
 
 
 class RegisterForm(UserCreationForm):
@@ -37,16 +37,31 @@ class EpicForm(forms.ModelForm):
         model = Epic
         fields = ('project', 'name', 'size', 'value', 'description')
         
+    def __init__(self, *args, **kwargs):
+        super(EpicForm, self).__init__(*args, **kwargs)
+        self.fields['description'].required = False
+        
 class StoryForm(forms.ModelForm):
     
     description = MartorFormField(label="Description")
  
     class Meta:
         model = Story
-        widgets = {'epic': forms.HiddenInput()}
-        fields = ('epic', 'name', 'description', 'assignees', 'sprints')
+        widgets = {'epic': forms.HiddenInput(), 'author': forms.HiddenInput()}
+        fields = ('epic', 'author', 'name', 'weight', 'description', 'assignees', 'sprints')
         
     def __init__(self, *args, **kwargs):
         super(StoryForm, self).__init__(*args, **kwargs)
+        self.fields['description'].required = False
         self.fields['assignees'].required = False
-        self.fields['sprints'].required = False
+        self.fields['sprints'].required = False
+
+class CommentForm(forms.ModelForm):
+    class Meta:
+        model = Comment
+        fields = ('content',)
+    content = MartorFormField(label="Ajouter un commentaire")
+    
+    
+    
+    

+ 51 - 14
main/migrations/0001_initial.py

@@ -1,8 +1,10 @@
-# Generated by Django 2.1.1 on 2018-09-27 07:00
+# Generated by Django 2.1.1 on 2018-10-29 07:59
 
 from django.conf import settings
 from django.db import migrations, models
 import django.db.models.deletion
+import martor.models
+import uuid
 
 
 class Migration(migrations.Migration):
@@ -14,17 +16,40 @@ class Migration(migrations.Migration):
     ]
 
     operations = [
+        migrations.CreateModel(
+            name='Comment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('updated', models.DateTimeField(auto_now=True)),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+                ('obj_uuid', models.UUIDField(default='')),
+                ('content', martor.models.MartorField(default='', verbose_name='Commentaire')),
+                ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Auteur')),
+            ],
+            options={
+                'verbose_name': 'commentaire',
+                'verbose_name_plural': 'commentaires',
+                'ordering': ('-created',),
+            },
+        ),
         migrations.CreateModel(
             name='Epic',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('updated', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(default='', max_length=200)),
-                ('size', models.CharField(choices=[('XXS', 'XXS (< 1 sprints)'), ('XS', 'XS (1 sprint)'), ('S', 'S (1-2 sprints)'), ('M', 'M (2-3 sprints)'), ('L', 'L (3-4 sprints)'), ('XL', 'XL (4-6 sprints)'), ('XXL', 'XXL  (> 6 sprints)')], default='M', max_length=10)),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+                ('name', models.CharField(default='', max_length=200, verbose_name='Nom')),
+                ('size', models.CharField(choices=[('XXS', 'XXS (< 1 sprints)'), ('XS', 'XS (1 sprint)'), ('S', 'S (1-2 sprints)'), ('M', 'M (2-3 sprints)'), ('L', 'L (3-4 sprints)'), ('XL', 'XL (4-6 sprints)'), ('XXL', 'XXL  (> 6 sprints)')], default='M', max_length=10, verbose_name='Taille')),
+                ('value', models.IntegerField(default=0, verbose_name='Valeur')),
+                ('description', martor.models.MartorField(blank=True, default='', verbose_name='Description')),
+                ('closed', models.BooleanField(default=False, verbose_name='Clôturée')),
             ],
             options={
-                'abstract': False,
+                'verbose_name': 'epic',
+                'verbose_name_plural': 'epics',
+                'ordering': ('-value',),
             },
         ),
         migrations.CreateModel(
@@ -33,10 +58,14 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('updated', models.DateTimeField(auto_now=True)),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
                 ('name', models.CharField(max_length=200)),
+                ('description', martor.models.MartorField(blank=True, default='', verbose_name='Description')),
+                ('color', models.CharField(default='#f6755e', max_length=7)),
             ],
             options={
-                'abstract': False,
+                'verbose_name': 'projet',
+                'verbose_name_plural': 'projets',
             },
         ),
         migrations.CreateModel(
@@ -45,11 +74,14 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('updated', models.DateTimeField(auto_now=True)),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
                 ('date_start', models.DateField()),
                 ('date_end', models.DateField()),
+                ('retro', martor.models.MartorField(blank=True, default='', verbose_name='Bilan / Rétrospective')),
             ],
             options={
-                'abstract': False,
+                'verbose_name_plural': 'sprints',
+                'ordering': ('-date_start',),
             },
         ),
         migrations.CreateModel(
@@ -58,20 +90,25 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('updated', models.DateTimeField(auto_now=True)),
-                ('name', models.CharField(default='', max_length=200)),
-                ('description', models.TextField(default='')),
-                ('closed', models.BooleanField(default=False)),
-                ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='stories', related_query_name='story', to=settings.AUTH_USER_MODEL)),
-                ('epic', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Epic')),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+                ('name', models.CharField(default='', max_length=200, verbose_name='Nom')),
+                ('weight', models.IntegerField(blank=True, choices=[(1, 1), (2, 2), (3, 3), (5, 5), (8, 8), (13, 13), (21, 21)], verbose_name='Poids')),
+                ('description', martor.models.MartorField(blank=True, default='', verbose_name='Description')),
+                ('closed', models.BooleanField(default=False, verbose_name='Clôturée')),
+                ('assignees', models.ManyToManyField(blank=True, related_name='assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignés')),
+                ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='stories', related_query_name='story', to=settings.AUTH_USER_MODEL, verbose_name='Auteur')),
+                ('epic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='stories', to='main.Epic', verbose_name='Epic')),
+                ('sprints', models.ManyToManyField(blank=True, related_name='stories', related_query_name='story', to='main.Sprint', verbose_name='Sprints')),
             ],
             options={
-                'verbose_name_plural': 'Stories',
-                'ordering': ('-updated',),
+                'verbose_name': 'story',
+                'verbose_name_plural': 'stories',
+                'ordering': ('closed', '-updated'),
             },
         ),
         migrations.AddField(
             model_name='epic',
             name='project',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Project'),
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Project', verbose_name='Projet'),
         ),
     ]

+ 0 - 51
main/migrations/0002_auto_20180928_1106.py

@@ -1,51 +0,0 @@
-# Generated by Django 2.1.1 on 2018-09-28 09:06
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('main', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='epic',
-            name='description',
-            field=models.TextField(blank=True, default=''),
-        ),
-        migrations.AddField(
-            model_name='epic',
-            name='value',
-            field=models.IntegerField(default=0),
-        ),
-        migrations.AddField(
-            model_name='project',
-            name='description',
-            field=models.TextField(blank=True, default=''),
-        ),
-        migrations.AddField(
-            model_name='project',
-            name='owner',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='projects', related_query_name='project', to=settings.AUTH_USER_MODEL),
-        ),
-        migrations.AddField(
-            model_name='story',
-            name='assignees',
-            field=models.ManyToManyField(related_name='assigned', to=settings.AUTH_USER_MODEL),
-        ),
-        migrations.AddField(
-            model_name='story',
-            name='sprints',
-            field=models.ManyToManyField(related_name='stories', related_query_name='story', to='main.Sprint'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='description',
-            field=models.TextField(blank=True, default=''),
-        ),
-    ]

+ 0 - 97
main/migrations/0003_auto_20181001_1721.py

@@ -1,97 +0,0 @@
-# Generated by Django 2.1.1 on 2018-10-01 15:21
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import martor.models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('main', '0002_auto_20180928_1106'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='epic',
-            options={'ordering': ('-value',), 'verbose_name': 'epic', 'verbose_name_plural': 'epics'},
-        ),
-        migrations.AlterModelOptions(
-            name='project',
-            options={'verbose_name': 'projet', 'verbose_name_plural': 'projets'},
-        ),
-        migrations.AlterModelOptions(
-            name='sprint',
-            options={'ordering': ('date_start',), 'verbose_name_plural': 'sprints'},
-        ),
-        migrations.AlterModelOptions(
-            name='story',
-            options={'ordering': ('closed', '-updated'), 'verbose_name': 'story', 'verbose_name_plural': 'stories'},
-        ),
-        migrations.AddField(
-            model_name='epic',
-            name='closed',
-            field=models.BooleanField(default=False, verbose_name='Clôturée'),
-        ),
-        migrations.AlterField(
-            model_name='epic',
-            name='description',
-            field=models.TextField(blank=True, default='', verbose_name='Description'),
-        ),
-        migrations.AlterField(
-            model_name='epic',
-            name='name',
-            field=models.CharField(default='', max_length=200, verbose_name='Nom'),
-        ),
-        migrations.AlterField(
-            model_name='epic',
-            name='project',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Project', verbose_name='Projet'),
-        ),
-        migrations.AlterField(
-            model_name='epic',
-            name='size',
-            field=models.CharField(choices=[('XXS', 'XXS (< 1 sprints)'), ('XS', 'XS (1 sprint)'), ('S', 'S (1-2 sprints)'), ('M', 'M (2-3 sprints)'), ('L', 'L (3-4 sprints)'), ('XL', 'XL (4-6 sprints)'), ('XXL', 'XXL  (> 6 sprints)')], default='M', max_length=10, verbose_name='Taille'),
-        ),
-        migrations.AlterField(
-            model_name='epic',
-            name='value',
-            field=models.IntegerField(default=0, verbose_name='Valeur'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='assignees',
-            field=models.ManyToManyField(related_name='assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignés'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='author',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='stories', related_query_name='story', to=settings.AUTH_USER_MODEL, verbose_name='Auteur'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='closed',
-            field=models.BooleanField(default=False, verbose_name='Clôturée'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='description',
-            field=martor.models.MartorField(verbose_name='Description'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='epic',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='stories', to='main.Epic', verbose_name='Epic'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='name',
-            field=models.CharField(default='', max_length=200, verbose_name='Nom'),
-        ),
-        migrations.AlterField(
-            model_name='story',
-            name='sprints',
-            field=models.ManyToManyField(related_name='stories', related_query_name='story', to='main.Sprint', verbose_name='Sprints'),
-        ),
-    ]

+ 0 - 17
main/migrations/0004_auto_20181002_0939.py

@@ -1,17 +0,0 @@
-# Generated by Django 2.1.1 on 2018-10-02 07:39
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('main', '0003_auto_20181001_1721'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='sprint',
-            options={'ordering': ('-date_start',), 'verbose_name_plural': 'sprints'},
-        ),
-    ]

+ 103 - 13
main/models.py

@@ -1,6 +1,10 @@
 
+import datetime
+import uuid
+
 from django.contrib.auth.models import User
-from django.db import models
+from django.db import models, connection
+from django.db.models.aggregates import Sum
 from martor.models import MartorField
 
 
@@ -8,26 +12,28 @@ class BaseModel(models.Model):
     created = models.DateTimeField(auto_now_add=True)
     updated = models.DateTimeField(auto_now=True)
     objects = models.Manager()
+    uuid = models.UUIDField(default=uuid.uuid4, editable=False)
     
     class Meta:
         abstract = True
 
     def model_name(self):
-        return self._meta.verbose_name
+        try:
+            return self._meta.verbose_name
+        except AttributeError:
+            return ""
 
+    def comments(self):
+        return Comment.objects.filter(obj_uuid = self.uuid)
+    
 class Project(BaseModel):
     class Meta:
         verbose_name = "projet"
         verbose_name_plural = "projets"
         
     name = models.CharField(max_length=200)
-    description = models.TextField(default="", blank=True)
-#     icon
-    owner = models.ForeignKey(User, 
-                               on_delete=models.PROTECT, 
-                               null=True,
-                               related_name="projects",
-                               related_query_name="project")
+    description = MartorField(blank=True, default="", verbose_name="Description")
+    color = models.CharField(max_length=7, default="#f6755e")
     
     def __str__(self):
         return self.name
@@ -49,7 +55,7 @@ class Epic(BaseModel):
     name = models.CharField(max_length=200, default="", verbose_name="Nom")
     size = models.CharField(max_length=10, default="M", choices=SIZES, verbose_name="Taille")
     value = models.IntegerField(default=0, verbose_name="Valeur")
-    description = models.TextField(default="", blank=True, verbose_name="Description")
+    description = MartorField(blank=True, default="", verbose_name="Description")
     project = models.ForeignKey(Project, on_delete=models.PROTECT, null=True, verbose_name="Projet")
     closed = models.BooleanField(default=False, verbose_name="Clôturée")
     
@@ -62,6 +68,21 @@ class Epic(BaseModel):
     def nb_closed_stories(self):
         return len(self.stories.filter(closed=True))
     
+    def nb_active_stories(self):
+        res = 0
+        for story in self.stories.filter(closed=False):
+            if story.running():
+                res += 1
+        return res
+    
+    def contributors(self):
+        qry = User.objects.raw("""SELECT DISTINCT auth_user.* 
+                                    FROM ((auth_user INNER JOIN main_story_assignees ON auth_user.id = main_story_assignees.user_id) 
+                                    INNER JOIN main_story ON main_story_assignees.story_id = main_story.id)
+                                    WHERE main_story.epic_id = {};
+                                    """.format(self.id))
+        return ", ".join(u.username for u in qry)
+    
 class Sprint(BaseModel):
     class Meta:
         verbose_name_plural = "sprint"
@@ -69,35 +90,104 @@ class Sprint(BaseModel):
         ordering = ('-date_start', )
     date_start = models.DateField()
     date_end = models.DateField()
+    retro = MartorField(blank=True, default="", verbose_name="Bilan / Rétrospective")
         
     def __str__(self):
         return "Sprint #{} ({:%d/%m/%Y} > {:%d/%m/%Y})".format(self.id, self.date_start, self.date_end)
     
+    def running(self):
+        return self.date_start < datetime.date.today() <= self.date_end
+    
+    def nb_stories(self):
+        return self.stories.count()
+    
+    def planned_velocity(self):
+        total =  self.stories.aggregate(Sum('weight'))['weight__sum']
+        return total if total is not None else "NA"
+    
+    def real_velocity(self):
+        if datetime.date.today() < self.date_start:
+            return "NA"
+        sql = """SELECT SUM(main_story.weight) as vel
+                FROM main_story 
+                    INNER JOIN (SELECT story_id, max(sprint_id) as sprint_id
+                                FROM main_story_sprints 
+                                GROUP BY story_id) as last_sprints 
+                    ON main_story.id = last_sprints.story_id
+                WHERE last_sprints.sprint_id = {} AND closed = 1;
+            """.format(self.id)
+        with connection.cursor() as cursor:
+            cursor.execute(sql)
+            row = cursor.fetchone()
+        return row[0] if row[0] else 0
+
+    @classmethod
+    def current(cls):
+        today = datetime.datetime.today()
+        try:
+            return Sprint.objects.filter(date_start__lte = today).filter(date_end__gt=today)[0]
+        except IndexError:
+            return None
+
 class Story(BaseModel):
     class Meta:
         verbose_name = "story"
         verbose_name_plural = "stories"
         ordering = ('closed', '-updated')
-    epic = models.ForeignKey(Epic, on_delete=models.PROTECT, null=True, related_name="stories", verbose_name="Epic")
+        
+    WEIGHTS = ((1, 1),(2, 2),(3, 3),(5, 5),(8, 8),(13, 13),(21, 21))
+        
+    epic = models.ForeignKey(Epic, 
+                             on_delete=models.PROTECT, 
+                             null=True, blank=True,
+                             related_name="stories", 
+                             verbose_name="Epic")
     name = models.CharField(max_length=200, default="", verbose_name="Nom")
-    description = MartorField(verbose_name="Description")
+    weight = models.IntegerField(blank=True, choices=WEIGHTS, verbose_name="Poids")
+    description = MartorField(blank=True, default="", verbose_name="Description")
     closed = models.BooleanField(default=False, verbose_name="Clôturée")
     author = models.ForeignKey(User, 
                                on_delete=models.PROTECT, 
-                               null=True,
+                               null=True, blank=True,
                                related_name="stories",
                                related_query_name="story", 
                                verbose_name="Auteur")
     
     assignees = models.ManyToManyField(User, 
+                                       blank=True,
                                        related_name="assigned", 
                                        verbose_name="Assignés")
     
     sprints = models.ManyToManyField(Sprint, 
+                                       blank=True,
                                        related_name="stories",
                                        related_query_name="story", 
                                        verbose_name="Sprints")
     
     def __str__(self):
         return self.name
+                
+    def running(self):
+        for sprint in self.sprints.all():
+            if sprint.running():
+                return True
+        return False
+
+
+class Comment(BaseModel):
+    class Meta:
+        verbose_name = "commentaire"
+        verbose_name_plural = "commentaires"
+        ordering = ('-created', )
+        
+    def __str__(self):
+        return "De {}, le {:%Y-%m-%d}: {}".format(self.author.username, self.created, self.content[:30])
+                
+    obj_uuid = models.UUIDField(default="")
+    content = MartorField(blank=False, default="", verbose_name="Commentaire")
+    author = models.ForeignKey(User, 
+                               on_delete=models.PROTECT, 
+                               verbose_name="Auteur")
+
+    
     

+ 160 - 4
main/static/css/custom.css

@@ -14,6 +14,10 @@
 	font-size: 0.8em;
 }
 
+.clickable {
+	cursor: pointer;
+}
+
 .datatable {
 	width: 100% !important;
 }
@@ -48,6 +52,12 @@
     display: none;
 }
 
+@media screen and (max-width: 1280px) {
+	.hide-on-small-screen {
+		display: none;
+	}
+}
+
 .disabled{
     pointer-events: none;
     color: #cccccc !important;
@@ -112,6 +122,74 @@ td {
 	color: red
 }
 
+select[multiple] {
+	height: 12em;
+}
+
+select[multiple] option {
+	padding: 0.3em 0.5em;
+}
+
+.raw-input {
+
+}
+
+.backlog-editor-col1 {
+	width: 80%;
+}
+
+.backlog-editor-col2 {
+	width: 100px;
+}
+
+.backlog-editor-col2 form {
+	margin: 0;
+}
+
+.backlog-editor-col2 input {
+	height: 2em !important;
+	line-height: 2em !important;
+	padding: 3px !important;
+	margin: 0 !important;
+}
+
+.backlog-editor-col3 {
+	display: flex;
+	flex-direction: row;
+	justify-content: space-around;
+}
+
+
+
+/* Main Header */
+
+#header .right {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+	flex-wrap: nowrap;
+}
+
+#header .right > * {
+	margin-right: 2em;
+}
+
+#search-bar {
+	margin-bottom: 0;
+}
+#search-bar input {
+	height: 2.5em;
+	line-height: 2.5em;
+	padding-left: 2em;
+}
+#search-bar .fa-search {
+	position: absolute;
+	left: 8px;
+	top: 35px;
+	font-size: 14px;
+	color: #999999;
+}
+
 /* The container <div> - needed to position the dropdown content */
 .dropdown {
     position: relative;
@@ -145,6 +223,19 @@ td {
 /* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
 .show {display:block;} 
 
+/* Main */
+
+#main-pannel {
+	width: 50%;
+	
+}
+
+#left-pannel, #right-pannel {
+	margin: 0 5%;
+	display: flex;
+	flex-direction: column;
+	width: 15%;
+}
 
 /* Breadcrumb */
 
@@ -176,6 +267,21 @@ td {
 }
 
 
+/* Commentaires */
+
+.comment-section {
+	margin: 0 2.5%; 
+}
+
+.comment-section .martor-field-content {
+	height: 50px;
+}
+
+.comment-section .comment-display {
+	border-left: solid 2px #d3c5c5;
+	padding: 5px 20px;
+	margin: 10px;
+}
 
 /* Add a slash symbol (/) before/behind each list item */
 #breadcrumb li+li:before {
@@ -184,20 +290,29 @@ td {
     content: "/\00a0";
 }
 
+ /* Index */
 
-/* Index */
-
-.issue-title a {
+.story-title a {
 	color: #2e2e2e;
 	font-size: 14px;
 	font-wright: 600;
 	text-decoration: none;
 }
 
-.issue-title a:hover {
+.story-title a:hover {
 	text-decoration: underline;
 }
 
+.running {
+	color: #006622;
+	height: 22px;
+	font-size: 12px;
+	font-weight: 600;
+	padding: 0 8px;
+	border: solid 1px #006622;
+	border-radius: 4px;
+}
+
 /* Epic details */
 
 .story-closed {
@@ -230,4 +345,45 @@ td {
 	margin: 0 0.5em;
 }
 
+.pagination {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+}
+
+.pagination a {
+	margin: 0 5px;
+	font-weight: 700;
+	font-size: 15px;
+	padding: 2px;
+}
 
+/* Stories detail */
+
+.title-bar * {
+	margin-top: 0;
+	margin-bottom: 0;
+}
+
+.weight-tag {
+	background-color: #c1260b;
+	height: 1.5em;
+	width: 2em;
+	text-align: center;
+	border-radius: 2em;
+	color: white;
+	font-weight: 700;
+	font-size: 1.5em;
+	padding-top: 0.2em;
+	margin-right: 1em;
+}
+
+/* Report: sprints list */
+
+.sprints-list {
+	list-style: none;
+}
+
+.sprint-li {
+	padding: 25px !important;
+}

+ 149 - 0
main/static/images/weight_base.svg

@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="68.182518"
+   height="68.230965"
+   viewBox="0 0 18.039957 18.052777"
+   version="1.1"
+   id="svg5305"
+   sodipodi:docname="weight_base.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <defs
+     id="defs5299">
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Colorize"
+       id="filter32-8">
+      <feComposite
+         k4="0"
+         k3="0"
+         in2="SourceGraphic"
+         operator="arithmetic"
+         k1="0"
+         k2="1"
+         result="composite1"
+         id="feComposite18-45" />
+      <feColorMatrix
+         in="composite1"
+         values="1"
+         type="saturate"
+         result="colormatrix1"
+         id="feColorMatrix20-0" />
+      <feFlood
+         flood-opacity="1"
+         flood-color="rgb(158,67,0)"
+         result="flood1"
+         id="feFlood22-9" />
+      <feBlend
+         in="flood1"
+         in2="colormatrix1"
+         mode="multiply"
+         result="blend1"
+         id="feBlend24-9" />
+      <feBlend
+         in2="blend1"
+         mode="screen"
+         result="blend2"
+         id="feBlend26-4" />
+      <feColorMatrix
+         in="blend2"
+         values="1"
+         type="saturate"
+         result="colormatrix2"
+         id="feColorMatrix28-87" />
+      <feComposite
+         in="colormatrix2"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite2"
+         id="feComposite30-16" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="203.0435"
+     inkscape:cy="-59.513968"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g4766"
+     showgrid="false"
+     units="px"
+     fit-margin-top="2"
+     fit-margin-right="2"
+     fit-margin-bottom="2"
+     fit-margin-left="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="1358"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5302">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-1.1440139,-0.8570281)">
+    <g
+       transform="translate(-55.603863,-32.360631)"
+       id="g4859">
+      <g
+         transform="translate(22.503236,5.2916667)"
+         id="g4766">
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:39.99998093px;line-height:1.25;font-family:'Freestyle Script';-inkscape-font-specification:'Freestyle Script, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#800000;fill-opacity:1;stroke:none;filter:url(#filter32-8)"
+           x="148.57227"
+           y="146.25365"
+           id="text5347"
+           transform="matrix(0.39589484,0,0,0.46557141,-15.251405,-26.541451)"><tspan
+             sodipodi:role="line"
+             x="148.57227"
+             y="146.25365"
+             id="tspan5345"><tspan
+               x="148.57227"
+               y="146.25365"
+               id="tspan5343"
+               style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:39.99998093px;font-family:'Freestyle Script';-inkscape-font-specification:'Freestyle Script, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;writing-mode:lr-tb;text-anchor:middle">13</tspan></tspan></text>
+        <ellipse
+           style="fill:#800000;fill-opacity:0;stroke-width:0.26458332"
+           id="path34-6"
+           cx="42.711308"
+           cy="36.574402"
+           rx="7.9375"
+           ry="7.7485118" />
+      </g>
+      <ellipse
+         transform="matrix(0.83722613,0.54685685,-0.53762423,0.84318455,0,0)"
+         ry="8.1363916"
+         rx="7.1481714"
+         cy="-0.86367416"
+         cx="78.580498"
+         id="path40-56"
+         style="fill:none;fill-opacity:1;stroke:#860000;stroke-width:1.19410014;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>

+ 25 - 4
main/static/js/custom.js

@@ -4,7 +4,6 @@ $(document).ready( function () {
 		window.location.href = $(this).data('url');
 	});
 	
-	
 	var loc_uri = URI.parse(window.location.href);
 	var loc_qry = URI.parseQuery(loc_uri.query)
 	
@@ -15,10 +14,10 @@ $(document).ready( function () {
 		}
 	});
 	
-	$('.filters-bar').on('change', 'select[data-filter]', function() {
+	$('#stories').on('change', 'select[data-filter]', function() {
 		
 		var filters = [];
-		var filtersbar = $(this).parent('.filters-bar');
+		var filtersbar = $('#stories > .filters-bar');
 		
 		filtersbar.children('select').each(function () {
 			if (this.value.length > 0) {
@@ -34,8 +33,30 @@ $(document).ready( function () {
 		window.location.href = target;
 	});
 
+	$('#stories').on('click', 'a[data-page]', function() {
+		
+		var filters = [];
+		var filtersbar = $('#stories > .filters-bar');
+		
+		filtersbar.children('select').each(function () {
+			if (this.value.length > 0) {
+				filters.push($(this).data('filter') + "=" + $(this).val());
+			}
+		});
+		
+		var url = document.URL;
+		var target = url.split('?')[0];
+		if (filters.length > 0) {
+			target = target + '?' + filters.join('&') + '&page=' + $(this).data('page');
+		} else {
+			target = target + '?page=' + $(this).data('page');
+		}
+
+		window.location.href = target;
+	});
+	
+	
 	$(document).on('click', function(event) {
-		console.log($(event.target))
 	    if(!$(event.target).closest('#user-panel').length) {
 	        if($('#user-dropdown').is(":visible")) {
 	            $('#user-dropdown').hide();

+ 25 - 21
main/templates/_layout.html

@@ -3,7 +3,7 @@
 <!DOCTYPE html>
 <html lang="fr">
 	<head>
-	    <title>Manche Numérique SIG - Backlog</title>
+	    <title>{% block title %}Backlog{% endblock %}</title>
 	    <meta charset="utf-8" />
 
 		<link rel="stylesheet" type="text/css" href="{% static 'css/templated.css' %}"/>
@@ -44,28 +44,31 @@
 				<a href="#menu"><span>Menu</span></a>
 			</nav>
 			
-			<a href="{% url 'index' %}" class="logo">SIG Backlog</a>
+			<a href="{% url 'index' %}" class="logo">Backlog</a>
 			
 	        <nav class="right">
-	        	<div class="flex-row flex-align-center">
-		        	{% if request.user.is_authenticated %}
-		        		<a class="button alt tool-btn" href="{% url 'story_index' %}?assignee={{ request.user.id }}" style="margin-right: 2em;" title="Mes Stories">
-		        			<i class="fa fa-check" style="color:#4183C4;"></i>
-		        		</a>
-		        		
-		        		<div id="user-panel" class="dropdown">
-		        			<a href="#" id="user-show-btn"><i class="fa fa-user"></i> <span>{{ request.user.first_name }} {{ request.user.last_name }}</span> <i class="fa fa-caret-down" style="margin-left: 0.5em;"></i></a>
-		        		
-							<div id="user-dropdown" class="dropdown-content">
-								<a href="{% url 'profile_update' %}">Mettre à jour son profil</a>
-								<a href="{% url 'change_password' %}">Changer son mot de passe</a>
-								<a href="{% url 'logout' %}">Se déconnecter</a>
-							</div>
+	        	{% if request.user.is_authenticated %}
+	        	    <form id="search-bar" action="{% url 'search' %}" method="get" accept-charset="utf-8">
+	        	    	<i class="fa fa-search"></i>
+				    	<input name="q" type="text" placeholder="Rechercher...">
+				    </form>
+	        		
+	        		<a class="tool-btn" href="{% url 'story_index' %}?assignee={{ request.user.id }}" style="margin-right: 2em;" title="Mes Stories">
+	        			<i class="fa fa-check-square-o" style="color:#4183c4;"></i><span class="hide-on-small-screen"> Mes stories</span>
+	        		</a>
+	        		
+	        		<div id="user-panel" class="dropdown">
+	        			<a href="#" id="user-show-btn"><i class="fa fa-user"></i><span class="hide-on-small-screen"> {{ request.user.first_name }} {{ request.user.last_name }}</span> <i class="fa fa-caret-down" style="margin-left: 0.5em;"></i></a>
+	        		
+						<div id="user-dropdown" class="dropdown-content">
+							<a href="{% url 'profile_update' %}">Mettre à jour son profil</a>
+							<a href="{% url 'change_password' %}">Changer son mot de passe</a>
+							<a href="{% url 'logout' %}">Se déconnecter</a>
 						</div>
-		        	{% else %}
-		        		<a href="">Se Connecter</a>
-		        	{% endif %}
-	        	</div>
+					</div>
+	        	{% else %}
+	        		<a href="">Se Connecter</a>
+	        	{% endif %}
 			</nav>
 		</header>
 		
@@ -76,7 +79,8 @@
 			<ul class="links">
 				<li><a href="{% url 'index' %}">Accueil</a></li>
 				<li><a href="{% url 'story_index' %}">Toutes les Stories</a></li>
-				<li><a href="" class="disabled">Rapports</a></li>
+				<li><a href="{% url 'reports' %}">Rapports</a></li>
+				<li><a href="{% url 'backlog_editor' %}">Modifier le Backlog</a></li>
 				<li><a href="{% url 'admin:index' %}">Administration</a></li>
 			</ul>
 		</nav>

+ 75 - 0
main/templates/backlog_editor.html

@@ -0,0 +1,75 @@
+{% extends '_layout.html' %}
+
+{% block title %}
+	Edition du backlog
+{% endblock %}
+
+
+{% block breadcrumb %}
+	<li><a href="{% url 'index' %}">Accueil</a></li>
+	<li><a>Edition du backlog</a></li>
+{% endblock %}
+
+
+{% block main %}
+	<header>
+		<div class="flex-row">
+			<h2 class="flex-extend">Backlog : Edition</h2>
+			<span>
+				<a href="{% url 'epic_create' %}"><i class="fa fa-plus"></i> Ajouter une Epic</a>
+			</span>
+		</div>
+	</header>
+
+
+	<table id="backlog-editor">
+		
+		<thead>
+			<th>Epic</th>
+			<th>Valeur</th>
+			<th>Options</th>
+		</thead>
+		
+		<tbody>
+			{% for epic in epics %}
+			<tr>
+				<td class="backlog-editor-col1">
+					<i class="fa fa-circle" style="margin-right: 0.5em; color:{{ epic.project.color }};" title="{{ epic.project.name }}"></i> 
+					<a href="{% url 'epic_details' epic_id=epic.id %}">{{ epic.name }}</a>
+				</td>
+				<td class="backlog-editor-col2">
+					<form action="{% url 'epic_value_update' epic_id=epic.id %}" method="post" accept-charset="utf-8">
+						{% csrf_token %}
+				    	<input class="raw-input" name="value" type="text" value="{{ epic.value }}">
+				    </form>
+				</td>
+				<td class="backlog-editor-col3">
+					<a href="{% url 'epic_edit' epic_id=epic.id from_='backlog_editor' %}"><i class="fa fa-pencil" title="Editer"></i></a>
+					<a href="{% url 'epic_close' epic_id=epic.id %}"><i class="fa fa-archive" title="Clôre l'Epic"></i></a>
+				</td>
+			</tr>
+			{% endfor %}
+			
+			{% for epic in closed %}
+			<tr>
+				<td class="backlog-editor-col1">
+					<i class="fa fa-circle" style="margin-right: 0.5em; color:{{ epic.project.color }};" title="{{ epic.project.name }}"></i> 
+					<a href="{% url 'epic_details' epic_id=epic.id %}" class="story-closed">[Fermée] {{ epic.name }}</a>
+				</td>
+				<td class="backlog-editor-col2">
+					<form action="{% url 'epic_value_update' epic_id=epic.id %}" method="post" accept-charset="utf-8">
+						{% csrf_token %}
+				    	<input class="raw-input" name="value" type="text" value="{{ epic.value }}">
+				    </form>
+				</td>
+				<td class="backlog-editor-col3">
+					<a href="{% url 'epic_edit' epic_id=epic.id from_='backlog_editor' %}"><i class="fa fa-pencil" title="Editer"></i></a>
+					<a href="{% url 'epic_reopen' epic_id=epic.id %}"><i class="fa fa-folder-open" title="Réouvrir l'Epic"></i></a>
+				</td>
+			</tr>
+			{% endfor %}
+		</tbody>
+	
+	</table>
+
+{% endblock %}

+ 33 - 26
main/templates/epic_details.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	{{ epic.name }}
+{% endblock %}
+
 {% block breadcrumb %}
 	<li><a href="{% url 'index' %}">Accueil</a></li>
 	<li><a>Epic #{{ epic.id }}</a></li>
@@ -30,46 +34,49 @@
 	
 	<div class="flex-row">
 		<h4 class="flex-extend">Stories</h4>
-		<a class="button special icon fa-plus tool-btn" href="{% url 'story_create' epic_id=epic.id %}" title="Créer une nouvelle Story"></a>
+		<a href="{% url 'story_create' epic_id=epic.id %}">
+			<i class="fa fa-plus"></i> Ajouter une story
+		</a>
 	</div>
 	
 	{% if epic.stories.count %}
 	    <ul class="alt issues-list">
 	    {% for story in epic.stories.all %}
 	    
-		    <li id="story_{{ story_id }}" class="issue" data-id="{{ story_id }}">
-		    	<div class="issue-frame flex-row">
-		    		<div class="issue-frame-left flex-extend">
-		    			<div class="issue-title">
-		    			{% if story.closed %}
-		        			<a href="{% url 'story_details' story_id=story.id %}" class="story-closed"> {{ story.name }} </a>
-		        		{% else %}
-		        			<a href="{% url 'story_details' story_id=story.id %}"> {{ story.name }} </a>
-		        		{% endif %}
-		        		</div>
-		        		<div class="issue-infos">
-		        			<span class="annotation">Créée le {{ story.created }}, par {{ story.author.username }}</span>
-		        		</div>
-		        	</div>
-		        	<div class="issue-frame-right">
-		        		<ul class="controls">
-		        		<!-- Icone commentaires ici -->
-		        		</ul>
-		        		<div class="annotation issue-last-update">Dernière mise à jour le {{ story.updated }}</div>
-		        	</div>
-		        </div>
-		    </li>
-	    
+	    	{% include 'story_li.html' with story=story from='epic_details' %}
+
 	    {% endfor %}
 	    </ul>
 	 {% else %}
 	 	<span class="annotation">(Aucune story)</span>
 	 {% endif %}
 	
-	<hr>
-	
 	<div class="description">
 		{{ epic.description|safe_markdown }}
 	</div>
 
+	<hr>
+	
+	<div class="comment-section">
+		<ul class="alt">
+		{% for comment in epic.comments.all %}
+			<li class="flex-col">
+				<div><h5><i class="fa fa-comment"></i> {{ comment.author }}, le {{ comment.created }}:</h5></b></div>
+				<div class="comment-display">
+					{{ comment.content|safe_markdown }}
+				</div>
+			</li>
+		{% endfor %}
+		</ul>
+		
+		<form action="{% url 'epic_comment' epic_id=epic.id %}" method="post" accept-charset="utf-8">
+			{% csrf_token %}
+			{{ comment_form.as_p }}
+			
+	    	<div class="flex-row flex-end" style="margin-top: 10px;">
+	    		<input type="submit">
+	    	</div>
+	    </form>
+    </div>
+    
 {% endblock %}

+ 8 - 0
main/templates/epic_form.html

@@ -1,11 +1,19 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	{% if form.instance.id %}
+	[Edition] {{ form.instance.name }}
+	{% else %}
+	Nouvelle Epic
+	{% endif %}
+{% endblock %}
 {% block breadcrumb %}
 	<li><a href="{% url 'index' %}">Accueil</a></li>
 	{% if form.instance.id %}
 		<li><a href="{% url 'epic_details' epic_id=form.instance.id %}">Epic #{{ form.instance.id }}</a></li>
 		<li><a>Edition</a></li>
 	{% else %}
+		<li><a href="{% url 'backlog_editor'%}">Edition du backlog</a></li>
 		<li><a>Nouvelle Epic</a></li>
 	{% endif %}
 {% endblock %}

+ 22 - 21
main/templates/index.html

@@ -5,48 +5,49 @@
 {% endblock %}
 
 {% block main %}
-
 <section id="backlog">
+
 	<header>
 		<div class="flex-row">
-			<h2 class="flex-extend">Backlog</h2>
-			<a class="button icon fa-plus tool-btn" href="{% url 'epic_create' %}" title="Créer une Epic"></a>
+		
+			<h2 class="flex-extend">Backlog <sup><a href="{% url 'backlog_editor' %}" style="margin-left: 5px;"><i class="fa fa-pencil"></i></a></sup></h2>
+			<span>
+				{% if current_sprint %}
+				<b>En cours:</b> <a href="{% url 'story_index' %}?sprint={{ current_sprint.id }}">{{ current_sprint }}</a>
+				{% else %}
+				<i>Sprint en cours: (non défini)</i>
+				{% endif %}
+			</span>
+			
 		</div>
 	</header>
 	
-<!-- 	<div class="flex-row flex-space-around" style="margin-bottom: 3em;">
-		{% if current_sprint %}
-		<b>En cours: <a href="{% url 'story_index' %}?sprint={{ current_sprint.id }}">{{ current_sprint }}</a></b>
-		{% else %}
-		<i>Sprint en cours: (non défini)</i>
-		{% endif %}
-	</div> -->
-	
 	<table class="alt">
 		
 		<thead>
-			<th>Epic</th>
-			<th>Projet</th>
+			<th width="70%">Epic</th>
 			<th>Dim.</th>
-			<th>Val.</th>
-			<th>Prog.</th>
-		
+			<th>Contrib.</th>
+			<th>Stories</th>
 		</thead>
 		
 		<tbody>
-		
 			{% for epic in epics %}
 			<tr data-url="{% url 'epic_details' epic_id=epic.id %}">
-				<td><h5>{{ epic.name }}</h5></td>
-				<td>{{ epic.project.name }}</td>
+				<td>
+					<i class="fa fa-circle" style="margin-right: 0.5em; color:{{ epic.project.color }};" title="{{ epic.project.name }}"></i> 
+					<b>{{ epic.name }}</b>
+				</td>
 				<td>{{ epic.size }}</td>
-				<td>{{ epic.value }}</td>
-				<td>{{ epic.nb_closed_stories }} / {{ epic.nb_stories }}</td>
+				<td>{{ epic.contributors }}</td>
+				<td>{{ epic.nb_stories }}</td>
 			</tr>
 			{% endfor %}
 		
 		</tbody>
 	
 	</table>
+	
 </section>
+	
 {% endblock %}

+ 4 - 0
main/templates/registration/change_password.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	Changement de mot de passe
+{% endblock %}
+
 {% block main %}
 
 	<form method="post">

+ 4 - 0
main/templates/registration/logged_out.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	Déconnecté
+{% endblock %}
+
 {% block main %}
 
 	<p>Vous avez  été déconnecté</p>

+ 4 - 0
main/templates/registration/login.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	Connexion
+{% endblock %}
+
 {% block main %}
 
 	{% if form.errors %}

+ 4 - 0
main/templates/registration/register.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	Inscription
+{% endblock %}
+
 {% block main %}
 
 

+ 0 - 0
main/templates/reports/__init__.py


+ 24 - 0
main/templates/reports/report_index.html

@@ -0,0 +1,24 @@
+{% extends '_layout.html' %}
+
+{% block title %}
+	Index des rapports
+{% endblock %}
+
+{% block breadcrumb %}
+	<li><a href="{% url 'index' %}">Accueil</a></li>
+	<li><a>Rapports</a></li>
+{% endblock %}
+
+{% block main %}
+
+	<header style="margin-bottom: 25px;">
+		<h2>Rapports</h2>
+	</header>
+
+	<ul>
+		<li><a href="{% url 'report_sprints' %}">Historique des sprints</a></li>
+		<li><a href="{% url 'report_projects' %}">Revue de projets</a></li>
+	</ul>
+
+
+{% endblock %}

+ 52 - 0
main/templates/reports/report_projects.html

@@ -0,0 +1,52 @@
+{% extends '_layout.html' %}
+
+{% block title %}
+	Rapport: Projets
+{% endblock %}
+
+{% block breadcrumb %}
+	<li><a href="{% url 'index' %}">Accueil</a></li>
+	<li><a href="{% url 'reports' %}">Rapports</a></li>
+	<li><a>Revue de Projets</a></li>
+{% endblock %}
+
+{% block main %}
+{% load martortags %}
+
+<section id="backlog">
+	<header>
+		<div class="flex-row" style="margin-bottom: 20px;">
+			<h2 class="flex-extend">Revue de Projets</h2>
+		</div>
+	</header>
+	
+	<ul class="alt">
+	{% for epic in epics %}
+		<li class="epic-li">
+			<div class="flex-col" style="margin-bottom: 20px;">
+			
+				<div class="flex-row">
+					<h4>{{ epic.name }}</h4>
+				</div>
+				
+				<div style="margin-bottom: 20px;">
+					<div>Contributeurs: {{ epic.contributors }}</div>
+					<div>Stories (Total / En cours / Terminées): 
+					{{ epic.nb_stories }} / 
+					<span class="valid">{{ epic.nb_active_stories }}</span> / 
+					<span class="disabled">{{ epic.nb_closed_stories }}</span></div>
+				</div>
+				
+				{% if epic.description %}
+				<div style="padding: 0 20px;">
+					{{ epic.description|safe_markdown }}
+				</div>
+				{% endif %}
+				
+			</div>
+		</li>
+	{% endfor %}
+	</ul>
+	
+</section>
+{% endblock %}

+ 50 - 0
main/templates/reports/report_sprints.html

@@ -0,0 +1,50 @@
+{% extends '_layout.html' %}
+
+{% block title %}
+	Rapport: Sprints
+{% endblock %}
+
+{% block breadcrumb %}
+	<li><a href="{% url 'index' %}">Accueil</a></li>
+	<li><a href="{% url 'reports' %}">Rapports</a></li>
+	<li><a>Les Sprints</a></li>
+{% endblock %}
+
+{% block main %}
+{% load martortags %}
+
+<section id="backlog">
+	<header>
+		<div class="flex-row">
+			<h2 class="flex-extend">Les Sprints</h2>
+		</div>
+	</header>
+	
+	<ul class="sprints-list">
+	{% for sprint in sprints %}
+		<li class="sprint-li">
+			<div class="flex-col">
+			
+				<div class="flex-row">
+					<h4>Sprint #{{ sprint.id }} : {{ sprint.date_start|date:"d M. Y" }} au {{ sprint.date_end|date:"d M. Y" }}</h4>
+					{% if sprint.running %}<span class="running" style="margin-left: 14px;">En cours</span>{% endif %}
+				</div>
+				
+				<div class="flex-row flex-space-around" style="margin-bottom: 20px;">
+					<b>Stories programmées: <a href="{% url 'story_index' %}?sprint={{ sprint.id }}">{{ sprint.nb_stories }}</a> </b>
+					<b>Vélocité prévue: {{ sprint.planned_velocity }} </b>
+					<b>Vélocité réelle: {{ sprint.real_velocity }} </b>
+				</div>
+				
+				{% if sprint.retro %}
+				<div style="padding: 0 20px;">
+					{{ sprint.retro|safe_markdown }}
+				</div>
+				{% endif %}
+			</div>
+		</li>
+	{% endfor %}
+	</ul>
+	
+</section>
+{% endblock %}

+ 33 - 0
main/templates/search_results.html

@@ -0,0 +1,33 @@
+{% extends '_layout.html' %}
+
+{% block breadcrumb %}
+	<li><a href="{% url 'index' %}">Accueil</a></li>
+	<li><a>Résultat de la recherche</a></li>
+{% endblock %}
+
+{% block main %}
+
+	<ul class="alt">
+	{% for r in results %}
+		<li>
+			<span style="display: inline-block; width: 50px;"><b>{{ r.model_name|title }}</b></span>
+			{% if r.model_name == 'EPIC' %}
+				<a href="{% url 'epic_details' epic_id=r.id %}">  {{ r.name }}</a>
+			{% else %}
+				<a href="{% url 'story_details' story_id=r.id %}">{{ r.name }}</a>
+			{% endif %}
+		</li>
+	{% endfor %}
+	</ul>
+
+	<div class="pagination">
+    	{% for numpage in pages %}
+    		{% if numpage == stories.number %}
+    			<a style="color: grey;">{{ numpage }}</a>
+    		{% else %}
+    			<a href="?page={{ numpage }}">{{ numpage }}</a>
+    		{% endif %}
+    	{% endfor %}
+	</div>
+	 
+{% endblock %}

+ 47 - 4
main/templates/story_details.html

@@ -1,8 +1,16 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	{{ story.name }}
+{% endblock %}
+
 {% block breadcrumb %}
 	<li><a href="{% url 'index' %}">Accueil</a></li>
+	{% if story.epic %}
 	<li><a href="{% url 'epic_details' epic_id=story.epic.id %}">Epic #{{ story.epic.id }}</a></li>
+	{% else %}
+	<li><a href="{% url 'story_index' %}">Toutes les Stories</a></li>
+	{% endif %}
 	<li><a>Story #{{ story.id }}</a></li>
 {% endblock %}
 
@@ -11,11 +19,18 @@
 	
 	<header>
 		<div class="flex-row">
-			<blockquote class="flex-extend"><a href="{% url 'epic_details' epic_id=story.epic.id %}"><b><i class="fa fa-backward"></i>Retour à {{ story.epic.name }}</b></a></blockquote>
+		    {% if story.epic %}
+			<span class="annotation flex-extend">
+				Epic: <a href="{% url 'epic_details' epic_id=story.epic.id %}">{{ story.epic.name }}</a>
+			</span>
+			{% endif %}
 			<blockquote class="annotation">Créée par {{ story.author.get_full_name }}, le {{ story.created }}</blockquote>
 		</div>
-		<div class="flex-row">
-		
+		<div class="flex-row title-bar">
+			{% if story.weight %}
+			<span style="margin-right: 10px;">{% include 'weight_svg.html' with weight=story.weight h=36 %}</span>
+			{% endif %}
+			
 			{% if story.closed %}
 				<h2 class="flex-extend" style="color: grey;">[Terminée] {{ story.name }}</h2>
 			{% else %}
@@ -37,7 +52,11 @@
 	<hr>
 	
 	<div class="description">
-		{{ story.description|safe_markdown }}
+		{% if story.description %}
+			{{ story.description|safe_markdown }}
+		{% else %}
+			<i style="color:grey;">(Pas de description)</i>
+		{% endif %}
 	</div>
 
 	<hr>
@@ -63,4 +82,28 @@
 		</ul>
 	</div>
 	
+	<hr>
+	
+	<div class="comment-section">
+		<ul class="alt">
+		{% for comment in story.comments.all %}
+			<li class="flex-col">
+				<div><h5><i class="fa fa-comment"></i> {{ comment.author }}, le {{ comment.created }}:</h5></b></div>
+				<div class="comment-display">
+					{{ comment.content|safe_markdown }}
+				</div>
+			</li>
+		{% endfor %}
+		</ul>
+		
+		<form action="{% url 'story_comment' story_id=story.id %}" method="post" accept-charset="utf-8">
+			{% csrf_token %}
+			{{ comment_form.as_p }}
+			
+	    	<div class="flex-row flex-end" style="margin-top: 10px;">
+	    		<input type="submit">
+	    	</div>
+	    </form>
+    </div>
+	
 {% endblock %}

+ 76 - 12
main/templates/story_form.html

@@ -1,8 +1,21 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	{% if form.instance.id %}
+	[Edition] {{ form.instance.name }}
+	{% else %}
+	Nouvelle Story
+	{% endif %}
+{% endblock %}
+
 {% block breadcrumb %}
 	<li><a href="{% url 'index' %}">Accueil</a></li>
+	{% if form.instance.epic_id %}
 	<li><a href="{% url 'epic_details' epic_id=form.instance.epic_id %}">Epic #{{ form.instance.epic_id }}</a></li>
+	{% else %}
+	<li><a href="{% url 'story_index' %}">Toutes les Stories</a></li>	
+	{% endif %}
+	
 	{% if form.instance.id %}
 		<li><a href="{% url 'story_details' story_id=form.instance.id %}">Story #{{ form.instance.id }}</a></li>
 		<li><a>Edition</a></li>
@@ -15,21 +28,72 @@
 	
 	<h2>{% if form.instance.id %}Edition de la Story{% else %}Nouvelle Story{% endif %}</h2>
 	<form action="." method="post" enctype="multipart/form-data">
-	{% csrf_token %}
+		{% csrf_token %}
 	
-	{{ form.as_p }}
+		{# {{ form.as_p }} #}
 
-	<div class="flex-row flex-end">
-		<span style="margin-right: 1em;">
-		{% if form.instance.id %}
-			<a class="button alt" href="{% url 'story_details' story_id=form.instance.id %}">Annuler</a>
-		{% else %}
-			<a class="button alt" href="{% url 'story_index' %}">Annuler</a>
-		{% endif %}
-		</span>
-		<input type="submit" value="Enregistrer">
-	</div>
+		{{ form.non_field_errors }}
+		
+		<p>
+		    {{ form.name.errors }}
+		    {{ form.name.label_tag }}
+		    {{ form.name }}
+		</p>
+		
+		<p>
+		    {{ form.weight.errors }}
+		    {{ form.weight.label_tag }}
+		    {{ form.weight }}
+		</p>
+		
+		<p>
+		    {{ form.description.errors }}
+		    {{ form.description.label_tag }}
+		    {{ form.description }}
+		</p>
+		
+		<p>
+		    {{ form.assignees.errors }}
+		    <div class="flex-row">
+		    	<label class="flex-extend" for="{{ form.assignees.id_for_label }}">Assignée à:</label>
+		    	<a class="clickable" id="btn-assign-myself">Assigner à moi</a>
+		    	<script>
+		    		$('#btn-assign-myself').on('click', function() {
+		    			$('#id_assignees').val([{{ request.user.id }}])
+		    		});
+		    	</script>
+		    </div>
+		    {{ form.assignees }}
+		</p>
+		
+		<p>
+		    {{ form.sprints.errors }}
+		    <div class="flex-row">
+			    <label class="flex-extend" for="{{ form.sprints.id_for_label }}">Sprints:</label>
+			    <a class="clickable" id="btn-this-sprint">Sprint en cours</a>
+		    	<script>
+		    		$('#btn-this-sprint').on('click', function() {
+		    			$('#id_sprints').val([{{ current_sprint_id }}])
+		    		});
+		    	</script>
+		    </div>
+		    {{ form.sprints }}
+		</p>
+		
+		<div class="flex-row flex-end">
+			<span style="margin-right: 1em;">
+			{% if form.instance.id %}
+				<a class="button alt" href="{% url 'story_details' story_id=form.instance.id %}">Annuler</a>
+			{% else %}
+				<a class="button alt" href="{% url 'story_index' %}">Annuler</a>
+			{% endif %}
+			</span>
+			
+			<input type="submit" value="Enregistrer">
+		</div>
 	
 	</form>
 		
+		
+	
 {% endblock %}

+ 61 - 70
main/templates/story_index.html

@@ -1,5 +1,9 @@
 {% extends '_layout.html' %}
 
+{% block title %}
+	Les Stories
+{% endblock %}
+
 {% block breadcrumb %}
 	<li><a href="{% url 'index' %}">Accueil</a></li>
 	<li><a>Toutes les Stories</a></li>
@@ -9,79 +13,66 @@
 
 	<header class="flex-row" style="margin-bottom: 2em;">
 		<h2 class="flex-extend">Stories</h2>
-
+		<a class="button icon fa-plus tool-btn" href="{% url 'story_create' %}" title="Nouvelle Story"></a>
 	</header>
 
-	<div class="flex-row filters-bar">
-		<label>Etat: </label>
-		<select data-filter="state">
-			<option value="" selected="selected">Tous</option>
-			<option value="open">Ouvert</option>
-			<option value="closed">Clôturé</option>
-		</select>
-	
-		<label>Sprint: </label>
-		<select data-filter="sprint">
-			<option value="" selected="selected">Tous</option>
-			<option value="None">(Aucun)</option>
-			{% for sprint in sprints %}
-				<option value="{{ sprint.id }}">{{ sprint }}</option>
-			{% endfor %}
-		</select>
+	<div id="stories">
+
+		<div class="flex-row filters-bar">
+			<label>Etat: </label>
+			<select data-filter="state">
+				<option value="" selected="selected">Tous</option>
+				<option value="open">Ouvert</option>
+				<option value="closed">Clôturé</option>
+			</select>
+		
+			<label>Sprint: </label>
+			<select data-filter="sprint">
+				<option value="" selected="selected">Tous</option>
+				<option value="None">(Aucun)</option>
+				{% for sprint in sprints %}
+					<option value="{{ sprint.id }}">{{ sprint }}</option>
+				{% endfor %}
+			</select>
+		
+			<label>Auteur: </label>
+			<select data-filter="author">
+				<option value="" selected="selected">Tous</option>
+				{% for user in users %}
+					<option value="{{ user.id }}">{{ user.first_name }} {{ user.last_name }}</option>
+				{% endfor %}
+			</select>
+		
+			<label>Assignées: </label>
+			<select data-filter="assignee">
+				<option value="" selected="selected">Tous</option>
+				{% for user in users %}
+					<option value="{{ user.id }}">{{ user.first_name }} {{ user.last_name }}</option>
+				{% endfor %}
+			</select>
+		</div>
 	
-		<label>Auteur: </label>
-		<select data-filter="author">
-			<option value="" selected="selected">Tous</option>
-			{% for user in users %}
-				<option value="{{ user.id }}">{{ user.first_name }} {{ user.last_name }}</option>
-			{% endfor %}
-		</select>
+		{% if stories %}
+		    <ul class="alt issues-list">
+		    {% for story in stories %}
+		    
+		    	{% include 'story_li.html' with story=story from='story_index' %}
+		    
+		    {% endfor %}
+		    </ul>
+		 {% else %}
+		 	<span class="annotation">(Rien à afficher)</span>
+		 {% endif %}
+		 
+		<div class="pagination">
+	    	{% for numpage in pages %}
+	    		{% if numpage == stories.number %}
+	    			<a style="color: grey;">{{ numpage }}</a>
+	    		{% else %}
+	    			<a class="clickable" data-page="{{ numpage }}">{{ numpage }}</a>
+	    		{% endif %}
+	    	{% endfor %}
+		</div>
 	
-		<label>Assignées: </label>
-		<select data-filter="assignee">
-			<option value="" selected="selected">Tous</option>
-			{% for user in users %}
-				<option value="{{ user.id }}">{{ user.first_name }} {{ user.last_name }}</option>
-			{% endfor %}
-		</select>
 	</div>
-
-	{% if stories %}
-	    <ul class="alt issues-list">
-	    {% for story in stories %}
-	    
-		    <li id="story_{{ story_id }}" class="issue" data-id="{{ story_id }}">
-		    	<div class="issue-frame flex-row">
-		    		<div class="issue-frame-left flex-extend">
-		    			<div class="issue-title">
-		    				{% if story.closed %}
-		        				<a class="story-closed" href="{% url 'story_details' story_id=story.id %}">{{ story.name }}</a>
-		        			{% else %}
-		        				<a href="{% url 'story_details' story_id=story.id %}">{{ story.name }}</a>
-		        			{% endif %}
-		        		</div>
-		        		<div class="issue-infos">
-		        			
-		        			<span class="annotation">Créée le {{ story.created }}, par {{ story.author.username }}. Dernière mise à jour le {{ story.updated }}.</span>
-		        		</div>
-		        	</div>
-		        	<div class="issue-frame-right">
-		        		<div class="flex-row controls">
-							<a href="#" class="disabled annotation"><i class="fa fa-comment"></i> 0</a>
-		        		</div>
-		        		{% if story.epic_id %}
-		        		<div class="annotation issue-last-update">		        			
-		        				<a href="{% url 'epic_details' epic_id=story.epic_id %}">{{ story.epic.name }}</a>
-	        			</div>
-	        			{% endif %}
-		        	</div>
-		        </div>
-		    </li>
-	    
-	    {% endfor %}
-	    </ul>
-	 {% else %}
-	 	<span class="annotation">(Rien à afficher)</span>
-	 {% endif %}
-	 
 {% endblock %}

+ 45 - 0
main/templates/story_li.html

@@ -0,0 +1,45 @@
+<!-- story item in used in stories indexes -->
+<li class="story-item" id="story_{{ story_id }}" data-id="{{ story_id }}">
+
+	<div class="flex-col">
+		
+		<div class="flex-row ">  {# ligne 1 #}
+
+			<div class="flex-extend">  {# partie gauche #}
+				<div class="story-title">
+					{% if story.closed %}
+	    			<a href="{% url 'story_details' story_id=story.id %}" class="story-closed"> {{ story.name }} </a>
+		    		{% else %}
+	    			<a href="{% url 'story_details' story_id=story.id %}"> {{ story.name }} </a>
+		    		{% endif %}
+	    		</div>
+	    	</div>
+	    	
+	    	<div>  {# partie droite #}
+	       		<div class="flex-row controls">
+	       			{% if story.running %}<span class="running">En cours</span>{% endif %}
+					{% if story.weight %}<span>{% include 'weight_svg.html' with weight=story.weight h=20 %}</span>{% endif %}
+					{# <a href="#" class="disabled annotation"><i class="fa fa-comment"></i> 0</a> #}
+	       		</div>
+	    	</div>
+	    </div>
+	    
+	    <div class="flex-row">   {# ligne 2 #}
+	    	
+	    	<div class="flex-extend"> {# partie gauche #}
+    			<span class="annotation">Ajoutée le {{ story.created|date:"d M. Y" }} par {{ story.author.username }}</span>
+	    	</div>
+	    	
+	       	<div>  {# partie droite #}
+	       		<div class="annotation">		      
+	       			{% if from == "story_index" %}	
+	       				{% if story.epic_id %}
+	    				<a href="{% url 'epic_details' epic_id=story.epic_id %}">{{ story.epic.name }}</a>
+	    				{% endif %}
+	    			{% endif %}
+	   			</div>
+	       	</div>
+	    </div>
+    </div>
+    
+</li>

+ 150 - 0
main/templates/weight_svg.html

@@ -0,0 +1,150 @@
+<object type="image/svg+xml">
+	<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+	<!-- Created with Inkscape (http://www.inkscape.org/) -->
+	<svg
+	   xmlns:dc="http://purl.org/dc/elements/1.1/"
+	   xmlns:cc="http://creativecommons.org/ns#"
+	   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+	   xmlns:svg="http://www.w3.org/2000/svg"
+	   xmlns="http://www.w3.org/2000/svg"
+	   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+	   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+	   width="{{ h }}"
+	   height="{{ h }}"
+	   viewBox="0 0 18.039957 18.052777"
+	   version="1.1"
+	   id="svg5305"
+	   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+	  <defs
+	     id="defs5299">
+	    <filter
+	       style="color-interpolation-filters:sRGB"
+	       inkscape:label="Colorize"
+	       id="filter32-8">
+	      <feComposite
+	         k4="0"
+	         k3="0"
+	         in2="SourceGraphic"
+	         operator="arithmetic"
+	         k1="0"
+	         k2="1"
+	         result="composite1"
+	         id="feComposite18-45" />
+	      <feColorMatrix
+	         in="composite1"
+	         values="1"
+	         type="saturate"
+	         result="colormatrix1"
+	         id="feColorMatrix20-0" />
+	      <feFlood
+	         flood-opacity="1"
+	         flood-color="rgb(158,67,0)"
+	         result="flood1"
+	         id="feFlood22-9" />
+	      <feBlend
+	         in="flood1"
+	         in2="colormatrix1"
+	         mode="multiply"
+	         result="blend1"
+	         id="feBlend24-9" />
+	      <feBlend
+	         in2="blend1"
+	         mode="screen"
+	         result="blend2"
+	         id="feBlend26-4" />
+	      <feColorMatrix
+	         in="blend2"
+	         values="1"
+	         type="saturate"
+	         result="colormatrix2"
+	         id="feColorMatrix28-87" />
+	      <feComposite
+	         in="colormatrix2"
+	         in2="SourceGraphic"
+	         operator="in"
+	         result="composite2"
+	         id="feComposite30-16" />
+	    </filter>
+	  </defs>
+	  <sodipodi:namedview
+	     id="base"
+	     pagecolor="#ffffff"
+	     bordercolor="#666666"
+	     borderopacity="1.0"
+	     inkscape:pageopacity="0.0"
+	     inkscape:pageshadow="2"
+	     inkscape:zoom="1.979899"
+	     inkscape:cx="203.0435"
+	     inkscape:cy="-59.513968"
+	     inkscape:document-units="mm"
+	     inkscape:current-layer="g4766"
+	     showgrid="false"
+	     units="px"
+	     fit-margin-top="2"
+	     fit-margin-right="2"
+	     fit-margin-bottom="2"
+	     fit-margin-left="2"
+	     inkscape:window-width="1920"
+	     inkscape:window-height="1017"
+	     inkscape:window-x="1358"
+	     inkscape:window-y="-8"
+	     inkscape:window-maximized="1" />
+	  <metadata
+	     id="metadata5302">
+	    <rdf:RDF>
+	      <cc:Work
+	         rdf:about="">
+	        <dc:format>image/svg+xml</dc:format>
+	        <dc:type
+	           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+	        <dc:title></dc:title>
+	      </cc:Work>
+	    </rdf:RDF>
+	  </metadata>
+	  <g
+	     inkscape:label="Calque 1"
+	     inkscape:groupmode="layer"
+	     id="layer1"
+	     transform="translate(-1.1440139,-0.8570281)">
+	    <g
+	       transform="translate(-55.603863,-32.360631)"
+	       id="g4859">
+	      <g
+	         transform="translate(22.503236,5.2916667)"
+	         id="g4766">
+	        <text
+	           xml:space="preserve"
+	           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:39.99998093px;line-height:1.25;font-family:'Freestyle Script';-inkscape-font-specification:'Freestyle Script, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#800000;fill-opacity:1;stroke:none;filter:url(#filter32-8)"
+	           x="148.57227"
+	           y="146.25365"
+	           id="text5347"
+	           transform="matrix(0.39589484,0,0,0.46557141,-15.251405,-26.541451)"><tspan
+	             sodipodi:role="line"
+	             x="148.57227"
+	             y="146.25365"
+	             id="tspan5345"><tspan
+	               x="148.57227"
+	               y="146.25365"
+	               id="tspan5343"
+	               style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:39.99998093px;font-family:'Freestyle Script';-inkscape-font-specification:'Freestyle Script, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;writing-mode:lr-tb;text-anchor:middle">{{ weight }}</tspan></tspan></text>
+	        <ellipse
+	           style="fill:#800000;fill-opacity:0;stroke-width:0.26458332"
+	           id="path34-6"
+	           cx="42.711308"
+	           cy="36.574402"
+	           rx="7.9375"
+	           ry="7.7485118" />
+	      </g>
+	      <ellipse
+	         transform="matrix(0.83722613,0.54685685,-0.53762423,0.84318455,0,0)"
+	         ry="8.1363916"
+	         rx="7.1481714"
+	         cy="-0.86367416"
+	         cx="78.580498"
+	         id="path40-56"
+	         style="fill:none;fill-opacity:1;stroke:#860000;stroke-width:1.19410014;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+	    </g>
+	  </g>
+	</svg>
+													
+</object>

+ 12 - 0
main/urls.py

@@ -13,15 +13,27 @@ urlpatterns = [
     path('register/', views.register, name='register'),
     path('profile_update/', views.profile_update, name='profile_update'),
     path(r'change_password/', views.change_password, name='change_password'),
+    path('edition/', views.backlog_editor, name='backlog_editor'),
     path('stories/', views.story_index, name='story_index'),
     path('stories/<int:story_id>', views.story_details, name='story_details'),
+    path('stories/create/', views.story_create, name='story_create'),
     path('stories/create/<int:epic_id>/', views.story_create, name='story_create'),
     path('stories/edit/<int:story_id>/', views.story_edit, name='story_edit'),
     path('stories/delete/<int:story_id>/', views.story_delete, name='story_delete'),
     path('stories/close/<int:story_id>/', views.story_close, name='story_close'),
     path('stories/reopen/<int:story_id>/', views.story_reopen, name='story_reopen'),
+    path('stories/comment/<int:story_id>/', views.story_comment, name='story_comment'),
     path('epics/<int:epic_id>', views.epic_details, name='epic_details'),
     path('epics/create/', views.epic_create, name='epic_create'),
     path('epics/edit/<int:epic_id>/', views.epic_edit, name='epic_edit'),
+    path('<str:from_>/edit/<int:epic_id>/', views.epic_edit, name='epic_edit'),
     path('epics/delete/<int:epic_id>/', views.epic_delete, name='epic_delete'),
+    path('epics/newval/<int:epic_id>/', views.epic_value_update, name='epic_value_update'),
+    path('epics/close/<int:epic_id>/', views.epic_close, name='epic_close'),
+    path('epics/reopen/<int:epic_id>/', views.epic_reopen, name='epic_reopen'),
+    path('epics/comment/<int:epic_id>/', views.epic_comment, name='epic_comment'),
+    path('reports/', views.reports, name='reports'),
+    path('reports/sprints/', views.report_sprints, name='report_sprints'),
+    path('reports/projects/', views.report_projects, name='report_projects'),
+    path('search/', views.search, name='search'),
 ]

+ 130 - 29
main/views.py

@@ -4,22 +4,19 @@ from django.contrib.auth import logout, login, update_session_auth_hash
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.forms import PasswordChangeForm
 from django.contrib.auth.models import User
+from django.core.paginator import Paginator
 from django.shortcuts import render, get_object_or_404, redirect
 
-from main.forms import StoryForm, EpicForm, RegisterForm, ProfileForm
-from main.models import Story, Epic, Sprint
+from main.forms import StoryForm, EpicForm, RegisterForm, ProfileForm, \
+    CommentForm
+from main.models import Story, Epic, Sprint, Comment
 
 
 @login_required
 def index(request):
-    today = datetime.today()
-    try:
-        current_sprint = Sprint.objects.filter(date_start__lte = today).filter(date_end__gt=today)[0]
-    except IndexError:
-        current_sprint = None
-    
+   
     epics = Epic.objects.filter(closed=False)
-    return render(request, 'index.html', {'current_sprint': current_sprint, 'epics': epics})
+    return render(request, 'index.html', {'current_sprint': Sprint.current(), 'epics': epics})
 
 def register(request):
     if request.method == 'POST':
@@ -32,6 +29,7 @@ def register(request):
         form = RegisterForm()
     return render(request, 'registration/register.html', {'form': form})
 
+@login_required
 def profile_update(request):
     if request.method == 'POST':
         user = get_object_or_404(User, username=request.user)
@@ -45,6 +43,7 @@ def profile_update(request):
         form = ProfileForm(instance=user)
     return render(request, 'registration/register.html', {'form': form})
 
+@login_required
 def change_password(request):
     if request.method == 'POST':
         form = PasswordChangeForm(request.user, request.POST)
@@ -56,12 +55,18 @@ def change_password(request):
         form = PasswordChangeForm(request.user)
     return render(request, 'registration/change_password.html', {'form': form})
 
-
 @login_required
 def logout(request):
     logout(request)
     return redirect("index")
 
+@login_required
+def backlog_editor(request):
+    
+    epics = Epic.objects.filter(closed=False)
+    closed = Epic.objects.filter(closed=True)
+    
+    return render(request, 'backlog_editor.html', {'epics': epics, 'closed': closed})
 
 @login_required
 def story_index(request):
@@ -84,30 +89,35 @@ def story_index(request):
     if 'assignee' in filters and filters['assignee']:
         stories = stories.filter(assignees__id=filters['assignee'])
     
-    return render(request, 'story_index.html', {'stories': stories.all(), 'sprints': sprints, 'users': users})
+    paginator = Paginator(stories.all(), 10)
+    page = request.GET.get('page')
+    stories = paginator.get_page(page)
+    
+    return render(request, 'story_index.html', {'stories': stories, 'sprints': sprints, 'users': users, 'pages': range(1, paginator.num_pages + 1)})
 
 @login_required
 def story_details(request, story_id):
     story = get_object_or_404(Story, id=story_id)
-    return render(request, 'story_details.html', {'story': story})
+    comment_form = CommentForm()
+    return render(request, 'story_details.html', {'story': story, 'comment_form': comment_form})
 
 @login_required
-def story_create(request, epic_id):
+def story_create(request, epic_id=None):
     if request.method == 'POST':
         
         form = StoryForm(request.POST)
         if form.is_valid():
-            story = form.save(commit=False)
-            story.author = User.objects.get(username=request.user)
-            story.save()
+            story = form.save()
             return redirect("story_details", story.id)
         
     else:
         story = Story()
-        story.epic = get_object_or_404(Epic, id=epic_id)
+        if epic_id is not None:
+            story.epic = get_object_or_404(Epic, id=epic_id)
+        story.author = User.objects.get(username=request.user)
         form = StoryForm(instance=story)
-         
-    return render(request, 'story_form.html', {'form': form})
+        
+    return render(request, 'story_form.html', {'form': form, 'current_sprint_id': Sprint.current().id})
 
 @login_required
 def story_edit(request, story_id):
@@ -122,7 +132,7 @@ def story_edit(request, story_id):
         story = get_object_or_404(Story, id=story_id)
         form = StoryForm(instance=story)
         
-    return render(request, 'story_form.html', {'form': form})
+    return render(request, 'story_form.html', {'form': form, 'current_sprint_id': Sprint.current().id})
 
 @login_required
 def story_delete(request, story_id):
@@ -133,23 +143,38 @@ def story_delete(request, story_id):
     else:
         story = get_object_or_404(Story, id=story_id)
         return render(request, 'deletion.html', {'object': story})
-    
+
+@login_required
 def story_close(request, story_id):
     story = get_object_or_404(Story, id=story_id)
     story.closed = True
     story.save()
     return render(request, 'epic_details.html', {'epic': story.epic})
 
+@login_required
 def story_reopen(request, story_id):
     story = get_object_or_404(Story, id=story_id)
     story.closed = False
     story.save()
     return render(request, 'story_details.html', {'story': story})
 
+@login_required
+def story_comment(request, story_id):
+    story = get_object_or_404(Story, id=story_id)
+    comment = Comment()
+    comment.obj_uuid = story.uuid
+    comment.author = get_object_or_404(User, username=request.user)
+    comment.content = request.POST["content"]
+    comment.save()
+    return redirect('story_details', story_id)
+
+@login_required
 def epic_details(request, epic_id):
     epic = get_object_or_404(Epic, id=epic_id)
-    return render(request, 'epic_details.html', {'epic': epic})
+    comment_form = CommentForm()
+    return render(request, 'epic_details.html', {'epic': epic, 'comment_form': comment_form})
 
+@login_required
 def epic_create(request):
     if request.method == 'POST':
         
@@ -158,7 +183,7 @@ def epic_create(request):
             epic = form.save(commit=False)
             epic.author = User.objects.get(username=request.user)
             epic.save()
-            return redirect("epic_details", epic.id)
+            return redirect("backlog_editor")
         
     else:
         form = EpicForm()
@@ -166,27 +191,103 @@ def epic_create(request):
     return render(request, 'epic_form.html', {'form': form})
 
 @login_required
-def epic_edit(request, epic_id):
+def epic_edit(request, epic_id, from_=""):
     if request.method == 'POST':
         epic = get_object_or_404(Epic, id=epic_id)
         form = EpicForm(request.POST, instance=epic)
         if form.is_valid():
             form.save()
-            return redirect("epic_details", epic.id)
-        
+            if from_:
+                return redirect(from_)
+            else:
+                return redirect("epic_details", epic.id)
     else:
+        
         epic = get_object_or_404(Epic, id=epic_id)
         form = EpicForm(instance=epic)
-        
+    
     return render(request, 'epic_form.html', {'form': form})
 
 @login_required
 def epic_delete(request, epic_id):
     if request.method == 'POST':
-        epic = Epic.objects.get(id=epic_id)
+        epic = get_object_or_404(Epic, id=epic_id)
         epic.delete()
         return redirect("index")
     else:
-        epic = Epic.objects.get(id=epic_id)
+        epic = get_object_or_404(Epic, id=epic_id)
         return render(request, 'deletion.html', {'object': epic})
+
+@login_required
+def epic_value_update(request, epic_id):
+    if request.method == 'POST':
+        epic = get_object_or_404(Epic, id=epic_id)
+        epic.value = request.POST["value"]
+        epic.save()
+        return redirect("backlog_edition")
+    
+@login_required
+def epic_close(_, epic_id):
+    epic = get_object_or_404(Epic, id=epic_id)
+    epic.closed = True
+    epic.save()
+    return redirect("backlog_edition")
+
+@login_required
+def epic_reopen(_, epic_id):
+    epic = get_object_or_404(Epic, id=epic_id)
+    epic.closed = False
+    epic.save()
+    return redirect("backlog_edition")
+
+@login_required
+def epic_comment(request, epic_id):
+    epic = get_object_or_404(Epic, id=epic_id)
+    comment = Comment()
+    comment.obj_uuid = epic.uuid
+    comment.author = get_object_or_404(User, username=request.user)
+    comment.content = request.POST["content"]
+    comment.save()
+    return redirect('epic_details', epic_id)
+
+@login_required
+def reports(request):
+    return render(request, 'reports/report_index.html')
+     
+@login_required
+def report_sprints(request):
+    sprints = Sprint.objects.all()
+    return render(request, 'reports/report_sprints.html', {'sprints': sprints})
+     
+@login_required
+def report_projects(request):
+    epics = Epic.objects.all()
+    return render(request, 'reports/report_projects.html', {'epics': epics})
+
+@login_required
+def search(request):
+    qstr = request.GET["q"]
+    
+    results = []
+    results += Epic.objects.filter(name__icontains=qstr)
+    results += Story.objects.filter(name__icontains=qstr)
+    
+    results += Epic.objects.filter(description__icontains=qstr)
+    results += Story.objects.filter(description__icontains=qstr)
+    
+    if len(results) == 1:
+        r = results[0]
+        if r is Epic:
+            return redirect("epic_details", r.id)
+        else:
+            return redirect("story_details", r.id)
+    else:
+        
+        paginator = Paginator(results, 10)
+        page = request.GET.get('page')
+        results = paginator.get_page(page)
+        
+        return render(request, 'search_results.html', {'results': results, 'pages': range(1, paginator.num_pages + 1)})
+    
+    
     

+ 72 - 0
migration.py

@@ -0,0 +1,72 @@
+import csv
+import uuid
+
+from path import Path
+
+
+SRC = "E:\Backlog\backlog_db.sqlite3.20181026"
+DST = "E:\Backlog\backlog_db_new.sqlite3"
+
+DIR = Path(r"E:\Backlog\tmp")
+
+csv_path = DIR / "main_sprint.csv"
+with open(csv_path, "r") as fcsv, open(csv_path + ".new", "w+", newline='') as fnew:
+    reader = csv.reader(fcsv, delimiter='|', quotechar='"')
+    writer = csv.writer(fnew, delimiter='|', quotechar='"')
+    first = True
+    for row in reader:
+        newrow = row
+        if first:
+            newrow.insert(3, 'uuid')
+            first = False
+        else:
+            newrow.insert(3, uuid.uuid4())
+        writer.writerow(newrow)
+        
+
+csv_path = DIR / "main_project.csv"
+with open(csv_path, "r") as fcsv, open(csv_path + ".new", "w+", newline='') as fnew:
+    reader = csv.reader(fcsv, delimiter='|', quotechar='"')
+    writer = csv.writer(fnew, delimiter='|', quotechar='"')
+    first = True
+    for row in reader:
+        del row[4]
+        newrow = row
+        if first:
+            newrow.insert(3, 'uuid')
+            newrow.append('color')
+            first = False
+        else:
+            newrow.insert(3, uuid.uuid4())
+            newrow.append('')
+        writer.writerow(newrow)
+
+csv_path = DIR / "main_epic.csv"
+with open(csv_path, "r") as fcsv, open(csv_path + ".new", "w+", newline='') as fnew:
+    reader = csv.reader(fcsv, delimiter='|', quotechar='"')
+    writer = csv.writer(fnew, delimiter='|', quotechar='"')
+    first = True
+    for row in reader:
+        newrow = row
+        if first:
+            newrow.insert(3, 'uuid')
+            first = False
+        else:
+            newrow.insert(3, uuid.uuid4())
+        writer.writerow(newrow)
+        
+csv_path = DIR / "main_story.csv"
+with open(csv_path, "r") as fcsv, open(csv_path + ".new", "w+", newline='') as fnew:
+    reader = csv.reader(fcsv, delimiter='|', quotechar='"')
+    writer = csv.writer(fnew, delimiter='|', quotechar='"')
+    first = True
+    for row in reader:
+        newrow = row
+        if first:
+            newrow.insert(3, 'uuid')
+            first = False
+        else:
+            newrow.insert(3, uuid.uuid4())
+        writer.writerow(newrow)
+        
+

+ 1 - 0
runserver.cmd

@@ -0,0 +1 @@
+python manage.py runserver 8008

+ 36 - 0
todo.txt

@@ -0,0 +1,36 @@
+x Authentification
+x Breadcrumb
+x Vue 'Mes stories', filtre par auteur / assignation , et par sprint
+x Accueil: affichage du sprint en cours
+x Liste des Stories: afficher le sprint si existant, sinon: "en attente"
+x l'assignation et la selection des sprints nefonctionne pas à la création
+x rendre description epic facultative
+x ajouter le poids aux stories
+x ajouter un champs 'retrospective' aux sprints
+x messages d'erreur de validation ne s'affichent pas
+x améliorer l'affichage de la saisie des assignés
+x liste des stories: ajouter une pagination
+x reprendre les infos du backlog et du sprint
+x décider de la terminologie
+x Prévoir le cas des stories sans epic: ajouter un bouton nouvelle story à l'index, rendre l'epic non requise
+x ajouter une description et une couleur aux catégories
+x afficher le poids des stories, le(s) sprint(s) et le ou les assignés dans les index de stories / au moins si 'en cours'
+x poids des stories: remplacer la saisie libre par une selection radio-btns
+x sprint: reporter le poids de la story sur le dernier sprint
+x ajouter des champs 'en_cours' aux stories
+x mettre à jour les titres des différentes pages (title affiché sur l'onglet)
+x revoir le css sur écran moins large
+x edition des story: ajouter 'assigner à moi', 'sprint en cours'
+x stories: les filtres ne sont pas transmis au changement de page
+
+rapports:
+x backlog avec: categorie, projet, stories, actu, participants
+x bilan des sprints: nb de stories accomplies / prévues, vélocité, retrospective
+
+long-terme:
+x permettre un meilleur tri du backlog
+x ajouter une barre de recherche
+x stories: ajouter les commentaires
+* ajouter une page de fin/debut de sprint (parcourir les stories, clore ou reconduire chacune, ajouter retro)
+* signaler évènements par mail: commentaires, assignation, clotures...
+* Ajouter un graphe 'activité' pour chaque epic