diff --git a/greenmine/base/admin.py b/greenmine/base/admin.py index 76fbeeb3..59a1ba76 100644 --- a/greenmine/base/admin.py +++ b/greenmine/base/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from django.contrib.auth.models import Group +from django.contrib.auth.admin import UserAdmin -from greenmine.base.models import Role +from greenmine.base.models import Role, User admin.site.unregister(Group) @@ -19,3 +20,4 @@ class RoleAdmin(admin.ModelAdmin): db_field, request=request, **kwargs) admin.site.register(Role, RoleAdmin) +admin.site.register(User, UserAdmin) diff --git a/greenmine/base/api.py b/greenmine/base/api.py index cb0050a9..3ea1fe6a 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -25,6 +25,7 @@ class ApiRoot(APIView): 'user-stories': reverse('user-story-list', request=request, format=format), 'changes': reverse('change-list', request=request, format=format), 'change-attachments': reverse('change-attachment-list', request=request, format=format), + 'issues': reverse('issue-list', request=request, format=format), 'tasks': reverse('task-list', request=request, format=format), 'severities': reverse('severity-list', request=request, format=format), 'issue-status': reverse('issue-status-list', request=request, format=format), diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 1943bb25..090522a1 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -64,31 +64,3 @@ class Role(models.Model): def __unicode__(self): return unicode(self.name) - - -if not hasattr(Group, 'role'): - field = models.ForeignKey(Role, blank=False, null=False, related_name='groups') - field.contribute_to_class(Group, 'role') - - -if not hasattr(Group, 'project'): - field = models.ForeignKey(Project, blank=False, null=False, related_name='groups') - field.contribute_to_class(Group, 'project') - - -@receiver(post_save, sender=Role) -def role_post_save(sender, instance, **kwargs): - """ - Recalculate projects groups - """ - from greenmine.base.services import RoleGroupsService - RoleGroupsService().replicate_role_on_all_projects(instance) - - -@receiver(m2m_changed, sender=Role.permissions.through) -def role_m2m_changed(sender, instance, **kwargs): - """ - Recalculate projects groups - """ - from greenmine.base.services import RoleGroupsService - RoleGroupsService().replicate_role_on_all_projects(instance) diff --git a/greenmine/scrum/admin.py b/greenmine/scrum/admin.py index c7207f51..9b769a5d 100644 --- a/greenmine/scrum/admin.py +++ b/greenmine/scrum/admin.py @@ -6,6 +6,12 @@ from greenmine.scrum import models import reversion +class MembershipInline(admin.TabularInline): + model = models.Membership + fields = ('user', 'project', 'role') + extra = 0 + + class MilestoneInline(admin.TabularInline): model = models.Milestone fields = ('name', 'owner', 'estimated_start', 'estimated_finish', 'closed', 'disponibility', 'order') @@ -27,7 +33,7 @@ class UserStoryInline(admin.TabularInline): class ProjectAdmin(reversion.VersionAdmin): list_display = ["name", "owner"] - inlines = [MilestoneInline, UserStoryInline] + inlines = [MembershipInline, MilestoneInline, UserStoryInline] admin.site.register(models.Project, ProjectAdmin) diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index 085333d0..329a3547 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -2,7 +2,7 @@ from rest_framework import generics from greenmine.scrum.serializers import * from greenmine.scrum.models import * - +from greenmine.scrum.permissions import * class SimpleFilterMixin(object): filter_fields = [] @@ -36,10 +36,14 @@ class ProjectList(generics.ListCreateAPIView): model = Project serializer_class = ProjectSerializer + def get_queryset(self): + return self.model.objects.filter(members=self.request.user) + class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): model = Project serializer_class = ProjectSerializer + permission_classes = (ProjectDetailPermission,) class MilestoneList(SimpleFilterMixin, generics.ListCreateAPIView): @@ -47,10 +51,14 @@ class MilestoneList(SimpleFilterMixin, generics.ListCreateAPIView): serializer_class = MilestoneSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView): model = Milestone serializer_class = MilestoneSerializer + permission_classes = (MilestoneDetailPermission,) class UserStoryList(SimpleFilterMixin, generics.ListCreateAPIView): @@ -58,6 +66,9 @@ class UserStoryList(SimpleFilterMixin, generics.ListCreateAPIView): serializer_class = UserStorySerializer filter_fields = ('project', 'milestone') + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView): model = UserStory @@ -68,6 +79,9 @@ class ChangeList(generics.ListCreateAPIView): model = Change serializer_class = ChangeSerializer + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class ChangeDetail(generics.RetrieveUpdateDestroyAPIView): model = Change @@ -78,17 +92,37 @@ class ChangeAttachmentList(generics.ListCreateAPIView): model = ChangeAttachment serializer_class = ChangeAttachmentSerializer + def get_queryset(self): + return self.model.objects.filter(change__project__members=self.request.user) + class ChangeAttachmentDetail(generics.RetrieveUpdateDestroyAPIView): model = ChangeAttachment serializer_class = ChangeAttachmentSerializer +class IssueList(generics.ListCreateAPIView): + model = Issue + serializer_class = IssueSerializer + filter_fields = ('project',) + + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + + +class IssueDetail(generics.RetrieveUpdateDestroyAPIView): + model = Issue + serializer_class = IssueSerializer + + class TaskList(generics.ListCreateAPIView): model = Task serializer_class = TaskSerializer filter_fields = ('user_story', 'milestone', 'project') + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class TaskDetail(generics.RetrieveUpdateDestroyAPIView): model = Task @@ -100,6 +134,9 @@ class SeverityList(generics.ListCreateAPIView): serializer_class = SeveritySerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class SeverityDetail(generics.RetrieveUpdateDestroyAPIView): model = Severity @@ -111,6 +148,9 @@ class IssueStatusList(generics.ListCreateAPIView): serializer_class = IssueStatusSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class IssueStatusDetail(generics.RetrieveUpdateDestroyAPIView): model = IssueStatus @@ -122,6 +162,9 @@ class TaskStatusList(SimpleFilterMixin, generics.ListCreateAPIView): serializer_class = TaskStatusSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class TaskStatusDetail(generics.RetrieveUpdateDestroyAPIView): model = TaskStatus @@ -133,6 +176,9 @@ class UserStoryStatusList(generics.ListCreateAPIView): serializer_class = UserStoryStatusSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class UserStoryStatusDetail(generics.RetrieveUpdateDestroyAPIView): model = UserStoryStatus @@ -144,6 +190,9 @@ class PriorityList(generics.ListCreateAPIView): serializer_class = PrioritySerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class PriorityDetail(generics.RetrieveUpdateDestroyAPIView): model = Priority @@ -155,6 +204,9 @@ class IssueTypeList(generics.ListCreateAPIView): serializer_class = IssueTypeSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class IssueTypeDetail(generics.RetrieveUpdateDestroyAPIView): model = IssueType @@ -166,6 +218,9 @@ class PointsList(generics.ListCreateAPIView): serializer_class = PointsSerializer filter_fields = ('project',) + def get_queryset(self): + return self.model.objects.filter(project__members=self.request.user) + class PointsDetail(generics.RetrieveUpdateDestroyAPIView): model = Points diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 5769c380..d49f5f29 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -105,6 +105,14 @@ class Points(models.Model): return u"project({0})/point({1})".format(self.project.id, self.name) +class Membership(models.Model): + user = models.ForeignKey("base.User") + project = models.ForeignKey("Project") + role = models.ForeignKey("base.Role") + + class Meta: + unique_together = ('user', 'project') + class Project(models.Model): uuid = models.CharField(max_length=40, unique=True, blank=True) name = models.CharField(max_length=250, unique=True) @@ -114,7 +122,8 @@ class Project(models.Model): created_date = models.DateTimeField(auto_now_add=True) modified_date = models.DateTimeField(auto_now_add=True, auto_now=True) - owner = models.ForeignKey("base.User", related_name="projects") + owner = models.ForeignKey("base.User", related_name="owned_projects") + members = models.ManyToManyField("base.User", related_name="projects", through='Membership') public = models.BooleanField(default=True) last_us_ref = models.BigIntegerField(null=True, default=1) @@ -170,6 +179,8 @@ class Project(models.Model): ('create_milestone', 'Can create milestones'), ('modify_milestone', 'Can modify milestones'), + ('view_milestone', 'Can view milestones'), + ('delete_milestone', 'Can delete milestones'), ('manage_users', 'Can manage users'), ) diff --git a/greenmine/scrum/permissions.py b/greenmine/scrum/permissions.py new file mode 100644 index 00000000..3baaaac2 --- /dev/null +++ b/greenmine/scrum/permissions.py @@ -0,0 +1,54 @@ +from rest_framework import permissions + +from greenmine.scrum.models import Membership + +def has_project_perm(user, project, perm): + if user.is_authenticated(): + try: + membership = Membership.objects.get(project=project, user=user) + if membership.role.permissions.filter(codename=perm).count() > 0: + return True + except Membership.DoesNotExist: + pass + return False + + +class BaseDetailPermission(permissions.BasePermission): + get_permission = None + put_permission = None + delete_permission = None + safe_methods = ['HEAD', 'OPTIONS'] + path_to_project = [] + + def has_object_permission(self, request, view, obj): + if request.method in self.safe_methods: + return True + + project_obj = obj + for attrib in self.path_to_project: + project_obj = getattr(project_obj, attrib) + + if request.method == "GET": + return has_project_perm(request.user, project_obj, self.get_permission) + + elif request.method == "PUT": + return has_project_perm(request.user, project_obj, self.put_permission) + + elif request.method == "DELETE": + return has_project_perm(request.user, project_obj, self.delete_permission) + + return False + +class ProjectDetailPermission(BaseDetailPermission): + get_permission = "view_projects" + put_permission = "modify_projects" + delete_permission = "delete_projects" + safe_methods = ['HEAD', 'OPTIONS'] + path_to_project = [] + +class MilestoneDetailPermission(BaseDetailPermission): + get_permission = "view_milestone" + put_permission = "modify_milestone" + delete_permission = "delete_milestone" + safe_methods = ['HEAD', 'OPTIONS'] + path_to_project = ['project'] diff --git a/greenmine/scrum/urls.py b/greenmine/scrum/urls.py index e01bece8..732fdebf 100644 --- a/greenmine/scrum/urls.py +++ b/greenmine/scrum/urls.py @@ -14,6 +14,8 @@ urlpatterns = format_suffix_patterns(patterns('', url(r'^changes/(?P[0-9]+)/$', api.ChangeDetail.as_view(), name='change-detail'), url(r'^change_attachments/$', api.ChangeAttachmentList.as_view(), name='change-attachment-list'), url(r'^change_attachments/(?P[0-9]+)/$', api.ChangeAttachmentDetail.as_view(), name='change-attachment-detail'), + url(r'^issues/$', api.IssueList.as_view(), name='issue-list'), + url(r'^issues/(?P[0-9]+)/$', api.IssueDetail.as_view(), name='issue-detail'), url(r'^tasks/$', api.TaskList.as_view(), name='task-list'), url(r'^tasks/(?P[0-9]+)/$', api.TaskDetail.as_view(), name='task-detail'), url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'), diff --git a/requirements.txt b/requirements.txt index 33c302c9..0da983d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ billiard==2.7.3.23 celery==3.0.17 django-celery==3.0.11 django-grappelli==2.4.4 -django-guardian==1.1.0.beta django-reversion==1.7 git+git://github.com/toastdriven/django-haystack.git django-picklefield==0.3.0