Refactor: greenmine.base

remotes/origin/enhancement/email-actions
David Barragán Merino 2013-10-01 13:17:02 +02:00
parent cda7e7d756
commit d8516a20c7
33 changed files with 372 additions and 508 deletions

View File

@ -1,161 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import uuid
from django.contrib.auth import logout, login, authenticate
from django.contrib.auth.views import login as auth_login, logout as auth_logout
from django.conf import settings
from django.db.models import Q
from django import http
from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework import views
from rest_framework import status, generics, viewsets, views
from haystack import query, inputs
from djmail.template_mail import MagicMailBuilder
from greenmine.base.serializers import (LoginSerializer, UserLogged,
UserSerializer, RoleSerializer,
SearchSerializer)
from greenmine.base.models import User, Role
from greenmine.base import exceptions as excp
from greenmine.scrum import models
class RolesViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = RoleSerializer
def list(self, request, pk=None):
queryset = Role.objects.all()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
try:
role = Role.objects.get(pk=pk)
except Role.DoesNotExist:
raise excp.NotFound()
serializer = self.serializer_class(role)
return Response(serializer.data)
class UsersViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def get_list_queryset(self):
own_projects = (models.Project.objects
.filter(members=self.request.user))
project = self.request.QUERY_PARAMS.get('project', None)
if project is not None:
own_projects = own_projects.filter(pk=project)
queryset = (User.objects.filter(projects__in=own_projects)
.order_by('username').distinct())
return queryset
def list(self, request, pk=None):
queryset = self.get_list_queryset()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
return Response({})
@action(methods=["POST"], permission_classes=[])
def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', None)
if not username_or_email:
return Response({"detail": "Invalid username or password"}, status.HTTP_400_BAD_REQUEST)
try:
queryset = User.objects.all()
user = queryset.get(Q(username=username_or_email) |
Q(email=username_or_email))
except User.DoesNotExist:
return Response({"detail": "Invalid username or password"}, status.HTTP_400_BAD_REQUEST)
user.token = str(uuid.uuid1())
user.save(update_fields=["token"])
mbuilder = MagicMailBuilder()
email = mbuilder.password_recovery(user.email, {"user": user})
return Response({"detail": "Mail sended successful!"})
class Login(viewsets.ViewSet):
permission_classes = (AllowAny,)
def create(self, request, **kwargs):
username = request.DATA.get('username', None)
password = request.DATA.get('password', None)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
if not user.check_password(password):
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
user = authenticate(username=username, password=password)
login(request, user)
serializer = UserSerializer(user)
response_data = serializer.data
response_data["token"] = request.session.session_key
return Response(response_data)
class Logout(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def list(self, request, **kwargs):
return self.logout(request)
def create(self, request, **kwargs):
return self.logout(request)
def logout(self, request):
logout(request)
return Response({})
class Search(viewsets.ViewSet):
def list(self, request, **kwargs):
text = request.QUERY_PARAMS.get('text', "")
project_id = request.QUERY_PARAMS.get('project', None)
try:
project = self._get_project(project_id)
except (models.Project.DoesNotExist, TypeError):
raise excp.PermissionDenied({"detail": "Wrong project id"})
#if not text:
# raise excp.BadRequest("text parameter must be contains text")
queryset = query.SearchQuerySet()
queryset = queryset.filter(text=inputs.AutoQuery(text))
queryset = queryset.filter(project_id=project_id)
return_data = SearchSerializer(queryset)
return Response(return_data.data)
def _get_project(self, project_id):
own_projects = (models.Project.objects
.filter(members=self.request.user))
return own_projects.get(pk=project_id)
class ApiRoot(views.APIView): class ApiRoot(views.APIView):

View File

@ -1,17 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import uuid
from django.db.models import signals from django.db.models import signals
from django.db import models
from django.db.models.signals import post_save, m2m_changed
from django.utils.timezone import now from django.utils.timezone import now
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import UserManager, AbstractUser, Group
from greenmine.scrum.models import Project, UserStory, Task
from greenmine.base.notifications.models import WatcherMixin
import uuid import uuid
@ -30,45 +21,6 @@ def attach_uuid(sender, instance, **kwargs):
instance.uuid = unicode(uuid.uuid1()) instance.uuid = unicode(uuid.uuid1())
class User(WatcherMixin, AbstractUser):
color = models.CharField(max_length=9, null=False, blank=False, default="#669933",
verbose_name=_('color'))
description = models.TextField(null=False, blank=True,
verbose_name=_('description'))
photo = models.FileField(upload_to='files/msg', max_length=500, null=True, blank=True,
verbose_name=_('photo'))
default_language = models.CharField(max_length=20, null=False, blank=True, default='',
verbose_name=_('default language'))
default_timezone = models.CharField(max_length=20, null=False, blank=True, default='',
verbose_name=_('default timezone'))
token = models.CharField(max_length=200, null=False, blank=True, default='',
verbose_name=_('token'))
colorize_tags = models.BooleanField(null=False, blank=True, default=False,
verbose_name=_('colorize tags'))
objects = UserManager()
class Meta:
ordering = ["username"]
class Role(models.Model):
name = models.CharField(max_length=200, null=False, blank=False,
verbose_name=_('name'))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
verbose_name=_('slug'))
permissions = models.ManyToManyField('auth.Permission',
related_name='roles',
verbose_name=_('permissions'))
class Meta:
verbose_name = u'role'
verbose_name_plural = u'roles'
ordering = ['slug']
def __unicode__(self):
return self.name
# Patch api view for correctly return 401 responses on # Patch api view for correctly return 401 responses on
# request is authenticated instead of 403 # request is authenticated instead of 403
from .monkey import patch_api_view; patch_api_view() from .monkey import patch_api_view; patch_api_view()

