diff --git a/greenmine/base/admin.py b/greenmine/base/admin.py new file mode 100644 index 00000000..ba63c907 --- /dev/null +++ b/greenmine/base/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import Site + + +class SiteAdmin(admin.ModelAdmin): + list_display = ('domain', 'name') + search_fields = ('domain', 'name') + +admin.site.register(Site, SiteAdmin) diff --git a/greenmine/base/apiviews.py b/greenmine/base/apiviews.py new file mode 100644 index 00000000..fd071765 --- /dev/null +++ b/greenmine/base/apiviews.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from rest_framework import viewsets +from rest_framework.response import Response + + +class SiteViewSet(viewsets.ViewSet): + def status(self, request, **kwargs): + return Response({}) + + +sitestatus = SiteViewSet.as_view({"head": "status", "get": "status"}) diff --git a/greenmine/base/auth/api.py b/greenmine/base/auth/api.py index cbeafd83..c10719df 100644 --- a/greenmine/base/auth/api.py +++ b/greenmine/base/auth/api.py @@ -2,21 +2,135 @@ from django.db.models.loading import get_model from django.contrib.auth import logout, login, authenticate +from django.shortcuts import get_object_or_404 from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status, viewsets +from rest_framework.decorators import list_route +from greenmine.base.models import SiteMember +from greenmine.base.sites import get_active_site +from greenmine.base.users.models import User, Role +from greenmine.base.users.serializers import UserSerializer from greenmine.base import exceptions as exc from greenmine.base import auth -from greenmine.base.users.models import User, Role -from greenmine.base.users.serializers import UserSerializer +from .serializers import (PublicRegisterSerializer, + PrivateRegisterSerializer, + PrivateGenericRegisterSerializer, + PrivateRegisterExistingSerializer) class AuthViewSet(viewsets.ViewSet): permission_classes = (AllowAny,) + def _create_response(self, user): + serializer = UserSerializer(user) + response_data = serializer.data + response_data["auth_token"] = auth.get_token_for_user(user) + return response_data + + def _create_site_member(self, user): + site = get_active_site() + + if SiteMember.objects.filter(site=site, user=user).count() == 0: + site_member = SiteMember(site=site, user=user, email=user.email, + is_owner=False, is_staff=False) + site_member.save() + + def _send_public_register_email(self, user): + context = {"user": user} + + mbuilder = MagicMailBuilder() + email = mbuilder.public_register_user(user.email, context) + email.send() + + def _public_register(self, request): + if not request.site.public_register: + raise exc.BadRequest("Public register is disabled for this site.") + + serializer = PublicRegisterSerializer(data=request.DATA) + if not serializer.is_valid(): + raise exc.BadRequest(serializer.errors) + + data = serializer.data + + user = User(username=data["username"], + first_name=data["first_name"], + last_name=data["last_name"], + email=data["email"]) + user.set_password(data["password"]) + user.save() + + self._create_site_member(user) + + #self._send_public_register_email(user) + + response_data = self._create_response(user) + return Response(response_data, status=status.HTTP_201_CREATED) + + def _send_private_register_email(self, user, **kwargs): + context = {"user": user} + context.update(kwargs) + + mbuilder = MagicMailBuilder() + email = mbuilder.private_register_user(user.email, context) + email.send() + + def _private_register(self, request): + base_serializer = PrivateGenericRegisterSerializer(data=request.DATA) + if not base_serializer.is_valid(): + raise exc.BadRequest(base_serializer.errors) + + membership_model = get_model("projects", "Membership") + try: + membership = membership_model.objects.get(token=base_serializer.data["token"]) + except membership_model.DoesNotExist as e: + raise exc.BadRequest("Invalid token") from e + + if base_serializer.data["existing"]: + serializer = PrivateRegisterExistingSerializer(data=request.DATA) + if not serializer.is_valid(): + raise exc.BadRequest(serializer.errors) + + user = get_object_or_404(User, username=serializer.data["username"]) + if not user.check_password(serializer.data["password"]): + raise exc.BadRequest({"password": "Incorrect password"}) + + else: + serializer = PrivateRegisterSerializer(data=request.DATA) + if not serializer.is_valid(): + raise exc.BadRequest(serializer.errors) + + data = serializer.data + user = User(username=data["username"], + first_name=data["first_name"], + last_name=data["last_name"], + email=data["email"]) + user.set_password(data["password"]) + user.save() + + self._create_site_member(user) + + membership.user = user + membership.save() + + #self._send_private_register_email(user, membership=membership) + + response_data = self._create_response(user) + return Response(response_data, status=status.HTTP_201_CREATED) + + @list_route(methods=["POST"], permission_classes=[AllowAny]) + def register(self, request, **kwargs): + type = request.DATA.get("type", None) + if type == "public": + return self._public_register(request) + elif type == "private": + return self._private_register(request) + + raise exc.BadRequest("invalid register type") + def create(self, request, **kwargs): username = request.DATA.get('username', None) password = request.DATA.get('password', None) @@ -29,7 +143,5 @@ class AuthViewSet(viewsets.ViewSet): if not user.check_password(password): raise exc.BadRequest("Invalid username or password") - serializer = UserSerializer(user) - response_data = serializer.data - response_data["auth_token"] = auth.get_token_for_user(user) + response_data = self._create_response(user) return Response(response_data, status=status.HTTP_200_OK) diff --git a/greenmine/base/auth/serializers.py b/greenmine/base/auth/serializers.py new file mode 100644 index 00000000..512873d8 --- /dev/null +++ b/greenmine/base/auth/serializers.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from rest_framework import serializers + +class BaseRegisterSerializer(serializers.Serializer): + first_name = serializers.CharField(max_length=200) + last_name = serializers.CharField(max_length=200) + email = serializers.EmailField(max_length=200) + username = serializers.CharField(max_length=200) + password = serializers.CharField(min_length=4) + + +class PublicRegisterSerializer(BaseRegisterSerializer): + pass + + +class PrivateRegisterSerializer(BaseRegisterSerializer): + pass + + +class PrivateGenericRegisterSerializer(serializers.Serializer): + token = serializers.CharField(max_length=255, required=True) + existing = serializers.BooleanField() + # existing = serializers.ChoiceField(choices=[("on", "on"), ("off", "off")]) + + +class PrivateRegisterExistingSerializer(serializers.Serializer): + username = serializers.CharField(max_length=200) + password = serializers.CharField(min_length=4) diff --git a/greenmine/base/auth/tests/tests_auth.py b/greenmine/base/auth/tests/tests_auth.py index c39878e8..3c8811b9 100644 --- a/greenmine/base/auth/tests/tests_auth.py +++ b/greenmine/base/auth/tests/tests_auth.py @@ -1,19 +1,28 @@ # -*- coding: utf-8 -*- +import uuid +import json + from django.core.urlresolvers import reverse from django.conf.urls import patterns, include, url from django import test +from django.db.models import get_model from rest_framework.views import APIView +from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from greenmine import urls from greenmine.base import auth -from greenmine.base.users.tests import create_user +from greenmine.base.users.tests import create_user, create_site +from greenmine.projects.tests import create_project + +from greenmine.base.models import Site, SiteMember +from greenmine.projects.models import Membership -class TestAuthView(APIView): +class TestAuthView(viewsets.ViewSet): authentication_classes = (auth.Token,) permission_classes = (IsAuthenticated,) @@ -22,11 +31,12 @@ class TestAuthView(APIView): urls.urlpatterns += patterns("", - url(r'^test-api/v1/auth/', TestAuthView.as_view(), name="test-token-auth"), + url(r'^test-api/v1/auth/', TestAuthView.as_view({"get": "get"}), name="test-token-auth"), ) -class SimpleTokenAuthTests(test.TestCase): +class TokenAuthTests(test.TestCase): + fixtures = ["initial_site.json"] def setUp(self): self.user1 = create_user(1) @@ -41,3 +51,129 @@ class SimpleTokenAuthTests(test.TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'"ok"') + +class RegisterTests(test.TestCase): + def setUp(self): + self.user1 = create_user(1) + self.site1 = create_site("localhost1", True) + self.site2 = create_site("localhost2", False) + self.role = self._create_role() + self.project = create_project(1, self.user1) + + def test_public_register_01(self): + data = { + "username": "pepe", + "password": "pepepepe", + "first_name": "pepe", + "last_name": "pepe", + "email": "pepe@pepe.com", + "type": "public", + } + + url = reverse("auth-register") + response = self.client.post(url, data, HTTP_X_HOST=self.site1.name) + self.assertEqual(response.status_code, 201) + + self.assertEqual(SiteMember.objects.filter(site=self.site1).count(), 1) + self.assertEqual(self.project.memberships.count(), 0) + + + def test_public_register_02(self): + data = { + "username": "pepe", + "password": "pepepepe", + "first_name": "pepe", + "last_name": "pepe", + "email": "pepe@pepe.com", + "type": "public", + } + + url = reverse("auth-register") + response = self.client.post(url, data, HTTP_X_HOST=self.site2.name) + self.assertEqual(response.status_code, 400) + + def test_private_register_01(self): + data = { + "username": "pepe", + "password": "pepepepe", + "first_name": "pepe", + "last_name": "pepe", + "email": "pepe@pepe.com", + "type": "private", + } + + url = reverse("auth-register") + response = self.client.post(url, data, HTTP_X_HOST=self.site2.name) + self.assertEqual(response.status_code, 400) + + def test_private_register_02(self): + membership = self._create_invitation("pepe@pepe.com") + + data = { + "username": "pepe", + "password": "pepepepe", + "first_name": "pepe", + "last_name": "pepe", + "email": "pepe@pepe.com", + "type": "private", + "existing": False, + "token": membership.token, + } + + self.assertEqual(self.project.memberships.exclude(user__isnull=True).count(), 0) + + url = reverse("auth-register") + response = self.client.post(url, data=json.dumps(data), + content_type="application/json", + HTTP_X_HOST=self.site2.name) + + self.assertEqual(response.status_code, 201) + self.assertEqual(self.project.memberships.exclude(user__isnull=True).count(), 1) + self.assertEqual(self.project.memberships.get().role, self.role) + self.assertEqual(SiteMember.objects.filter(site=self.site1).count(), 0) + self.assertEqual(SiteMember.objects.filter(site=self.site2).count(), 1) + + def test_private_register_03(self): + membership = self._create_invitation("pepe@pepe.com") + + data = { + "username": self.user1.username, + "password": self.user1.username, + "type": "private", + "existing": True, + "token": membership.token, + } + + self.assertEqual(self.project.memberships.exclude(user__isnull=True).count(), 0) + + url = reverse("auth-register") + response = self.client.post(url, data=json.dumps(data), + content_type="application/json", + HTTP_X_HOST=self.site2.name) + + self.assertEqual(response.status_code, 201) + self.assertEqual(self.project.memberships.exclude(user__isnull=True).count(), 1) + self.assertEqual(self.project.memberships.get().role, self.role) + self.assertEqual(SiteMember.objects.filter(site=self.site1).count(), 0) + self.assertEqual(SiteMember.objects.filter(site=self.site2).count(), 1) + + + def _create_invitation(self, email): + token = str(uuid.uuid1()) + membership_model = get_model("projects", "Membership") + + instance = membership_model(project=self.project, + email=email, + role=self.role, + user=None, + token=token) + instance.save() + return instance + + def _create_role(self): + role_model = get_model("users", "Role") + instance = role_model(name="foo", slug="foo", + order=1, computable=True) + + instance.save() + return instance diff --git a/greenmine/base/exceptions.py b/greenmine/base/exceptions.py index bd161330..eaee687e 100644 --- a/greenmine/base/exceptions.py +++ b/greenmine/base/exceptions.py @@ -2,6 +2,12 @@ from rest_framework import exceptions from rest_framework import status +from rest_framework.response import Response + +from django.core.exceptions import PermissionDenied as DjangoPermissionDenied +from django.http import Http404 + +from .utils.json import to_json class BaseException(exceptions.APIException): @@ -66,3 +72,53 @@ class NotAuthenticated(exceptions.NotAuthenticated): exception. """ pass + + +def format_exception(exc): + # TODO: this method need a refactor. + # TODO: should return in uniform way all exceptions. + + if isinstance(exc.detail, (dict, list, tuple,)): + detail = exc.detail + else: + class_name = exc.__class__.__name__ + class_module = exc.__class__.__module__ + detail = { + "_error_message": exc.detail, + "_error_type": "{0}.{1}".format(class_module, class_name) + } + + return detail + + +def exception_handler(exc): + """ + Returns the response that should be used for any given exception. + + By default we handle the REST framework `APIException`, and also + Django's builtin `Http404` and `PermissionDenied` exceptions. + + Any unhandled exceptions may return `None`, which will cause a 500 error + to be raised. + """ + + if isinstance(exc, exceptions.APIException): + headers = {} + if getattr(exc, 'auth_header', None): + headers['WWW-Authenticate'] = exc.auth_header + if getattr(exc, 'wait', None): + headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + + detail = format_exception(exc) + return Response(detail, status=exc.status_code, headers=headers) + + elif isinstance(exc, Http404): + return Response({'_error_message': 'Not found'}, + status=status.HTTP_404_NOT_FOUND) + + elif isinstance(exc, DjangoPermissionDenied): + return Response({'_error_message': 'Permission denied'}, + status=status.HTTP_403_FORBIDDEN) + + # Note: Unhandled exceptions will raise a 500 error. + return None diff --git a/greenmine/base/fixtures/initial_site.json b/greenmine/base/fixtures/initial_site.json new file mode 100644 index 00000000..a50d85c3 --- /dev/null +++ b/greenmine/base/fixtures/initial_site.json @@ -0,0 +1,10 @@ +[ +{ + "model": "base.site", + "fields": { + "domain": "localhost", + "name": "localhost" + }, + "pk": 1 +} +] diff --git a/greenmine/base/middleware.py b/greenmine/base/middleware.py index 7f1dc98c..8f1591b0 100644 --- a/greenmine/base/middleware.py +++ b/greenmine/base/middleware.py @@ -1,32 +1,22 @@ -import time +# -*- coding: utf-8 -*- + +import json -from django.conf import settings from django import http -from django.utils.cache import patch_vary_headers -from django.utils.http import cookie_date -from django.utils.importlib import import_module - -from django.contrib.sessions.middleware import SessionMiddleware - - -class GreenmineSessionMiddleware(SessionMiddleware): - def process_request(self, request): - engine = import_module(settings.SESSION_ENGINE) - session_key = request.META.get(settings.SESSION_HEADER_NAME, None) - if not session_key: - session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) - request.session = engine.SessionStore(session_key) - +from greenmine.base import sites COORS_ALLOWED_ORIGINS = '*' -COORS_ALLOWED_METHODS = ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE', 'PATCH'] -COORS_ALLOWED_HEADERS = ['Content-Type', 'X-Requested-With', - 'Authorization', 'Accept-Encoding', - 'X-Disable-Pagination'] +COORS_ALLOWED_METHODS = ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE', 'PATCH', 'HEAD'] +COORS_ALLOWED_HEADERS = ['content-type', 'x-requested-with', + 'authorization', 'accept-encoding', + 'x-disable-pagination', 'x-host'] COORS_ALLOWED_CREDENTIALS = True -COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", - "x-paginated-by", "x-pagination-current"] +COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by", + "x-paginated-by", "x-pagination-current", "x-site-host", + "x-site-register"] + +from .exceptions import format_exception class CoorsMiddleware(object): @@ -49,3 +39,28 @@ class CoorsMiddleware(object): def process_response(self, request, response): self._populate_response(response) return response + + +class SitesMiddleware(object): + def process_request(self, request): + domain = request.META.get("HTTP_X_HOST", None) + if domain is not None: + try: + site = sites.get_site_for_domain(domain) + except sites.SiteNotFound as e: + detail = format_exception(e) + return http.HttpResponseBadRequest(json.dumps(detail)) + else: + site = sites.get_default_site() + + request.site = site + sites.activate(site) + + def process_response(self, request, response): + sites.deactivate() + + if hasattr(request, "site"): + response["X-Site-Host"] = request.site.domain + response["X-Site-Register"] = "on" if request.site.public_register else "off" + + return response diff --git a/greenmine/base/migrations/0001_initial.py b/greenmine/base/migrations/0001_initial.py new file mode 100644 index 00000000..fc281e75 --- /dev/null +++ b/greenmine/base/migrations/0001_initial.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + pass + + def backwards(self, orm): + pass + + models = { + + } + + complete_apps = ['base'] \ No newline at end of file diff --git a/greenmine/base/migrations/0002_auto__add_site__add_sitemember__add_unique_sitemember_site_user.py b/greenmine/base/migrations/0002_auto__add_site__add_sitemember__add_unique_sitemember_site_user.py new file mode 100644 index 00000000..c897f4dd --- /dev/null +++ b/greenmine/base/migrations/0002_auto__add_site__add_sitemember__add_unique_sitemember_site_user.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Site' + db.create_table('base_site', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('domain', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('scheme', self.gf('django.db.models.fields.CharField')(max_length=60, default=None, null=True)), + ('public_register', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('base', ['Site']) + + # Adding model 'SiteMember' + db.create_table('base_sitemember', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('site', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['base.Site'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['users.User'])), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=255)), + ('is_owner', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('base', ['SiteMember']) + + # Adding unique constraint on 'SiteMember', fields ['site', 'user'] + db.create_unique('base_sitemember', ['site_id', 'user_id']) + + def backwards(self, orm): + # Removing unique constraint on 'SiteMember', fields ['site', 'user'] + db.delete_unique('base_sitemember', ['site_id', 'user_id']) + + # Deleting model 'Site' + db.delete_table('base_site') + + # Deleting model 'SiteMember' + db.delete_table('base_sitemember') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'to': "orm['auth.Permission']", 'symmetrical': 'False'}) + }, + 'auth.permission': { + 'Meta': {'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'base.site': { + 'Meta': {'object_name': 'Site', 'ordering': "('domain',)"}, + 'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scheme': ('django.db.models.fields.CharField', [], {'max_length': '60', 'default': 'None', 'null': 'True'}) + }, + 'base.sitemember': { + 'Meta': {'object_name': 'SiteMember', 'ordering': "['email']", 'unique_together': "(('site', 'user'),)"}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['base.Site']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['users.User']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'object_name': 'ContentType', 'db_table': "'django_content_type'", 'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '9', 'default': "'#669933'"}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '20', 'default': "''"}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '20', 'default': "''"}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}), + 'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'to': "orm['auth.Group']", 'related_name': "'user_set'", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'max_length': '32', 'default': "'all_owned_projects'"}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'max_length': '500', 'null': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '200', 'default': 'None', 'null': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'to': "orm['auth.Permission']", 'related_name': "'user_set'", 'symmetrical': 'False'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + } + } + + complete_apps = ['base'] diff --git a/greenmine/base/migrations/0003_initial_sites_data.py b/greenmine/base/migrations/0003_initial_sites_data.py new file mode 100644 index 00000000..ea161e97 --- /dev/null +++ b/greenmine/base/migrations/0003_initial_sites_data.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + + from django.core.management import call_command + call_command("loaddata", "initial_site.json") + + site = orm["base.Site"].objects.get(pk=1) + + for user in orm["users.User"].objects.all(): + orm["base.SiteMember"].objects.create(user=user, site=site, email=user.email) + + orm["base.SiteMember"].objects.filter(user_id=1).update(is_staff=True, is_owner=True) + + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Permission']"}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'base.site': { + 'Meta': {'object_name': 'Site', 'ordering': "('domain',)"}, + 'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scheme': ('django.db.models.fields.CharField', [], {'default': 'None', 'null': 'True', 'max_length': '60'}) + }, + 'base.sitemember': { + 'Meta': {'unique_together': "(('site', 'user'),)", 'object_name': 'SiteMember', 'ordering': "['email']"}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['base.Site']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['users.User']", 'null': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'", 'ordering': "('name',)"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'default': "'#669933'", 'blank': 'True', 'max_length': '9'}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'default': "''", 'blank': 'True', 'max_length': '20'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}), + 'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'symmetrical': 'False', 'related_name': "'user_set'", 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'default': "'all_owned_projects'", 'max_length': '32'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'null': 'True', 'max_length': '500'}), + 'token': ('django.db.models.fields.CharField', [], {'default': 'None', 'blank': 'True', 'null': 'True', 'max_length': '200'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'symmetrical': 'False', 'related_name': "'user_set'", 'to': "orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + } + } + + complete_apps = ['base'] + symmetrical = True diff --git a/greenmine/base/migrations/__init__.py b/greenmine/base/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/base/models.py b/greenmine/base/models.py index d0ca951c..afa278df 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -1,8 +1,74 @@ # -*- coding: utf-8 -*- +import string + +from django.db import models +from django.db.models.signals import pre_save, pre_delete +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError + +from . import sites + + +def _simple_domain_name_validator(value): + """ + Validates that the given value contains no whitespaces to prevent common + typos. + """ + if not value: + return + checks = ((s in value) for s in string.whitespace) + if any(checks): + raise ValidationError( + _("The domain name cannot contain any spaces or tabs."), + code='invalid', + ) + + +class Site(models.Model): + domain = models.CharField(_('domain name'), max_length=255, unique=True, + validators=[_simple_domain_name_validator]) + name = models.CharField(_('display name'), max_length=255) + scheme = models.CharField(_('scheme'), max_length=60, null=True, default=None) + + # Site Metadata + public_register = models.BooleanField(default=False) + + class Meta: + verbose_name = _('site') + verbose_name_plural = _('sites') + ordering = ('domain',) + + def __str__(self): + return self.domain + + +class SiteMember(models.Model): + site = models.ForeignKey("Site", related_name="+") + user = models.ForeignKey("users.User", related_name="+", null=True) + + email = models.EmailField(max_length=255) + is_owner = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + + class Meta: + ordering = ["email"] + verbose_name = "Site Member" + verbose_name_plural = "Site Members" + unique_together = ("site", "user") + + def __str__(self): + return "SiteMember: {0}:{1}".format(self.site, self.user) + + +pre_save.connect(sites.clear_site_cache, sender=Site) +pre_delete.connect(sites.clear_site_cache, sender=Site) + + # Patch api view for correctly return 401 responses on # request is authenticated instead of 403 from . import monkey monkey.patch_api_view() monkey.patch_serializer() monkey.patch_import_module() +monkey.patch_south_hacks() diff --git a/greenmine/base/monkey.py b/greenmine/base/monkey.py index 83531c62..1bbb6967 100644 --- a/greenmine/base/monkey.py +++ b/greenmine/base/monkey.py @@ -66,3 +66,18 @@ def patch_import_module(): import importlib django_importlib.import_module = importlib.import_module + + +def patch_south_hacks(): + from south.hacks import django_1_0 + + orig_set_installed_apps = django_1_0.Hacks.set_installed_apps + def set_installed_apps(self, apps, preserve_models=True): + return orig_set_installed_apps(self, apps, preserve_models=preserve_models) + + orig__redo_app_cache = django_1_0.Hacks._redo_app_cache + def _redo_app_cache(self, preserve_models=True): + return orig__redo_app_cache(self, preserve_models=preserve_models) + + django_1_0.Hacks.set_installed_apps = set_installed_apps + django_1_0.Hacks._redo_app_cache = _redo_app_cache diff --git a/greenmine/base/sites.py b/greenmine/base/sites.py new file mode 100644 index 00000000..4bf024ac --- /dev/null +++ b/greenmine/base/sites.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +import logging +from threading import local + +from django.db.models import get_model +from django.core.exceptions import ImproperlyConfigured + +from . import exceptions as exc + + +_local = local() +log = logging.getLogger("greenmine.sites") + + +class SiteNotFound(exc.BaseException): + pass + + +def get_default_site(): + from django.conf import settings + try: + sid = settings.SITE_ID + except AttributeError: + raise ImproperlyConfigured("You're using the \"sites framework\" without having " + "set the SITE_ID setting. Create a site in your database " + "and set the SITE_ID setting to fix this error.") + + model_cls = get_model("base", "Site") + cached = getattr(_local, "default_site", None) + if cached is None: + try: + cached = _local.default_site = model_cls.objects.get(pk=sid) + except model_cls.DoesNotExist: + raise ImproperlyConfigured("default site not found on database.") + + return cached + + +def get_site_for_domain(domain): + log.debug("Trying activate site for domain: {}".format(domain)) + cache = getattr(_local, "cache", {}) + + if domain in cache: + return cache[domain] + + model_cls = get_model("base", "Site") + + try: + site = model_cls.objects.get(domain=domain) + except model_cls.DoesNotExist: + log.warning("Site does not exist for domain: {}".format(domain)) + raise SiteNotFound("site not found") + else: + cache[domain] = site + + return site + + +def activate(site): + log.debug("Activating site: {}".format(site)) + _local.active_site = site + + +def deactivate(): + if hasattr(_local, "active_site"): + log.debug("Deactivating site: {}".format(_local.active_site)) + del _local.active_site + + +def get_active_site(): + active_site = getattr(_local, "active_site", None) + if active_site is None: + return get_default_site() + return active_site + +def clear_site_cache(**kwargs): + if hasattr(_local, "default_site"): + del _local.default_site + + if hasattr(_local, "cache"): + del _local.cache diff --git a/greenmine/base/users/api.py b/greenmine/base/users/api.py index 60fd7c84..a0c4f63f 100644 --- a/greenmine/base/users/api.py +++ b/greenmine/base/users/api.py @@ -15,55 +15,17 @@ from djmail.template_mail import MagicMailBuilder from greenmine.base import exceptions as exc from greenmine.base.filters import FilterBackend -from greenmine.base.api import ModelCrudViewSet +from greenmine.base.api import ModelCrudViewSet, RetrieveModelMixin from .models import User, Role from .serializers import UserSerializer, RoleSerializer, RecoverySerializer -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 exc.NotFound() - - serializer = self.serializer_class(role) - return Response(serializer.data) - - -class ProjectMembershipFilter(FilterBackend): - def filter_queryset(self, request, queryset, view): - queryset = super().filter_queryset(request, queryset, view) - - if request.user.is_superuser: - return queryset - - project_model = get_model("projects", "Project") - own_projects = project_model.objects.filter(members=request.user) - - project = request.QUERY_PARAMS.get('project', None) - if project is not None: - own_projects = own_projects.filter(pk=project) - - queryset = (queryset.filter(projects__in=own_projects) - .order_by('username').distinct()) - return queryset - class UsersViewSet(ModelCrudViewSet): permission_classes = (IsAuthenticated,) serializer_class = UserSerializer queryset = User.objects.all() - filter_backends = (ProjectMembershipFilter,) filter_fields = [("project", "memberships__project__pk")] def pre_conditions_on_save(self, obj): @@ -129,3 +91,22 @@ class UsersViewSet(ModelCrudViewSet): request.user.set_password(password) request.user.save(update_fields=["password"]) return Response(status=status.HTTP_204_NO_CONTENT) + + +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 exc.NotFound() + + serializer = self.serializer_class(role) + return Response(serializer.data) diff --git a/greenmine/base/users/tests/__init__.py b/greenmine/base/users/tests/__init__.py index 29fff068..8e22a779 100644 --- a/greenmine/base/users/tests/__init__.py +++ b/greenmine/base/users/tests/__init__.py @@ -21,3 +21,14 @@ def create_user(id, save=True, is_superuser=False): if save: instance.save() return instance + + +def create_site(name, public_register=False): + site_model = get_model("base", "Site") + + instance = site_model(name=name, + domain=name, + public_register=public_register) + + instance.save() + return instance diff --git a/greenmine/base/utils/json.py b/greenmine/base/utils/json.py new file mode 100644 index 00000000..b212339f --- /dev/null +++ b/greenmine/base/utils/json.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +import json +from rest_framework.utils import encoders + + +def to_json(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder): + return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii) + + +def from_json(data): + return json.loads(data) diff --git a/greenmine/front/__init__.py b/greenmine/front/__init__.py index 1f9b2784..7262609e 100644 --- a/greenmine/front/__init__.py +++ b/greenmine/front/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from django.conf import settings -import django_sites as sites - from django_jinja import library +from greenmine.base import sites + URLS = { "home": "/", @@ -15,20 +15,16 @@ URLS = { "issue": "/#/project/{0}/issues/{1}", "project-admin": "/#/project/{0}/admin", "change-password": "/#/change-password/{0}", + "invitation": "/#/invitation/{0}", } lib = library.Library() -def get_current_site(): - current_site_id = getattr(settings, "SITE_ID") - front_sites = getattr(settings, "SITES_FRONT") - return sites.Site(front_sites[current_site_id]) - @lib.global_function(name="resolve_front_url") def resolve(type, *args): - site = get_current_site() + site = sites.get_active_site() url_tmpl = "{scheme}//{domain}{url}" scheme = site.scheme and "{0}:".format(site.scheme) or "" diff --git a/greenmine/projects/api.py b/greenmine/projects/api.py index c8ae67b2..de864671 100644 --- a/greenmine/projects/api.py +++ b/greenmine/projects/api.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- +import uuid + from django.db.models import Q -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.decorators import detail_route +from rest_framework import viewsets +from rest_framework import status + +from djmail.template_mail import MagicMailBuilder from greenmine.base import filters -from greenmine.base.api import ModelCrudViewSet, ModelListViewSet +from greenmine.base import exceptions as exc +from greenmine.base.api import ModelCrudViewSet, ModelListViewSet, RetrieveModelMixin from greenmine.base.notifications.api import NotificationSenderMixin from greenmine.projects.aggregates.tags import get_all_tags @@ -53,6 +60,12 @@ class ProjectViewSet(ModelCrudViewSet): def pre_save(self, obj): obj.owner = self.request.user + + # Assign site only if it current + # value is None + if not obj.site: + obj.site = self.request.site + super().pre_save(obj) @@ -61,6 +74,54 @@ class MembershipViewSet(ModelCrudViewSet): serializer_class = serializers.MembershipSerializer permission_classes = (IsAuthenticated, permissions.MembershipPermission) + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.DATA, files=request.FILES) + + if serializer.is_valid(): + qs = self.model.objects.filter(Q(project_id=serializer.data["project"], + user__email=serializer.data["email"]) | + Q(project_id=serializer.data["project"], + email=serializer.data["email"])) + if qs.count() > 0: + raise exc.WrongArguments("Already exist user with specified email address.") + + self.pre_save(serializer.object) + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, + headers=headers) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def pre_save(self, object): + # Only assign new token if a current token value is empty. + if not object.token: + object.token = str(uuid.uuid1()) + + super().pre_save(object) + + def post_save(self, object, created=False): + super().post_save(object, created=created) + + if not created: + return + + # Send email only if a new membership is created + mbuilder = MagicMailBuilder() + email = mbuilder.membership_invitation(object.email, {"membership": object}) + email.send() + + +class InvitationViewSet(RetrieveModelMixin, viewsets.GenericViewSet): + """ + Only used by front for get invitation by it token. + """ + queryset = models.Membership.objects.all() + serializer_class = serializers.MembershipSerializer + lookup_field = "token" + permission_classes = (AllowAny,) + # User Stories commin ViewSets diff --git a/greenmine/projects/migrations/0005_auto__add_field_project_site__add_field_membership_email__add_field_me.py b/greenmine/projects/migrations/0005_auto__add_field_project_site__add_field_membership_email__add_field_me.py new file mode 100644 index 00000000..0fba441e --- /dev/null +++ b/greenmine/projects/migrations/0005_auto__add_field_project_site__add_field_membership_email__add_field_me.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Membership', fields ['user', 'project'] + db.delete_unique('projects_membership', ['user_id', 'project_id']) + + # Adding field 'Project.site' + db.add_column('projects_project', 'site', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='projects', null=True, to=orm['base.Site']), + keep_default=False) + + # Adding field 'Membership.email' + db.add_column('projects_membership', 'email', + self.gf('django.db.models.fields.EmailField')(max_length=255, default=None, null=True), + keep_default=False) + + # Adding field 'Membership.created_at' + db.add_column('projects_membership', 'created_at', + self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, blank=True, auto_now_add=True), + keep_default=False) + + # Adding field 'Membership.token' + db.add_column('projects_membership', 'token', + self.gf('django.db.models.fields.CharField')(max_length=60, default=None, blank=True, null=True, unique=True), + keep_default=False) + + + # Changing field 'Membership.user' + db.alter_column('projects_membership', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['users.User'])) + + def backwards(self, orm): + # Deleting field 'Project.site' + db.delete_column('projects_project', 'site_id') + + # Deleting field 'Membership.email' + db.delete_column('projects_membership', 'email') + + # Deleting field 'Membership.created_at' + db.delete_column('projects_membership', 'created_at') + + # Deleting field 'Membership.token' + db.delete_column('projects_membership', 'token') + + + # User chose to not deal with backwards NULL issues for 'Membership.user' + raise RuntimeError("Cannot reverse this migration. 'Membership.user' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration + # Changing field 'Membership.user' + db.alter_column('projects_membership', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['users.User'])) + # Adding unique constraint on 'Membership', fields ['user', 'project'] + db.create_unique('projects_membership', ['user_id', 'project_id']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Permission']"}) + }, + 'auth.permission': { + 'Meta': {'object_name': 'Permission', 'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'base.site': { + 'Meta': {'object_name': 'Site', 'ordering': "('domain',)"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scheme': ('django.db.models.fields.CharField', [], {'max_length': '60', 'default': 'None', 'null': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'object_name': 'ContentType', 'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'projects.attachment': { + 'Meta': {'object_name': 'Attachment', 'ordering': "['project', 'created_date']"}, + 'attached_file': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_attachments'", 'to': "orm['users.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuestatus': { + 'Meta': {'object_name': 'IssueStatus', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuetype': { + 'Meta': {'object_name': 'IssueType', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"}) + }, + 'projects.membership': { + 'Meta': {'object_name': 'Membership', 'ordering': "['project', 'role']"}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True', 'auto_now_add': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'default': 'None', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}), + 'role': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.Role']"}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '60', 'default': 'None', 'blank': 'True', 'null': 'True', 'unique': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'memberships'", 'null': 'True', 'to': "orm['users.User']", 'blank': 'True'}) + }, + 'projects.points': { + 'Meta': {'object_name': 'Points', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'points'", 'to': "orm['projects.Project']"}), + 'value': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + 'projects.priority': { + 'Meta': {'object_name': 'Priority', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'priorities'", 'to': "orm['projects.Project']"}) + }, + 'projects.project': { + 'Meta': {'object_name': 'Project', 'ordering': "['name']"}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.IssueStatus']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.IssueType']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.Points']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.Priority']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_question_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.QuestionStatus']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.Severity']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.TaskStatus']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'unique': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['projects.UserStoryStatus']", 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_issue_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'last_task_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'last_us_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'projects'", 'through': "orm['projects.Membership']", 'to': "orm['users.User']"}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'projects'", 'null': 'True', 'to': "orm['base.Site']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True', 'unique': 'True'}), + 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}), + 'total_milestones': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}) + }, + 'projects.questionstatus': { + 'Meta': {'object_name': 'QuestionStatus', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_status'", 'to': "orm['projects.Project']"}) + }, + 'projects.severity': { + 'Meta': {'object_name': 'Severity', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"}) + }, + 'projects.taskstatus': { + 'Meta': {'object_name': 'TaskStatus', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.userstorystatus': { + 'Meta': {'object_name': 'UserStoryStatus', 'ordering': "['project', 'order', 'name']", 'unique_together': "(('project', 'name'),)"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'us_statuses'", 'to': "orm['projects.Project']"}) + }, + 'users.role': { + 'Meta': {'object_name': 'Role', 'ordering': "['order', 'slug']"}, + 'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'roles'", 'to': "orm['auth.Permission']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'blank': 'True', 'unique': 'True'}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '9', 'default': "'#669933'", 'blank': 'True'}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "''", 'blank': 'True'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "''", 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'max_length': '32', 'default': "'all_owned_projects'"}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '200', 'default': 'None', 'null': 'True', 'blank': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + } + } + + complete_apps = ['projects'] \ No newline at end of file diff --git a/greenmine/projects/migrations/0006_initial_sites_data.py b/greenmine/projects/migrations/0006_initial_sites_data.py new file mode 100644 index 00000000..88392fb1 --- /dev/null +++ b/greenmine/projects/migrations/0006_initial_sites_data.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + depends_on = ( + ("base", "0003_initial_sites_data"), + ) + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + site = orm["base.Site"].objects.get(pk=1) + for project in orm["projects.Project"].objects.all(): + project.site = site + project.save() + + for member in orm["projects.Membership"].objects.all(): + member.email = member.user.email + member.save() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'blank': 'True', 'to': "orm['auth.Permission']"}) + }, + 'auth.permission': { + 'Meta': {'object_name': 'Permission', 'unique_together': "(('content_type', 'codename'),)", 'ordering': "('content_type__app_label', 'content_type__model', 'codename')"}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'base.site': { + 'Meta': {'object_name': 'Site', 'ordering': "('domain',)"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'public_register': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scheme': ('django.db.models.fields.CharField', [], {'max_length': '60', 'default': 'None', 'null': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'object_name': 'ContentType', 'unique_together': "(('app_label', 'model'),)", 'ordering': "('name',)", 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'projects.attachment': { + 'Meta': {'object_name': 'Attachment', 'ordering': "['project', 'created_date']"}, + 'attached_file': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'blank': 'True', 'null': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_attachments'", 'to': "orm['users.User']"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuestatus': { + 'Meta': {'object_name': 'IssueStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuetype': { + 'Meta': {'object_name': 'IssueType', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"}) + }, + 'projects.membership': { + 'Meta': {'object_name': 'Membership', 'ordering': "['project', 'role']"}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'blank': 'True', 'auto_now_add': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'default': 'None', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}), + 'role': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.Role']"}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '60', 'unique': 'True', 'default': 'None', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'memberships'", 'default': 'None', 'null': 'True', 'to': "orm['users.User']"}) + }, + 'projects.points': { + 'Meta': {'object_name': 'Points', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'points'", 'to': "orm['projects.Project']"}), + 'value': ('django.db.models.fields.FloatField', [], {'blank': 'True', 'default': 'None', 'null': 'True'}) + }, + 'projects.priority': { + 'Meta': {'object_name': 'Priority', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'priorities'", 'to': "orm['projects.Project']"}) + }, + 'projects.project': { + 'Meta': {'object_name': 'Project', 'ordering': "['name']"}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.IssueStatus']"}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.IssueType']"}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.Points']"}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.Priority']"}), + 'default_question_status': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.QuestionStatus']"}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.Severity']"}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.TaskStatus']"}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'on_delete': 'models.SET_NULL', 'unique': 'True', 'null': 'True', 'related_name': "'+'", 'blank': 'True', 'to': "orm['projects.UserStoryStatus']"}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_issue_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'last_task_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'last_us_ref': ('django.db.models.fields.BigIntegerField', [], {'default': '1', 'null': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'projects'", 'symmetrical': 'False', 'to': "orm['users.User']", 'through': "orm['projects.Membership']"}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'default': 'None', 'null': 'True', 'to': "orm['base.Site']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'unique': 'True', 'blank': 'True'}), + 'tags': ('picklefield.fields.PickledObjectField', [], {'blank': 'True'}), + 'total_milestones': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'default': '0', 'null': 'True'}), + 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True'}) + }, + 'projects.questionstatus': { + 'Meta': {'object_name': 'QuestionStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_status'", 'to': "orm['projects.Project']"}) + }, + 'projects.severity': { + 'Meta': {'object_name': 'Severity', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"}) + }, + 'projects.taskstatus': { + 'Meta': {'object_name': 'TaskStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.userstorystatus': { + 'Meta': {'object_name': 'UserStoryStatus', 'unique_together': "(('project', 'name'),)", 'ordering': "['project', 'order', 'name']"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'us_statuses'", 'to': "orm['projects.Project']"}) + }, + 'users.role': { + 'Meta': {'object_name': 'Role', 'ordering': "['order', 'slug']"}, + 'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.Permission']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '250', 'unique': 'True', 'blank': 'True'}) + }, + 'users.user': { + 'Meta': {'object_name': 'User', 'ordering': "['username']"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '9', 'default': "'#669933'", 'blank': 'True'}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "''", 'blank': 'True'}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "''", 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_set'", 'blank': 'True', 'symmetrical': 'False', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'notify_changes_by_me': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'notify_level': ('django.db.models.fields.CharField', [], {'max_length': '32', 'default': "'all_owned_projects'"}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'max_length': '500', 'blank': 'True', 'null': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True', 'default': 'None', 'null': 'True'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_set'", 'blank': 'True', 'symmetrical': 'False', 'to': "orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + } + } + + complete_apps = ['projects'] + symmetrical = True diff --git a/greenmine/projects/models.py b/greenmine/projects/models.py index 7459733b..8c30fead 100644 --- a/greenmine/projects/models.py +++ b/greenmine/projects/models.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +import itertools +import collections + from django.db import models from django.db.models.loading import get_model from django.conf import settings @@ -8,104 +11,47 @@ from django.contrib.contenttypes.models import ContentType 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 import timezone from picklefield.fields import PickledObjectField +import reversion + from greenmine.base.utils.slug import slugify_uniquely from greenmine.base.utils.dicts import dict_sum from greenmine.projects.userstories.models import UserStory from . import choices -import reversion -import itertools -import collections - - -def get_attachment_file_path(instance, filename): - return "attachment-files/{project}/{model}/{filename}".format( - project=instance.project.slug, - model=instance.content_type.model, - filename=filename - ) - - -class Attachment(models.Model): - owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, - related_name="change_attachments", - verbose_name=_("owner")) - project = models.ForeignKey("Project", null=False, blank=False, - related_name="attachments", verbose_name=_("project")) - content_type = models.ForeignKey(ContentType, null=False, blank=False, - verbose_name=_("content type")) - object_id = models.PositiveIntegerField(null=False, blank=False, - verbose_name=_("object id")) - content_object = generic.GenericForeignKey("content_type", "object_id") - created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, - verbose_name=_("created date")) - modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, - verbose_name=_("modified date")) - attached_file = models.FileField(max_length=500, null=True, blank=True, - upload_to=get_attachment_file_path, - verbose_name=_("attached file")) - - class Meta: - verbose_name = "attachment" - verbose_name_plural = "attachments" - ordering = ["project", "created_date"] - permissions = ( - ("view_attachment", "Can view attachment"), - ) - - def __str__(self): - return "Attachment: {}".format(self.id) - class Membership(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, + # This model stores all project memberships. Also + # stores invitations to memberships that does not have + # assigned user. + + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None, related_name="memberships") project = models.ForeignKey("Project", null=False, blank=False, related_name="memberships") role = models.ForeignKey("users.Role", null=False, blank=False, related_name="memberships") + # Invitation metadata + email = models.EmailField(max_length=255, default=None, null=True) + created_at = models.DateTimeField(auto_now_add=True, default=timezone.now) + token = models.CharField(max_length=60, unique=True, blank=True, null=True, + default=None) + class Meta: verbose_name = "membership" verbose_name_plural = "membershipss" - unique_together = ("user", "project") - ordering = ["project", "role", "user"] + ordering = ["project", "role"] permissions = ( ("view_membership", "Can view membership"), ) -class Project(models.Model): - name = models.CharField(max_length=250, unique=True, null=False, blank=False, - verbose_name=_("name")) - slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, - verbose_name=_("slug")) - description = models.TextField(null=False, blank=False, - verbose_name=_("description")) - created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, - verbose_name=_("created date")) - modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, - verbose_name=_("modified date")) - owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, - related_name="owned_projects", verbose_name=_("owner")) - members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects", - through="Membership", verbose_name=_("members")) - public = models.BooleanField(default=True, null=False, blank=True, - verbose_name=_("public")) - last_us_ref = models.BigIntegerField(null=True, blank=False, default=1, - verbose_name=_("last us ref")) - last_task_ref = models.BigIntegerField(null=True, blank=False, default=1, - verbose_name=_("last task ref")) - last_issue_ref = models.BigIntegerField(null=True, blank=False, default=1, - verbose_name=_("last issue ref")) - total_milestones = models.IntegerField(default=0, null=True, blank=True, - verbose_name=_("total of milestones")) - total_story_points = models.FloatField(default=None, null=True, blank=False, - verbose_name=_("total story points")) - tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags")) + +class ProjectDefaults(models.Model): default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL, related_name="+", null=True, blank=True, verbose_name=_("default points")) @@ -136,19 +82,53 @@ class Project(models.Model): related_name="+", null=True, blank=True, verbose_name=_("default questions " "status")) + class Meta: + abstract = True + + +class Project(ProjectDefaults, models.Model): + name = models.CharField(max_length=250, unique=True, null=False, blank=False, + verbose_name=_("name")) + slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, + verbose_name=_("slug")) + description = models.TextField(null=False, blank=False, + verbose_name=_("description")) + created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, + verbose_name=_("created date")) + modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, + verbose_name=_("modified date")) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, + related_name="owned_projects", verbose_name=_("owner")) + members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects", + through="Membership", verbose_name=_("members")) + public = models.BooleanField(default=True, null=False, blank=True, + verbose_name=_("public")) + last_us_ref = models.BigIntegerField(null=True, blank=False, default=1, + verbose_name=_("last us ref")) + last_task_ref = models.BigIntegerField(null=True, blank=False, default=1, + verbose_name=_("last task ref")) + last_issue_ref = models.BigIntegerField(null=True, blank=False, default=1, + verbose_name=_("last issue ref")) + total_milestones = models.IntegerField(default=0, null=True, blank=True, + verbose_name=_("total of milestones")) + total_story_points = models.FloatField(default=None, null=True, blank=False, + verbose_name=_("total story points")) + tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags")) + site = models.ForeignKey("base.Site", related_name="projects", null=True, default=None) notifiable_fields = [ "name", "total_milestones", "total_story_points", - "default_points", - "default_us_status", - "default_task_status", - "default_priority", - "default_severity", - "default_issue_status", - "default_issue_type", - "default_question_status", + # This realy should be in this list? + # "default_points", + # "default_us_status", + # "default_task_status", + # "default_priority", + # "default_severity", + # "default_issue_status", + # "default_issue_type", + # "default_question_status", "description" ] @@ -178,9 +158,8 @@ class Project(models.Model): def get_users(self): user_model = get_user_model() - return user_model.objects.filter( - id__in=list(self.memberships.values_list("user", flat=True)) - ) + members = self.memberships.values_list("user", flat=True) + return user_model.objects.filter(id__in=list(members)) def update_role_points(self): rolepoints_model = get_model("userstories", "RolePoints") @@ -251,6 +230,45 @@ class Project(models.Model): return self._get_user_stories_points(self.user_stories.filter(milestone__isnull=False)) +def get_attachment_file_path(instance, filename): + return "attachment-files/{project}/{model}/{filename}".format( + project=instance.project.slug, + model=instance.content_type.model, + filename=filename + ) + + +class Attachment(models.Model): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, + related_name="change_attachments", + verbose_name=_("owner")) + project = models.ForeignKey("Project", null=False, blank=False, + related_name="attachments", verbose_name=_("project")) + content_type = models.ForeignKey(ContentType, null=False, blank=False, + verbose_name=_("content type")) + object_id = models.PositiveIntegerField(null=False, blank=False, + verbose_name=_("object id")) + content_object = generic.GenericForeignKey("content_type", "object_id") + created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, + verbose_name=_("created date")) + modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, + verbose_name=_("modified date")) + attached_file = models.FileField(max_length=500, null=True, blank=True, + upload_to=get_attachment_file_path, + verbose_name=_("attached file")) + + class Meta: + verbose_name = "attachment" + verbose_name_plural = "attachments" + ordering = ["project", "created_date"] + permissions = ( + ("view_attachment", "Can view attachment"), + ) + + def __str__(self): + return "Attachment: {}".format(self.id) + + # User Stories common Models class UserStoryStatus(models.Model): name = models.CharField(max_length=255, null=False, blank=False, diff --git a/greenmine/projects/serializers.py b/greenmine/projects/serializers.py index 3f727d3c..3ffe8888 100644 --- a/greenmine/projects/serializers.py +++ b/greenmine/projects/serializers.py @@ -89,6 +89,12 @@ class QuestionStatusSerializer(serializers.ModelSerializer): # Projects class MembershipSerializer(serializers.ModelSerializer): + class Meta: + model = models.Membership + read_only_fields = ("user",) + + +class ProjectMembershipSerializer(serializers.ModelSerializer): role_name = serializers.CharField(source='role.name', required=False) full_name = serializers.CharField(source='user.get_full_name', required=False) @@ -101,14 +107,14 @@ class ProjectSerializer(serializers.ModelSerializer): class Meta: model = models.Project - read_only_fields = ("created_date", "modified_date", "owner") + read_only_fields = ("created_date", "modified_date", "owner", "site") exclude = ("last_us_ref", "last_task_ref", "last_issue_ref") class ProjectDetailSerializer(ProjectSerializer): list_of_milestones = serializers.SerializerMethodField("get_list_of_milestones") roles = serializers.SerializerMethodField("get_list_of_roles") - memberships = MembershipSerializer(many=True, required=False) + memberships = ProjectMembershipSerializer(many=True, required=False) us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories points = PointsSerializer(many=True, required=False) task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks diff --git a/greenmine/projects/templates/emails/membership_invitation-body-html.jinja b/greenmine/projects/templates/emails/membership_invitation-body-html.jinja new file mode 100644 index 00000000..24f7d658 --- /dev/null +++ b/greenmine/projects/templates/emails/membership_invitation-body-html.jinja @@ -0,0 +1,23 @@ +{% extends "emails/base.jinja" %} + +{% set final_url = resolve_front_url("invitation", membership.token) %} +{% set final_url_name = "Greenmine - Invitation to join on {0} project.".format(membership.project) %} + +{% block body %} + + + + +
+

