first commit for v0.2
This commit is contained in:
parent
6ba03afc73
commit
1463854a45
143 changed files with 20775 additions and 2764 deletions
1
tracker/__init__.py
Normal file
1
tracker/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = 'tracker.apps.TrackerConfig'
|
9
tracker/admin.py
Normal file
9
tracker/admin.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from tracker.models import *
|
||||
|
||||
|
||||
admin.site.register(Project)
|
||||
admin.site.register(Issue)
|
||||
admin.site.register(Label)
|
||||
admin.site.register(Milestone)
|
10
tracker/apps.py
Normal file
10
tracker/apps.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TrackerConfig(AppConfig):
|
||||
|
||||
name = 'tracker'
|
||||
verbose_name = 'Tracker'
|
||||
|
||||
def ready(self):
|
||||
import tracker.signals
|
9
tracker/context_processors.py
Normal file
9
tracker/context_processors.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from tracker.models import Project
|
||||
|
||||
|
||||
def projects(request):
|
||||
|
||||
if hasattr(request, 'projects'):
|
||||
return {'projects': request.projects}
|
||||
else:
|
||||
return {}
|
44
tracker/forms.py
Normal file
44
tracker/forms.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from django import forms
|
||||
|
||||
from bootstrap3_datetime.widgets import DateTimePicker
|
||||
from django_markdown.widgets import MarkdownWidget
|
||||
|
||||
from tracker.models import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ProjectForm', 'LabelForm', 'IssueForm', 'MilestoneForm', 'CommentForm'
|
||||
]
|
||||
|
||||
|
||||
class ProjectForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ['display_name', 'name', 'description', 'access']
|
||||
help_texts = {
|
||||
'name': 'Warning: if you change this value, this will break existing URLs.'
|
||||
}
|
||||
|
||||
|
||||
class IssueForm(forms.Form):
|
||||
title = forms.CharField(max_length=128)
|
||||
description = forms.CharField(widget=MarkdownWidget, required=False)
|
||||
|
||||
|
||||
class CommentForm(forms.Form):
|
||||
comment = forms.CharField(widget=MarkdownWidget)
|
||||
|
||||
|
||||
class LabelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ['name', 'color', 'inverted']
|
||||
|
||||
|
||||
class MilestoneForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Milestone
|
||||
fields = ['name', 'due_date']
|
||||
widgets = {
|
||||
'due_date': DateTimePicker(format="%Y-%m-%d %H:%M"),
|
||||
}
|
66
tracker/middleware.py
Normal file
66
tracker/middleware.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Q
|
||||
|
||||
from tracker.models import Project
|
||||
from permissions.models import PermissionModel as PermModel
|
||||
|
||||
|
||||
# This middleware protect only views of the following modules
|
||||
modules = [ 'accounts.views', 'permissions.views', 'tracker.views' ]
|
||||
|
||||
|
||||
class ProjectMiddleware:
|
||||
"""
|
||||
This middleware must be call after authentication middleware.
|
||||
"""
|
||||
|
||||
def process_view(self, request, view, view_args, view_kwargs):
|
||||
|
||||
if view.__module__ not in modules:
|
||||
return
|
||||
|
||||
if not hasattr(request, 'user'):
|
||||
raise ImproperlyConfigured(
|
||||
"The project middleware requires the"
|
||||
" authentication middleware to be installed. Edit your"
|
||||
" MIDDLEWARE_CLASSES setting to insert"
|
||||
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
|
||||
" before the ProjectMiddleware class.")
|
||||
|
||||
# projects
|
||||
if request.user.is_authenticated() and request.user.is_staff:
|
||||
projects = Project.objects.all()
|
||||
else:
|
||||
query = Q(access=Project.ACCESS_PUBLIC)
|
||||
if request.user.is_authenticated():
|
||||
query |= Q(access=Project.ACCESS_REGISTERED)
|
||||
# access granted through a team
|
||||
teams = request.user.teams.values_list('name')
|
||||
query |= Q(permissions__grantee_type=PermModel.GRANTEE_TEAM,
|
||||
permissions__grantee_name__in=teams)
|
||||
# access granted through a group
|
||||
groups = request.user.groups.values_list('name')
|
||||
query |= Q(permissions__grantee_type=PermModel.GRANTEE_GROUP,
|
||||
permissions__grantee_name__in=groups)
|
||||
# access granted by specific permission
|
||||
query |= Q(permissions__grantee_type=PermModel.GRANTEE_USER,
|
||||
permissions__grantee_name=request.user.username)
|
||||
projects = Project.objects.filter(query).distinct()
|
||||
request.projects = projects
|
||||
|
||||
# project
|
||||
project = view_kwargs.get('project')
|
||||
if not project:
|
||||
return
|
||||
try:
|
||||
project = projects.get(name=project)
|
||||
except ObjectDoesNotExist:
|
||||
if request.user.is_authenticated():
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
return login_required(view)(request, *view_args, **view_kwargs)
|
||||
view_kwargs['project'] = project
|
||||
request.project = project
|
131
tracker/migrations/0001_initial.py
Normal file
131
tracker/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import colorful.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('code', models.IntegerField(default=0)),
|
||||
('_args', models.CharField(default='{}', max_length=1024, blank=True)),
|
||||
('additionnal_section', models.TextField(default='', blank=True)),
|
||||
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Issue',
|
||||
fields=[
|
||||
('primarykey', models.AutoField(serialize=False, primary_key=True)),
|
||||
('id', models.IntegerField(editable=False)),
|
||||
('title', models.CharField(max_length=128)),
|
||||
('opened_at', models.DateTimeField(auto_now_add=True)),
|
||||
('closed', models.BooleanField(default=False)),
|
||||
('assignee', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, blank=True)),
|
||||
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('subscribers', models.ManyToManyField(null=True, to=settings.AUTH_USER_MODEL, blank=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='issue',
|
||||
field=models.ForeignKey(to='tracker.Issue'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Label',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('name', models.CharField(max_length=32)),
|
||||
('deleted', models.BooleanField(default=False)),
|
||||
('color', colorful.fields.RGBColorField(default='#000000', verbose_name='Background color')),
|
||||
('inverted', models.BooleanField(default=True, verbose_name='Inverse text color')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='labels',
|
||||
field=models.ManyToManyField(null=True, to='tracker.Label', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Milestone',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('name', models.CharField(validators=[django.core.validators.RegexValidator(regex='^[a-z0-9_.-]+$', message='Please enter only lowercase characters, number, dot, underscores or hyphens.')], max_length=32)),
|
||||
('due_date', models.DateTimeField(null=True, blank=True)),
|
||||
('closed', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='milestone',
|
||||
field=models.ForeignKey(to='tracker.Milestone', null=True, blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('display_name', models.CharField(max_length=32, verbose_name='Project name', unique=True)),
|
||||
('name', models.SlugField(max_length=32, verbose_name='URL name', unique=True)),
|
||||
('description', models.TextField(default='', verbose_name='Description', blank=True)),
|
||||
('access', models.IntegerField(default=1, choices=[(1, 'Public'), (2, 'Registration required'), (3, 'Private')])),
|
||||
('subscribers', models.ManyToManyField(null=True, to=settings.AUTH_USER_MODEL, blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='milestone',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='tracker.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='milestone',
|
||||
unique_together=set([('project', 'name')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='tracker.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='tracker.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='issue',
|
||||
unique_together=set([('project', 'id')]),
|
||||
),
|
||||
]
|
0
tracker/migrations/__init__.py
Normal file
0
tracker/migrations/__init__.py
Normal file
371
tracker/models.py
Normal file
371
tracker/models.py
Normal file
|
@ -0,0 +1,371 @@
|
|||
from django.db import models
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django import VERSION
|
||||
|
||||
from colorful.fields import RGBColorField
|
||||
|
||||
import json
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
from tracker.templatetags.tracker_tags import *
|
||||
|
||||
|
||||
__all__ = [ 'Project', 'Issue', 'Label', 'Milestone', 'Event' ]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Project(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = [ 'name' ]
|
||||
|
||||
ACCESS_PUBLIC = 1
|
||||
ACCESS_REGISTERED = 2
|
||||
ACCESS_PRIVATE = 3
|
||||
ACCESS_TYPE = (
|
||||
(ACCESS_PUBLIC, 'Public'),
|
||||
(ACCESS_REGISTERED, 'Registration required'),
|
||||
(ACCESS_PRIVATE, 'Private'),
|
||||
)
|
||||
|
||||
display_name = models.CharField(max_length=32, unique=True,
|
||||
verbose_name="Project name")
|
||||
|
||||
name = models.SlugField(max_length=32, unique=True,
|
||||
verbose_name="URL name")
|
||||
|
||||
description = models.TextField(blank=True, default="",
|
||||
verbose_name="Description")
|
||||
|
||||
access = models.IntegerField(choices=ACCESS_TYPE, default=ACCESS_PUBLIC)
|
||||
|
||||
subscribers = models.ManyToManyField(User, blank=True, null=True,
|
||||
related_name='subscribed_projects')
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Label(models.Model):
|
||||
|
||||
project = models.ForeignKey(Project, related_name='labels')
|
||||
|
||||
name = models.CharField(max_length=32)
|
||||
|
||||
deleted = models.BooleanField(default=False)
|
||||
|
||||
color = RGBColorField(default='#000000',
|
||||
verbose_name="Background color")
|
||||
|
||||
inverted = models.BooleanField(default=True,
|
||||
verbose_name="Inverse text color")
|
||||
|
||||
@property
|
||||
def quotted_name(self):
|
||||
if ' ' in self.name:
|
||||
name = '"' + escape(self.name) + '"'
|
||||
else:
|
||||
name = escape(self.name)
|
||||
return mark_safe(name)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Milestone(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = [ 'due_date' ]
|
||||
|
||||
name_validator = RegexValidator(regex='^[a-z0-9_.-]+$',
|
||||
message="Please enter only lowercase characters, number, "
|
||||
"dot, underscores or hyphens.")
|
||||
|
||||
project = models.ForeignKey(Project, related_name='milestones')
|
||||
|
||||
name = models.CharField(max_length=32, validators=[name_validator])
|
||||
|
||||
class Meta:
|
||||
unique_together = ['project', 'name']
|
||||
|
||||
due_date = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
closed = models.BooleanField(default=False)
|
||||
|
||||
def closed_issues(self):
|
||||
|
||||
return self.issues.filter(closed=True).count()
|
||||
|
||||
def total_issues(self):
|
||||
|
||||
return self.issues.count()
|
||||
|
||||
def progress(self):
|
||||
|
||||
closed = self.closed_issues()
|
||||
total = self.total_issues()
|
||||
|
||||
if total:
|
||||
return int(100 * closed / total)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Issue(models.Model):
|
||||
|
||||
# id is the id in the project, not the pk, so we need one
|
||||
primarykey = models.AutoField(primary_key=True)
|
||||
|
||||
project = models.ForeignKey(Project, related_name='issues')
|
||||
id = models.IntegerField(editable=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['project', 'id']
|
||||
|
||||
title = models.CharField(max_length=128)
|
||||
|
||||
author = models.ForeignKey(User, related_name='+')
|
||||
|
||||
opened_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
closed = models.BooleanField(default=False)
|
||||
|
||||
labels = models.ManyToManyField(Label, blank=True, null=True,
|
||||
related_name='issues')
|
||||
|
||||
milestone = models.ForeignKey(Milestone, blank=True, null=True,
|
||||
related_name='issues')
|
||||
|
||||
assignee = models.ForeignKey(User, blank=True, null=True, related_name='+')
|
||||
|
||||
subscribers = models.ManyToManyField(User, blank=True, null=True,
|
||||
related_name='subscribed_issues')
|
||||
|
||||
@staticmethod
|
||||
def next_id(project):
|
||||
|
||||
last_issue = project.issues.last()
|
||||
if last_issue:
|
||||
return last_issue.id + 1
|
||||
else:
|
||||
return 1
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
|
||||
return self.events.filter(code=Event.COMMENT)
|
||||
|
||||
def getdesc(self):
|
||||
desc = self.events.filter(code=Event.DESCRIBE)
|
||||
if desc.exists():
|
||||
return desc.first().additionnal_section
|
||||
else:
|
||||
return None
|
||||
|
||||
def setdesc(self, value):
|
||||
desc = self.events.filter(code=Event.DESCRIBE)
|
||||
if desc.exists():
|
||||
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(code=Event.DESCRIBE)
|
||||
if desc.exists():
|
||||
desc.first().delete()
|
||||
description = property(getdesc, setdesc, deldesc)
|
||||
|
||||
def add_label(self, author, label, commit=True):
|
||||
if self.labels.filter(pk=label.pk).exists():
|
||||
return
|
||||
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 add_milestone(self, author, milestone, commit=True):
|
||||
if self.milestone == milestone:
|
||||
return
|
||||
if self.milestone:
|
||||
event = Event(issue=self, author=author,
|
||||
code=Event.CHANGE_MILESTONE,
|
||||
args={'old_milestone': self.milestone.name,
|
||||
'new_milestone': milestone.name})
|
||||
event.save()
|
||||
else:
|
||||
event = Event(issue=self, author=author,
|
||||
code=Event.SET_MILESTONE,
|
||||
args={'milestone': milestone.name})
|
||||
event.save()
|
||||
self.milestone = milestone
|
||||
if commit:
|
||||
self.save()
|
||||
|
||||
def remove_milestone(self, author, milestone, commit=True):
|
||||
self.milestone = None
|
||||
if commit:
|
||||
self.save()
|
||||
event = Event(issue=self, author=author,
|
||||
code=Event.UNSET_MILESTONE,
|
||||
args={'milestone': milestone.name})
|
||||
event.save()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Event(models.Model):
|
||||
|
||||
UNKNOW = 0
|
||||
CLOSE = 1
|
||||
REOPEN = 2
|
||||
RENAME = 3
|
||||
ADD_LABEL = 4
|
||||
DEL_LABEL = 5
|
||||
SET_MILESTONE = 6
|
||||
CHANGE_MILESTONE = 7
|
||||
UNSET_MILESTONE = 8
|
||||
REFERENCE = 9
|
||||
COMMENT = 10
|
||||
DESCRIBE = 11
|
||||
ASSIGN = 12
|
||||
UNASSIGN = 13
|
||||
|
||||
issue = models.ForeignKey(Issue, related_name="%(class)ss")
|
||||
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
author = models.ForeignKey(User)
|
||||
|
||||
code = models.IntegerField(default=UNKNOW)
|
||||
|
||||
_args = models.CharField(max_length=1024, blank=True, default="{}")
|
||||
|
||||
def getargs(self):
|
||||
return json.loads(self._args)
|
||||
|
||||
def setargs(self, args):
|
||||
self._args = json.dumps(args)
|
||||
|
||||
def delargs(self):
|
||||
self._args = "{}"
|
||||
args = property(getargs, setargs, delargs)
|
||||
|
||||
additionnal_section = models.TextField(blank=True, default="")
|
||||
|
||||
def editable(self):
|
||||
|
||||
return self.code == Event.COMMENT or self.code == Event.DESCRIBE
|
||||
|
||||
def glyphicon(self):
|
||||
|
||||
if self.code == Event.COMMENT:
|
||||
return "comment"
|
||||
elif self.code == Event.DESCRIBE:
|
||||
return "edit"
|
||||
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 \
|
||||
or self.code == Event.DEL_LABEL:
|
||||
return "tag"
|
||||
elif self.code == Event.SET_MILESTONE \
|
||||
or self.code == Event.CHANGE_MILESTONE \
|
||||
or self.code == Event.UNSET_MILESTONE:
|
||||
return "road"
|
||||
elif self.code == Event.REFERENCE:
|
||||
return "transfer"
|
||||
elif self.code == Event.ASSIGN \
|
||||
or self.code == Event.UNASSIGN:
|
||||
return "user"
|
||||
else:
|
||||
return "cog"
|
||||
|
||||
def __str__(self):
|
||||
|
||||
args = self.args
|
||||
|
||||
if self.code == Event.COMMENT or self.code == Event.DESCRIBE:
|
||||
description = "commented"
|
||||
elif self.code == Event.CLOSE:
|
||||
description = "closed this issue"
|
||||
elif self.code == Event.REOPEN:
|
||||
description = "reopened this issue"
|
||||
elif self.code == Event.RENAME:
|
||||
description = "changed the title from <mark>{old_title}</mark> " \
|
||||
"to <mark>{new_title}</mark>"
|
||||
elif self.code == Event.ADD_LABEL or self.code == Event.DEL_LABEL:
|
||||
label = Label.objects.get(id=args['label'])
|
||||
if self.code == Event.ADD_LABEL:
|
||||
action = 'added'
|
||||
else:
|
||||
action = 'removed'
|
||||
description = '%s the <a href="%s" class="label" ' \
|
||||
'style="%s">%s</a> label' \
|
||||
% (action, same_label(label),
|
||||
label_style(label), label)
|
||||
elif self.code == Event.SET_MILESTONE \
|
||||
or self.code == Event.UNSET_MILESTONE:
|
||||
milestone = Milestone(name=args['milestone'],
|
||||
project=self.issue.project)
|
||||
if self.code == Event.SET_MILESTONE:
|
||||
action = 'added'
|
||||
else:
|
||||
action = 'removed'
|
||||
description = '%s this to the <span class="glyphicon ' \
|
||||
'glyphicon-road"></span> <a href="%s">' \
|
||||
'<b>{milestone}</b></a> milestone' \
|
||||
% (action, same_milestone(milestone))
|
||||
elif self.code == Event.CHANGE_MILESTONE:
|
||||
old_ms = Milestone(name=args['old_milestone'],
|
||||
project=self.issue.project)
|
||||
new_ms = Milestone(name=args['new_milestone'],
|
||||
project=self.issue.project)
|
||||
description = 'moved this from the <span class="glyphicon ' \
|
||||
'glyphicon-road"></span> <a href="%s">' \
|
||||
'<b>{old_milestone}</b></a> milestone ' \
|
||||
'to the <span class="glyphicon ' \
|
||||
'glyphicon-road"></span> <a href="%s">' \
|
||||
'<b>{new_milestone}</b></a> milestone' \
|
||||
% (same_milestone(old_ms), same_milestone(new_ms))
|
||||
elif self.code == Event.REFERENCE:
|
||||
description = "referenced this issue"
|
||||
else:
|
||||
return None
|
||||
|
||||
# Escape args
|
||||
safe_args = {k: escape(v) for k, v in args.items()}
|
||||
|
||||
return mark_safe(description.format(**safe_args))
|
||||
|
||||
|
||||
if VERSION < (1, 7):
|
||||
import tracker.signals
|
111
tracker/notifications.py
Normal file
111
tracker/notifications.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
if 'djcelery' in settings.INSTALLED_APPS:
|
||||
from tracker.tasks import send_mass_mail
|
||||
else:
|
||||
from django.core.mail import send_mass_mail
|
||||
|
||||
|
||||
__all__ = [
|
||||
'notify_new_issue', 'notify_new_comment',
|
||||
'notify_close_issue', 'notify_reopen_issue'
|
||||
]
|
||||
|
||||
|
||||
def notify_new_issue(issue):
|
||||
|
||||
project = issue.project
|
||||
dests = project.subscribers.all().distinct()
|
||||
|
||||
if hasattr(settings, 'FROM_ADDR'):
|
||||
from_addr = settings.FROM_ADDR
|
||||
else:
|
||||
return
|
||||
|
||||
subject = "[%s] %s" % (project, issue.title)
|
||||
|
||||
data = []
|
||||
for dest in dests:
|
||||
|
||||
if dest == issue.author:
|
||||
continue
|
||||
|
||||
dest_addr = dest.email
|
||||
if not dest_addr:
|
||||
continue
|
||||
|
||||
c = {
|
||||
'description': issue.description,
|
||||
'uri': settings.BASE_URL + reverse('show-issue',
|
||||
args=[project.name, issue.id]),
|
||||
}
|
||||
|
||||
message = render_to_string('emails/new_issue.html', c)
|
||||
|
||||
data += [(subject, message,
|
||||
"%s <%s>" % (issue.author.username, from_addr), [dest_addr])]
|
||||
|
||||
if 'djcelery' in settings.INSTALLED_APPS:
|
||||
send_mass_mail.delay(tuple(data))
|
||||
else:
|
||||
send_mass_mail(tuple(data))
|
||||
|
||||
|
||||
def notify_new_comment(event):
|
||||
notify_event(event, 'new_comment')
|
||||
|
||||
|
||||
def notify_close_issue(event):
|
||||
notify_event(event, 'close_issue')
|
||||
|
||||
|
||||
def notify_reopen_issue(event):
|
||||
notify_event(event, 'reopen_issue')
|
||||
|
||||
|
||||
def notify_event(event, template):
|
||||
|
||||
issue = event.issue
|
||||
project = issue.project
|
||||
|
||||
dests = issue.subscribers.all()
|
||||
dests |= project.subscribers.all()
|
||||
dests = dests.distinct()
|
||||
|
||||
if hasattr(settings, 'FROM_ADDR'):
|
||||
from_addr = settings.FROM_ADDR
|
||||
else:
|
||||
return
|
||||
|
||||
subject = "Re: [%s] %s" % (project, issue.title)
|
||||
|
||||
data = []
|
||||
|
||||
for dest in dests:
|
||||
|
||||
if dest == event.author:
|
||||
continue
|
||||
|
||||
dest_addr = dest.email
|
||||
if not dest_addr:
|
||||
continue
|
||||
|
||||
c = {
|
||||
'comment': event.additionnal_section,
|
||||
'uri': settings.BASE_URL + reverse('show-issue',
|
||||
args=[project.name, issue.id]),
|
||||
}
|
||||
|
||||
message = render_to_string('emails/%s.html' % template, c)
|
||||
|
||||
data += [(subject, message,
|
||||
'%s <%s>' % (event.author.username, from_addr), [dest_addr])]
|
||||
|
||||
if 'djcelery' in settings.INSTALLED_APPS:
|
||||
send_mass_mail.delay(tuple(data))
|
||||
else:
|
||||
send_mass_mail(tuple(data))
|
14
tracker/signals.py
Normal file
14
tracker/signals.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from tracker.models import Project, Label
|
||||
|
||||
|
||||
@receiver(post_save, sender=Project, dispatch_uid="Default project labels.")
|
||||
def create_default_project_labels(sender, instance, created, **kwargs):
|
||||
if not created:
|
||||
return
|
||||
if not instance.labels.exists():
|
||||
Label(project=instance, name='bug', color='#FF0000').save()
|
||||
Label(project=instance, name='feature', color='#00A000').save()
|
||||
Label(project=instance, name='documentation', color='#1D3DBE').save()
|
8
tracker/tasks.py
Normal file
8
tracker/tasks.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.core.mail import send_mass_mail as django_send_mass_mail
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_mass_mail(datatuple):
|
||||
django_send_mass_mail(datatuple)
|
0
tracker/templatetags/__init__.py
Normal file
0
tracker/templatetags/__init__.py
Normal file
56
tracker/templatetags/tracker_tags.py
Normal file
56
tracker/templatetags/tracker_tags.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from django import template
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def label_style(label):
|
||||
|
||||
if label.inverted:
|
||||
fg = '#fff'
|
||||
else:
|
||||
fg = '#000'
|
||||
|
||||
style = "background-color: {bg}; color: {fg}; vertical-align: middle;"
|
||||
|
||||
return style.format(bg=label.color, fg=fg)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def labeled(label):
|
||||
|
||||
html = '<span class="label" style="{style}">{name}</span>'
|
||||
|
||||
return mark_safe(html.format(style=label_style(label),
|
||||
name=escape(label.name)))
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def same_label(label):
|
||||
|
||||
url = reverse('list-issue', kwargs={'project': label.project.name})
|
||||
url += '?q=is:open%20label:' + label.quotted_name
|
||||
|
||||
return mark_safe(url)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def same_milestone(milestone):
|
||||
|
||||
url = reverse('list-issue', kwargs={'project': milestone.project.name})
|
||||
url += '?q=is:open%20milestone:' + milestone.name
|
||||
|
||||
return mark_safe(url)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def same_author(context, author):
|
||||
|
||||
url = reverse('list-issue', kwargs={'project': context['project'].name})
|
||||
url += '?q=is:open%20author:' + author.username
|
||||
|
||||
return mark_safe(url)
|
3
tracker/tests.py
Normal file
3
tracker/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
45
tracker/urls.py
Normal file
45
tracker/urls.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from django.conf.urls import url, include
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Administration: redirect on first available admin page
|
||||
url(r'^admin/$', 'tracker.views.admin', name='admin'),
|
||||
# Settings
|
||||
url(r'^admin/settings/$', 'tracker.views.settings', name='settings'),
|
||||
# Projects
|
||||
url(r'^$', 'tracker.views.project_list', name='list-project'),
|
||||
url(r'^add/$', 'tracker.views.project_add', name='add-project'),
|
||||
url(r'^(?P<project>[-\w]+)/edit/$', 'tracker.views.project_edit', name='edit-project'),
|
||||
url(r'^(?P<project>[-\w]+)/delete/$', 'tracker.views.project_delete', name='delete-project'),
|
||||
url(r'^(?P<project>[-\w]+)/subscribe/$', 'tracker.views.project_subscribe', name='subscribe-project'),
|
||||
url(r'^(?P<project>[-\w]+)/unsubscribe/$', 'tracker.views.project_unsubscribe', name='unsubscribe-project'),
|
||||
# Issues
|
||||
url(r'^(?P<project>[-\w]+)/issues/$', 'tracker.views.issue_list', name='list-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/add/$', 'tracker.views.issue_edit', name='add-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/$', 'tracker.views.issue_details', name='show-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/edit/$', 'tracker.views.issue_edit', name='edit-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/delete/$', 'tracker.views.issue_delete', name='delete-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/close/$', 'tracker.views.issue_close', name='close-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/reopen/$', 'tracker.views.issue_reopen', name='reopen-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/comment/$', 'tracker.views.issue_comment_edit', name='comment-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/comments/(?P<comment>[0-9]+)/edit/$', 'tracker.views.issue_comment_edit', name='edit-comment'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/comments/(?P<comment>[0-9]+)/delete/$', 'tracker.views.issue_comment_delete', name='delete-comment'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/subscribe/$', 'tracker.views.issue_subscribe', name='subscribe-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/unsubscribe/$', 'tracker.views.issue_unsubscribe', name='unsubscribe-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/add-label/(?P<label>[0-9]+)/$', 'tracker.views.issue_add_label', name='add-label-to-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/remove-label/(?P<label>[0-9]+)/$', 'tracker.views.issue_remove_label', name='remove-label-from-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/add-milestone/(?P<milestone>[a-z0-9_.-]+)/$', 'tracker.views.issue_add_milestone', name='add-milestone-to-issue'),
|
||||
url(r'^(?P<project>[-\w]+)/issues/(?P<issue>[0-9]+)/remove-milestone/(?P<milestone>[a-z0-9_.-]+)/$', 'tracker.views.issue_remove_milestone', name='remove-milestone-from-issue'),
|
||||
# Labels
|
||||
url(r'^(?P<project>[-\w]+)/labels/$', 'tracker.views.label_list', name='list-label'),
|
||||
url(r'^(?P<project>[-\w]+)/labels/add/$', 'tracker.views.label_edit', name='add-label'),
|
||||
url(r'^(?P<project>[-\w]+)/labels/(?P<id>[0-9]+)/edit/$', 'tracker.views.label_edit', name='edit-label'),
|
||||
url(r'^(?P<project>[-\w]+)/labels/(?P<id>[0-9]+)/delete/$', 'tracker.views.label_delete', name='delete-label'),
|
||||
# Milestones
|
||||
url(r'^(?P<project>[-\w]+)/milestones/$', 'tracker.views.milestone_list', name='list-milestone'),
|
||||
url(r'^(?P<project>[-\w]+)/milestones/add/$', 'tracker.views.milestone_edit', name='add-milestone'),
|
||||
url(r'^(?P<project>[-\w]+)/milestones/(?P<name>[a-z0-9_.-]+)/edit/$', 'tracker.views.milestone_edit', name='edit-milestone'),
|
||||
url(r'^(?P<project>[-\w]+)/milestones/(?P<name>[a-z0-9_.-]+)/close/$', 'tracker.views.milestone_close', name='close-milestone'),
|
||||
url(r'^(?P<project>[-\w]+)/milestones/(?P<name>[a-z0-9_.-]+)/reopen/$', 'tracker.views.milestone_reopen', name='reopen-milestone'),
|
||||
url(r'^(?P<project>[-\w]+)/milestones/(?P<name>[a-z0-9_.-]+)/delete/$', 'tracker.views.milestone_delete', name='delete-milestone'),
|
||||
]
|
760
tracker/views.py
Normal file
760
tracker/views.py
Normal file
|
@ -0,0 +1,760 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from tracker.forms import *
|
||||
from tracker.models import *
|
||||
from tracker.notifications import *
|
||||
from accounts.models import User
|
||||
from permissions.models import ProjectPermission
|
||||
from permissions.decorators import project_perm_required
|
||||
|
||||
import shlex
|
||||
|
||||
|
||||
#########
|
||||
# Admin #
|
||||
#########
|
||||
|
||||
@login_required
|
||||
def admin(request):
|
||||
if request.user.has_perm('manage_settings'):
|
||||
return redirect('settings')
|
||||
elif request.user.has_perm('manage_user'):
|
||||
return redirect('list-user')
|
||||
elif request.user.has_perm('manage_group'):
|
||||
return redirect('list-group')
|
||||
elif request.user.has_perm('manage_team'):
|
||||
return redirect('list-team')
|
||||
elif request.user.has_perm('manage_global_permission'):
|
||||
return redirect('list-global-permission')
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
############
|
||||
# Settings #
|
||||
############
|
||||
|
||||
@project_perm_required('manage_settings')
|
||||
def settings(request):
|
||||
return render(request, 'tracker/settings.html')
|
||||
|
||||
|
||||
############
|
||||
# Projects #
|
||||
############
|
||||
|
||||
def project_list(request):
|
||||
|
||||
if not request.projects.exists():
|
||||
|
||||
if request.user.has_perm('create_project'):
|
||||
messages.info(request, 'Start by creating a project.')
|
||||
return redirect('add-project')
|
||||
|
||||
return render(request, 'tracker/project_list.html')
|
||||
|
||||
|
||||
@project_perm_required('create_project')
|
||||
def project_add(request):
|
||||
|
||||
form = ProjectForm(request.POST or None)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
name = form.cleaned_data['name']
|
||||
if Project.objects.filter(name__iexact=name).exists():
|
||||
form._errors['name'] = ['There is already a project '
|
||||
'with a similar name.']
|
||||
else:
|
||||
project = form.save()
|
||||
messages.success(request, 'Project added successfully.')
|
||||
project.subscribers.add(request.user)
|
||||
ProjectPermission.objects.create(project=project,
|
||||
manage_project_permission=True,
|
||||
grantee=request.user)
|
||||
return redirect('list-project-permission', project.name)
|
||||
|
||||
c = {
|
||||
'form': form,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/project_add.html', c)
|
||||
|
||||
|
||||
@project_perm_required('modify_project')
|
||||
def project_edit(request, project):
|
||||
|
||||
form = ProjectForm(request.POST or None, instance=project)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
name = form.cleaned_data['display_name']
|
||||
if Project.objects.filter(display_name__iexact=name) \
|
||||
.exclude(pk=project.pk).exists():
|
||||
form._errors['display_name'] = ['There is already a project '
|
||||
'with a similar name.']
|
||||
else:
|
||||
project = form.save()
|
||||
messages.success(request, 'Project modified successfully.')
|
||||
return redirect('list-issue', project.name)
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'form': form,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/project_edit.html', c)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('delete_project')
|
||||
def project_delete(request, project):
|
||||
|
||||
project.delete()
|
||||
|
||||
messages.success(request, 'Project deleted successfully.')
|
||||
|
||||
return redirect('list-project')
|
||||
|
||||
|
||||
@login_required
|
||||
def project_subscribe(request, project):
|
||||
|
||||
if project.subscribers.filter(pk=request.user.pk).exists():
|
||||
messages.warning(request,
|
||||
'You are already subscribed to this project.')
|
||||
else:
|
||||
project.subscribers.add(request.user)
|
||||
project.save()
|
||||
messages.success(request,
|
||||
'You have been subscribed to this project successfully.')
|
||||
|
||||
next = request.GET.get('next')
|
||||
if next:
|
||||
return redirect(next)
|
||||
else:
|
||||
return redirect('list-issue', project.name)
|
||||
|
||||
|
||||
@login_required
|
||||
def project_unsubscribe(request, project):
|
||||
|
||||
if project.subscribers.filter(pk=request.user.pk).exists():
|
||||
project.subscribers.remove(request.user)
|
||||
project.save()
|
||||
messages.success(request, 'You will not receive any notifications '
|
||||
'for this project anymore.')
|
||||
else:
|
||||
messages.warning(request, 'You are not subscribed to this project.')
|
||||
|
||||
next = request.GET.get('next')
|
||||
if next:
|
||||
return redirect(next)
|
||||
else:
|
||||
return redirect('list-issue', project.name)
|
||||
|
||||
|
||||
##########
|
||||
# Issues #
|
||||
##########
|
||||
|
||||
def issue_list(request, project):
|
||||
|
||||
issues = project.issues
|
||||
|
||||
labels = Label.objects.filter(project=project)
|
||||
milestones = Milestone.objects.filter(project=project)
|
||||
|
||||
sort = request.GET.get('sort', '')
|
||||
if sort:
|
||||
sort = '&' + sort
|
||||
|
||||
is_open = ''
|
||||
is_close = ''
|
||||
is_all = ''
|
||||
is_all_query = ''
|
||||
|
||||
query = request.GET.get('q', '')
|
||||
|
||||
if query == '':
|
||||
query = 'is:open'
|
||||
|
||||
syntaxe_error = False
|
||||
for constraint in shlex.split(query):
|
||||
|
||||
if constraint == '*':
|
||||
continue
|
||||
|
||||
args = constraint.split(':')
|
||||
|
||||
if len(args) != 2:
|
||||
messages.error(request, 'There is a syntaxe error in your filter.')
|
||||
issues = None
|
||||
break
|
||||
|
||||
key = args[0]
|
||||
value = args[1]
|
||||
|
||||
if key == '':
|
||||
continue
|
||||
|
||||
elif key == 'is':
|
||||
if value == 'open':
|
||||
issues = issues.filter(closed=False)
|
||||
is_open = ' active'
|
||||
elif value == 'close':
|
||||
issues = issues.filter(closed=True)
|
||||
is_close = ' active'
|
||||
else:
|
||||
messages.error(request, "The keyword 'is' must be followed "
|
||||
"by 'open' or 'close'.")
|
||||
issues = None
|
||||
break
|
||||
|
||||
elif key == 'label':
|
||||
try:
|
||||
label = Label.objects.get(project=project,
|
||||
name=value, deleted=False)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, "The label '%s' does not exist "
|
||||
"or has been deleted." % value)
|
||||
issues = None
|
||||
break
|
||||
else:
|
||||
issues = issues.filter(labels=label)
|
||||
labels = labels.exclude(pk=label.pk)
|
||||
|
||||
elif key == 'milestone':
|
||||
try:
|
||||
milestone = Milestone.objects.get(project=project, name=value)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, "The milestone '%s' does not exist."
|
||||
% value)
|
||||
issues = None
|
||||
break
|
||||
else:
|
||||
issues = issues.filter(milestone=milestone)
|
||||
milestones = milestones.exclude(pk=milestone.pk)
|
||||
|
||||
elif key == 'author' or key == 'user':
|
||||
if User.objects.filter(username=value).exists():
|
||||
issues = issues.filter(author__username=value)
|
||||
else:
|
||||
messages.error(request, "The user '%s' does not exist."
|
||||
% value)
|
||||
issues = None
|
||||
break
|
||||
|
||||
else:
|
||||
messages.error(request, "Unknow '%s' filtering criterion." % key)
|
||||
issues = None
|
||||
break
|
||||
|
||||
if key != 'is':
|
||||
is_all_query += ' ' + constraint
|
||||
|
||||
if issues:
|
||||
issues = issues.extra(order_by=['-opened_at'])
|
||||
|
||||
if is_open == '' and is_close == '':
|
||||
is_all = ' active'
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'issues': issues,
|
||||
'query': query,
|
||||
'sort': sort,
|
||||
'is_open': is_open,
|
||||
'is_close': is_close,
|
||||
'is_all': is_all,
|
||||
'is_all_query': is_all_query[1:],
|
||||
'labels': labels,
|
||||
'milestones': milestones,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/issue_list.html', c)
|
||||
|
||||
|
||||
@login_required
|
||||
def issue_edit(request, project, issue=None):
|
||||
|
||||
if issue:
|
||||
if not request.user.has_perm('modify_issue', project):
|
||||
raise PermissionDenied()
|
||||
issue = get_object_or_404(Issue, project=project.name, id=issue)
|
||||
init_data = {'title': issue.title,
|
||||
'description': issue.description}
|
||||
else:
|
||||
if not request.user.has_perm('create_issue', project):
|
||||
raise PermissionDenied()
|
||||
issue = None
|
||||
init_data = None
|
||||
|
||||
form = IssueForm(request.POST or init_data)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
title = form.cleaned_data['title']
|
||||
description = form.cleaned_data['description']
|
||||
|
||||
if issue:
|
||||
|
||||
modified = False
|
||||
|
||||
if issue.title != title:
|
||||
old_title = issue.title
|
||||
issue.title = title
|
||||
issue.save()
|
||||
event = Event(issue=issue, author=request.user,
|
||||
code=Event.RENAME,
|
||||
args={'old_title': old_title, 'new_title': title})
|
||||
event.save()
|
||||
modified = True
|
||||
|
||||
if issue.description != description:
|
||||
issue.description = description
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
messages.success(request, 'Issue updated successfully.')
|
||||
else:
|
||||
messages.info(request, 'Issue not modified.')
|
||||
|
||||
else:
|
||||
|
||||
issue = Issue(title=title, author=request.user,
|
||||
project=project, id=Issue.next_id(project))
|
||||
issue.save()
|
||||
issue.subscribers.add(request.user)
|
||||
issue.description = description
|
||||
notify_new_issue(issue)
|
||||
messages.success(request, 'Issue created successfully.')
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'form': form,
|
||||
'issue': issue,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/issue_edit.html', c)
|
||||
|
||||
|
||||
def issue_details(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
|
||||
labels = Label.objects.filter(project=project, deleted=False) \
|
||||
.exclude(id__in=issue.labels.all().values_list('id'))
|
||||
milestones = Milestone.objects.filter(project=project)
|
||||
if issue.milestone:
|
||||
milestones = milestones.exclude(name=issue.milestone.name)
|
||||
|
||||
events = issue.events.all()
|
||||
|
||||
if request.user.has_perm('create_comment', project):
|
||||
form = CommentForm(request.POST or None)
|
||||
else:
|
||||
form = None
|
||||
|
||||
c = {
|
||||
'labels': labels,
|
||||
'milestones': milestones,
|
||||
'project': project,
|
||||
'issue': issue,
|
||||
'events': events,
|
||||
'form': form,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/issue_details.html', c)
|
||||
|
||||
|
||||
@login_required
|
||||
def issue_comment_edit(request, project, issue, comment=None):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
|
||||
if comment:
|
||||
if not request.user.has_perm('modify_comment', project):
|
||||
raise PermissionDenied()
|
||||
event = get_object_or_404(Event, code=Event.COMMENT,
|
||||
issue=issue, id=comment)
|
||||
init_data = {'comment': event.additionnal_section}
|
||||
else:
|
||||
if not request.user.has_perm('create_comment', project):
|
||||
raise PermissionDenied()
|
||||
event = None
|
||||
init_data = None
|
||||
|
||||
form = CommentForm(request.POST or init_data)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
comment = form.cleaned_data['comment']
|
||||
|
||||
if event:
|
||||
|
||||
if event.additionnal_section != comment:
|
||||
event.additionnal_section = comment
|
||||
event.save()
|
||||
messages.success(request, 'Comment modified successfully.')
|
||||
else:
|
||||
messages.info(request, 'Comment not modified.')
|
||||
|
||||
else:
|
||||
|
||||
event = Event(issue=issue, author=request.user,
|
||||
code=Event.COMMENT, additionnal_section=comment)
|
||||
event.save()
|
||||
issue.subscribers.add(request.user)
|
||||
notify_new_comment(event)
|
||||
messages.success(request, 'Comment added successfully.')
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'issue': issue,
|
||||
'comment': event,
|
||||
'form': form,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/comment_edit.html', c)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('delete_comment')
|
||||
def issue_comment_delete(request, project, issue, comment):
|
||||
|
||||
comment = get_object_or_404(Event,
|
||||
issue__project=project, issue__id=issue, id=comment)
|
||||
|
||||
comment.delete()
|
||||
messages.success(request, 'Comment deleted successfully.')
|
||||
|
||||
return redirect('show-issue', project.name, issue)
|
||||
|
||||
|
||||
@project_perm_required('manage_issue')
|
||||
def issue_close(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue, closed=False)
|
||||
|
||||
issue.closed = True
|
||||
issue.save()
|
||||
|
||||
event = Event(issue=issue, author=request.user, code=Event.CLOSE)
|
||||
event.save()
|
||||
|
||||
notify_close_issue(event)
|
||||
|
||||
return redirect('list-issue', project.name)
|
||||
|
||||
|
||||
@project_perm_required('manage_issue')
|
||||
def issue_reopen(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue, closed=True)
|
||||
|
||||
issue.closed = False
|
||||
issue.save()
|
||||
|
||||
event = Event(issue=issue, author=request.user, code=Event.REOPEN)
|
||||
event.save()
|
||||
|
||||
notify_reopen_issue(event)
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('delete_issue')
|
||||
def issue_delete(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
|
||||
issue.delete()
|
||||
|
||||
messages.success(request, 'Issue deleted successfully.')
|
||||
|
||||
return redirect('list-issue', project.name)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def issue_add_label(request, project, issue, label):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
label = get_object_or_404(Label, project=project, id=label)
|
||||
|
||||
issue.add_label(request.user, label)
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def issue_remove_label(request, project, issue, label):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
label = get_object_or_404(Label, project=project, id=label)
|
||||
|
||||
issue.remove_label(request.user, label)
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def issue_add_milestone(request, project, issue, milestone):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
milestone = get_object_or_404(Milestone, project=project, name=milestone)
|
||||
|
||||
issue.add_milestone(request.user, milestone)
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def issue_remove_milestone(request, project, issue, milestone):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
milestone = get_object_or_404(Milestone, project=project, name=milestone)
|
||||
|
||||
issue.remove_milestone(request.user, milestone)
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def issue_subscribe(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
|
||||
if issue.subscribers.filter(pk=user.pk).exists():
|
||||
messages.warning(request, 'You are already subscribed to this issue.')
|
||||
else:
|
||||
issue.subscribers.add(request.user)
|
||||
issue.save()
|
||||
messages.success(request,
|
||||
'You have been subscribed to this issue successfully.')
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def issue_unsubscribe(request, project, issue):
|
||||
|
||||
issue = get_object_or_404(Issue, project=project, id=issue)
|
||||
|
||||
if issue.subscribers.filter(pk=user.pk).exists():
|
||||
issue.subscribers.remove(request.user)
|
||||
issue.save()
|
||||
messages.success(request, 'You will not receive any notifications '
|
||||
'for this issue anymore.')
|
||||
else:
|
||||
messages.warning(request, 'You are not subscribed to this issue.')
|
||||
|
||||
return redirect('show-issue', project.name, issue.id)
|
||||
|
||||
|
||||
##########
|
||||
# Labels #
|
||||
##########
|
||||
|
||||
def label_list(request, project):
|
||||
|
||||
labels = project.labels.filter(deleted=False)
|
||||
|
||||
return render(request, 'tracker/label_list.html', {
|
||||
'project': project,
|
||||
'labels': labels,
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def label_edit(request, project, id=None):
|
||||
|
||||
if id:
|
||||
label = get_object_or_404(Label, project=project, id=id)
|
||||
else:
|
||||
label = None
|
||||
|
||||
form = LabelForm(request.POST or None, instance=label)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
similar = Label.objects.filter(project=project,
|
||||
name=form.cleaned_data['name'], deleted=False)
|
||||
|
||||
if label:
|
||||
similar = similar.exclude(pk=label.pk)
|
||||
|
||||
if similar.exists():
|
||||
|
||||
form._errors['name'] = ['There is already a label with this name.']
|
||||
|
||||
else:
|
||||
|
||||
if label:
|
||||
form.save()
|
||||
messages.success(request, 'Label modified successfully.')
|
||||
else:
|
||||
label = form.save(commit=False)
|
||||
label.project = project
|
||||
label.save()
|
||||
messages.success(request, 'Label added successfully.')
|
||||
|
||||
issue = request.GET.get('issue')
|
||||
if issue:
|
||||
return redirect('add-label-to-issue', project.name,
|
||||
issue, label.id)
|
||||
|
||||
return redirect('list-label', project.name)
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'form': form,
|
||||
'label': label,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/label_edit.html', c)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('delete_tags')
|
||||
def label_delete(request, project, id):
|
||||
|
||||
label = get_object_or_404(Label, project=project, id=id)
|
||||
|
||||
for issue in label.issues.all():
|
||||
issue.remove_label(request.user, label)
|
||||
label.deleted = True
|
||||
label.save()
|
||||
|
||||
messages.success(request, "Label deleted successfully.")
|
||||
|
||||
return redirect('list-label', project.name)
|
||||
|
||||
|
||||
##########
|
||||
# Labels #
|
||||
##########
|
||||
|
||||
def milestone_list(request, project):
|
||||
|
||||
show = request.GET.get('show', 'open')
|
||||
|
||||
if show == 'open':
|
||||
milestones = project.milestones.filter(closed=False)
|
||||
elif show == 'close':
|
||||
milestones = project.milestones.filter(closed=True)
|
||||
elif show == 'all':
|
||||
milestones = project.milestones.all()
|
||||
else:
|
||||
messages.error(request, 'There is an error in your filter.')
|
||||
milestones = None
|
||||
|
||||
return render(request, 'tracker/milestone_list.html', {
|
||||
'project': project,
|
||||
'milestones': milestones,
|
||||
'show': show,
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def milestone_edit(request, project, name=None):
|
||||
|
||||
if name:
|
||||
milestone = get_object_or_404(Milestone, project=project, name=name)
|
||||
else:
|
||||
milestone = None
|
||||
|
||||
form = MilestoneForm(request.POST or None, instance=milestone)
|
||||
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
|
||||
similar = Milestone.objects.filter(project=project,
|
||||
name=form.cleaned_data['name'])
|
||||
|
||||
if milestone:
|
||||
similar = similar.exclude(pk=milestone.pk)
|
||||
|
||||
if similar.exists():
|
||||
|
||||
form._errors['name'] = ['There is already a milestone '
|
||||
'with this name.']
|
||||
|
||||
else:
|
||||
|
||||
if milestone:
|
||||
if name != form.cleaned_data['name']:
|
||||
for issue in milestone.issues.all():
|
||||
event = Event(issue=issue, author=request.user,
|
||||
code=Event.CHANGE_MILESTONE, args={
|
||||
'old_milestone': name,
|
||||
'new_milestone': form.cleaned_data['name']
|
||||
})
|
||||
event.save()
|
||||
form.save()
|
||||
messages.success(request, 'Milestone modified successfully.')
|
||||
else:
|
||||
milestone = form.save(commit=False)
|
||||
milestone.project = project
|
||||
milestone.save()
|
||||
messages.success(request, 'Milestone added successfully.')
|
||||
|
||||
issue = request.GET.get('issue')
|
||||
if issue:
|
||||
return redirect('add-milestone-to-issue', project.name, issue,
|
||||
milestone.name)
|
||||
|
||||
return redirect('list-milestone', project.name)
|
||||
|
||||
c = {
|
||||
'project': project,
|
||||
'form': form,
|
||||
'milestone': milestone,
|
||||
}
|
||||
|
||||
return render(request, 'tracker/milestone_edit.html', c)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def milestone_close(request, project, name):
|
||||
|
||||
milestone = get_object_or_404(Milestone, project=project, name=name)
|
||||
|
||||
milestone.closed = True
|
||||
milestone.save()
|
||||
|
||||
return redirect('list-milestone', project.name)
|
||||
|
||||
|
||||
@project_perm_required('manage_tags')
|
||||
def milestone_reopen(request, project, name):
|
||||
|
||||
milestone = get_object_or_404(Milestone, project=project, name=name)
|
||||
|
||||
milestone.closed = False
|
||||
milestone.save()
|
||||
|
||||
return redirect('list-milestone', project.name)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('delete_tags')
|
||||
def milestone_delete(request, project, name):
|
||||
|
||||
milestone = get_object_or_404(Milestone, project=project, name=name)
|
||||
|
||||
for issue in milestone.issues.all():
|
||||
issue.remove_milestone(request.user, milestone)
|
||||
milestone.delete()
|
||||
|
||||
messages.success(request, "Label deleted successfully.")
|
||||
|
||||
return redirect('list-milestone', project.name)
|
Loading…
Add table
Add a link
Reference in a new issue