View File

@ -18,13 +18,4 @@ class DefaultRouter(routers.DefaultRouter):
] ]
class SimpleRouter(routers.SimpleRouter): __all__ = ["DefaultRouter"]
routes = [
routers.SimpleRouter.routes[0],
actions_router,
routers.SimpleRouter.routes[2],
routers.SimpleRouter.routes[1]
]
__all__ = ["DefaultRouter", "SimpleRouter"]

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from django.db.models.loading import get_model
from rest_framework.response import Response
from rest_framework import viewsets
from haystack import query, inputs
from greenmine.base import exceptions as excp
from .serializers import SearchSerializer
class SearchViewSet(viewsets.ViewSet):
def list(self, request, **kwargs):
project_model = get_model("scrum", "Project")
text = request.QUERY_PARAMS.get('text', "")
project_id = request.QUERY_PARAMS.get('project', None)
try:
project = self._get_project(project_id)
except (project_model.DoesNotExist, TypeError):
raise excp.PermissionDenied({"detail": "Wrong project id"})
#if not text:
# raise excp.BadRequest("text parameter must be contains text")
queryset = query.SearchQuerySet()
queryset = queryset.filter(text=inputs.AutoQuery(text))
queryset = queryset.filter(project_id=project_id)
return_data = SearchSerializer(queryset)
return Response(return_data.data)
def _get_project(self, project_id):
project_model = get_model("scrum", "Project")
own_projects = (project_model.objects
.filter(members=self.request.user))
return own_projects.get(pk=project_id)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers
class SearchSerializer(serializers.Serializer):
id = serializers.CharField(max_length=255)
model_name = serializers.CharField(max_length=255)
pk = serializers.IntegerField()
score = serializers.FloatField()
stored_fields = serializers.SerializerMethodField('get_stored_fields')
def get_stored_fields(self, obj):
return obj.get_stored_fields()
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
return instance
return attrs

View File

@ -1,121 +0,0 @@
# -*- coding: utf-8 -*-
import json
from django.test import TestCase
from django.core import mail
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from greenqueue import send_task
class LowLevelEmailTests(TestCase):
def setUp(self):
mail.outbox = []
def test_send_one_mail(self):
send_task("send-mail", args=["subject", "template", ["hola@niwi.be"]])
self.assertEqual(len(mail.outbox), 1)
def test_send_bulk_mail(self):
send_task("send-bulk-mail", args=[[
('s1', 't1', ['hola@niwi.be']),
('s2', 't2', ['hola@niwi.be']),
]])
self.assertEqual(len(mail.outbox), 2)
class UserMailTests(TestCase):
def setUp(self):
self.user1 = User.objects.create(
username='test1',
email='test1@test.com',
is_active=True,
is_staff=True,
is_superuser=True,
)
self.user2 = User.objects.create(
username='test2',
email='test2@test.com',
is_active=True,
is_staff=False,
is_superuser=False,
)
self.user1.set_password("test")
self.user2.set_password("test")
self.user1.save()
self.user2.save()
mail.outbox = []
def test_remember_password(self):
url = reverse("remember-password")
post_params = {'email': 'test2@test.com'}
response = self.client.post(url, post_params, follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 1)
jdata = json.loads(response.content)
self.assertIn("valid", jdata)
self.assertTrue(jdata['valid'])
def test_remember_password_not_exists(self):
url = reverse("remember-password")
post_params = {'email': 'test2@testa.com'}
response = self.client.post(url, post_params, follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 0)
jdata = json.loads(response.content)
self.assertIn("valid", jdata)
self.assertFalse(jdata['valid'])
def test_send_recovery_password_by_staff(self):
url = reverse("users-recovery-password", args=[self.user2.pk])
ok = self.client.login(username="test1", password="test")
self.assertTrue(ok)
# pre test
self.assertTrue(self.user2.is_active)
self.assertEqual(self.user2.token, None)
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
# expected redirect
self.assertEqual(response.redirect_chain, [('http://testserver/users/2/edit/', 302)])
# test mail sending
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Greenmine: password recovery.")
# test user model modification
self.user2 = User.objects.get(pk=self.user2.pk)
self.assertTrue(self.user2.is_active)
self.assertFalse(self.user2.has_usable_password())
self.assertNotEqual(self.user2.token, None)
url = reverse('password-recovery', args=[self.user2.token])
post_params = {
'password': '123123',
'password2': '123123',
}
response = self.client.post(url, post_params, follow=True)
self.assertEqual(response.status_code, 200)
# expected redirect
self.assertEqual(response.redirect_chain, [('http://testserver/login/', 302)])
ok = self.client.login(username="test2", password="123123")
self.assertTrue(ok)