Hi,

+

you have been invited to the project '{{ membership.project }}'.

+

If you want to join to this project go to this link + for accept this invitation.

+
+{% endblock %} + +{% block footer %} +

+ More info at: {{ final_url_name }} +

+{% endblock %} diff --git a/greenmine/projects/templates/emails/membership_invitation-body-text.jinja b/greenmine/projects/templates/emails/membership_invitation-body-text.jinja new file mode 100644 index 00000000..d0cb137c --- /dev/null +++ b/greenmine/projects/templates/emails/membership_invitation-body-text.jinja @@ -0,0 +1,11 @@ +{% set final_url = resolve_front_url("invitation", membership.token) %} +{% set final_url_name = "Greenmine - Invitation to join on {0} project.".format(membership.project) %} + +Hi, + +you have been invited to the project '{{ membership.project }}'. + +If you want to join to this project go to {{ final_url }} for accept this invitation. + + +** More info at ({{ final_url }}) ** diff --git a/greenmine/projects/templates/emails/membership_invitation-subject.jinja b/greenmine/projects/templates/emails/membership_invitation-subject.jinja new file mode 100644 index 00000000..3d46285c --- /dev/null +++ b/greenmine/projects/templates/emails/membership_invitation-subject.jinja @@ -0,0 +1 @@ +[Greenmine] Invitation to join to the project '{{ membership.project}}' diff --git a/greenmine/projects/tests/__init__.py b/greenmine/projects/tests/__init__.py index 66171fe1..c73028c1 100644 --- a/greenmine/projects/tests/__init__.py +++ b/greenmine/projects/tests/__init__.py @@ -20,8 +20,7 @@ def create_project(id, owner, save=True): def add_membership(project, user, role_slug=None): model = get_model("users", "Role") - roles = model.objects.filter(slug=role_slug) - role = roles[0] if roles.exists() else model.objects.all()[0] + role = model.objects.get(slug=role_slug) model = get_model("projects", "Membership") instance = model.objects.create( diff --git a/greenmine/projects/tests/tests_api.py b/greenmine/projects/tests/tests_api.py index fc10000d..037f27ec 100644 --- a/greenmine/projects/tests/tests_api.py +++ b/greenmine/projects/tests/tests_api.py @@ -8,12 +8,12 @@ from django.core import mail from django.db.models import get_model from greenmine.base.users.tests import create_user -from greenmine.projects.models import Project +from greenmine.projects.models import Project, Membership from . import create_project, add_membership class ProfileTestCase(test.TestCase): - fixtures = ["initial_role.json", ] + fixtures = ["initial_role.json", "initial_site.json"] def setUp(self): self.user1 = create_user(1, is_superuser=True) @@ -24,9 +24,10 @@ class ProfileTestCase(test.TestCase): self.project2 = create_project(2, self.user1) self.project3 = create_project(3, self.user2) - add_membership(self.project1, self.user3, "dev") - add_membership(self.project3, self.user3, "dev") - add_membership(self.project3, self.user2, "dev") + add_membership(self.project1, self.user3, "back") + add_membership(self.project3, self.user3, "back") + add_membership(self.project3, self.user2, "back") + def test_list_users(self): response = self.client.login(username=self.user3.username, @@ -37,7 +38,7 @@ class ProfileTestCase(test.TestCase): self.assertEqual(response.status_code, 200) users_list = response.data - self.assertEqual(len(users_list), 2) + self.assertEqual(len(users_list), 3) def test_update_users(self): @@ -153,19 +154,79 @@ class ProfileTestCase(test.TestCase): class ProjectsTestCase(test.TestCase): - fixtures = ["initial_role.json", ] + fixtures = ["initial_role.json", "initial_site.json"] def setUp(self): self.user1 = create_user(1) self.user2 = create_user(2) self.user3 = create_user(3) + self.user3 = create_user(4) self.project1 = create_project(1, self.user1) self.project2 = create_project(2, self.user1) self.project3 = create_project(3, self.user2) + self.project4 = create_project(4, self.user2) - add_membership(self.project1, self.user3, "dev") - add_membership(self.project3, self.user3, "dev") + add_membership(self.project1, self.user3, "back") + add_membership(self.project3, self.user3, "back") + + self.dev_role = get_model("users", "Role").objects.get(slug="back") + + def test_send_invitations_01(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + url = reverse("memberships-list") + data = {"role": self.dev_role.id, + "email": "pepe@pepe.com", + "project": self.project4.id} + + response = self.client.post(url, data=json.dumps(data), content_type="application/json") + self.assertEqual(response.status_code, 201) + self.assertEqual(self.project4.memberships.count(), 1) + + self.assertEqual(len(mail.outbox), 1) + self.assertNotEqual(len(mail.outbox[0].body), 0) + + def test_send_invitations_02(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + url = reverse("memberships-list") + data = {"role": self.dev_role.id, + "email": "pepe@pepe.com", + "project": self.project4.id} + + response = self.client.post(url, data=json.dumps(data), content_type="application/json") + + self.assertEqual(response.status_code, 201) + self.assertEqual(self.project4.memberships.count(), 1) + self.assertEqual(len(mail.outbox), 1) + self.assertNotEqual(len(mail.outbox[0].body), 0) + + response = self.client.post(url, data=json.dumps(data), content_type="application/json") + + self.assertEqual(self.project4.memberships.count(), 1) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 1) + self.assertNotEqual(len(mail.outbox[0].body), 0) + + def test_send_invitations_03(self): + response = self.client.login(username=self.user1.username, + password=self.user1.username) + self.assertTrue(response) + + url = reverse("memberships-list") + data = {"role": self.dev_role.id, + "email": self.user3.email, + "project": self.project3.id} + + response = self.client.post(url, data=json.dumps(data), content_type="application/json") + self.assertEqual(response.status_code, 400) + + self.assertEqual(len(mail.outbox), 0) def test_list_projects_by_anon(self): response = self.client.get(reverse("projects-list")) @@ -187,7 +248,7 @@ class ProjectsTestCase(test.TestCase): response = self.client.get(reverse("projects-list")) self.assertEqual(response.status_code, 200) projects_list = response.data - self.assertEqual(len(projects_list), 1) + self.assertEqual(len(projects_list), 2) self.client.logout() def test_list_projects_by_membership(self): @@ -239,13 +300,13 @@ class ProjectsTestCase(test.TestCase): "total_story_points": 10 } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.post( reverse("projects-list"), json.dumps(data), content_type="application/json") self.assertEqual(response.status_code, 401) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) def test_create_project_by_auth(self): data = { @@ -254,16 +315,15 @@ class ProjectsTestCase(test.TestCase): "total_story_points": 10 } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.login(username=self.user1.username, password=self.user1.username) self.assertTrue(response) - response = self.client.post( - reverse("projects-list"), - json.dumps(data), - content_type="application/json") + response = self.client.post(reverse("projects-list"), json.dumps(data), + content_type="application/json") + self.assertEqual(response.status_code, 201) - self.assertEqual(Project.objects.all().count(), 4) + self.assertEqual(Project.objects.all().count(), 5) self.client.logout() def test_edit_project_by_anon(self): @@ -271,21 +331,21 @@ class ProjectsTestCase(test.TestCase): "description": "Edited project description", } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.assertNotEqual(data["description"], self.project1.description) response = self.client.patch( reverse("projects-detail", args=(self.project1.id,)), json.dumps(data), content_type="application/json") self.assertEqual(response.status_code, 401) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) def test_edit_project_by_owner(self): data = { "description": "Modified project description", } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.assertNotEqual(data["description"], self.project1.description) response = self.client.login(username=self.user1.username, password=self.user1.username) @@ -296,7 +356,7 @@ class ProjectsTestCase(test.TestCase): content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(data["description"], response.data["description"]) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.client.logout() def test_edit_project_by_membership(self): @@ -304,7 +364,7 @@ class ProjectsTestCase(test.TestCase): "description": "Edited project description", } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.assertNotEqual(data["description"], self.project1.description) response = self.client.login(username=self.user3.username, password=self.user3.username) @@ -315,7 +375,7 @@ class ProjectsTestCase(test.TestCase): content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(data["description"], response.data["description"]) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.client.logout() def test_edit_project_by_not_membership(self): @@ -323,7 +383,7 @@ class ProjectsTestCase(test.TestCase): "description": "Edited project description", } - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.assertNotEqual(data["description"], self.project1.description) response = self.client.login(username=self.user2.username, password=self.user2.username) @@ -333,41 +393,41 @@ class ProjectsTestCase(test.TestCase): json.dumps(data), content_type="application/json") self.assertEqual(response.status_code, 404) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.client.logout() def test_delete_project_by_anon(self): - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) self.assertEqual(response.status_code, 401) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) def test_delete_project_by_owner(self): - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.login(username=self.user1.username, password=self.user1.username) self.assertTrue(response) response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) self.assertEqual(response.status_code, 204) - self.assertEqual(Project.objects.all().count(), 2) + self.assertEqual(Project.objects.all().count(), 3) self.client.logout() def test_delete_project_by_membership(self): - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.login(username=self.user3.username, password=self.user3.username) self.assertTrue(response) response = self.client.delete(reverse("projects-detail", args=(self.project1.id,))) self.assertEqual(response.status_code, 204) - self.assertEqual(Project.objects.all().count(), 2) + self.assertEqual(Project.objects.all().count(), 3) self.client.logout() def test_delete_project_by_not_membership(self): - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) response = self.client.login(username=self.user1.username, password=self.user1.username) self.assertTrue(response) response = self.client.delete(reverse("projects-detail", args=(self.project3.id,))) self.assertEqual(response.status_code, 404) - self.assertEqual(Project.objects.all().count(), 3) + self.assertEqual(Project.objects.all().count(), 4) self.client.logout() diff --git a/greenmine/routers.py b/greenmine/routers.py index 6ccb79a5..1a2fb930 100644 --- a/greenmine/routers.py +++ b/greenmine/routers.py @@ -4,7 +4,7 @@ from greenmine.base import routers from greenmine.base.auth.api import AuthViewSet from greenmine.base.users.api import RolesViewSet, UsersViewSet from greenmine.base.searches.api import SearchViewSet -from greenmine.projects.api import ProjectViewSet, MembershipViewSet +from greenmine.projects.api import ProjectViewSet, MembershipViewSet, InvitationViewSet from greenmine.projects.milestones.api import MilestoneViewSet from greenmine.projects.userstories.api import UserStoryViewSet, UserStoryAttachmentViewSet from greenmine.projects.tasks.api import TaskViewSet, TaskAttachmentViewSet @@ -27,6 +27,7 @@ router.register(r"search", SearchViewSet, base_name="search") # greenmine.projects router.register(r"projects", ProjectViewSet, base_name="projects") router.register(r"memberships", MembershipViewSet, base_name="memberships") +router.register(r"invitations", InvitationViewSet, base_name="invitations") # greenmine.projects.milestones router.register(r"milestones", MilestoneViewSet, base_name="milestones") diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index 9964f2ac..aee008b3 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -15,6 +15,8 @@ OUT_PROJECT_ROOT = os.path.abspath( os.path.join(PROJECT_ROOT, "..") ) +USE_X_FORWARDED_HOST = True + APPEND_SLASH = False @@ -151,17 +153,17 @@ TEMPLATE_LOADERS = [ ] MIDDLEWARE_CLASSES = [ + 'greenmine.base.middleware.CoorsMiddleware', + 'greenmine.base.middleware.SitesMiddleware', + # Common middlewares 'django.middleware.common.CommonMiddleware', 'django.middleware.locale.LocaleMiddleware', - 'greenmine.base.middleware.CoorsMiddleware', # Only needed by django admin 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - - # 'greenmine.base.middleware.GreenmineSessionMiddleware', ] TEMPLATE_CONTEXT_PROCESSORS = [ @@ -182,9 +184,6 @@ TEMPLATE_DIRS = [ ] INSTALLED_APPS = [ - # 'grappelli.dashboard', - # 'grappelli', - 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -192,10 +191,10 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.staticfiles', - 'greenmine.base', - 'greenmine.base.notifications', 'greenmine.base.users', + 'greenmine.base.notifications', 'greenmine.base.searches', + 'greenmine.base', 'greenmine.projects', 'greenmine.projects.milestones', 'greenmine.projects.userstories', @@ -210,7 +209,6 @@ INSTALLED_APPS = [ 'reversion', 'rest_framework', 'djmail', - 'django_sites', ] WSGI_APPLICATION = 'greenmine.wsgi.application' @@ -224,9 +222,12 @@ LOGGING = { } }, 'formatters': { - 'simple': { + 'complete': { 'format': '%(levelname)s:%(asctime)s:%(module)s %(message)s' }, + 'simple': { + 'format': '%(levelname)s:%(asctime)s: %(message)s' + }, 'null': { 'format': '%(message)s', }, @@ -239,7 +240,7 @@ LOGGING = { 'console':{ 'level':'DEBUG', 'class':'logging.StreamHandler', - 'formatter': 'null', + 'formatter': 'simple', }, 'mail_admins': { 'level': 'ERROR', @@ -258,11 +259,16 @@ LOGGING = { 'level': 'ERROR', 'propagate': False, }, - 'main': { + 'greenmine': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': False, - } + }, + 'greenmine.site': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, } } @@ -291,6 +297,7 @@ REST_FRAMEWORK = { 'greenmine.base.auth.Session', ), 'FILTER_BACKEND': 'greenmine.base.filters.FilterBackend', + 'EXCEPTION_HANDLER': 'greenmine.base.exceptions.exception_handler', 'PAGINATE_BY': 30, 'MAX_PAGINATE_BY': 1000, } diff --git a/greenmine/urls.py b/greenmine/urls.py index beef742c..47c33fca 100644 --- a/greenmine/urls.py +++ b/greenmine/urls.py @@ -13,6 +13,7 @@ admin.autodiscover() urlpatterns = patterns('', url(r'^api/v1/', include(router.urls)), url(r'^api/v1/api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^api/v1/sites', "greenmine.base.apiviews.sitestatus"), url(r'^admin/', include(admin.site.urls)), url(r'^grappelli/', include('grappelli.urls')), ) diff --git a/regenerate.sh b/regenerate.sh index 303a4a6d..2e9e9bab 100755 --- a/regenerate.sh +++ b/regenerate.sh @@ -8,6 +8,8 @@ createdb greenmine echo "-> Run syncdb" python manage.py syncdb --migrate --noinput --traceback +# echo "-> Load initial Site" +# python manage.py loaddata initial_site --traceback echo "-> Load initial user" python manage.py loaddata initial_user --traceback echo "-> Load initial roles" diff --git a/requirements.txt b/requirements.txt index abc24b8c..d8222121 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ git+https://github.com/tomchristie/django-rest-framework.git@2.4.0 git+https://github.com/etianen/django-reversion.git@django-1.6 Django==1.6.0 -South==0.8.2 +South==0.8.3 anyjson==0.3.3 Werkzeug==0.9.4 celery==3.0.24 @@ -19,4 +19,3 @@ djmail>=0.4 django-jinja>=0.21 jinja2==2.7.1 pygments>=1.6 -django-sites>=0.4