From 7f91c790ec79d4d09a75fa99231dfd83bef65de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Bouttier?= Date: Sat, 2 Aug 2014 19:17:18 -0700 Subject: [PATCH] A lot of new features and improvements --- issue/migrations/0001_initial.py | 38 ++-- issue/models.py | 150 +++++++++--- issue/static/css/issue.css | 4 + issue/templates/base.html | 13 +- issue/templates/issue/issue.html | 171 ++++++++++---- issue/templates/issue/issue_comment.html | 2 + issue/templates/issue/issue_edit.html | 4 + issue/templates/issue/issue_list.html | 39 ++-- issue/templates/issue/label_edit.html | 16 +- issue/templates/issue/label_list.html | 6 +- issue/templates/issue/project.html | 6 +- issue/templates/issue/project_list.html | 51 +++-- issue/urls.py | 38 ++-- issue/views.py | 277 +++++++++++++++++------ requirements.txt | 1 + 15 files changed, 587 insertions(+), 229 deletions(-) diff --git a/issue/migrations/0001_initial.py b/issue/migrations/0001_initial.py index b75f316..f0f3d10 100644 --- a/issue/migrations/0001_initial.py +++ b/issue/migrations/0001_initial.py @@ -2,10 +2,9 @@ from __future__ import unicode_literals from django.db import models, migrations -from django.conf import settings -import re import colorful.fields import django.core.validators +from django.conf import settings class Migration(migrations.Migration): @@ -18,11 +17,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Event', fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), - ('date', models.DateTimeField(auto_now=True)), + ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), ('code', models.IntegerField(default=0)), - ('_args', models.CharField(default='{}', blank=True, max_length=1024)), - ('additionnal_section', models.TextField(default='', blank=True)), + ('_args', models.CharField(blank=True, default='{}', max_length=1024)), + ('additionnal_section', models.TextField(blank=True, default='')), ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ], options={ @@ -32,12 +31,12 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Issue', fields=[ - ('global_id', models.AutoField(serialize=False, primary_key=True)), + ('global_id', models.AutoField(primary_key=True, serialize=False)), ('id', models.IntegerField(editable=False)), ('title', models.CharField(max_length=128)), - ('opened_at', models.DateTimeField(auto_now=True)), + ('opened_at', models.DateTimeField(auto_now_add=True)), ('closed', models.BooleanField(default=False)), - ('assignee', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ('assignee', models.ForeignKey(blank=True, null=True, to=settings.AUTH_USER_MODEL)), ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ], options={ @@ -53,10 +52,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Label', fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), + ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')), ('name', models.CharField(max_length=32)), - ('color', colorful.fields.RGBColorField(default='#FFFFFF')), - ('inverted', models.BooleanField(default=True)), + ('deleted', models.BooleanField(default=False)), + ('color', colorful.fields.RGBColorField(verbose_name='Background color', default='#000000')), + ('inverted', models.BooleanField(default=True, verbose_name='Inverse text color')), ], options={ }, @@ -65,13 +65,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='issue', name='labels', - field=models.ManyToManyField(blank=True, null=True, to='issue.Label'), + field=models.ManyToManyField(blank=True, to='issue.Label', null=True), preserve_default=True, ), migrations.CreateModel( name='Milestone', fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), + ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')), ('name', models.CharField(max_length=32)), ('progression', models.SmallIntegerField(default=0)), ('due_date', models.DateTimeField(null=True)), @@ -83,9 +83,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Project', fields=[ - ('name', models.CharField(serialize=False, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+$', 32), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')], primary_key=True, max_length=32, verbose_name='Short name (used in URL)')), - ('display_name', models.CharField(max_length=32, verbose_name='Project name')), - ('description', models.TextField(default='', blank=True, verbose_name='Description')), + ('name', models.CharField(primary_key=True, verbose_name='Short name (used in URL, definitive)', validators=[django.core.validators.RegexValidator(message='Please enter only lowercase characters, number, underscores or hyphens.', regex='^[a-z0-9_-]+$')], serialize=False, max_length=32)), + ('display_name', models.CharField(verbose_name='Project name', unique=True, max_length=32)), + ('description', models.TextField(blank=True, default='', verbose_name='Description')), ], options={ }, @@ -107,10 +107,6 @@ class Migration(migrations.Migration): field=models.ForeignKey(to='issue.Project'), preserve_default=True, ), - migrations.AlterUniqueTogether( - name='label', - unique_together=set([('project', 'name')]), - ), migrations.AddField( model_name='issue', name='project', diff --git a/issue/models.py b/issue/models.py index d4e6b29..be9df93 100644 --- a/issue/models.py +++ b/issue/models.py @@ -1,6 +1,10 @@ from django.db import models from django.contrib.auth.models import User -from django.core.validators import validate_slug +from django.core.validators import RegexValidator +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe +from django.utils.html import escape +from django.core.urlresolvers import reverse import json @@ -9,17 +13,28 @@ from colorful.fields import RGBColorField class Project(models.Model): - name = models.CharField(primary_key=True, blank=False, max_length=32, - verbose_name="Short name (used in URL)", - validators=[validate_slug]) + url_name_validator = RegexValidator(regex='^[a-z0-9_-]+$', + message="Please enter only lowercase characters, number, " + "underscores or hyphens.") - display_name = models.CharField(max_length=32, + name = models.CharField(primary_key=True, blank=False, max_length=32, + verbose_name="Short name (used in URL, definitive)", + validators=[url_name_validator]) + + display_name = models.CharField(max_length=32, unique=True, verbose_name="Project name") description = models.TextField(blank=True, default="", verbose_name="Description") + def create_default_labels(self): + + Label(project=self, name='bug', color='#FF0000').save() + Label(project=self, name='feature', color='#00A000').save() + Label(project=self, name='documentation', color='#1D3DBE').save() + def __str__(self): + return self.display_name class Label(models.Model): @@ -28,12 +43,11 @@ class Label(models.Model): name = models.CharField(max_length=32) - class Meta: - unique_together = [ 'project', 'name' ] + deleted = models.BooleanField(default=False) - color = RGBColorField(default='#FFFFFF') + color = RGBColorField(default='#000000', verbose_name="Background color") - inverted = models.BooleanField(default=True) + inverted = models.BooleanField(default=True, verbose_name="Inverse text color") def style(self): @@ -79,11 +93,11 @@ class Issue(models.Model): author = models.ForeignKey(User, related_name='+') - opened_at = models.DateTimeField(auto_now=True) + opened_at = models.DateTimeField(auto_now_add=True) closed = models.BooleanField(default=False) - labels = models.ManyToManyField(Label, blank=True, null=True) + labels = models.ManyToManyField(Label, blank=True, null=True, related_name='issues') assignee = models.ForeignKey(User, blank=True, null=True, related_name='+') @@ -98,9 +112,47 @@ class Issue(models.Model): def comments(self): - comments = self.events.filter(code=Event.COMMENT) + comments = self.events.filter(issue=self,code=Event.COMMENT) - return comments[1:] + return comments + + def getdesc(self): + desc = self.events.filter(issue=self,code=Event.DESCRIBE) + if desc.count(): + return desc.first().additionnal_section + else: + return None + def setdesc(self, value): + desc = self.events.filter(issue=self,code=Event.DESCRIBE) + if desc.count(): + desc = desc.first() + desc.additionnal_section = value + desc.save() + else: + desc = Event(issue=self, author=self.author, code=Event.DESCRIBE, + additionnal_section=value) + desc.save() + def deldesc(self): + desc = self.events.filter(issue=self,code=Event.DESCRIBE) + if desc.count(): + desc.first().delete() + description = property(getdesc, setdesc, deldesc) + + def add_label(self, author, label, commit=True): + self.labels.add(label) + if commit: + self.save() + event = Event(issue=self, author=author, + code=Event.ADD_LABEL, args={'label': label.id}) + event.save() + + def remove_label(self, author, label, commit=True): + self.labels.remove(label) + if commit: + self.save() + event = Event(issue=self, author=author, + code=Event.DEL_LABEL, args={'label': label.id}) + event.save() def __str__(self): return self.title @@ -118,10 +170,11 @@ class Event(models.Model): DEL_MILESTONE = 8 REFERENCE = 9 COMMENT = 10 + DESCRIBE = 11 issue = models.ForeignKey(Issue, related_name="%(class)ss") - date = models.DateTimeField(auto_now=True) + date = models.DateTimeField(auto_now_add=True) author = models.ForeignKey(User) @@ -139,34 +192,73 @@ class Event(models.Model): additionnal_section = models.TextField(blank=True, default="") + def editable(self): + + return self.code == Event.COMMENT or self.code == Event.DESCRIBE + def boxed(self): - return self.code == Event.COMMENT + + return self.code == Event.COMMENT or self.code == Event.DESCRIBE + + def glyphicon(self): + + if self.code == Event.COMMENT or self.code == Event.DESCRIBE: + return "pencil" + elif self.code == Event.CLOSE: + return "ban-circle" + elif self.code == Event.REOPEN: + return "refresh" + elif self.code == Event.RENAME: + return "transfer" + elif self.code == Event.ADD_LABEL: + return "tag" + elif self.code == Event.DEL_LABEL: + return "tag" + elif self.code == Event.SET_MILESTONE: + return "road" + elif self.code == Event.CHANGE_MILESTONE: + return "road" + elif self.code == Event.DEL_MILESTONE: + return "road" + elif self.code == Event.REFERENCE: + return "transfer" + else: + return "cog" def __str__(self): args = self.args - if self.code == Event.COMMENT: - description = "{author} commented" + if self.code == Event.COMMENT or self.code == Event.DESCRIBE: + description = "commented" elif self.code == Event.CLOSE: - description = "{author} closed this issue" + description = "closed this issue" elif self.code == Event.REOPEN: - description = "{author} reopened this issue" + description = "reopened this issue" elif self.code == Event.RENAME: - description = "{author} changed the title from {old_title} to {new_title}" - elif self.code == Event.ADD_LABEL: - description = "{author} added the {label} label" - elif self.code == Event.DEL_LABEL: - description = "{author} deleted the {label} label" + description = "changed the title from {old_title} to {new_title}" + elif self.code == Event.ADD_LABEL or self.code == Event.DEL_LABEL: + label = Label.objects.get(id=args['label']) + description = '{action} the {label} label' + args['label'] = label.name + args['url'] = reverse('list-issue', kwargs={'project': self.issue.project.name}) + args['style'] = label.style() + if self.code == Event.ADD_LABEL: + args['action'] = 'added' + else: + args['action'] = 'removed' elif self.code == Event.SET_MILESTONE: - description = "{author} added this to the {milestone} milestone" + description = "added this to the {milestone} milestone" elif self.code == Event.CHANGE_MILESTONE: - description = "{author} moved this from the {old_milestone} milestone to the {new_mileston} milestone" + description = "moved this from the {old_milestone} milestone to the {new_mileston} milestone" elif self.code == Event.DEL_MILESTONE: - description = "{author} deleted this from the {milestone} milestone" + description = "deleted this from the {milestone} milestone" elif self.code == Event.REFERENCE: - description = "{author} referenced this issue" + description = "referenced this issue" else: return None - return description.format(author=self.author, **args) + # Escape args + safe_args = {k: escape(v) for k, v in args.items()} + + return mark_safe(description.format(**safe_args)) diff --git a/issue/static/css/issue.css b/issue/static/css/issue.css index 28a16f7..c3688c0 100644 --- a/issue/static/css/issue.css +++ b/issue/static/css/issue.css @@ -6,3 +6,7 @@ body { .navbar { margin-bottom: 20px; } + +a .remove-label { + color: #CCC; +} diff --git a/issue/templates/base.html b/issue/templates/base.html index e19a098..923f64a 100644 --- a/issue/templates/base.html +++ b/issue/templates/base.html @@ -15,12 +15,16 @@ - Issue Tracker + {% block title %}PonyTracker{% endblock %} {% block css %}{% endblock %} + + {% block js %}{% endblock %} + + {% block media %}{% endblock %} @@ -37,7 +41,7 @@ - {% block title %}Issue Tracker{% endblock %} + {% block page_title %}PonyTracker{% endblock %}