View File

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from . import api, routers
router = routers.SimpleRouter(trailing_slash=False)
router.register(r"users", api.UsersViewSet, base_name="users")
router.register(r"roles", api.RolesViewSet, base_name="roles")
router.register(r"search", api.Search, base_name="search")
router.register(r"auth/login", api.Login, base_name="auth-login")
router.register(r"auth/logout", api.Logout, base_name="auth-logout")
urlpatterns = router.urls

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -1,14 +1,18 @@
# -*- coding: utf-8 -*-
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from greenmine.base.models import Role, User from .models import Role, User
from greenmine.base.forms import UserChangeForm, UserCreationForm from .forms import UserChangeForm, UserCreationForm
admin.site.unregister(Group) admin.site.unregister(Group)
class RoleAdmin(admin.ModelAdmin): class RoleAdmin(admin.ModelAdmin):
list_display = ["name"] list_display = ["name"]
filter_horizontal = ('permissions',) filter_horizontal = ('permissions',)
@ -22,8 +26,10 @@ class RoleAdmin(admin.ModelAdmin):
return super(RoleAdmin, self).formfield_for_manytomany( return super(RoleAdmin, self).formfield_for_manytomany(
db_field, request=request, **kwargs) db_field, request=request, **kwargs)
admin.site.register(Role, RoleAdmin) admin.site.register(Role, RoleAdmin)
class UserAdmin(DjangoUserAdmin): class UserAdmin(DjangoUserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),

134
greenmine/base/users/api.py Normal file
View File

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
from django.db.models.loading import get_model
from django.contrib.auth import logout, login, authenticate
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import status, viewsets
from djmail.template_mail import MagicMailBuilder
from greenmine.base import exceptions as excp
from .serializers import (
LoginSerializer,
UserLogged,
UserSerializer,
RoleSerializer,
)
from .models import User, Role
import uuid
class RolesViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = RoleSerializer
def list(self, request, pk=None):
queryset = Role.objects.all()
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
try:
role = Role.objects.get(pk=pk)
except Role.DoesNotExist:
raise excp.NotFound()
serializer = self.serializer_class(role)
return Response(serializer.data)
class UsersViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def get_list_queryset(self):
project_model = get_model("scrum", "Project")
own_projects = (project_model.objects
.filter(members=self.request.user))
project = self.request.QUERY_PARAMS.get('project', None)
if project is not None:
own_projects = own_projects.filter(pk=project)
queryset = (User.objects.filter(projects__in=own_projects)
.order_by('username').distinct())
return queryset
def list(self, request, pk=None):
queryset = self.get_list_queryset()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
return Response({})
@action(methods=["POST"], permission_classes=[])
def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', None)
if not username_or_email:
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
try:
queryset = User.objects.all()
user = queryset.get(Q(username=username_or_email) |
Q(email=username_or_email))
except User.DoesNotExist:
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
user.token = str(uuid.uuid1())
user.save(update_fields=["token"])
mbuilder = MagicMailBuilder()
email = mbuilder.password_recovery(user.email, {"user": user})
return Response({"detail": "Mail sended successful!"})
class LoginViewSet(viewsets.ViewSet):
permission_classes = (AllowAny,)
def create(self, request, **kwargs):
username = request.DATA.get('username', None)
password = request.DATA.get('password', None)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
if not user.check_password(password):
return Response({"detail": "Invalid username or password"},
status.HTTP_400_BAD_REQUEST)
user = authenticate(username=username, password=password)
login(request, user)
serializer = UserSerializer(user)
response_data = serializer.data
response_data["token"] = request.session.session_key
return Response(response_data)
class LogoutViewSet(viewsets.ViewSet):
permission_classes = (IsAuthenticated,)
def list(self, request, **kwargs):
return self.logout(request)
def create(self, request, **kwargs):
return self.logout(request)
def logout(self, request):
logout(request)
return Response({})

View File

@ -22,3 +22,5 @@ class SessionAuthentication(BaseAuthentication):
return (user, None) return (user, None)

View File

@ -1,7 +1,7 @@
[ [
{ {
"pk": 1, "pk": 1,
"model": "base.role", "model": "users.role",
"fields": { "fields": {
"permissions": [ "permissions": [
[ [
@ -51,32 +51,32 @@
], ],
[ [
"add_role", "add_role",
"base", "users",
"role" "role"
], ],
[ [
"change_role", "change_role",
"base", "users",
"role" "role"
], ],
[ [
"delete_role", "delete_role",
"base", "users",
"role" "role"
], ],
[ [
"add_user", "add_user",
"base", "users",
"user" "user"
], ],
[ [
"change_user", "change_user",
"base", "users",
"user" "user"
], ],
[ [
"delete_user", "delete_user",
"base", "users",
"user" "user"
], ],
[ [

View File

@ -1,7 +1,7 @@
[ [
{ {
"pk": 1, "pk": 1,
"model": "base.user", "model": "users.user",
"fields": { "fields": {
"username": "admin", "username": "admin",
"first_name": "", "first_name": "",

View File

@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm as DjangoUserCreationForm, UserChangeForm as DjangoUserChangeForm from django.contrib.auth.forms import (
from greenmine.base.models import User UserCreationForm as DjangoUserCreationForm,
UserChangeForm as DjangoUserChangeForm
)
from .models import User
class UserCreationForm(DjangoUserCreationForm): class UserCreationForm(DjangoUserCreationForm):

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import UserManager, AbstractUser
from greenmine.base.notifications.models import WatcherMixin
class User(WatcherMixin, AbstractUser):
color = models.CharField(max_length=9, null=False, blank=False, default="#669933",
verbose_name=_('color'))
description = models.TextField(null=False, blank=True,
verbose_name=_('description'))
photo = models.FileField(upload_to='files/msg', max_length=500, null=True, blank=True,
verbose_name=_('photo'))
default_language = models.CharField(max_length=20, null=False, blank=True, default='',
verbose_name=_('default language'))
default_timezone = models.CharField(max_length=20, null=False, blank=True, default='',
verbose_name=_('default timezone'))
token = models.CharField(max_length=200, null=False, blank=True, default='',
verbose_name=_('token'))
colorize_tags = models.BooleanField(null=False, blank=True, default=False,
verbose_name=_('colorize tags'))
objects = UserManager()
class Meta:
ordering = ["username"]
class Role(models.Model):
name = models.CharField(max_length=200, null=False, blank=False,
verbose_name=_('name'))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
verbose_name=_('slug'))
permissions = models.ManyToManyField('auth.Permission',
related_name='roles',
verbose_name=_('permissions'))
class Meta:
verbose_name = u'role'
verbose_name_plural = u'roles'
ordering = ['slug']
def __unicode__(self):
return self.name

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from greenmine.base.models import User, Role from .models import User, Role
class UserLogged(object): class UserLogged(object):
@ -70,23 +70,3 @@ class RoleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
fields = ('id', 'name', 'slug', 'permissions',) fields = ('id', 'name', 'slug', 'permissions',)
class SearchSerializer(serializers.Serializer):
id = serializers.CharField(max_length=255)
model_name = serializers.CharField(max_length=255)
pk = serializers.IntegerField()
score = serializers.FloatField()
stored_fields = serializers.SerializerMethodField('get_stored_fields')
def get_stored_fields(self, obj):
return obj.get_stored_fields()
def restore_object(self, attrs, instance=None):
"""
Given a dictionary of deserialized field values, either update
an existing model instance, or create a new model instance.
"""
if instance is not None:
return instance
return attrs

View File

@ -1,6 +1,7 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from django.db import models from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from picklefield.fields import PickledObjectField from picklefield.fields import PickledObjectField
@ -21,7 +22,7 @@ class Document(models.Model):
project = models.ForeignKey('scrum.Project', null=False, blank=False, project = models.ForeignKey('scrum.Project', null=False, blank=False,
related_name='documents', related_name='documents',
verbose_name=_('project')) verbose_name=_('project'))
owner = models.ForeignKey('base.User', null=False, blank=False, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name='owned_documents', related_name='owned_documents',
verbose_name=_('owner')) verbose_name=_('owner'))
attached_file = models.FileField(max_length=1000, null=True, blank=True, attached_file = models.FileField(max_length=1000, null=True, blank=True,

View File

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
from greenmine.base import routers
from . import api
router = routers.SimpleRouter(trailing_slash=False)
router.register(r"documents", api.DocumentsViewSet, base_name="documents")
urlpatterns = router.urls

View File

@ -2,6 +2,7 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.dispatch import receiver from django.dispatch import receiver
@ -37,7 +38,7 @@ class Question(models.Model):
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_('ref')) verbose_name=_('ref'))
owner = models.ForeignKey('base.User', null=True, blank=True, default=None, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
related_name='owned_questions', related_name='owned_questions',
verbose_name=_('owner')) verbose_name=_('owner'))
status = models.ForeignKey('QuestionStatus', null=False, blank=False, status = models.ForeignKey('QuestionStatus', null=False, blank=False,
@ -60,14 +61,14 @@ class Question(models.Model):
verbose_name=_('milestone')) verbose_name=_('milestone'))
finished_date = models.DateTimeField(null=True, blank=True, finished_date = models.DateTimeField(null=True, blank=True,
verbose_name=_('finished date')) verbose_name=_('finished date'))
assigned_to = models.ForeignKey('base.User', null=True, blank=True, default=None, assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
related_name='questions_assigned_to_me', related_name='questions_assigned_to_me',
verbose_name=_('assigned_to')) verbose_name=_('assigned_to'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('created date')) verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('modified date')) verbose_name=_('modified date'))
watchers = models.ManyToManyField('base.User', null=True, blank=True, watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='watched_questions', related_name='watched_questions',
verbose_name=_('watchers')) verbose_name=_('watchers'))
tags = PickledObjectField(null=False, blank=True, tags = PickledObjectField(null=False, blank=True,

View File

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from . import api
urlpatterns = format_suffix_patterns(patterns('',
url(r'^questions/$', api.QuestionList.as_view(), name='question-list'),
url(r'^questions/(?P<pk>[0-9]+)/$', api.QuestionDetail.as_view(), name='question-detail'),
))

View File

@ -5,7 +5,7 @@ from django.db.models import Q
from rest_framework import mixins, viewsets from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from greenmine.base.models import * from greenmine.base.users.models import *
from greenmine.base.notifications.api import NotificationSenderMixin from greenmine.base.notifications.api import NotificationSenderMixin
from greenmine.scrum.serializers import * from greenmine.scrum.serializers import *

View File

@ -11,7 +11,7 @@ from django.utils.timezone import now
from django.contrib.webdesign import lorem_ipsum from django.contrib.webdesign import lorem_ipsum
from greenmine.base.models import User, Role from greenmine.base.users.models import User, Role
from greenmine.scrum.models import * from greenmine.scrum.models import *
from greenmine.questions.models import * from greenmine.questions.models import *

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models.loading import get_model from django.db.models.loading import get_model
@ -174,11 +176,11 @@ class Points(models.Model):
class Membership(models.Model): class Membership(models.Model):
user = models.ForeignKey('base.User', null=False, blank=False, user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name="memberships") related_name="memberships")
project = models.ForeignKey('Project', null=False, blank=False, project = models.ForeignKey('Project', null=False, blank=False,
related_name="memberships") related_name="memberships")
role = models.ForeignKey('base.Role', null=False, blank=False, role = models.ForeignKey('users.Role', null=False, blank=False,
related_name="memberships") related_name="memberships")
class Meta: class Meta:
@ -198,10 +200,10 @@ class Project(models.Model, WatchedMixin):
verbose_name=_('created date')) verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
verbose_name=_('modified date')) verbose_name=_('modified date'))
owner = models.ForeignKey('base.User', null=False, blank=False, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name='owned_projects', related_name='owned_projects',
verbose_name=_('owner')) verbose_name=_('owner'))
members = models.ManyToManyField('base.User', related_name='projects', through='Membership', members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='projects', through='Membership',
verbose_name=_('members')) verbose_name=_('members'))
public = models.BooleanField(default=True, null=False, blank=True, public = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_('public')) verbose_name=_('public'))
@ -264,12 +266,12 @@ class Project(models.Model, WatchedMixin):
@property @property
def list_roles(self): def list_roles(self):
role_model = get_model('base', 'Role') role_model = get_model('users', 'Role')
return role_model.objects.filter(id__in=list(self.memberships.values_list('role', flat=True))) return role_model.objects.filter(id__in=list(self.memberships.values_list('role', flat=True)))
@property @property
def list_users(self): def list_users(self):
user_model = get_model('base', 'User') user_model = get_user_model()
return user_model.objects.filter(id__in=list(self.memberships.values_list('user', flat=True))) return user_model.objects.filter(id__in=list(self.memberships.values_list('user', flat=True)))
def update_role_points(self): def update_role_points(self):
@ -298,7 +300,7 @@ class Milestone(models.Model, WatchedMixin):
verbose_name=_('name')) verbose_name=_('name'))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
verbose_name=_('slug')) verbose_name=_('slug'))
owner = models.ForeignKey('base.User', null=True, blank=True, related_name='owned_milestones', owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='owned_milestones',
verbose_name=_('owner')) verbose_name=_('owner'))
project = models.ForeignKey('Project', null=False, blank=False, related_name='milestones', project = models.ForeignKey('Project', null=False, blank=False, related_name='milestones',
verbose_name=_('project')) verbose_name=_('project'))
@ -403,7 +405,7 @@ class RolePoints(models.Model):
user_story = models.ForeignKey('UserStory', null=False, blank=False, user_story = models.ForeignKey('UserStory', null=False, blank=False,
related_name='role_points', related_name='role_points',
verbose_name=_('user story')) verbose_name=_('user story'))
role = models.ForeignKey('base.Role', null=False, blank=False, role = models.ForeignKey('users.Role', null=False, blank=False,
related_name='role_points', related_name='role_points',
verbose_name=_('role')) verbose_name=_('role'))
points = models.ForeignKey('Points', null=False, blank=False, points = models.ForeignKey('Points', null=False, blank=False,
@ -425,7 +427,7 @@ class UserStory(WatchedMixin, models.Model):
project = models.ForeignKey('Project', null=False, blank=False, project = models.ForeignKey('Project', null=False, blank=False,
related_name='user_stories', related_name='user_stories',
verbose_name=_('project')) verbose_name=_('project'))
owner = models.ForeignKey('base.User', null=True, blank=True, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='owned_user_stories', related_name='owned_user_stories',
verbose_name=_('owner')) verbose_name=_('owner'))
status = models.ForeignKey('UserStoryStatus', null=False, blank=False, status = models.ForeignKey('UserStoryStatus', null=False, blank=False,
@ -447,7 +449,7 @@ class UserStory(WatchedMixin, models.Model):
verbose_name=_('subject')) verbose_name=_('subject'))
description = models.TextField(null=False, blank=True, description = models.TextField(null=False, blank=True,
verbose_name=_('description')) verbose_name=_('description'))
watchers = models.ManyToManyField('base.User', null=True, blank=True, watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='watched_us', related_name='watched_us',
verbose_name=_('watchers')) verbose_name=_('watchers'))
client_requirement = models.BooleanField(default=False, null=False, blank=True, client_requirement = models.BooleanField(default=False, null=False, blank=True,
@ -504,7 +506,7 @@ class UserStory(WatchedMixin, models.Model):
class Attachment(models.Model): class Attachment(models.Model):
owner = models.ForeignKey('base.User', null=False, blank=False, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name='change_attachments', related_name='change_attachments',
verbose_name=_('owner')) verbose_name=_('owner'))
project = models.ForeignKey('Project', null=False, blank=False, project = models.ForeignKey('Project', null=False, blank=False,
@ -541,7 +543,7 @@ class Task(models.Model, WatchedMixin):
verbose_name=_('user story')) verbose_name=_('user story'))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_('ref')) verbose_name=_('ref'))
owner = models.ForeignKey('base.User', null=True, blank=True, default=None, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
related_name='owned_tasks', related_name='owned_tasks',
verbose_name=_('owner')) verbose_name=_('owner'))
status = models.ForeignKey('TaskStatus', null=False, blank=False, status = models.ForeignKey('TaskStatus', null=False, blank=False,
@ -563,10 +565,10 @@ class Task(models.Model, WatchedMixin):
verbose_name=_('subject')) verbose_name=_('subject'))
description = models.TextField(null=False, blank=True, description = models.TextField(null=False, blank=True,
verbose_name=_('description')) verbose_name=_('description'))
assigned_to = models.ForeignKey('base.User', blank=True, null=True, default=None, assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None,
related_name='user_storys_assigned_to_me', related_name='user_storys_assigned_to_me',
verbose_name=_('assigned to')) verbose_name=_('assigned to'))
watchers = models.ManyToManyField('base.User', null=True, blank=True, watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='watched_tasks', related_name='watched_tasks',
verbose_name=_('watchers')) verbose_name=_('watchers'))
tags = PickledObjectField(null=False, blank=True, tags = PickledObjectField(null=False, blank=True,
@ -624,7 +626,7 @@ class Issue(models.Model, WatchedMixin):
verbose_name=_('uuid')) verbose_name=_('uuid'))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_('ref')) verbose_name=_('ref'))
owner = models.ForeignKey('base.User', null=True, blank=True, default=None, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
related_name='owned_issues', related_name='owned_issues',
verbose_name=_('owner')) verbose_name=_('owner'))
status = models.ForeignKey('IssueStatus', null=False, blank=False, status = models.ForeignKey('IssueStatus', null=False, blank=False,
@ -655,10 +657,10 @@ class Issue(models.Model, WatchedMixin):
verbose_name=_('subject')) verbose_name=_('subject'))
description = models.TextField(null=False, blank=True, description = models.TextField(null=False, blank=True,
verbose_name=_('description')) verbose_name=_('description'))
assigned_to = models.ForeignKey('base.User', blank=True, null=True, default=None, assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None,
related_name='issues_assigned_to_me', related_name='issues_assigned_to_me',
verbose_name=_('assigned to')) verbose_name=_('assigned to'))
watchers = models.ManyToManyField('base.User', null=True, blank=True, watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='watched_issues', related_name='watched_issues',
verbose_name=_('watchers')) verbose_name=_('watchers'))
tags = PickledObjectField(null=False, blank=True, tags = PickledObjectField(null=False, blank=True,

View File

@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
from greenmine.base.models import * from greenmine.base.users.models import *
from greenmine.scrum.models import * from greenmine.scrum.models import *
from picklefield.fields import dbsafe_encode, dbsafe_decode from picklefield.fields import dbsafe_encode, dbsafe_decode

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from greenmine.base import signals from greenmine.base import signals
from greenmine.base.utils.auth import set_token from greenmine.base.users.utils import set_token
from greenmine.base.mail.tasks import send_mail, send_bulk_mail from greenmine.base.mail.tasks import send_mail, send_bulk_mail

View File

@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.scrum import api
from greenmine.base import routers
router = routers.DefaultRouter(trailing_slash=False)
router.register(r"projects", api.ProjectViewSet, base_name="projects")
router.register(r"milestones", api.MilestoneViewSet, base_name="milestones")
router.register(r"userstories", api.UserStoryViewSet, base_name="userstories")
router.register(r"issue-attachments", api.IssuesAttachmentViewSet, base_name="issue-attachments")
router.register(r"task-attachments", api.TasksAttachmentViewSet, base_name="task-attachments")
router.register(r"tasks", api.TaskViewSet, base_name="tasks")
router.register(r"issues", api.IssueViewSet, base_name="issues")
router.register(r"severities", api.SeverityViewSet, base_name="severities")
router.register(r"issue-statuses", api.IssueStatusViewSet, base_name="issue-statuses")
router.register(r"task-statuses", api.TaskStatusViewSet, base_name="task-statuses")
router.register(r"userstory-statuses", api.UserStoryStatusViewSet, base_name="userstory-statuses")
router.register(r"priorities", api.PriorityViewSet, base_name="priorities")
router.register(r"issue-types", api.IssueTypeViewSet, base_name="issue-types")
router.register(r"points", api.PointsViewSet, base_name="points")
urlpatterns = router.urls
#urlpatterns = format_suffix_patterns(patterns('',
# url(r'^projects/$', api.ProjectList.as_view(), name='project-list'),
# url(r'^projects/(?P<pk>[0-9]+)/$', api.ProjectDetail.as_view(), name='project-detail'),
# url(r'^milestones/$', api.MilestoneList.as_view(), name='milestone-list'),
# url(r'^milestones/(?P<pk>[0-9]+)/$', api.MilestoneDetail.as_view(), name='milestone-detail'),
# url(r'^user-stories/$', api.UserStoryList.as_view(), name='user-story-list'),
# url(r'^user-stories/(?P<pk>[0-9]+)/$', api.UserStoryDetail.as_view(), name='user-story-detail'),
# url(r'^user-stories/points/$', api.PointsList.as_view(), name='points-list'),
# url(r'^user-stories/points/(?P<pk>[0-9]+)/$', api.PointsDetail.as_view(), name='points-detail'),
# url(r'^user-stories/statuses/$', api.UserStoryStatusList.as_view(), name='user-story-status-list'),
# url(r'^user-stories/statuses/(?P<pk>[0-9]+)/$', api.UserStoryStatusDetail.as_view(), name='user-story-status-detail'),
# url(r'^issues/$', api.IssueList.as_view(), name='issues-list'),
# url(r'^issues/(?P<pk>[0-9]+)/$', api.IssueDetail.as_view(), name='issues-detail'),
# url(r'^issues/attachments/$', api.IssuesAttachmentList.as_view(), name='issues-attachment-list'),
# url(r'^issues/attachments/(?P<pk>[0-9]+)/$', api.IssuesAttachmentDetail.as_view(), name='issues-attachment-detail'),
# url(r'^issues/statuses/$', api.IssueStatusList.as_view(), name='issues-status-list'),
# url(r'^issues/statuses/(?P<pk>[0-9]+)/$', api.IssueStatusDetail.as_view(), name='issues-status-detail'),
# url(r'^issues/types/$', api.IssueTypeList.as_view(), name='issues-type-list'),
# url(r'^issues/types/(?P<pk>[0-9]+)/$', api.IssueTypeDetail.as_view(), name='issues-type-detail'),
# url(r'^tasks/$', api.TaskList.as_view(), name='tasks-list'),
# url(r'^tasks/(?P<pk>[0-9]+)/$', api.TaskDetail.as_view(), name='tasks-detail'),
# url(r'^tasks/attachments/$', api.TasksAttachmentList.as_view(), name='tasks-attachment-list'),
# url(r'^tasks/attachments/(?P<pk>[0-9]+)/$', api.TasksAttachmentDetail.as_view(), name='tasks-attachment-detail'),
# url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'),
# url(r'^severities/(?P<pk>[0-9]+)/$', api.SeverityDetail.as_view(), name='severity-detail'),
# url(r'^tasks/statuses/$', api.TaskStatusList.as_view(), name='tasks-status-list'),
# url(r'^tasks/statuses/(?P<pk>[0-9]+)/$', api.TaskStatusDetail.as_view(), name='tasks-status-detail'),
# url(r'^priorities/$', api.PriorityList.as_view(), name='priority-list'),
# url(r'^priorities/(?P<pk>[0-9]+)/$', api.PriorityDetail.as_view(), name='priority-detail'),
#))

View File

@ -191,8 +191,10 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'greenmine.base', 'greenmine.base',
'greenmine.base.mail',
'greenmine.base.notifications', 'greenmine.base.notifications',
'greenmine.base.users',
'greenmine.base.mail',
'greenmine.base.searches',
'greenmine.scrum', 'greenmine.scrum',
'greenmine.wiki', 'greenmine.wiki',
'greenmine.questions', 'greenmine.questions',
@ -258,7 +260,7 @@ LOGGING = {
} }
AUTH_USER_MODEL = 'base.User' AUTH_USER_MODEL = 'users.User'
FORMAT_MODULE_PATH = 'greenmine.base.formats' FORMAT_MODULE_PATH = 'greenmine.base.formats'
DATE_INPUT_FORMATS = ( DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%b %d %Y', '%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%b %d %Y',
@ -289,7 +291,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'greenmine.base.auth.SessionAuthentication', 'greenmine.base.users.auth.SessionAuthentication',
), ),
'FILTER_BACKEND': 'greenmine.base.filters.SimpleFilterBackend', 'FILTER_BACKEND': 'greenmine.base.filters.SimpleFilterBackend',
} }

