first commit for v0.2
This commit is contained in:
parent
6ba03afc73
commit
1463854a45
143 changed files with 20775 additions and 2764 deletions
7
accounts/admin.py
Normal file
7
accounts/admin.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from accounts.models import *
|
||||
|
||||
|
||||
admin.site.register(User)
|
||||
admin.site.register(Team)
|
17
accounts/forms.py
Normal file
17
accounts/forms.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.forms.models import modelform_factory
|
||||
from django.forms.widgets import PasswordInput
|
||||
|
||||
from accounts.models import *
|
||||
|
||||
|
||||
__all__ = [ 'UserForm', 'GroupForm', 'TeamForm' ]
|
||||
|
||||
|
||||
UserForm = modelform_factory(User,
|
||||
fields=['username', 'first_name', 'last_name',
|
||||
'password', 'email', 'is_superuser'],
|
||||
widgets={'password': PasswordInput})
|
||||
GroupForm = modelform_factory(Group,
|
||||
fields=['name'])
|
||||
TeamForm = modelform_factory(Team,
|
||||
fields=['name'])
|
68
accounts/migrations/0001_initial.py
Normal file
68
accounts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, verbose_name='superuser status', help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||
('username', models.CharField(verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True)),
|
||||
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
|
||||
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
|
||||
('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)),
|
||||
('is_staff', models.BooleanField(default=False, verbose_name='staff status', help_text='Designates whether the user can log into this admin site.')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='active', help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='groups', blank=True)),
|
||||
('user_permissions', models.ManyToManyField(to='auth.Permission', verbose_name='user permissions', blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['username'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('name', models.CharField(max_length=128, unique=True)),
|
||||
('users', models.ManyToManyField(null=True, to=settings.AUTH_USER_MODEL, blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Group',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'proxy': True,
|
||||
},
|
||||
bases=('auth.group',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(null=True, to='accounts.Group', blank=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
72
accounts/models.py
Normal file
72
accounts/models.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib import auth
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
__all__ = [ 'User', 'Group', 'Team' ]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class User(AbstractUser):
|
||||
|
||||
class Meta:
|
||||
ordering = [ 'username' ]
|
||||
|
||||
@property
|
||||
def teams(self):
|
||||
query = Q(groups__in=self.groups.all()) | Q(users=self)
|
||||
return Team.objects.filter(query).distinct()
|
||||
|
||||
@property
|
||||
def username_and_fullname(self):
|
||||
fullname = self.fullname
|
||||
if fullname:
|
||||
return "%s (%s)" % (self.username, fullname)
|
||||
else:
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
fullname = ''
|
||||
if self.first_name:
|
||||
fullname += self.first_name
|
||||
if self.last_name:
|
||||
if fullname:
|
||||
fullname += ' '
|
||||
fullname += self.last_name
|
||||
return fullname
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class Group(auth.models.Group):
|
||||
|
||||
class Meta:
|
||||
ordering = [ 'name' ]
|
||||
proxy = True
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return User.objects.filter(groups=self)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Team(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = [ 'name' ]
|
||||
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
|
||||
# We dont want related field on User object because we use
|
||||
# a special function that retrieve also team through group
|
||||
users = models.ManyToManyField(User, blank=True, null=True,
|
||||
related_name='+')
|
||||
groups = models.ManyToManyField(Group, blank=True, null=True,
|
||||
related_name='teams')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
1225
accounts/static/css/jquery-ui.css
vendored
Normal file
1225
accounts/static/css/jquery-ui.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
29
accounts/static/js/accounts.js
Normal file
29
accounts/static/js/accounts.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/* This script is used to remove user from group,
|
||||
* user from team or group from team dynamically
|
||||
* with a ajax request. */
|
||||
$('a[role="remove"]').on("click", function () {
|
||||
var a = $(this);
|
||||
var href = a.data('href');
|
||||
var type = a.data('type');
|
||||
a.html('removing...');
|
||||
$.ajax(href)
|
||||
.done(function(data, textStatus) {
|
||||
a.parents('li').remove();
|
||||
var counter = $('#' + type + '-counter');
|
||||
var empty = $('#' + type + '-empty');
|
||||
var count = parseInt(counter.html());
|
||||
count--;
|
||||
counter.html(count);
|
||||
if (count < 0) {
|
||||
// should not happen
|
||||
window.location.reload();
|
||||
} else if (count == 0) {
|
||||
empty.removeClass('hidden');
|
||||
} else {
|
||||
empty.addClass('hidden');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
/* This script set the action url of the deletion form
|
||||
* and update messages. */
|
||||
$('#confirm-delete').on('show.bs.modal', function(e) {
|
||||
var item = $(e.relatedTarget).data('item');
|
||||
if (!item) {
|
13
accounts/static/js/team.js
Normal file
13
accounts/static/js/team.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* This script switch the visible add form when we
|
||||
* change between user and group tab on team page. */
|
||||
$('a[data-toggle="tab"]').on("show.bs.tab", function () {
|
||||
var tab = $(this).data('tab');
|
||||
var hiddentab;
|
||||
if (tab == 'user') {
|
||||
hiddentab = 'group';
|
||||
} else {
|
||||
hiddentab = 'user';
|
||||
}
|
||||
$('#add-' + hiddentab + '-form').addClass('hidden');
|
||||
$('#add-' + tab + '-form').removeClass('hidden');
|
||||
});
|
0
accounts/templatetags/__init__.py
Normal file
0
accounts/templatetags/__init__.py
Normal file
12
accounts/templatetags/accounts_tags.py
Normal file
12
accounts/templatetags/accounts_tags.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
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.inclusion_tag('accounts/tags/delete_modal.html')
|
||||
def delete_modal():
|
||||
return {}
|
3
accounts/tests.py
Normal file
3
accounts/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
32
accounts/urls.py
Normal file
32
accounts/urls.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.conf.urls import url, include
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Profile
|
||||
url(r'^profile$', 'accounts.views.profile', name='profile'),
|
||||
# Users
|
||||
url(r'^admin/users/$', 'accounts.views.user_list', name='list-user'),
|
||||
url(r'^admin/users/add/$', 'accounts.views.user_edit', name='add-user'),
|
||||
url(r'^admin/users/(?P<user>[0-9]+)/edit/$', 'accounts.views.user_edit', name='edit-user'),
|
||||
url(r'^admin/users/(?P<user>[0-9]+)/delete/$', 'accounts.views.user_delete', name='delete-user'),
|
||||
url(r'^admin/users/(?P<user>[0-9]+)/activate/$', 'accounts.views.user_activate', name='activate-user'),
|
||||
url(r'^admin/users/(?P<user>[0-9]+)/disable/$', 'accounts.views.user_disable', name='disable-user'),
|
||||
# Groups
|
||||
url(r'^admin/groups/$', 'accounts.views.group_list', name='list-group'),
|
||||
url(r'^admin/groups/add/$', 'accounts.views.group_edit', name='add-group'),
|
||||
url(r'^admin/groups/(?P<group>[0-9]+)/$', 'accounts.views.group_details', name='show-group'),
|
||||
url(r'^admin/groups/(?P<group>[0-9]+)/edit/$', 'accounts.views.group_edit', name='edit-group'),
|
||||
url(r'^admin/groups/(?P<group>[0-9]+)/delete/$', 'accounts.views.group_delete', name='delete-group'),
|
||||
url(r'^admin/groups/(?P<group>[0-9]+)/add-user/$', 'accounts.views.group_add_user', name='add-user-to-group'),
|
||||
url(r'^admin/groups/(?P<group>[0-9]+)/remove-user/(?P<user>[0-9]+)/$', 'accounts.views.group_remove_user', name='remove-user-from-group'),
|
||||
# Teams
|
||||
url(r'^admin/teams/$', 'accounts.views.team_list', name='list-team'),
|
||||
url(r'^admin/teams/add/$', 'accounts.views.team_edit', name='add-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/$', 'accounts.views.team_details', name='show-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/edit$', 'accounts.views.team_edit', name='edit-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/delete$', 'accounts.views.team_delete', name='delete-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/add-user/$', 'accounts.views.team_add_user', name='add-user-to-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/remove-user/(?P<user>[0-9]+)/$', 'accounts.views.team_remove_user', name='remove-user-from-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/add-group/$', 'accounts.views.team_add_group', name='add-group-to-team'),
|
||||
url(r'^admin/teams/(?P<team>[0-9]+)/remove-group/(?P<group>[0-9]+)/$', 'accounts.views.team_remove_group', name='remove-group-from-team'),
|
||||
]
|
329
accounts/views.py
Normal file
329
accounts/views.py
Normal file
|
@ -0,0 +1,329 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib import messages
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
|
||||
from permissions.decorators import project_perm_required
|
||||
|
||||
from accounts.models import *
|
||||
from accounts.forms import *
|
||||
|
||||
|
||||
###########
|
||||
# Profile #
|
||||
###########
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
return render(request, 'accounts/profile.html')
|
||||
|
||||
|
||||
#########
|
||||
# Users #
|
||||
#########
|
||||
|
||||
@project_perm_required('manage_user')
|
||||
def user_list(request):
|
||||
return render(request, 'accounts/user_list.html', {
|
||||
'users': User.objects.all(),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_user')
|
||||
def user_edit(request, user=None):
|
||||
|
||||
if user:
|
||||
user = get_object_or_404(User, id=user)
|
||||
|
||||
form = UserForm(request.POST or None, instance=user)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
if user:
|
||||
messages.success(request, 'User modified successfully.')
|
||||
else:
|
||||
messages.success(request, 'User added successfully.')
|
||||
return redirect('list-user')
|
||||
|
||||
return render(request, 'accounts/user_edit.html', {
|
||||
'user': user,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_user')
|
||||
def user_activate(request, user):
|
||||
user = get_object_or_404(User, id=user)
|
||||
if user.is_active:
|
||||
messages.info(request, 'Account already activated.')
|
||||
else:
|
||||
user.is_active = True
|
||||
user.save()
|
||||
messages.success(request, 'Account activated successfully.')
|
||||
return redirect('list-user')
|
||||
|
||||
|
||||
@project_perm_required('manage_user')
|
||||
def user_disable(request, user):
|
||||
user = get_object_or_404(User, id=user)
|
||||
if user.is_active:
|
||||
user.is_active = False
|
||||
user.save()
|
||||
messages.success(request, 'Account disabled successfully.')
|
||||
else:
|
||||
messages.info(request, 'Account already disabled.')
|
||||
return redirect('list-user')
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('manage_user')
|
||||
def user_delete(request, user):
|
||||
user = get_object_or_404(User, id=user)
|
||||
user.delete()
|
||||
messages.success(request, 'User deleted successfully.')
|
||||
return redirect('list-user')
|
||||
|
||||
|
||||
##########
|
||||
# Groups #
|
||||
##########
|
||||
|
||||
@project_perm_required('manage_group')
|
||||
def group_list(request):
|
||||
return render(request, 'accounts/group_list.html', {
|
||||
'groups': Group.objects.all(),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_group')
|
||||
def group_details(request, group):
|
||||
return render(request, 'accounts/group_details.html', {
|
||||
'group': get_object_or_404(Group, id=group),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_group')
|
||||
def group_edit(request, group=None):
|
||||
|
||||
if group:
|
||||
group = get_object_or_404(Group, id=group)
|
||||
|
||||
form = GroupForm(request.POST or None, instance=group)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
formgroup = form.save()
|
||||
if group:
|
||||
messages.success(request, 'Group modified successfully.')
|
||||
else:
|
||||
messages.success(request, 'Group added successfully.')
|
||||
return redirect('show-group', formgroup.id)
|
||||
|
||||
return render(request, 'accounts/group_edit.html', {
|
||||
'group': group,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('manage_group')
|
||||
def group_delete(request, group):
|
||||
group = get_object_or_404(Group, id=group)
|
||||
group.delete()
|
||||
messages.success(request, 'Group deleted successfully.')
|
||||
return redirect('list-group')
|
||||
|
||||
|
||||
@project_perm_required('manage_group')
|
||||
def group_add_user(request, group):
|
||||
group = get_object_or_404(Group, id=group)
|
||||
if request.method == 'POST':
|
||||
user = request.POST.get('user')
|
||||
if user:
|
||||
try:
|
||||
user = User.objects.get(username=user)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, 'User not found.')
|
||||
else:
|
||||
if group.users.filter(id=user.id).exists():
|
||||
messages.info(request, 'User already in group.')
|
||||
else:
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
messages.success(request, 'User added to group successfully.')
|
||||
else:
|
||||
messages.error(request, 'User not found.')
|
||||
return redirect('show-group', group.id)
|
||||
else:
|
||||
term = request.GET.get('term')
|
||||
if not term:
|
||||
return Http404()
|
||||
query = Q(username__icontains=term) \
|
||||
| Q(first_name__icontains=term) \
|
||||
| Q(last_name__icontains=term)
|
||||
users = User.objects.exclude(groups=group).filter(query)[:10]
|
||||
response = []
|
||||
for user in users:
|
||||
response += [ {
|
||||
'label': user.username_and_fullname,
|
||||
'value': user.username,
|
||||
}]
|
||||
return JsonResponse(response, safe=False)
|
||||
|
||||
|
||||
@project_perm_required('manage_group')
|
||||
def group_remove_user(request, group, user):
|
||||
group = get_object_or_404(Group, id=group)
|
||||
user = get_object_or_404(User, id=user)
|
||||
user.groups.remove(group)
|
||||
user.save()
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
#########
|
||||
# Teams #
|
||||
#########
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_list(request):
|
||||
return render(request, 'accounts/team_list.html', {
|
||||
'teams': Team.objects.all(),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_details(request, team):
|
||||
tab = request.session.pop('team-tab', 'user')
|
||||
return render(request, 'accounts/team_details.html', {
|
||||
'team': get_object_or_404(Team, pk=team),
|
||||
'tab': tab,
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_edit(request, team=None):
|
||||
|
||||
if team:
|
||||
team = get_object_or_404(Team, pk=team)
|
||||
|
||||
form = TeamForm(request.POST or None, instance=team)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
formteam = form.save()
|
||||
if team:
|
||||
messages.success(request, 'Team modified successfully.')
|
||||
else:
|
||||
messages.success(request, 'Team added successfully.')
|
||||
return redirect('show-team', formteam.id)
|
||||
|
||||
c = {
|
||||
'team': team,
|
||||
'form': form,
|
||||
}
|
||||
|
||||
return render(request, 'accounts/team_edit.html', c)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('manage_team')
|
||||
def team_delete(request, team):
|
||||
team = get_object_or_404(Team, pk=team)
|
||||
team.delete()
|
||||
messages.success(request, 'Team deleted successfully.')
|
||||
return redirect('list-team')
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_add_user(request, team):
|
||||
team = get_object_or_404(Team, id=team)
|
||||
if request.method == 'POST':
|
||||
user = request.POST.get('user')
|
||||
if user:
|
||||
try:
|
||||
user = User.objects.get(username=user)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, 'User not found.')
|
||||
else:
|
||||
if team.users.filter(id=user.id).exists():
|
||||
messages.info(request, 'User already in team.')
|
||||
else:
|
||||
team.users.add(user)
|
||||
team.save()
|
||||
messages.success(request, 'User added to team successfully.')
|
||||
else:
|
||||
messages.error(request, 'User not found.')
|
||||
request.session['team-tab'] = 'user'
|
||||
return redirect('show-team', team.id)
|
||||
else:
|
||||
term = request.GET.get('term')
|
||||
if not term:
|
||||
return Http404()
|
||||
query = Q(username__icontains=term) \
|
||||
| Q(first_name__icontains=term) \
|
||||
| Q(last_name__icontains=term)
|
||||
users = User.objects \
|
||||
.exclude(groups__in=team.groups.all()) \
|
||||
.exclude(id__in=team.users.values('id')) \
|
||||
.filter(query)[:10]
|
||||
response = []
|
||||
for user in users:
|
||||
response += [ {
|
||||
'label': user.username_and_fullname,
|
||||
'value': user.username,
|
||||
}]
|
||||
return JsonResponse(response, safe=False)
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_remove_user(request, team, user):
|
||||
team = get_object_or_404(Team, pk=team)
|
||||
user = get_object_or_404(User, pk=user)
|
||||
team.users.remove(user)
|
||||
team.save()
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_add_group(request, team):
|
||||
team = get_object_or_404(Team, id=team)
|
||||
if request.method == 'POST':
|
||||
group = request.POST.get('group')
|
||||
if group:
|
||||
try:
|
||||
group = Group.objects.get(name=group)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, 'Group not found.')
|
||||
else:
|
||||
if team.groups.filter(id=group.id).exists():
|
||||
messages.info(request, 'Group already in team.')
|
||||
else:
|
||||
team.groups.add(group)
|
||||
team.save()
|
||||
messages.success(request, 'Group added to team successfully.')
|
||||
else:
|
||||
messages.error(request, 'Group not found.')
|
||||
request.session['team-tab'] = 'group'
|
||||
return redirect('show-team', team.id)
|
||||
else:
|
||||
term = request.GET.get('term')
|
||||
if not term:
|
||||
return Http404()
|
||||
groups = Group.objects \
|
||||
.exclude(id__in=team.groups.values('id')) \
|
||||
.filter(name__icontains=term)[:10]
|
||||
response = []
|
||||
for group in groups:
|
||||
response += [ {
|
||||
'label': group.name,
|
||||
'value': group.name,
|
||||
}]
|
||||
return JsonResponse(response, safe=False)
|
||||
|
||||
|
||||
@project_perm_required('manage_team')
|
||||
def team_remove_group(request, team, group):
|
||||
team = get_object_or_404(Team, pk=team)
|
||||
group = get_object_or_404(Group, pk=group)
|
||||
team.groups.remove(group)
|
||||
team.save()
|
||||
return HttpResponse()
|
|
@ -1 +0,0 @@
|
|||
default_app_config = 'issue.apps.IssueConfig'
|
|
@ -1,13 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from issue.models import *
|
||||
|
||||
admin.site.register(User)
|
||||
admin.site.register(Project)
|
||||
admin.site.register(Issue)
|
||||
admin.site.register(Event)
|
||||
admin.site.register(Label)
|
||||
admin.site.register(Milestone)
|
||||
admin.site.register(Settings)
|
||||
admin.site.register(Team)
|
||||
admin.site.register(GlobalPermission)
|
||||
admin.site.register(ProjectPermission)
|
|
@ -1,10 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IssueConfig(AppConfig):
|
||||
|
||||
name = 'issue'
|
||||
verbose_name = "Issue Tracker"
|
||||
|
||||
def ready(self):
|
||||
import issue.signals
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,80 +0,0 @@
|
|||
from django import forms
|
||||
from django.forms.models import modelform_factory
|
||||
|
||||
from bootstrap3_datetime.widgets import DateTimePicker
|
||||
from django_markdown.widgets import MarkdownWidget
|
||||
|
||||
from issue.models import *
|
||||
|
||||
|
||||
AddProjectForm = modelform_factory(Project,
|
||||
fields=['display_name', 'name', 'description', 'access'])
|
||||
EditProjectForm = modelform_factory(Project,
|
||||
fields=['display_name', 'description', 'access'])
|
||||
LabelForm = modelform_factory(Label,
|
||||
fields=['name', 'color', 'inverted'])
|
||||
TeamForm = modelform_factory(Team,
|
||||
fields=['name', 'users', 'groups'])
|
||||
|
||||
|
||||
class MilestoneForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Milestone
|
||||
fields = ['name', 'due_date']
|
||||
widgets = {
|
||||
'due_date': DateTimePicker(format="%Y-%m-%d %H:%M"),
|
||||
}
|
||||
|
||||
|
||||
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 PermissionForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PermissionModel
|
||||
exclude = []
|
||||
abstract = True
|
||||
|
||||
def clean(self):
|
||||
|
||||
data = super(PermissionForm, self).clean()
|
||||
|
||||
if 'grantee_name' not in data or 'grantee_type' not in data:
|
||||
# a field required error will be printed so we dont care
|
||||
return data
|
||||
|
||||
name = data['grantee_name']
|
||||
|
||||
if int(data['grantee_type']) == PermissionModel.GRANTEE_USER:
|
||||
if not User.objects.filter(username=name).exists():
|
||||
raise ValidationError("User '%s' does not exists." % name)
|
||||
elif int(data['grantee_type']) == PermissionModel.GRANTEE_GROUP:
|
||||
if not Group.objects.filter(name=name).exists():
|
||||
raise ValidationError("Group '%s' does not exists." % name)
|
||||
elif int(data['grantee_type']) == PermissionModel.GRANTEE_TEAM:
|
||||
if not Team.objects.filter(name=name).exists():
|
||||
raise ValidationError("Team '%s' does not exists." % name)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class GlobalPermissionForm(PermissionForm):
|
||||
|
||||
class Meta:
|
||||
model = GlobalPermission
|
||||
exclude = []
|
||||
|
||||
|
||||
class ProjectPermissionForm(PermissionForm):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPermission
|
||||
exclude = []
|
|
@ -1,230 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import colorful.fields
|
||||
from django.conf import settings
|
||||
import django.utils.timezone
|
||||
import django.core.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
('sites', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('password', models.CharField(verbose_name='password', max_length=128)),
|
||||
('last_login', models.DateTimeField(verbose_name='last login', default=django.utils.timezone.now)),
|
||||
('is_superuser', models.BooleanField(verbose_name='superuser status', default=False, help_text='Designates that this user has all permissions without explicitly assigning them.')),
|
||||
('username', models.CharField(unique=True, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30)),
|
||||
('first_name', models.CharField(blank=True, verbose_name='first name', max_length=30)),
|
||||
('last_name', models.CharField(blank=True, verbose_name='last name', max_length=30)),
|
||||
('email', models.EmailField(blank=True, verbose_name='email address', max_length=75)),
|
||||
('is_staff', models.BooleanField(verbose_name='staff status', default=False, help_text='Designates whether the user can log into this admin site.')),
|
||||
('is_active', models.BooleanField(verbose_name='active', default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.')),
|
||||
('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
|
||||
('groups', models.ManyToManyField(verbose_name='groups', to='auth.Group', blank=True)),
|
||||
('user_permissions', models.ManyToManyField(verbose_name='user permissions', to='auth.Permission', blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'abstract': False,
|
||||
'verbose_name_plural': 'users',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('code', models.IntegerField(default=0)),
|
||||
('_args', models.CharField(max_length=1024, default='{}', blank=True)),
|
||||
('additionnal_section', models.TextField(blank=True, default='')),
|
||||
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GlobalPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('grantee_type', models.IntegerField(choices=[(0, 'User'), (1, 'Group'), (2, 'Team')], verbose_name='Type', default=0)),
|
||||
('grantee_name', models.CharField(verbose_name='Name', max_length=50)),
|
||||
('create_project', models.BooleanField(default=True)),
|
||||
('modify_project', models.BooleanField(default=False)),
|
||||
('delete_project', models.BooleanField(default=False)),
|
||||
('add_team', models.BooleanField(default=True)),
|
||||
('manage_team', models.BooleanField(default=False)),
|
||||
('manage_global_permission', models.BooleanField(default=False)),
|
||||
('manage_project_permission', models.BooleanField(default=False)),
|
||||
('create_issue', models.BooleanField(default=True)),
|
||||
('modify_issue', models.BooleanField(default=False)),
|
||||
('manage_issue', models.BooleanField(default=False)),
|
||||
('delete_issue', models.BooleanField(default=False)),
|
||||
('create_comment', models.BooleanField(default=True)),
|
||||
('modify_comment', models.BooleanField(default=False)),
|
||||
('delete_comment', models.BooleanField(default=False)),
|
||||
('manage_tags', models.BooleanField(default=False)),
|
||||
('delete_tags', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Issue',
|
||||
fields=[
|
||||
('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_add=True)),
|
||||
('closed', models.BooleanField(default=False)),
|
||||
('assignee', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('subscribers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='issue',
|
||||
field=models.ForeignKey(to='issue.Issue'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Label',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('name', models.CharField(max_length=32)),
|
||||
('deleted', models.BooleanField(default=False)),
|
||||
('color', colorful.fields.RGBColorField(verbose_name='Background color', default='#000000')),
|
||||
('inverted', models.BooleanField(verbose_name='Inverse text color', default=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='labels',
|
||||
field=models.ManyToManyField(blank=True, to='issue.Label', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Milestone',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('name', models.CharField(max_length=32, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9_.-]+$', message='Please enter only lowercase characters, number, dot, underscores or hyphens.')])),
|
||||
('due_date', models.DateTimeField(blank=True, null=True)),
|
||||
('closed', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='milestone',
|
||||
field=models.ForeignKey(blank=True, to='issue.Milestone', null=True),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('name', models.CharField(primary_key=True, verbose_name='Short name (used in URL, definitive)', validators=[django.core.validators.RegexValidator(regex='^[a-z0-9_-]+$', message='Please enter only lowercase characters, number, underscores or hyphens.')], serialize=False, max_length=32)),
|
||||
('display_name', models.CharField(unique=True, verbose_name='Project name', max_length=32)),
|
||||
('description', models.TextField(verbose_name='Description', default='', blank=True)),
|
||||
('public', models.BooleanField(verbose_name='Do unregistered users have read access to this project?', default=True)),
|
||||
('subscribers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='milestone',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='issue.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='milestone',
|
||||
unique_together=set([('project', 'name')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='issue.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='issue.Project'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='issue',
|
||||
unique_together=set([('project', 'id')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('grantee_type', models.IntegerField(choices=[(0, 'User'), (1, 'Group'), (2, 'Team')], verbose_name='Type', default=0)),
|
||||
('grantee_name', models.CharField(verbose_name='Name', max_length=50)),
|
||||
('manage_project_permission', models.BooleanField(default=False)),
|
||||
('create_issue', models.BooleanField(default=True)),
|
||||
('modify_issue', models.BooleanField(default=False)),
|
||||
('manage_issue', models.BooleanField(default=False)),
|
||||
('delete_issue', models.BooleanField(default=False)),
|
||||
('create_comment', models.BooleanField(default=True)),
|
||||
('modify_comment', models.BooleanField(default=False)),
|
||||
('delete_comment', models.BooleanField(default=False)),
|
||||
('manage_tags', models.BooleanField(default=False)),
|
||||
('delete_tags', models.BooleanField(default=False)),
|
||||
('project', models.ForeignKey(editable=False, to='issue.Project')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Settings',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('site', models.OneToOneField(to='sites.Site')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Settings',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
|
||||
('name', models.CharField(unique=True, max_length=128)),
|
||||
('groups', models.ManyToManyField(blank=True, to='auth.Group', null=True)),
|
||||
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('issue', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='access',
|
||||
field=models.IntegerField(choices=[(1, 'Public'), (2, 'Connected users'), (3, 'Private')], default=1),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='public',
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Permission denied
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<em>Sorry, you are not allowed to access this page.</em>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Page not found
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<em>This is not the web page you are looking for.</em>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Server error
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<em>Sorry, an error occured. Please try again in few minutes.</em>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,123 +0,0 @@
|
|||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
{% comment %}<link rel="icon" href="{% static 'favicon.ico' %}">{% endcomment %}
|
||||
|
||||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<title>{% block title %}PonyTracker{% endblock %}</title>
|
||||
|
||||
{% bootstrap_css %}
|
||||
<link href="{% static 'css/issue.css' %}" rel="stylesheet">
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
<script src="{% bootstrap_jquery_url %}"></script>
|
||||
{% block js %}{% endblock %}
|
||||
|
||||
{% block media %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Static navbar -->
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'list-project' %}">{% block page_title %}PonyTracker{% endblock %}</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% block navbar %}{% endblock %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Project <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% if projects.exists %}
|
||||
{% for p in projects %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'list-issue' p.name %}">{{ p }}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li role="presentation"><a role="munitem" tabindex="-1" href="#"><em>No project</em></a></li>
|
||||
{% endif %}
|
||||
{% block projectmenu %}
|
||||
{% if perm.create_project %}
|
||||
<li role="presentation" class="divider"></li>
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'add-project' %}"><span class="glyphicon glyphicon-plus"></span> New project…</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ request.user.username }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li role="presentation"><a role="menuitem" href="{% url 'profile' %}"><span class="glyphicon glyphicon-user"></span> My profile</a></li>
|
||||
<li role="presentation"><a role="menuitem" href="{% url 'list-team' %}"><span class="glyphicon glyphicon-dashboard"></span> Teams</a></li>
|
||||
{% if perm.manage_global_permission %}
|
||||
<li role="presentation"><a role="menuitem" href="{% url 'list-global-permission' %}"><span class="glyphicon glyphicon-cog"></span> Manage permissions</a></li>
|
||||
{% endif %}
|
||||
<li role="presentation" class="divider"></li>
|
||||
<li role="presentation"><a role="menuitem" href="{% url 'logout' %}"><span class="glyphicon glyphicon-log-out"></span> Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login' %}?next={{ request.get_full_path }}&prev={{ request.get_full_path }}"><span class="glyphicon glyphicon-log-in"></span> Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div><!--/.container-fluid -->
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<div class="modal" id="confirm-delete" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading" id="confirm-delete-title">
|
||||
Delete
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="#" method="post" role="form" id="confirm-delete-form" class="text-center">
|
||||
{% csrf_token %}
|
||||
<p id="confirm-delete-message">Are you sure?</p>
|
||||
<button type="submit" class="btn btn-danger">{% block confirm-ok %}Confirm{% endblock %}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
||||
{% bootstrap_javascript %}
|
||||
<script src="{% static 'js/confirm.js' %}"></script>
|
||||
{% block js_end %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,4 +0,0 @@
|
|||
Issue closed.
|
||||
|
||||
--
|
||||
You can see the issue on PonyTracker: {{ uri }}
|
|
@ -1,4 +0,0 @@
|
|||
{{ comment }}
|
||||
|
||||
--
|
||||
Respond on PonyTracker: {{ uri }}
|
|
@ -1,8 +0,0 @@
|
|||
{% if description %}
|
||||
{{ description }}
|
||||
{% else %}
|
||||
No description.
|
||||
{% endif %}
|
||||
|
||||
--
|
||||
Comment it on PonyTracker: {{ uri }}
|
|
@ -1,4 +0,0 @@
|
|||
Issue reopened.
|
||||
|
||||
--
|
||||
You can see the issue on PonyTracker: {{ uri }}
|
|
@ -1,29 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading clearfix">
|
||||
<h1>
|
||||
Global permissions
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-global-permission' %}" class="btn btn-warning">Go back to list</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,89 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load django_markdown %}
|
||||
{% load issue_filters %}
|
||||
{% load issue_tags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Global permissions
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'add-global-permission' %}" class="btn btn-success">Add new permission</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if permissions.count %}
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th rowspan="3" class="text-center">Type</th>
|
||||
<th rowspan="3" class="text-center">Name</th>
|
||||
<th colspan="6" class="text-center">Global</th>
|
||||
<th colspan="10" class="text-center">Project</th>
|
||||
<th rowspan="3" class="col-md-2"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3" class="text-center">Project</th>
|
||||
<th colspan="2" class="text-center">Team</th>
|
||||
<th colspan="2" class="text-center">Permissions</th>
|
||||
<th colspan="4" class="text-center">Issues</th>
|
||||
<th colspan="3" class="text-center">Comments</th>
|
||||
<th colspan="2" class="text-center">Tags</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Modify?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Modify?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Modify?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
</tr>
|
||||
{% for perm in permissions %}
|
||||
<tr>
|
||||
<td>{{ perm.type }}</td>
|
||||
<td>{{ perm.name }}</td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'create-project' %}">{{ perm.create_project|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'modify-project' %}">{{ perm.modify_project|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'delete-project' %}">{{ perm.delete_project|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'add-team' %}">{{ perm.add_team|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'manage-team' %}">{{ perm.manage_team|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'manage-global-permission' %}">{{ perm.manage_global_permission|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'manage-project-permission' %}">{{ perm.manage_project_permission|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'create-issue' %}">{{ perm.create_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'manage-issue' %}">{{ perm.manage_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'modify-issue' %}">{{ perm.modify_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'delete-issue' %}">{{ perm.delete_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'create-comment' %}">{{ perm.create_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'modify-comment' %}">{{ perm.modify_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'delete-comment' %}">{{ perm.delete_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'manage-tags' %}">{{ perm.manage_tags|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-global-permission' perm.id 'delete-tags' %}">{{ perm.delete_tags|boolean }}</a></td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'edit-global-permission' perm.id %}" class="btn btn-primary btn-xs"><span class="glyphicon glyphicon-edit"></span> Edit</a>
|
||||
<a href=#" data-item="permission" data-action="{% url 'delete-global-permission' perm.id %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger btn-xs"><span class="glyphicon glyphicon-remove"></span> Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
There aren't any permissions defined yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,17 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block media %}{{ form.media }}{% endblock %}
|
||||
|
||||
{% block issuetab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,43 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block issuetab %} class="active"{% endblock %}
|
||||
|
||||
{% block media %}
|
||||
{{ form.media }}
|
||||
{% endblock media %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
{% if issue %}
|
||||
Edit issue
|
||||
{% else %}
|
||||
New issue
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
{% if issue %}
|
||||
<a href="{% url 'show-issue' project.name issue.id %}" class="btn btn-warning">Go back to issue</a>
|
||||
{% else %}
|
||||
<a href="{% url 'list-issue' project.name %}" class="btn btn-warning">Go back to issues</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-2 col-md-8">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,82 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load humanize %}
|
||||
{% load issue_tags %}
|
||||
|
||||
{% block issuetab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading clearfix">
|
||||
<h1>
|
||||
Issues
|
||||
<div class="pull-right">
|
||||
<form class="form-inline" role="form" method="GET" action="" id="filter-form">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="dropdown">
|
||||
{% if is_all_query %}
|
||||
<a href="{% url 'list-issue' project.name %}" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-remove"> Reset filter</span></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'list-issue' project.name %}?q=is:open{% if is_all_query %} {{ is_all_query }}{% endif %}" class="btn btn-default btn-sm{{ is_open }}">Open</a>
|
||||
<a href="{% url 'list-issue' project.name %}?q=is:close{% if is_all_query %} {{ is_all_query }}{% endif %}" class="btn btn-default btn-sm{{ is_close }}">Close</a>
|
||||
<a href="{% url 'list-issue' project.name %}?q={% if is_all_query %}{{ is_all_query }}{% else %}*{% endif %}" class="btn btn-default btn-sm{{ is_all }}">Both</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input class="form-control" name="q" type="text" placeholder="filter" value="{{ query }}"/>
|
||||
<div class="input-group-addon">
|
||||
<a href="#" onclick="$('#filter-form').submit();"><span class="glyphicon glyphicon-search"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if perm.create_issue %}
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<a href="{% url 'add-issue' project.name %}" class="btn btn-success">New issue</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</h1>
|
||||
<i>{{ issues.count }} issue{{ issues.count|pluralize }} displayed</i>
|
||||
</div>
|
||||
|
||||
{% if issues.count %}
|
||||
<table class="table">
|
||||
{% for issue in issues %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if issue.closed %}
|
||||
<span class="text-danger"><span class="glyphicon glyphicon-ok-circle"></span></span>
|
||||
{% else %}
|
||||
<span class="text-success"><span class="glyphicon glyphicon-hand-right"></span></span>
|
||||
{% endif %}
|
||||
<a href="{% url 'show-issue' project.name issue.id %}"><b>{{ issue }}</b></a>
|
||||
{% for label in issue.labels.all %}
|
||||
<a href="{% same_label label %}" class="label" style="{% label_style label %}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
<br />
|
||||
<small>#{{ issue.id }} opened by <a href="{% same_author issue.author %}"><b>{{ issue.author.username }}</b></a> {{ issue.opened_at|naturaltime }}</small>
|
||||
{% if issue.milestone %}
|
||||
 –  <span class="glyphicon glyphicon-road"></span> <a href="{% same_milestone issue.milestone %}"><b>{{ issue.milestone }}</b></a>
|
||||
{% endif %}
|
||||
 –  <span class="badge">{{ issue.comments.count }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
No issues match your desired criteria.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,36 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block labeltab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
{% if label %}
|
||||
Edit label
|
||||
{% else %}
|
||||
New label
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-label' project.name %}" class="btn btn-warning">Go back to labels</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-4 col-md-4">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,50 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load issue_tags %}
|
||||
|
||||
{% block labeltab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading clearfix">
|
||||
<h1>
|
||||
Labels
|
||||
{% if perm.manage_tags %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'add-label' project.name %}" class="btn btn-success">New label</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if labels.count %}
|
||||
<table class="table">
|
||||
{% for label in labels %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% same_label label %}" class="btn" style="{% label_style label %}"><span class="glyphicon glyphicon-tag" style="font-size: 100%;"></span> {{ label }}</a>
|
||||
{% if perm.manage_tags or perm.delete_tags %}
|
||||
<div class="pull-right">
|
||||
{% if perm.manage_tags %}
|
||||
<a href="{% url 'edit-label' project.name label.id %}" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> Edit</a>
|
||||
{% endif %}
|
||||
{% if perm.delete_tags %}
|
||||
<a href="#" data-item="label" data-action="{% url 'delete-label' project.name label.id %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger"><span class="glyphicon glyphicon-remove"></span> Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
There aren't any labels for this repository quite yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block milestonetab %} class="active"{% endblock %}
|
||||
|
||||
{% block media %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
{% if milestone %}
|
||||
Edit milestone
|
||||
{% else %}
|
||||
New milestone
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-milestone' project.name %}" class="btn btn-warning">Go back to milestones</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-4 col-md-4">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,68 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% block milestonetab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading clearfix">
|
||||
<h1>
|
||||
Milestone
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-milestone' project.name %}?show=open" class="btn btn-default btn-sm{% if show == 'open' %} active{% endif %}">Open</a>
|
||||
<a href="{% url 'list-milestone' project.name %}?show=close" class="btn btn-default btn-sm{% if show == 'close' %} active{% endif %}">Close</a>
|
||||
<a href="{% url 'list-milestone' project.name %}?show=all" class="btn btn-default btn-sm{% if show == 'all' %} active{% endif %}">All</a>
|
||||
{% if perm.manage_tags %}
|
||||
<a href="{% url 'add-milestone' project.name %}" class="btn btn-success">New milestone</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</h1>
|
||||
<i>{{ milestones.count }} milestone{{ milestones.count|pluralize }} displayed</i>
|
||||
</div>
|
||||
|
||||
{% if milestones.count %}
|
||||
<table class="table">
|
||||
{% for milestone in milestones %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
{% if perm.manage_tags %}
|
||||
{% if milestone.closed %}
|
||||
<a href="{% url 'reopen-milestone' project.name milestone.name %}"><button class="btn btn-info"><span class="glyphicon glyphicon-repeat"></span> Reopen</button></a>
|
||||
{% else %}
|
||||
<a href="{% url 'close-milestone' project.name milestone.name %}"><button class="btn btn-info"><span class="glyphicon glyphicon-ok-circle"></span> Close</button></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'edit-milestone' project.name milestone.name %}"><button class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> Edit</button></a>
|
||||
{% endif %}
|
||||
{% if perm.delete_tags %}
|
||||
<a href="#" data-item="milestone" data-action="{% url 'delete-milestone' project.name milestone.name %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger"><span class="glyphicon glyphicon-remove"></span> Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<b style="font-size: 200%;">
|
||||
<span class="glyphicon glyphicon-road"></span> <a href="{% url 'list-issue' project.name %}?q=is:open%20milestone:{{ milestone.name }}">{{ milestone }}</a>
|
||||
</b>
|
||||
 
|
||||
<small>
|
||||
<span style="white-space: nowrap;"><span class="glyphicon glyphicon-calendar"></span> {% if milestone.due_date %}Due by {{ milestone.due_date }}{% else %}No due date{% endif %}</span>
|
||||
</small>
|
||||
<br /><br />
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{ milestone.progress }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ milestone.progress }}%;">
|
||||
{{ milestone.progress }}% complete
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
{{ no_milestone_message }}
|
||||
There aren't any milestones matching your desired criteria.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,38 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load django_markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<h3>Your groups</h3>
|
||||
{% if groups.exists %}
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<em>You belong to no groups.</em>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<h3>Your teams</h3>
|
||||
{% if teams.exists %}
|
||||
<ul>
|
||||
{% for team in teams %}
|
||||
<li>{{ team }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<em>You belong to no teams.</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,36 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ project }} - PonyTracker{% endblock %}
|
||||
{% block page_title %}{{ project }}{% endblock %}
|
||||
|
||||
{% block projectmenu %}
|
||||
{% if request.user.is_authenticated or perm.manage_project_permission or perm.modify_project or perm.delete_project or perm.create_project %}
|
||||
<li role="presentation" class="divider"></li>
|
||||
{% if request.user in project.subscribers.all %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'unsubscribe-project' project.name %}?next={{ request.get_full_path }}"><span class="glyphicon glyphicon-eye-close"></span> Unsubscribe</a></li>
|
||||
{% else %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'subscribe-project' project.name %}?next={{ request.get_full_path }}"><span class="glyphicon glyphicon-eye-open"></span> Subscribe</a></li>
|
||||
{% endif %}
|
||||
{% if perm.manage_project_permission %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'list-project-permission' project.name %}"><span class="glyphicon glyphicon-cog"></span> Manage permissions</a></li>
|
||||
{% endif %}
|
||||
{% if perm.modify_project %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'edit-project' project.name %}"><span class="glyphicon glyphicon-wrench"></span> Modify this project</a></li>
|
||||
{% endif %}
|
||||
{% if perm.delete_project %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="#" data-item="project" data-action="{% url 'delete-project' project.name %}" data-toggle="modal" data-target="#confirm-delete"><span class="glyphicon glyphicon-trash"></span> Delete this project</a></li>
|
||||
{% endif %}
|
||||
{% if perm.create_project %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'add-project' %}"><span class="glyphicon glyphicon-plus"></span> New project…</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<li{% block issuetab %}{% endblock %}><a href="{% url 'list-issue' project.name %}">Issues</a></li>
|
||||
<li{% block labeltab %}{% endblock %}><a href="{% url 'list-label' project.name %}">Labels</a></li>
|
||||
<li{% block milestonetab %}{% endblock %}><a href="{% url 'list-milestone' project.name %}">Milestones</a></li>
|
||||
{% if perm.manage_project_permission %}
|
||||
<li{% block permissiontab %}{% endblock %}><a href="{% url 'list-project-permission' project.name %}">Permissions</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
New project
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-project' %}" class="btn btn-warning">Go back to list</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-2 col-md-8">
|
||||
<form action="" method="post" role="form">
|
||||
{%bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Edit project
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-issue' project.name %}" class="btn btn-warning">Go back to issues</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-2 col-md-8">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,53 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load django_markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Projects
|
||||
{% if perm.create_project %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'add-project' %}" class="btn btn-success">Add project</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
{% if projects.exists %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<strong>Name</strong>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<strong>Description</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% for project in projects %}
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<a href="{% url 'list-issue' project.name %}">{{ project }}</a>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
{% if project.description %}
|
||||
{{ project.description|linebreaksbr }}
|
||||
{% else %}
|
||||
<em>No description provided.</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif user.is_authenticated %}
|
||||
<em>Sorry, you have no access to any project.</em>
|
||||
{% else %}
|
||||
<em>There is not any public project. You should probably <a href="{% url 'login' %}">login</a>.</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,31 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block permissiontab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading clearfix">
|
||||
<h1>
|
||||
Permissions of '{{ project }}' project
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-project-permission' project.name %}" class="btn btn-warning">Go back to list</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,72 +0,0 @@
|
|||
{% extends 'issue/project.html' %}
|
||||
|
||||
{% load django_markdown %}
|
||||
{% load issue_filters %}
|
||||
{% load issue_tags %}
|
||||
|
||||
{% block permissiontab %} class="active"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Permissions of '{{ project }}' project
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'add-project-permission' project.name %}" class="btn btn-success">Add new permission</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if permissions.count %}
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th rowspan="2">Type</th>
|
||||
<th rowspan="2">Name</th>
|
||||
<th colspan="4" class="text-center">Issues</th>
|
||||
<th colspan="3" class="text-center">Comments</th>
|
||||
<th colspan="2" class="text-center">Labels &<br />Milestones</th>
|
||||
<th class="text-center">Permissions</th>
|
||||
<th rowspan="2" class="col-md-2"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Modify?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Create?' %}</th>
|
||||
<th class="text-center">{% vertical 'Modify?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
<th class="text-center">{% vertical 'Delete?' %}</th>
|
||||
<th class="text-center">{% vertical 'Manage?' %}</th>
|
||||
</tr>
|
||||
{% for perm in permissions %}
|
||||
<tr>
|
||||
<td>{{ perm.type }}</td>
|
||||
<td>{{ perm.name }}</td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'create-issue' %}">{{ perm.create_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'manage-issue' %}">{{ perm.manage_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'modify-issue' %}">{{ perm.modify_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'delete-issue' %}">{{ perm.delete_issue|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'create-comment' %}">{{ perm.create_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'modify-comment' %}">{{ perm.modify_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'delete-comment' %}">{{ perm.delete_comment|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'manage-tags' %}">{{ perm.manage_tags|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'delete-tags' %}">{{ perm.delete_tags|boolean }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'toggle-project-permission' project.name perm.id 'manage-project-permission' %}">{{ perm.manage_project_permission|boolean }}</a></td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'edit-project-permission' project.name perm.id %}" class="btn btn-primary btn-xs"><span class="glyphicon glyphicon-edit"></span> Edit</a>
|
||||
<a href="#" data-item="permission" data-action="{% url 'delete-project-permission' project.name perm.id %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger btn-xs"><span class="glyphicon glyphicon-remove"></span> Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
There aren't any permissions defined yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,49 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Teams
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'list-team' %}" class="btn btn-success">Go back to teams</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<h3>Users</h3>
|
||||
{% if team.users.exists %}
|
||||
<ul>
|
||||
{% for user in team.users.all %}
|
||||
<li>{{ user }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<em>There aren't any users in this team.</em>
|
||||
{% endif %}
|
||||
<hr />
|
||||
<h3>Groups</h3>
|
||||
{% if team.groups.exists %}
|
||||
<ul>
|
||||
{% for group in team.groups.all %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<em>There aren't any groups in this team.</em>
|
||||
{% endif %}
|
||||
<hr />
|
||||
<div class="row text-center">
|
||||
<a href="{% url 'list-team' %}" class="btn btn-default"><span class="glyphicon glyphicon-chevron-left"></span> Go back to list</a>
|
||||
{% if perm.manage_team %}
|
||||
<a href="{% url 'edit-team' team.pk %}" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> Modify team</a>
|
||||
<a href="#" data-item="team" data-action="{% url 'delete-team' team.pk %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span> Delete team</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,38 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
{% if team %}
|
||||
Edit team
|
||||
{% else %}
|
||||
Add team
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
{% if team %}
|
||||
<a href="{% url 'show-team' team.pk %}" class="btn btn-warning">Go back to team</a>
|
||||
{% else %}
|
||||
<a href="{% url 'list-team' %}" class="btn btn-warning">Go back to teams</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,43 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load humanize %}
|
||||
{% load issue_filters %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Teams
|
||||
{% if perm.add_team %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'add-team' %}" class="btn btn-success">Add team</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{% if teams.exists %}
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th class="col-md-2">Name</th>
|
||||
<th class="col-md-5">Users</th>
|
||||
<th class="col-md-5">Groups</th>
|
||||
</tr>
|
||||
{% for team in teams %}
|
||||
<tr>
|
||||
<td><a href="{% url 'show-team' team.pk %}"><b>{{ team }}</b></a></td>
|
||||
<td>{{ team.users|first_few:'user' }}</td>
|
||||
<td>{{ team.groups|first_few:'group' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
<em>There aren't any teams quite yet.</em>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
|
||||
<div class="panel-heading">
|
||||
<h1>
|
||||
Login
|
||||
<div class="pull-right">
|
||||
<a href="{% if request.GET.prev %}{{ request.GET.prev }}{% else %}{% url 'list-project' %}{% endif %}"><button class="btn btn-warning">Cancel</button></a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="col-md-offset-4 col-md-4">
|
||||
<form action="" method="post" role="form">
|
||||
{% bootstrap_form form %}
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,31 +0,0 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def boolean(value):
|
||||
if value:
|
||||
glyph = 'ok'
|
||||
else:
|
||||
glyph = 'remove'
|
||||
return mark_safe('<span class="glyphicon glyphicon-'
|
||||
+ glyph + '" style="vertical-align: middle;"></span>')
|
||||
|
||||
|
||||
@register.filter
|
||||
def first_few(items, arg='item', max_items=5):
|
||||
if items.exists():
|
||||
if items.count() <= max_items:
|
||||
return ', '.join(map(lambda x: x.__str__(), items.all()))
|
||||
else:
|
||||
r = ', '.join(map(lambda x: x.__str__(),
|
||||
items.all()[0:max_items - 1]))
|
||||
plural = 's' if items.count() > max_items else ''
|
||||
r += ', ... (%s other%s)' \
|
||||
% (items.count() - max_items + 1, plural)
|
||||
return r
|
||||
else:
|
||||
return 'no ' + arg + 's'
|
621
issue/tests.py
621
issue/tests.py
|
@ -1,621 +0,0 @@
|
|||
from django.test import TestCase, Client
|
||||
from django import VERSION
|
||||
|
||||
from issue.models import *
|
||||
|
||||
|
||||
class TestPermissions(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_team_user_membership(self):
|
||||
user = User.objects.get(username='user1')
|
||||
team = Team.objects.get(name='team1')
|
||||
self.assertEqual(user.teams.count(), 1)
|
||||
self.assertEqual(user.teams.first(), team)
|
||||
|
||||
def test_team_group_membership(self):
|
||||
user = User.objects.get(username='user2')
|
||||
team = Team.objects.get(name='team2')
|
||||
self.assertEqual(user.teams.count(), 1)
|
||||
self.assertEqual(user.teams.first(), team)
|
||||
|
||||
def test_global_no_perms(self):
|
||||
user = User.objects.get(username='user4')
|
||||
self.assertFalse(user.has_perm('create_project'))
|
||||
self.assertFalse(user.has_perm('modify_project'))
|
||||
self.assertFalse(user.has_perm('delete_project'))
|
||||
|
||||
def test_global_user_perms(self):
|
||||
user = User.objects.get(username='user3')
|
||||
self.assertTrue(user.has_perm('create_project'))
|
||||
self.assertFalse(user.has_perm('modify_project'))
|
||||
self.assertFalse(user.has_perm('delete_project'))
|
||||
|
||||
def test_global_group_perms(self):
|
||||
user = User.objects.get(username='user2')
|
||||
self.assertFalse(user.has_perm('create_project'))
|
||||
self.assertTrue(user.has_perm('modify_project'))
|
||||
self.assertFalse(user.has_perm('delete_project'))
|
||||
|
||||
def test_global_team_perms(self):
|
||||
user = User.objects.get(username='user1')
|
||||
self.assertFalse(user.has_perm('create_project'))
|
||||
self.assertFalse(user.has_perm('modify_project'))
|
||||
self.assertTrue(user.has_perm('delete_project'))
|
||||
|
||||
def test_project_no_perms(self):
|
||||
user = User.objects.get(username='user4')
|
||||
project = Project.objects.get(name='project-1')
|
||||
self.assertFalse(user.has_perm('create_issue', project))
|
||||
self.assertFalse(user.has_perm('modify_issue', project))
|
||||
self.assertFalse(user.has_perm('delete_issue', project))
|
||||
|
||||
def test_project_user_perms(self):
|
||||
user = User.objects.get(username='user3')
|
||||
project = Project.objects.get(name='project-1')
|
||||
self.assertTrue(user.has_perm('create_issue', project))
|
||||
self.assertFalse(user.has_perm('modify_issue', project))
|
||||
self.assertFalse(user.has_perm('delete_issue', project))
|
||||
|
||||
def test_project_group_perms(self):
|
||||
user = User.objects.get(username='user2')
|
||||
project = Project.objects.get(name='project-1')
|
||||
self.assertFalse(user.has_perm('create_issue', project))
|
||||
self.assertTrue(user.has_perm('modify_issue', project))
|
||||
self.assertFalse(user.has_perm('delete_issue', project))
|
||||
|
||||
def test_project_team_perms(self):
|
||||
user = User.objects.get(username='user1')
|
||||
project = Project.objects.get(name='project-1')
|
||||
self.assertFalse(user.has_perm('create_issue', project))
|
||||
self.assertFalse(user.has_perm('modify_issue', project))
|
||||
self.assertTrue(user.has_perm('delete_issue', project))
|
||||
|
||||
|
||||
class TestNoProject(TestCase):
|
||||
|
||||
fixtures = ['test_no_project']
|
||||
|
||||
def test_ano(self):
|
||||
url = reverse('list-project')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'There is not any public project')
|
||||
|
||||
def test_without_add_permission(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
url = reverse('list-project')
|
||||
expected_url = reverse('add-project')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response,
|
||||
'Sorry, you have no access to any project')
|
||||
|
||||
def test_with_add_permission(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('list-project')
|
||||
expected_url = reverse('add-project')
|
||||
response = self.client.get(url)
|
||||
if VERSION >= (1, 7):
|
||||
self.assertRedirects(response, expected_url,
|
||||
# don't fetch redirect to don't loose message
|
||||
fetch_redirect_response=False)
|
||||
response = self.client.get(expected_url)
|
||||
self.assertContains(response, 'Start by creating a project')
|
||||
else:
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
|
||||
class TestGlobalViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_404(self):
|
||||
response = self.client.get('/deliberately/broken')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_profile_ano(self):
|
||||
url = reverse('profile')
|
||||
expected_url = reverse('login') + '?next=' + url
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
def test_profile_user1(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
url = reverse('profile')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'group1')
|
||||
self.assertContains(response, 'team1')
|
||||
self.assertNotContains(response, 'team2')
|
||||
|
||||
def test_profile_user2(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('profile')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'group1')
|
||||
self.assertNotContains(response, 'team1')
|
||||
self.assertContains(response, 'team2')
|
||||
|
||||
|
||||
class TestProjectsViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_home_as_anonymous(self):
|
||||
expected = Project.objects.filter(name='project-1')
|
||||
url = reverse('list-project')
|
||||
self.assertEqual(url, '/')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertQuerysetEqual(expected, response.context['projects'],
|
||||
lambda x: x)
|
||||
|
||||
def test_home_as_user1(self):
|
||||
expected = Project.objects \
|
||||
.filter(Q(name='project-1') | Q(name='project-3'))
|
||||
self.client.login(username='user1', password='user1')
|
||||
url = reverse('list-project')
|
||||
self.assertEqual(url, '/')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertQuerysetEqual(expected, response.context['projects'],
|
||||
lambda x: x, ordered=False)
|
||||
self.assertNotContains(response, 'New project')
|
||||
|
||||
def test_home_as_user2(self):
|
||||
expected = Project.objects.all()
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('list-project')
|
||||
self.assertEqual(url, '/')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertQuerysetEqual(expected, response.context['projects'],
|
||||
lambda x: x, ordered=False)
|
||||
self.assertNotContains(response, 'New project')
|
||||
|
||||
def test_home_as_user3(self):
|
||||
expected = Project.objects \
|
||||
.filter(Q(name='project-1') | Q(name='project-3'))
|
||||
self.client.login(username='user3', password='user3')
|
||||
url = reverse('list-project')
|
||||
self.assertEqual(url, '/')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertQuerysetEqual(expected, response.context['projects'],
|
||||
lambda x: x, ordered=False)
|
||||
self.assertContains(response, 'New project')
|
||||
|
||||
def test_home_as_admin(self):
|
||||
expected = Project.objects.all()
|
||||
self.client.login(username='admin', password='admin')
|
||||
url = reverse('list-project')
|
||||
self.assertEqual(url, '/')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertQuerysetEqual(expected, response.context['projects'],
|
||||
lambda x: x, ordered=False)
|
||||
self.assertContains(response, 'New project')
|
||||
|
||||
def test_add_project_granted(self):
|
||||
self.client.login(username='user3', password='user3')
|
||||
expected_url = reverse('list-project-permission', args=['project-4'])
|
||||
url = reverse('add-project')
|
||||
response = self.client.post(url, {
|
||||
'name': 'project-4',
|
||||
'display_name': 'Project 4',
|
||||
'description': 'This is the fourth project.',
|
||||
'access': Project.ACCESS_PUBLIC,
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3, 4)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_add_project_forbidden(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
url = reverse('add-project')
|
||||
response = self.client.post(url, {
|
||||
'name': 'project-4',
|
||||
'display_name': 'Project 4',
|
||||
'description': 'This is the foorth project.',
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_add_project_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' + reverse('add-project')
|
||||
url = reverse('add-project')
|
||||
response = self.client.post(url, {
|
||||
'name': 'project-4',
|
||||
'display_name': 'Project 4',
|
||||
'description': 'This is the foorth project.',
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_delete_project_get(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
expected_url = reverse('list-project')
|
||||
url = reverse('delete-project', args=['project-1'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_delete_project_granted(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
expected_url = reverse('list-project')
|
||||
url = reverse('delete-project', args=['project-1'])
|
||||
response = self.client.post(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_delete_project_forbidden(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('delete-project', args=['project-1'])
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
def test_delete_project_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('delete-project', args=['project-1'])
|
||||
url = reverse('delete-project', args=['project-1'])
|
||||
response = self.client.post(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
self.assertQuerysetEqual(Project.objects.all(), ['project-%s' % x
|
||||
for x in (1, 2, 3)], lambda x: x.name, ordered=False)
|
||||
|
||||
|
||||
class TestIssuesViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_list_issue_granted(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('list-issue', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_list_issue_forbidden(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('list-issue', args=['project-2'])
|
||||
url = reverse('list-issue', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_list_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('list-issue', args=['project-2'])
|
||||
url = reverse('list-issue', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
|
||||
def test_show_issue_granted(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('show-issue', args=['project-2', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_show_issue_granted_ano(self):
|
||||
url = reverse('show-issue', args=['project-1', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_show_issue_forbidden(self):
|
||||
self.client.login(username='user1', password='user1')
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('show-issue', args=['project-2', 1])
|
||||
url = reverse('show-issue', args=['project-2', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_add_issue_granted(self):
|
||||
self.client.login(username='user5', password='user5')
|
||||
expected_url = reverse('show-issue', args=['project-2', 3])
|
||||
url = reverse('add-issue', args=['project-2'])
|
||||
response = self.client.post(url, {
|
||||
'title': 'Issue 3',
|
||||
'description': 'This is the third issue.',
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2', 'Issue 3'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_add_issue_forbidden(self):
|
||||
self.client.login(username='user6', password='user6')
|
||||
url = reverse('add-issue', args=['project-2'])
|
||||
response = self.client.post(url, {
|
||||
'title': 'Issue 3',
|
||||
'description': 'This is the third issue.',
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_add_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('add-issue', args=['project-2'])
|
||||
url = reverse('add-issue', args=['project-2'])
|
||||
response = self.client.post(url, {
|
||||
'title': 'Issue 3',
|
||||
'description': 'This is the third issue.',
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_delete_issue_get(self):
|
||||
self.client.login(username='user8', password='user8')
|
||||
expected_url = reverse('list-issue', args=['project-2'])
|
||||
url = reverse('delete-issue', args=['project-2', 2])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_delete_issue_granted(self):
|
||||
self.client.login(username='user8', password='user8')
|
||||
expected_url = reverse('list-issue', args=['project-2'])
|
||||
url = reverse('delete-issue', args=['project-2', 2])
|
||||
response = self.client.post(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_delete_issue_forbidden(self):
|
||||
self.client.login(username='user5', password='user5')
|
||||
url = reverse('delete-issue', args=['project-2', 2])
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_delete_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('delete-issue', args=['project-2', 2])
|
||||
url = reverse('delete-issue', args=['project-2', 2])
|
||||
response = self.client.post(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issues = Issue.objects.filter(project__name='project-2')
|
||||
self.assertQuerysetEqual(issues, ['Issue 1', 'Issue 2'],
|
||||
lambda x: x.title, ordered=False)
|
||||
|
||||
def test_close_issue_granted(self):
|
||||
self.client.login(username='user6', password='user6')
|
||||
expected_url = reverse('list-issue', args=['project-2'])
|
||||
url = reverse('close-issue', args=['project-2', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.closed, True)
|
||||
|
||||
def test_close_issue_forbidden(self):
|
||||
self.client.login(username='user5', password='user5')
|
||||
url = reverse('close-issue', args=['project-2', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.closed, False)
|
||||
|
||||
def test_close_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('close-issue', args=['project-2', 1])
|
||||
url = reverse('close-issue', args=['project-2', 1])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.closed, False)
|
||||
|
||||
def test_reopen_issue_granted(self):
|
||||
self.client.login(username='user6', password='user6')
|
||||
expected_url = reverse('show-issue', args=['project-2', 2])
|
||||
url = reverse('reopen-issue', args=['project-2', 2])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=2)
|
||||
self.assertEqual(issue.closed, False)
|
||||
|
||||
def test_reopen_issue_forbidden(self):
|
||||
self.client.login(username='user5', password='user5')
|
||||
url = reverse('reopen-issue', args=['project-2', 2])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issue = Issue.objects.get(project__name='project-2', id=2)
|
||||
self.assertEqual(issue.closed, True)
|
||||
|
||||
def test_reopen_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('reopen-issue', args=['project-2', 2])
|
||||
url = reverse('reopen-issue', args=['project-2', 2])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=2)
|
||||
self.assertEqual(issue.closed, True)
|
||||
|
||||
def test_modify_issue_granted(self):
|
||||
self.client.login(username='user7', password='user7')
|
||||
expected_url = reverse('show-issue', args=['project-2', 1])
|
||||
url = reverse('edit-issue', args=['project-2', 1])
|
||||
response = self.client.post(url, {
|
||||
'title': '*THE* Issue 1',
|
||||
'description': 'This is *THE* first issue.',
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.title, "*THE* Issue 1")
|
||||
self.assertEqual(issue.description, "This is *THE* first issue.")
|
||||
|
||||
def test_modify_issue_forbidden(self):
|
||||
self.client.login(username='user5', password='user5')
|
||||
url = reverse('edit-issue', args=['project-2', 1])
|
||||
response = self.client.post(url, {
|
||||
'title': '*THE* Issue 1',
|
||||
'description': 'This is *THE* first issue.',
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.title, "Issue 1")
|
||||
self.assertEqual(issue.description, "This is the first issue.")
|
||||
|
||||
def test_modify_issue_forbidden_ano(self):
|
||||
expected_url = reverse('login') + '?next=' \
|
||||
+ reverse('edit-issue', args=['project-2', 1])
|
||||
url = reverse('edit-issue', args=['project-2', 1])
|
||||
response = self.client.post(url, {
|
||||
'title': '*THE* Issue 1',
|
||||
'description': 'This is *THE* first issue.',
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
self.assertEqual(issue.title, "Issue 1")
|
||||
self.assertEqual(issue.description, "This is the first issue.")
|
||||
|
||||
|
||||
class TestCommentsViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_comment_issue_granted(self):
|
||||
self.client.login(username='user9', password='user9')
|
||||
msg = 'I have a lot to say.'
|
||||
expected_url = reverse('show-issue', args=['project-2', 1])
|
||||
url = reverse('comment-issue', args=['project-2', 1])
|
||||
response = self.client.post(url, {
|
||||
'comment': msg,
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, msg)
|
||||
|
||||
def test_comment_issue_forbidden(self):
|
||||
self.client.login(username='user10', password='user10')
|
||||
msg = 'I have a lot to say.'
|
||||
url = reverse('comment-issue', args=['project-2', 1])
|
||||
response = self.client.post(url, {
|
||||
'comment': msg,
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, 'Missing things')
|
||||
|
||||
def test_edit_comment_granted(self):
|
||||
self.client.login(username='user10', password='user10')
|
||||
msg = 'Missing a lot of things'
|
||||
expected_url = reverse('show-issue', args=['project-2', 1])
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
url = reverse('edit-comment', args=['project-2', issue.id, event.id])
|
||||
response = self.client.post(url, {
|
||||
'comment': msg,
|
||||
})
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, msg)
|
||||
|
||||
def test_edit_comment_forbidden(self):
|
||||
self.client.login(username='user9', password='user9')
|
||||
msg = 'Missing a lot of things'
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
url = reverse('edit-comment', args=['project-2', issue.id, event.id])
|
||||
response = self.client.post(url, {
|
||||
'comment': msg,
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_delete_comment_granted_get(self):
|
||||
self.client.login(username='user11', password='user11')
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
url = reverse('delete-comment', args=['project-2', issue.id, event.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 405)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, 'Missing things')
|
||||
|
||||
def test_delete_comment_granted(self):
|
||||
self.client.login(username='user11', password='user11')
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
url = reverse('delete-comment', args=['project-2', issue.id, event.id])
|
||||
expected_url = reverse('show-issue', args=['project-2', 1])
|
||||
response = self.client.post(url)
|
||||
self.assertRedirects(response, expected_url)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, 'Done')
|
||||
|
||||
def test_delete_comment_forbidden(self):
|
||||
self.client.login(username='user9', password='user9')
|
||||
msg = 'Missing a lot of things'
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
url = reverse('delete-comment', args=['project-2', issue.id, event.id])
|
||||
response = self.client.post(url, {
|
||||
'comment': msg,
|
||||
})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
issue = Issue.objects.get(project__name='project-2', id=1)
|
||||
event = Event.objects.filter(issue=issue, code=Event.COMMENT).last()
|
||||
self.assertEqual(event.additionnal_section, 'Missing things')
|
||||
|
||||
|
||||
class TestLabelsViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_list(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('list-label', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'documentation')
|
||||
|
||||
|
||||
class TestMilestonesViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_list(self):
|
||||
self.client.login(username='user2', password='user2')
|
||||
url = reverse('list-milestone', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'v1.0')
|
||||
|
||||
|
||||
class TestPermissionsViews(TestCase):
|
||||
|
||||
fixtures = ['test_perms']
|
||||
|
||||
def test_global_list(self):
|
||||
self.client.login(username='user15', password='user15')
|
||||
url = reverse('list-global-permission')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Global permissions')
|
||||
|
||||
def test_project_list(self):
|
||||
self.client.login(username='user14', password='user14')
|
||||
url = reverse('list-project-permission', args=['project-2'])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Permissions of 'Project 2' project")
|
|
@ -1,58 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', 'issue.views.project_list', name='list-project'),
|
||||
url(r'^add$', 'issue.views.project_add', name='add-project'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/edit$', 'issue.views.project_edit', name='edit-project'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/delete$', 'issue.views.project_delete', name='delete-project'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/subscribe$', 'issue.views.project_subscribe', name='subscribe-project'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/unsubscribe$', 'issue.views.project_unsubscribe', name='unsubscribe-project'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues$', 'issue.views.issue_list', name='list-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/add$', 'issue.views.issue_edit', name='add-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)$', 'issue.views.issue', name='show-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/edit$', 'issue.views.issue_edit', name='edit-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/close$', 'issue.views.issue_close', name='close-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/reopen$', 'issue.views.issue_reopen', name='reopen-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/comment$', 'issue.views.issue_edit_comment', name='comment-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/comments/(?P<comment>[0-9]+)/edit$', 'issue.views.issue_edit_comment', name='edit-comment'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/comments/(?P<comment>[0-9]+)/delete$', 'issue.views.issue_delete_comment', name='delete-comment'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/delete$', 'issue.views.issue_delete', name='delete-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/subscribe$', 'issue.views.issue_subscribe', name='subscribe-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/unsubscribe$', 'issue.views.issue_unsubscribe', name='unsubscribe-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/add-label/(?P<label>[0-9]+)$', 'issue.views.issue_add_label', name='add-label-to-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/remove-label/(?P<label>[0-9]+)$', 'issue.views.issue_remove_label', name='remove-label-from-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/add-milestone/(?P<milestone>[a-z0-9_.-]+)$', 'issue.views.issue_add_milestone', name='add-milestone-to-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/issues/(?P<issue>[0-9]+)/remove-milestone/(?P<milestone>[a-z0-9_.-]+)$', 'issue.views.issue_remove_milestone', name='remove-milestone-from-issue'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/labels$', 'issue.views.label_list', name='list-label'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/labels/add$', 'issue.views.label_edit', name='add-label'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/labels/(?P<id>[0-9]+)/edit$', 'issue.views.label_edit', name='edit-label'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/labels/(?P<id>[0-9]+)/delete$', 'issue.views.label_delete', name='delete-label'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones$', 'issue.views.milestone_list', name='list-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones/add$', 'issue.views.milestone_edit', name='add-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones/(?P<name>[a-z0-9_.-]+)/edit$', 'issue.views.milestone_edit', name='edit-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones/(?P<name>[a-z0-9_.-]+)/close$', 'issue.views.milestone_close', name='close-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones/(?P<name>[a-z0-9_.-]+)/reopen$', 'issue.views.milestone_reopen', name='reopen-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/milestones/(?P<name>[a-z0-9_.-]+)/delete$', 'issue.views.milestone_delete', name='delete-milestone'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/permissions$', 'issue.views.project_permission_list', name='list-project-permission'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/permissions/add$', 'issue.views.project_permission_edit', name='add-project-permission'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/permissions/(?P<id>[0-9]+)/edit$', 'issue.views.project_permission_edit', name='edit-project-permission'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/permissions/(?P<id>[0-9]+)/toggle/(?P<perm>[a-z-]+)$', 'issue.views.project_permission_toggle', name='toggle-project-permission'),
|
||||
url(r'^(?P<project>[a-z0-9_-]+)/permissions/(?P<id>[0-9]+)/delete$', 'issue.views.project_permission_delete', name='delete-project-permission'),
|
||||
url(r'^permissions$', 'issue.views.global_permission_list', name='list-global-permission'),
|
||||
url(r'^permissions/add$', 'issue.views.global_permission_edit', name='add-global-permission'),
|
||||
url(r'^permissions/(?P<id>[0-9]+)/edit$', 'issue.views.global_permission_edit', name='edit-global-permission'),
|
||||
url(r'^permissions/(?P<id>[0-9]+)/toggle/(?P<perm>[a-z-]+)$', 'issue.views.global_permission_toggle', name='toggle-global-permission'),
|
||||
url(r'^permissions/(?P<id>[0-9]+)/delete$', 'issue.views.global_permission_delete', name='delete-global-permission'),
|
||||
url(r'^teams$', 'issue.views.team_list', name='list-team'),
|
||||
url(r'^teams/add$', 'issue.views.team_edit', name='add-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)$', 'issue.views.team', name='show-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/edit$', 'issue.views.team_edit', name='edit-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/users/(?P<user>[0-9]+)/add$', 'issue.views.team_add_user', name='add-user-to-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/users/(?P<user>[0-9]+)/delete$', 'issue.views.team_remove_user', name='remove-user-from-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/groups/(?P<group>[0-9]+)/add$', 'issue.views.team_add_group', name='add-group-to-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/groups/(?P<group>[0-9]+)/delete$', 'issue.views.team_remove_group', name='remove-group-from-team'),
|
||||
url(r'^teams/(?P<team>[0-9]+)/delete$', 'issue.views.team_delete', name='delete-team'),
|
||||
url(r'^login$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}, name='login'),
|
||||
url(r'^profile$', 'issue.views.profile', name='profile'),
|
||||
url(r'^logout$', 'django.contrib.auth.views.logout', {'next_page': '/'}, name='logout'),
|
||||
]
|
0
permissions/__init__.py
Normal file
0
permissions/__init__.py
Normal file
7
permissions/admin.py
Normal file
7
permissions/admin.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from permissions.models import *
|
||||
|
||||
|
||||
admin.site.register(GlobalPermission)
|
||||
admin.site.register(ProjectPermission)
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from issue.models import *
|
||||
from tracker.models import Project
|
||||
from permissions.models import GlobalPermission
|
||||
|
||||
|
||||
def user_has_perm(user, perm, perms):
|
||||
|
@ -10,7 +11,7 @@ def user_has_perm(user, perm, perms):
|
|||
return True
|
||||
|
||||
|
||||
class ProjectBackend(ModelBackend):
|
||||
class Backend(ModelBackend):
|
||||
|
||||
def has_perm(self, user, perm, obj=None):
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
from issue.models import Project
|
||||
|
||||
|
||||
def projects(request):
|
||||
|
||||
if hasattr(request, 'projects'):
|
||||
return {'projects': request.projects}
|
||||
else:
|
||||
return {}
|
||||
from tracker.models import Project
|
||||
|
||||
|
||||
class PermWrapper:
|
|
@ -1,9 +1,9 @@
|
|||
from functools import wraps
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from issue.models import Project
|
||||
from functools import wraps
|
||||
|
||||
from tracker.models import Project
|
||||
|
||||
|
||||
def project_perm_required(perm):
|
68
permissions/forms.py
Normal file
68
permissions/forms.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.widgets import HiddenInput
|
||||
|
||||
from permissions.models import *
|
||||
from permissions.models import PermissionModel
|
||||
from accounts.models import *
|
||||
|
||||
|
||||
__all__ = [ 'GlobalPermissionForm', 'ProjectPermissionForm' ]
|
||||
|
||||
|
||||
class PermissionForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
# The grantee_id is a hidden field.
|
||||
# The user complete the grantee_name field, and the validation
|
||||
# method will set the correct value is the grantee_id.
|
||||
grantee_name = forms.CharField(max_length=32)
|
||||
|
||||
def clean(self):
|
||||
|
||||
data = super(PermissionForm, self).clean()
|
||||
|
||||
if 'grantee_name' not in data or 'grantee_type' not in data:
|
||||
# a field required error will be printed so we dont care
|
||||
return data
|
||||
|
||||
name = data['grantee_name']
|
||||
|
||||
if int(data['grantee_type']) == PermissionModel.GRANTEE_USER:
|
||||
grantees = User.objects.filter(username=name)
|
||||
if not grantees.exists():
|
||||
raise ValidationError("User '%s' does not exists." % name)
|
||||
elif int(data['grantee_type']) == PermissionModel.GRANTEE_GROUP:
|
||||
grantees = Group.objects.filter(name=name)
|
||||
if not grantees.exists():
|
||||
raise ValidationError("Group '%s' does not exists." % name)
|
||||
elif int(data['grantee_type']) == PermissionModel.GRANTEE_TEAM:
|
||||
grantees = Team.objects.filter(name=name)
|
||||
if not grantees.exists():
|
||||
raise ValidationError("Team '%s' does not exists." % name)
|
||||
|
||||
data['grantee_id'] = grantees.first().id
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class GlobalPermissionForm(PermissionForm):
|
||||
|
||||
class Meta:
|
||||
model = GlobalPermission
|
||||
fields = [ 'grantee_type', 'grantee_id' ]
|
||||
widgets = {
|
||||
'grantee_id': HiddenInput,
|
||||
}
|
||||
|
||||
|
||||
class ProjectPermissionForm(PermissionForm):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPermission
|
||||
fields = [ 'grantee_type', 'grantee_id' ]
|
||||
widgets = {
|
||||
'grantee_id': HiddenInput,
|
||||
}
|
75
permissions/migrations/0001_initial.py
Normal file
75
permissions/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tracker', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('grantee_type', models.IntegerField(default=0, verbose_name='Grantee type', choices=[(0, 'User'), (1, 'Group'), (2, 'Team')])),
|
||||
('grantee_id', models.IntegerField(blank=True)),
|
||||
('create_project', models.BooleanField(default=True)),
|
||||
('modify_project', models.BooleanField(default=False)),
|
||||
('delete_project', models.BooleanField(default=False)),
|
||||
('manage_settings', models.BooleanField(default=False)),
|
||||
('manage_user', models.BooleanField(default=False)),
|
||||
('manage_group', models.BooleanField(default=False)),
|
||||
('manage_team', models.BooleanField(default=False)),
|
||||
('manage_global_permission', models.BooleanField(default=False)),
|
||||
('create_issue', models.BooleanField(default=True)),
|
||||
('modify_issue', models.BooleanField(default=False)),
|
||||
('manage_issue', models.BooleanField(default=False)),
|
||||
('delete_issue', models.BooleanField(default=False)),
|
||||
('create_comment', models.BooleanField(default=True)),
|
||||
('modify_comment', models.BooleanField(default=False)),
|
||||
('delete_comment', models.BooleanField(default=False)),
|
||||
('manage_tags', models.BooleanField(default=False)),
|
||||
('delete_tags', models.BooleanField(default=False)),
|
||||
('manage_project_permission', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='globalpermission',
|
||||
unique_together=set([('grantee_type', 'grantee_id')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('grantee_type', models.IntegerField(default=0, verbose_name='Grantee type', choices=[(0, 'User'), (1, 'Group'), (2, 'Team')])),
|
||||
('grantee_id', models.IntegerField(blank=True)),
|
||||
('manage_project_permission', models.BooleanField(default=False)),
|
||||
('create_issue', models.BooleanField(default=True)),
|
||||
('modify_issue', models.BooleanField(default=False)),
|
||||
('manage_issue', models.BooleanField(default=False)),
|
||||
('delete_issue', models.BooleanField(default=False)),
|
||||
('create_comment', models.BooleanField(default=True)),
|
||||
('modify_comment', models.BooleanField(default=False)),
|
||||
('delete_comment', models.BooleanField(default=False)),
|
||||
('manage_tags', models.BooleanField(default=False)),
|
||||
('delete_tags', models.BooleanField(default=False)),
|
||||
('project', models.ForeignKey(editable=False, to='tracker.Project')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='projectpermission',
|
||||
unique_together=set([('grantee_type', 'grantee_id')]),
|
||||
),
|
||||
]
|
18
permissions/migrations/0002_auto_20140829_2335.py
Normal file
18
permissions/migrations/0002_auto_20140829_2335.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('permissions', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='projectpermission',
|
||||
unique_together=set([('project', 'grantee_type', 'grantee_id')]),
|
||||
),
|
||||
]
|
0
permissions/migrations/__init__.py
Normal file
0
permissions/migrations/__init__.py
Normal file
144
permissions/models.py
Normal file
144
permissions/models.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from tracker.models import Project
|
||||
from accounts.models import *
|
||||
|
||||
|
||||
__all__ = [ 'GlobalPermission', 'ProjectPermission' ]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PermissionModel(models.Model):
|
||||
|
||||
GRANTEE_USER = 0
|
||||
GRANTEE_GROUP = 1
|
||||
GRANTEE_TEAM = 2
|
||||
GRANTEE_TYPE = (
|
||||
(GRANTEE_USER, 'User'),
|
||||
(GRANTEE_GROUP, 'Group'),
|
||||
(GRANTEE_TEAM, 'Team'),
|
||||
)
|
||||
|
||||
grantee_type = models.IntegerField(choices=GRANTEE_TYPE,
|
||||
default=GRANTEE_USER, verbose_name="Grantee type")
|
||||
grantee_id = models.IntegerField(blank=True)
|
||||
|
||||
def get_grantee(self):
|
||||
if self.grantee_type == self.GRANTEE_USER:
|
||||
Model = User
|
||||
elif self.grantee_type == self.GRANTEE_GROUP:
|
||||
Model = Group
|
||||
else:
|
||||
Model = Team
|
||||
return Model.objects.get(id=self.grantee_id)
|
||||
|
||||
def set_grantee(self, grantee):
|
||||
if isinstance(grantee, User):
|
||||
self.grantee_type = self.GRANTEE_USER
|
||||
elif isinstance(grantee, Group):
|
||||
self.grantee_type = self.GRANTEE_GROUP
|
||||
elif isinstance(grantee, Team):
|
||||
self.grantee_type = self.GRANTEE_TEAM
|
||||
else:
|
||||
raise ValueError('Grantee object must be '
|
||||
'an User, a Group or a Team instance.')
|
||||
self.grantee_id = grantee.id
|
||||
|
||||
grantee = property(get_grantee, set_grantee)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def granted_to(self, user):
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
if self.grantee_type == self.GRANTEE_USER:
|
||||
return user.id == self.grantee_id
|
||||
elif self.grantee_type == self.GRANTEE_GROUP:
|
||||
return user.groups.filter(id=self.grantee_id).exists()
|
||||
elif self.grantee_type == self.GRANTEE_TEAM:
|
||||
return Team.objects.filter(id=self.grantee_id) \
|
||||
.filter(Q(groups__in=user.groups.all()) | Q(users=user)) \
|
||||
.exists()
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.get_grantee_type_display()
|
||||
# return dict(self.GRANTEE_TYPE)[self.grantee_type]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.grantee.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.grantee.__str__() + "'s permissions"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class GlobalPermission(PermissionModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = [ 'grantee_type', 'grantee_id' ]
|
||||
|
||||
# Global permissions
|
||||
|
||||
create_project = models.BooleanField(default=True)
|
||||
modify_project = models.BooleanField(default=False)
|
||||
delete_project = models.BooleanField(default=False)
|
||||
|
||||
manage_settings = models.BooleanField(default=False)
|
||||
manage_user = models.BooleanField(default=False)
|
||||
manage_group = models.BooleanField(default=False)
|
||||
manage_team = models.BooleanField(default=False)
|
||||
manage_global_permission = models.BooleanField(default=False)
|
||||
|
||||
# Project permissions, given on ALL projects
|
||||
|
||||
create_issue = models.BooleanField(default=True)
|
||||
modify_issue = models.BooleanField(default=False)
|
||||
manage_issue = models.BooleanField(default=False)
|
||||
delete_issue = models.BooleanField(default=False)
|
||||
|
||||
create_comment = models.BooleanField(default=True)
|
||||
modify_comment = models.BooleanField(default=False)
|
||||
delete_comment = models.BooleanField(default=False)
|
||||
|
||||
manage_tags = models.BooleanField(default=False)
|
||||
delete_tags = models.BooleanField(default=False)
|
||||
|
||||
manage_project_permission = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.grantee.__str__() + "'s global permissions"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ProjectPermission(PermissionModel):
|
||||
|
||||
class Meta:
|
||||
unique_together = [ 'project', 'grantee_type', 'grantee_id' ]
|
||||
|
||||
project = models.ForeignKey(Project, editable=False,
|
||||
related_name='permissions')
|
||||
|
||||
manage_project_permission = models.BooleanField(default=False)
|
||||
|
||||
create_issue = models.BooleanField(default=True)
|
||||
modify_issue = models.BooleanField(default=False)
|
||||
manage_issue = models.BooleanField(default=False)
|
||||
delete_issue = models.BooleanField(default=False)
|
||||
|
||||
create_comment = models.BooleanField(default=True)
|
||||
modify_comment = models.BooleanField(default=False)
|
||||
delete_comment = models.BooleanField(default=False)
|
||||
|
||||
manage_tags = models.BooleanField(default=False)
|
||||
delete_tags = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.grantee.__str__() + "'s permissions on " \
|
||||
+ self.project.name + " project"
|
13
permissions/static/js/perm.js
Normal file
13
permissions/static/js/perm.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
$('.perm-toggle').click(function() {
|
||||
var a = $(this);
|
||||
a.html('<span class="glyphicon glyphicon-time"></span>');
|
||||
var href = a.data('href');
|
||||
$.ajax(href)
|
||||
.done(function(data, textStatus) {
|
||||
if (data == '1') {
|
||||
a.html('<span class="glyphicon glyphicon-ok"></span>');
|
||||
} else {
|
||||
a.html('<span class="glyphicon glyphicon-remove"></span>');
|
||||
}
|
||||
});
|
||||
});
|
0
permissions/templatetags/__init__.py
Normal file
0
permissions/templatetags/__init__.py
Normal file
15
permissions/templatetags/permissions_filters.py
Normal file
15
permissions/templatetags/permissions_filters.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def boolean(value):
|
||||
if value:
|
||||
glyph = 'ok'
|
||||
else:
|
||||
glyph = 'remove'
|
||||
return mark_safe('<span class="glyphicon glyphicon-'
|
||||
+ glyph + '" style="vertical-align: middle;"></span>')
|
24
permissions/templatetags/permissions_tags.py
Normal file
24
permissions/templatetags/permissions_tags.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from django import template
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('permissions/tags/perm_form.html', takes_context=True)
|
||||
def add_perm_form(context):
|
||||
return {
|
||||
'form': context['add_form'],
|
||||
'type': 'add',
|
||||
'title': 'Add permission',
|
||||
'action': reverse('add-global-permission'),
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('permissions/tags/perm_form.html', takes_context=True)
|
||||
def edit_perm_form(context):
|
||||
return {
|
||||
'form': context['edit_form'],
|
||||
'type': 'edit',
|
||||
'title': 'Edit permission',
|
||||
}
|
3
permissions/tests.py
Normal file
3
permissions/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
17
permissions/urls.py
Normal file
17
permissions/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.conf.urls import url, include
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# Global permissions
|
||||
url(r'^admin/permissions/$', 'permissions.views.global_perm_list', name='list-global-permission'),
|
||||
url(r'^admin/permissions/add/$', 'permissions.views.global_perm_edit', name='add-global-permission'),
|
||||
url(r'^admin/permissions/(?P<id>[0-9]+)/edit/$', 'permissions.views.global_perm_edit', name='edit-global-permission'),
|
||||
url(r'^admin/permissions/(?P<id>[0-9]+)/delete/$', 'permissions.views.global_perm_delete', name='delete-global-permission'),
|
||||
url(r'^admin/permissions/(?P<id>[0-9]+)/toggle/(?P<perm>[a-z-]+)/$', 'permissions.views.global_perm_toggle', name='toggle-global-permission'),
|
||||
# Project permissions
|
||||
url(r'^(?P<project>[-\w]+)/permissions/$', 'permissions.views.project_perm_list', name='list-project-permission'),
|
||||
url(r'^(?P<project>[-\w]+)/permissions/add/$', 'permissions.views.project_perm_edit', name='add-project-permission'),
|
||||
url(r'^(?P<project>[-\w]+)/permissions/(?P<id>[0-9]+)/edit/$', 'permissions.views.project_perm_edit', name='edit-project-permission'),
|
||||
url(r'^(?P<project>[-\w]+)/permissions/(?P<id>[0-9]+)/delete/$', 'permissions.views.project_perm_delete', name='delete-project-permission'),
|
||||
url(r'^(?P<project>[-\w]+)/permissions/(?P<id>[0-9]+)/toggle/(?P<perm>[a-z-]+)/$', 'permissions.views.project_perm_toggle', name='toggle-project-permission'),
|
||||
]
|
131
permissions/views.py
Normal file
131
permissions/views.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib import messages
|
||||
from django.http import Http404, HttpResponse
|
||||
|
||||
|
||||
from permissions.models import *
|
||||
from permissions.forms import *
|
||||
from permissions.decorators import project_perm_required
|
||||
|
||||
|
||||
@project_perm_required('manage_global_permission')
|
||||
def global_perm_list(request):
|
||||
return render(request, 'permissions/global_perm_list.html', {
|
||||
'permissions': GlobalPermission.objects.all(),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_global_permission')
|
||||
def global_perm_edit(request, id=None):
|
||||
if id:
|
||||
perm = get_object_or_404(GlobalPermission, id=id)
|
||||
else:
|
||||
perm = None
|
||||
form = GlobalPermissionForm(request.POST or None, instance=perm)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
if id:
|
||||
messages.success(request, 'Permission updated successfully.')
|
||||
else:
|
||||
messages.success(request, 'Permission added successfully.')
|
||||
return redirect('list-global-permission')
|
||||
name = request.POST.get('grantee_name')
|
||||
if not name:
|
||||
if perm:
|
||||
name = perm.grantee.__str__()
|
||||
else:
|
||||
name = ''
|
||||
return render(request, 'permissions/global_perm_edit.html', {
|
||||
'perm': perm,
|
||||
'form': form,
|
||||
'name': name,
|
||||
})
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@project_perm_required('manage_global_permission')
|
||||
def global_perm_delete(request, id):
|
||||
perm = get_object_or_404(GlobalPermission, id=id)
|
||||
perm.delete()
|
||||
messages.success(request, 'Permission deleted successfully.')
|
||||
return redirect('list-global-permission')
|
||||
|
||||
|
||||
@project_perm_required('manage_global_permission')
|
||||
def global_perm_toggle(request, id, perm):
|
||||
permission = get_object_or_404(GlobalPermission, id=id)
|
||||
# to be sure to dont modify other attribut with the following trick
|
||||
if '-' not in perm:
|
||||
raise Http404
|
||||
perm = perm.replace('-', '_')
|
||||
if hasattr(permission, perm):
|
||||
state = not getattr(permission, perm)
|
||||
setattr(permission, perm, state)
|
||||
permission.save()
|
||||
return HttpResponse('1' if state else '0')
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
|
||||
@project_perm_required('manage_project_permission')
|
||||
def project_perm_list(request, project):
|
||||
return render(request, 'permissions/project_perm_list.html', {
|
||||
'project': project,
|
||||
'permissions': ProjectPermission.objects.filter(project=project).all(),
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_project_permission')
|
||||
def project_perm_edit(request, project, id=None):
|
||||
if id:
|
||||
perm = get_object_or_404(ProjectPermission, project=project, id=id)
|
||||
else:
|
||||
perm = None
|
||||
form = ProjectPermissionForm(request.POST or None, instance=perm)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
if id:
|
||||
form.save()
|
||||
messages.success(request, 'Permission updated successfully.')
|
||||
else:
|
||||
perm = form.save(commit=False)
|
||||
perm.project = project
|
||||
perm.save()
|
||||
messages.success(request, 'Permission added successfully.')
|
||||
return redirect('list-project-permission', project.name)
|
||||
name = request.POST.get('grantee_name')
|
||||
if not name:
|
||||
if perm:
|
||||
name = perm.grantee.__str__()
|
||||
else:
|
||||
name = ''
|
||||
return render(request, 'permissions/project_perm_edit.html', {
|
||||
'project': project,
|
||||
'perm': perm,
|
||||
'form': form,
|
||||
'name': name,
|
||||
})
|
||||
|
||||
|
||||
@project_perm_required('manage_project_permission')
|
||||
def project_perm_delete(request, project, id):
|
||||
perm = get_object_or_404(ProjectPermission, project=project, id=id)
|
||||
perm.delete()
|
||||
messages.success(request, 'Permission deleted successfully.')
|
||||
return redirect('list-project-permission', project.name)
|
||||
|
||||
|
||||
@project_perm_required('manage_project_permission')
|
||||
def project_perm_toggle(request, project, id, perm):
|
||||
permission = get_object_or_404(ProjectPermission, project=project, id=id)
|
||||
# to be sure to dont modify other attribut with the following trick
|
||||
if '-' not in perm:
|
||||
raise Http404
|
||||
perm = perm.replace('-', '_')
|
||||
if hasattr(permission, perm):
|
||||
state = not getattr(permission, perm)
|
||||
setattr(permission, perm, state)
|
||||
permission.save()
|
||||
return HttpResponse('1' if state else '0')
|
||||
else:
|
||||
raise Http404
|
|
@ -24,6 +24,14 @@ DEBUG = True
|
|||
|
||||
TEMPLATE_DEBUG = True
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
)
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
|
@ -44,7 +52,9 @@ INSTALLED_APPS = (
|
|||
'bootstrap3_datetime',
|
||||
'bootstrap3',
|
||||
'colorful',
|
||||
'issue',
|
||||
'accounts',
|
||||
'permissions',
|
||||
'tracker',
|
||||
)
|
||||
|
||||
from django import VERSION
|
||||
|
@ -61,7 +71,7 @@ if VERSION >= (1, 7):
|
|||
MIDDLEWARE_CLASSES += (
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'issue.middleware.ProjectMiddleware',
|
||||
'tracker.middleware.ProjectMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'ponytracker.urls'
|
||||
|
@ -106,13 +116,13 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||
'django.contrib.messages.context_processors.messages',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.request',
|
||||
'issue.context_processors.projects',
|
||||
'issue.context_processors.perm',
|
||||
'tracker.context_processors.projects',
|
||||
'permissions.context_processors.perm',
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'issue.backends.ProjectBackend',
|
||||
'permissions.backends.Backend',
|
||||
)
|
||||
|
||||
SITE_ID = 1
|
||||
|
@ -150,4 +160,6 @@ CELERY_ACCEPT_CONTENT = ['json']
|
|||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
|
||||
AUTH_USER_MODEL = 'issue.User'
|
||||
AUTH_USER_MODEL = 'accounts.User'
|
||||
|
||||
RESERVED_PROJECT_NAME = [ 'login', 'logout', 'profile', 'admin', 'django-admin' ]
|
||||
|
|
|
@ -2,7 +2,17 @@ from django.conf.urls import patterns, include, url
|
|||
from django.contrib import admin
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
# django admin
|
||||
url(r'^django-admin/', include(admin.site.urls)),
|
||||
# markdown preview
|
||||
url(r'^markdown/', include('django_markdown.urls')),
|
||||
url(r'^', include('issue.urls')),
|
||||
# tracker
|
||||
url(r'^', include('tracker.urls')),
|
||||
# permissions managment
|
||||
url(r'^', include('permissions.urls')),
|
||||
# account managment
|
||||
url(r'^', include('accounts.urls')),
|
||||
# login / logout
|
||||
url(r'^login$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}, name='login'),
|
||||
url(r'^logout$', 'django.contrib.auth.views.logout', {'next_page': '/'}, name='logout'),
|
||||
)
|
||||
|
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
16375
static/js/jquery-ui.js
vendored
Normal file
16375
static/js/jquery-ui.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
13
static/js/jquery-ui.min.js
vendored
Normal file
13
static/js/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
static/js/ponytracker.js
Normal file
4
static/js/ponytracker.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* activate tooltips */
|
||||
$(function () {
|
||||
$("[rel='tooltip']").tooltip();
|
||||
})
|
13
templates/403.html
Normal file
13
templates/403.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Permission denied
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>Sorry, you are not allowed to access this page.</p>
|
||||
|
||||
{% endblock %}
|
13
templates/404.html
Normal file
13
templates/404.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Page not found
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>This is not the web page you are looking for.</p>
|
||||
|
||||
{% endblock %}
|
13
templates/500.html
Normal file
13
templates/500.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Server error
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p>Sorry, an error occured. Please try again in few minutes.</p>
|
||||
|
||||
{% endblock %}
|
76
templates/accounts/group_details.html
Normal file
76
templates/accounts/group_details.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
{% extends 'base_settings.html' %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load humanize %}
|
||||
{% load bootstrap3 %}
|
||||
{% load accounts_tags %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static 'css/jquery-ui.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block grouptab %} class="active"{% endblock %}
|
||||
|
||||
{% block moretabs %}
|
||||
<a href="{% url 'add-group' %}" class="pull-right btn btn-success">Add group</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block tabcontent %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Group {{ group }}
|
||||
 
|
||||
<small><span id="users-counter">{{ group.users.count }}</span> users</small>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'edit-group' group.id %}" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> edit</a>
|
||||
<a href="javascript:void(0);" data-item="group" data-action="{% url 'delete-group' group.id %}" data-toggle="modal" data-target="#confirm-delete" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span> delete</a>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a href="#" role="tab">Members</a></li>
|
||||
<form class="form-inline pull-right" method="post" action="{% url 'add-user-to-group' group.id %}" role="form" id="add-user-form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<div class="input-group ui-widget">
|
||||
<input type="text" class="form-control" name="user" placeholder="add users to group" value="">
|
||||
<div class="input-group-addon">
|
||||
<a href="javascript:void(0);" onclick="$('#add-user-form').submit();"><span class="glyphicon glyphicon-plus"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="tab-pane">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item{% if group.users.exists %} hidden{% endif %}" id="users-empty">
|
||||
<em>No users belong to this group.</em>
|
||||
</li>
|
||||
{% for user in group.users.all %}
|
||||
<li class="list-group-item">
|
||||
{{ user.username }}
|
||||
{% if user.fullname %}
|
||||
<span class="text-muted">— {{ user.fullname }}</span>
|
||||
{% endif %}
|
||||
<a href="javascript:void(0);" data-href="{% url 'remove-user-from-group' group.id user.id %}" data-type="users" class="pull-right btn btn-danger btn-xs" role="remove">remove</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% delete_modal %}
|
||||
|
||||
<script src="{% static 'js/jquery-ui.min.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
$('input[name="user"]').autocomplete({
|
||||
source: "{% url 'add-user-to-group' group.id %}"
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/accounts.js' %}"></script>
|
||||
|
||||
{% endblock %}
|
42
templates/accounts/group_edit.html
Normal file
42
templates/accounts/group_edit.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% extends 'base_settings.html' %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load humanize %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block grouptab %} class="active"{% endblock %}
|
||||
|
||||
{% block tabcontent %}
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-offset-3 col-md-6">
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if group %}
|
||||
Edit group
|
||||
{% else %}
|
||||
New group
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<form action="#" method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if group %}
|
||||
<a href="{% url 'show-group' group.id %}" class="btn btn-default">Cancel</a>
|
||||
{% else %}
|
||||
<a href="{% url 'list-group' %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
31
templates/accounts/group_list.html
Normal file
31
templates/accounts/group_list.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{% extends 'base_settings.html' %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load humanize %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block grouptab %} class="active"{% endblock %}
|
||||
|
||||
{% block moretabs %}
|
||||
<a href="{% url 'add-group' %}" class="pull-right btn btn-success">Add group</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block tabcontent %}
|
||||
|
||||
<ul class="list-group">
|
||||
{% if not groups.exists %}
|
||||
<li class="list-group-item">
|
||||
<em>There are no groups quit yet.</em>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for group in groups %}
|
||||
<a class="list-group-item" href="{% url 'show-group' group.id %}">
|
||||
<strong>
|
||||
{{ group }}
|
||||
</strong>
|
||||
— {{ group.users.count }} user{{ group.users.count|pluralize }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
74
templates/accounts/profile.html
Normal file
74
templates/accounts/profile.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load django_markdown %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item disabled">
|
||||
<h3>Your groups</h3>
|
||||
</li>
|
||||
{% for group in request.user.groups.all %}
|
||||
<li class="list-group-item">
|
||||
{{ group }}
|
||||
<div class="pull-right">
|
||||
<a href="javascript:alert('Not yet implemented.');" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-log-out"></span> leave
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not request.user.groups.exists %}
|
||||
<li class="list-group-item">
|
||||
<em>You belong to no groups.</em>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item disabled">
|
||||
<h3>Your teams</h3>
|
||||
</li>
|
||||
{% for team in request.user.teams.all %}
|
||||
<li class="list-group-item">
|
||||
{{ team }}
|
||||
<div class="pull-right">
|
||||
<a href="javascript:alert('Not yet implemented.');" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-log-out"></span> leave
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not request.user.teams.exists %}
|
||||
<li class="list-group-item">
|
||||
<em>You belong to no teams.</em>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item disabled">
|
||||
<h3>Projects that you are watching</h3>
|
||||
</li>
|
||||
{% for project in request.user.subscribed_projects.all %}
|
||||
<li class="list-group-item">
|
||||
{{ project }}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'unsubscribe-project' project.name %}?next={{ request.path }}" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-eye-close"></span> unwatch
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not request.user.subscribed_projects.exists %}
|
||||
<li class="list-group-item">
|
||||
<em>You are not watching any projects.</em>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
21
templates/accounts/tags/delete_modal.html
Normal file
21
templates/accounts/tags/delete_modal.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% load staticfiles %}
|
||||
|
||||
<div class="modal" id="confirm-delete" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading" id="confirm-delete-title">
|
||||
Delete
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="#" method="post" role="form" id="confirm-delete-form" class="text-center">
|
||||
{% csrf_token %}
|
||||
<p id="confirm-delete-message">Are you sure?</p>
|
||||
<button type="submit" class="btn btn-danger">{% block confirm-ok %}Confirm{% endblock %}</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/delete_modal.js' %}"></script>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue