diff --git a/greenmine/base/api.py b/greenmine/base/api.py index 59a28bc0..5cbafba8 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -1,111 +1,95 @@ # -*- 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.renderers import JSONRenderer -from rest_framework.parsers import JSONParser -from rest_framework.reverse import reverse -from rest_framework.views import APIView +from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from rest_framework import generics +from rest_framework import status, generics, viewsets +from rest_framework import exceptions as excp +import django_filters from haystack import query, inputs +from djmail.template_mail import MagicMailBuilder + +from greenmine.base.serializers import (LoginSerializer, UserLogged, + UserSerializer, RoleSerializer) -from greenmine.base.serializers import LoginSerializer, UserLogged, UserSerializer, RoleSerializer from greenmine.base.serializers import SearchSerializer from greenmine.base.models import User, Role from greenmine.scrum import models -from django.conf import settings - -import django_filters -class ApiRoot(APIView): - def get(self, request, format=None): - return Response({ - 'login': reverse('login', request=request, format=format), - 'logout': reverse('logout', request=request, format=format), - 'projects': reverse('project-list', request=request, format=format), - 'milestones': reverse('milestone-list', request=request, format=format), - 'user-stories': reverse('user-story-list', request=request, format=format), - 'user-stories/statuses': reverse('user-story-status-list', request=request, format=format), - 'user-stories/points': reverse('points-list', request=request, format=format), - 'issues/attachments': reverse('issues-attachment-list', request=request, format=format), - 'issues/statuses': reverse('issues-status-list', request=request, format=format), - 'issues/types': reverse('issues-type-list', request=request, format=format), - 'issues': reverse('issues-list', request=request, format=format), - 'tasks': reverse('tasks-list', request=request, format=format), - 'tasks/statuses': reverse('tasks-status-list', request=request, format=format), - 'tasks/attachments': reverse('tasks-attachment-list', request=request, format=format), - 'severities': reverse('severity-list', request=request, format=format), - 'priorities': reverse('priority-list', request=request, format=format), - 'documents': reverse('document-list', request=request, format=format), - 'questions': reverse('question-list', request=request, format=format), - 'wiki/pages': reverse('wiki-page-list', request=request, format=format), - 'users': reverse('user-list', request=request, format=format), - 'roles': reverse('user-roles', request=request, format=format), - 'search': reverse('search', request=request, format=format), - }) - - -class RoleList(generics.ListCreateAPIView): - model = Role +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): + role = Role.objects.get(pk=pk) + serializer = self.serializer_class(role) + return Response(serializer.data) + + +class UsersViewSet(viewsets.ViewSet): permission_classes = (IsAuthenticated,) - def get_queryset(self): - return self.model.objects.all() + def get_list_queryset(self): + own_projects = (models.Project.objects + .filter(members=self.request.user)) - -class RoleDetail(generics.RetrieveAPIView): - model = Role - serializer_class = RoleSerializer - permission_classes = (IsAuthenticated,) - - -class UserFilter(django_filters.FilterSet): - class Meta: - model = User - fields = ['is_active'] - - -class UserList(generics.ListCreateAPIView): - model = User - serializer_class = UserSerializer - filter_class = UserFilter - permission_classes = (IsAuthenticated,) - - def get_queryset(self): - projects = models.Project.objects.filter(members=self.request.user) - - #Project filtering project = self.request.QUERY_PARAMS.get('project', None) if project is not None: - projects = projects.filter(id=project) + own_projects = own_projects.filter(pk=project) - return super(UserList, self).get_queryset().filter(projects__in=projects)\ - .order_by('id').distinct() + queryset = (User.objects.filter(projects__in=own_projects) + .order_by('username').distinct()) - def pre_save(self, obj): - pass + return queryset + def list(self, request, pk=None): + queryset = self.get_list_queryset() + serializer = UserSerializer(queryset, many=True) + return Response(serializer.data) -class UserDetail(generics.RetrieveUpdateDestroyAPIView): - model = User - serializer_class = UserSerializer - permission_classes = (IsAuthenticated,) + def retrieve(self, request, pk=None): + return Response({}) -import uuid -from django.db.models import Q -from djmail.template_mail import MagicMailBuilder + @action(methods=["POST"], permission_classes=[]) + def login(self, request, pk=None): + 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) -class RecoveryPassword(APIView): - def post(self, 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) + + @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: @@ -126,63 +110,31 @@ class RecoveryPassword(APIView): return Response({"detail": "Mail sended successful!"}) - -class Login(APIView): - def post(self, request, format=None): - username = request.DATA.get('username', None) - password = request.DATA.get('password', None) - - try: - user = User.objects.get(username=username) - if user.check_password(password): - user = authenticate(username=username, password=password) - login(request, user) - - return_data = LoginSerializer(UserLogged(**{ - 'token': request.session.session_key, - 'username': request.user.username, - 'first_name': request.user.first_name, - 'last_name': request.user.last_name, - 'email': request.user.email, - 'last_login': request.user.last_login, - 'color': request.user.color, - 'description': request.user.description, - 'default_language': request.user.default_language, - 'default_timezone': request.user.default_timezone, - 'colorize_tags': request.user.colorize_tags, - })) - - return Response(return_data.data) - except User.DoesNotExist: - pass - - return Response({"detail": "Invalid username or password"}, status.HTTP_400_BAD_REQUEST) - - - - -class Logout(APIView): - def post(self, request, format=None): + @action(methods=["GET", "POST"]) + def logout(self, request, pk=None) logout(request) return Response() -class Search(APIView): - def get(self, request, format=None): +class Search(viewsets.ViewSet): + def list(self, request, **kwargs): text = request.QUERY_PARAMS.get('text', None) - project = request.QUERY_PARAMS.get('project', None) + project_id = request.QUERY_PARAMS.get('project', None) - if text and project: - #TODO: permission check - queryset = query.SearchQuerySet() - queryset = queryset.filter(text=inputs.AutoQuery(text)) - queryset = queryset.filter(project_id=project) - - return_data = SearchSerializer(queryset) - return Response(return_data.data) - - return Response({"detail": "Parameter text can't be empty"}, status.HTTP_400_BAD_REQUEST) + try: + project = self._get_project(project_id) + except models.Project.DoesNotExist: + raise excp.PermissionDenied({"detail": "Wrong project id"}) + 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) diff --git a/greenmine/base/fields.py b/greenmine/base/fields.py deleted file mode 100644 index b27d6658..00000000 --- a/greenmine/base/fields.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2011 Andrei Antoukh -# License: BSD-3 - -from django.db import models -from base64 import b64encode, b64decode - -try: - import cPickle as pickle -except ImportError: - import pickle - -import logging - -logger = logging.getLogger("niwi") - - -class DictField(models.Field): - """ Dictionary pickled field. """ - __metaclass__ = models.SubfieldBase - __prefix__ = "pickle_dict::" - __pickleproto__ = -1 - - def to_python(self, value): - if isinstance(value, dict): - return value - if isinstance(value, (str, unicode)) and value.startswith(self.__prefix__): - local_value = value[len(self.__prefix__):] - return pickle.loads(b64decode(str(local_value))) - else: - return {} - - def get_db_prep_value(self, value, connection, prepared=False): - if value is not None: - if isinstance(value, dict): - value = self.__prefix__ + b64encode(pickle.dumps(value, protocol=self.__pickleproto__)) - else: - raise TypeError('This field can only store dictionaries.') - - return value - - def get_internal_type(self): - return 'TextField' - - def value_to_string(self, obj): - if not obj: - return "" - - value = getattr(obj, self.attname) - assert isinstance(value, dict) - return self.__prefix__ + b64encode(pickle.dumps(value, protocol=self.__pickleproto__)) - return self.token.join(map(unicode, value)) - - def south_field_triple(self): - from south.modelsinspector import introspector - field_class = "django.db.models.fields.TextField" - args, kwargs = introspector(self) - return (field_class, args, kwargs) - - -class ListField(models.Field): - """ Pickled list field. """ - __metaclass__ = models.SubfieldBase - __prefix__ = "pickle_list::" - __pickleproto__ = -1 - - def to_python(self, value): - if isinstance(value, (list, tuple)): - return value - - if isinstance(value, (str, unicode)) and value.startswith(self.__prefix__): - local_value = value[len(self.__prefix__):] - return pickle.loads(b64decode(str(local_value))) - else: - return [] - - def get_db_prep_value(self, value, connection, prepared=False): - if value is not None: - if isinstance(value, (list, tuple)): - value = self.__prefix__ + b64encode(pickle.dumps(value, protocol=self.__pickleproto__)) - else: - raise TypeError('This field can only store list or tuple objects') - - return value - - def get_internal_type(self): - return 'TextField' - - def value_to_string(self, obj): - if not obj: - return "" - - value = getattr(obj, self.attname) - assert isinstance(value, (list, tuple)) - return self.__prefix__ + b64encode(pickle.dumps(value, protocol=self.__pickleproto__)) - return self.token.join(map(unicode, value)) - - def south_field_triple(self): - from south.modelsinspector import introspector - field_class = "django.db.models.fields.TextField" - args, kwargs = introspector(self) - return (field_class, args, kwargs) - - -class CSVField(models.TextField): - __metaclass__ = models.SubfieldBase - - def __init__(self, *args, **kwargs): - self.token = kwargs.pop('token', ',') - super(CSVField, self).__init__(*args, **kwargs) - - def to_python(self, value): - if not value: - return - if isinstance(value, list): - return value - return value.split(self.token) - - def get_db_prep_value(self, value): - if not value: - return - assert(isinstance(value, list) or isinstance(value, tuple)) - return self.token.join([unicode(s) for s in value]) - - def value_to_string(self, obj): - value = self._get_val_from_obj(obj) - return self.get_db_prep_value(value) - - def south_field_triple(self): - from south.modelsinspector import introspector - field_class = "django.db.models.fields.TextField" - args, kwargs = introspector(self) - return (field_class, args, kwargs) diff --git a/greenmine/base/middleware.py b/greenmine/base/middleware.py index 7fb410ed..a85f1d60 100644 --- a/greenmine/base/middleware.py +++ b/greenmine/base/middleware.py @@ -18,6 +18,7 @@ class GreenmineSessionMiddleware(SessionMiddleware): request.session = engine.SessionStore(session_key) + COORS_ALLOWED_ORIGINS = getattr(settings, 'COORS_ALLOWED_ORIGINS', '*') COORS_ALLOWED_METHODS = getattr(settings, 'COORS_ALLOWED_METHODS', ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE', 'PATCH']) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 0476ca55..521d6745 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -47,6 +47,9 @@ class User(WatcherMixin, AbstractUser): verbose_name=_('colorize tags')) objects = UserManager() + class Meta: + ordering = ["username"] + class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, diff --git a/greenmine/base/urls.py b/greenmine/base/urls.py index 42373ab5..9e3bce48 100644 --- a/greenmine/base/urls.py +++ b/greenmine/base/urls.py @@ -1,17 +1,21 @@ # -*- coding: utf-8 -*- from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework import routers + from django.conf.urls import patterns, url from greenmine.base import api +# Special router for actions. +actions_router = routers.Route(url=r'^{prefix}/actions/{methodname}{trailing_slash}$', + mapping={'{httpmethod}': '{methodname}'}, + name='{basename}-{methodnamehyphen}', + initkwargs={}) -urlpatterns = format_suffix_patterns(patterns('', - url(r'^auth/login/$', api.Login.as_view(), name='login'), - url(r'^auth/logout/$', api.Logout.as_view(), name='logout'), - url(r'^users/$', api.UserList.as_view(), name="user-list"), - url(r'^users/(?P[0-9]+)/$', api.UserDetail.as_view(), name="user-detail"), - url(r'^roles/$', api.RoleList.as_view(), name="roles"), - url(r'^roles/(?P[0-9]+)/$', api.RoleDetail.as_view(), name='role-detail'), - url(r'^search/$', api.Search.as_view(), name="search"), - url(r'^$', api.ApiRoot.as_view(), name='api_root'), -)) +router = routers.DefaultRouter(trailing_slash=False) +router.routes.append(actions_router) +router.register("users", api.UsersViewSet, base_name="users") +router.register("roles", api.RolesViewSet, base_name="roles") +router.register("search", api.Search, base_name="search") + +urlpatterns = router.urls diff --git a/greenmine/documents/models.py b/greenmine/documents/models.py index 211f060c..eef450c8 100644 --- a/greenmine/documents/models.py +++ b/greenmine/documents/models.py @@ -3,8 +3,8 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from picklefield.fields import PickledObjectField from greenmine.base.utils.slug import slugify_uniquely as slugify -from greenmine.base.fields import DictField class Document(models.Model): @@ -27,7 +27,7 @@ class Document(models.Model): attached_file = models.FileField(max_length=1000, null=True, blank=True, upload_to='documents', verbose_name=_('attached_file')) - tags = DictField(null=False, blank=True, + tags = PickledObjectField(null=False, blank=True, verbose_name=_('tags')) class Meta: diff --git a/greenmine/questions/models.py b/greenmine/questions/models.py index 4e7e1b40..68b1c4e8 100644 --- a/greenmine/questions/models.py +++ b/greenmine/questions/models.py @@ -6,7 +6,6 @@ from django.utils import timezone from django.dispatch import receiver from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely -from greenmine.base.fields import DictField from greenmine.scrum.models import Project from picklefield.fields import PickledObjectField diff --git a/greenmine/urls.py b/greenmine/urls.py index cdf74ee0..766fdfd4 100644 --- a/greenmine/urls.py +++ b/greenmine/urls.py @@ -6,7 +6,7 @@ from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', - url(r'^api/', include('greenmine.base.urls')), + url(r'^api/core/', include('greenmine.base.urls')), url(r'^api/scrum/', include('greenmine.scrum.urls')), url(r'^api/documents/', include('greenmine.documents.urls')), url(r'^api/questions/', include('greenmine.questions.urls')), diff --git a/requirements.txt b/requirements.txt index 21e804ca..76d12e9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ git+git://github.com/toastdriven/django-haystack.git django-picklefield==0.3.0 django-reversion==1.7 django-sampledatahelper==0.0.1 -djangorestframework==2.2.5 +djangorestframework==2.3.6 gunicorn==17.5 kombu==2.5.12 mimeparse==0.1.3