View File

@ -6,13 +6,65 @@ from django.contrib import admin
admin.autodiscover() admin.autodiscover()
from greenmine.base import routers
from greenmine.base.api import ApiRoot from greenmine.base.api import ApiRoot
from greenmine.base.users.api import (
LoginViewSet,
LogoutViewSet,
RolesViewSet,
UsersViewSet
)
from greenmine.base.searches.api import SearchViewSet
from greenmine.scrum.api import (
MilestoneViewSet,
PriorityViewSet,
ProjectViewSet,
SeverityViewSet,
UserStoryStatusViewSet,
UserStoryViewSet,
TaskStatusViewSet,
TaskViewSet,
TasksAttachmentViewSet,
PointsViewSet,
IssueStatusViewSet,
IssueTypeViewSet,
IssueViewSet,
IssuesAttachmentViewSet
)
router = routers.DefaultRouter(trailing_slash=False)
# greenmine.base.users
router.register(r"users", UsersViewSet, base_name="users")
router.register(r"roles", RolesViewSet, base_name="roles")
router.register(r"auth/login", LoginViewSet, base_name="auth-login")
router.register(r"auth/logout", LogoutViewSet, base_name="auth-logout")
# greenmine.base.searches
router.register(r"search", SearchViewSet, base_name="search")
# greenmine.scrum
router.register(r"projects", ProjectViewSet, base_name="projects")
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"issue-attachments", IssuesAttachmentViewSet, base_name="issue-attachments")
router.register(r"task-attachments", TasksAttachmentViewSet, base_name="task-attachments")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"severities", SeverityViewSet, base_name="severities")
router.register(r"issue-statuses", IssueStatusViewSet, base_name="issue-statuses")
router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses")
router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses")
router.register(r"priorities", PriorityViewSet, base_name="priorities")
router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types")
router.register(r"points", PointsViewSet, base_name="points")
#greenmine.issues
#greenmine.wiki
#greenmine.documents
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^api/v1/', include('greenmine.base.urls')), url(r'^api/v1$', ApiRoot.as_view()),
url(r'^api/v1/', include('greenmine.scrum.urls')), url(r'^api/v1/', include(router.urls)),
url(r'^api/v1/', include('greenmine.wiki.urls')), # TODO: Refactor to use ViewSet
#url(r'^api/v1/', include('greenmine.wiki.urls')),
# TODO: Finish the documents and questions app # TODO: Finish the documents and questions app
#url(r'^api/v1/', include('greenmine.questions.urls')), #url(r'^api/v1/', include('greenmine.questions.urls')),
#url(r'^api/v1/', include('greenmine.documents.urls')), #url(r'^api/v1/', include('greenmine.documents.urls')),

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -12,10 +13,10 @@ class WikiPage(models.Model):
verbose_name=_('slug')) verbose_name=_('slug'))
content = models.TextField(null=False, blank=True, content = models.TextField(null=False, blank=True,
verbose_name=_('content')) verbose_name=_('content'))
owner = models.ForeignKey('base.User', null=True, blank=True, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='owned_wiki_pages', related_name='owned_wiki_pages',
verbose_name=_('owner')) verbose_name=_('owner'))
watchers = models.ManyToManyField('base.User', null=True, blank=True, watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='watched_wiki_pages', related_name='watched_wiki_pages',
verbose_name=_('watchers')) verbose_name=_('watchers'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
@ -42,7 +43,7 @@ class WikiPageAttachment(models.Model):
wikipage = models.ForeignKey('WikiPage', null=False, blank=False, wikipage = models.ForeignKey('WikiPage', null=False, blank=False,
related_name='attachments', related_name='attachments',
verbose_name=_('wiki page')) verbose_name=_('wiki page'))
owner = models.ForeignKey('base.User', null=False, blank=False, owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
related_name='owned_wiki_attachments', related_name='owned_wiki_attachments',
verbose_name=_('owner')) verbose_name=_('owner'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,

View File

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.wiki import api
urlpatterns = format_suffix_patterns(patterns('',
url(r'^pages$', api.WikiPageList.as_view(), name='wiki-page-list'),
url(r'^pages/(?P<projectid>\d+)-(?P<slug>[\w\-\d]+)$', api.WikiPageDetail.as_view(), name='wiki-page-detail'),
#url(r'^wiki_page_attachments/$', api.WikiPageAttachmentList.as_view(), name='wiki-page-attachment-list'),
#url(r'^wiki_page_attachments/(?P<pk>[0-9]+)/$', api.WikiPageAttachmentDetail.as_view(), name='wiki-page-attachment-detail'),
))