Skip to content

Commit

Permalink
Merge pull request #3056 from onepercentclub/release/edit-running-pro…
Browse files Browse the repository at this point in the history
…ject

Release/edit running project
  • Loading branch information
gannetson committed Dec 21, 2017
2 parents 6601c63 + c2c112b commit 0153740
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 25 deletions.
6 changes: 3 additions & 3 deletions bluebottle/bb_projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
RetrieveUpdateDestroyAPIView, OwnerListViewMixin
)
from bluebottle.utils.permissions import (
OneOf, ResourcePermission, ResourceOwnerPermission, RelatedResourceOwnerPermission
OneOf, ResourcePermission, ResourceOwnerPermission, RelatedResourceOwnerPermission,
)
from bluebottle.projects.permissions import IsEditableOrReadOnly
from bluebottle.projects.permissions import IsEditableOrReadOnly, CanEditOwnRunningProjects
from .models import ProjectTheme, ProjectPhase


Expand Down Expand Up @@ -294,7 +294,7 @@ class ManageProjectDetail(RetrieveUpdateAPIView):
queryset = Project.objects.all()
permission_classes = (
ResourceOwnerPermission,
IsEditableOrReadOnly,
OneOf(IsEditableOrReadOnly, CanEditOwnRunningProjects)
)
serializer_class = ManageProjectSerializer
lookup_field = 'slug'
Expand Down
129 changes: 117 additions & 12 deletions bluebottle/bb_tasks/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ def setUp(self):
self.another_token = "JWT {0}".format(self.another_user.get_jwt_token())
self.another_project = ProjectFactory.create(owner=self.another_user)

self.task = TaskFactory.create(project=self.some_project)

self.skill1 = SkillFactory.create()
self.skill2 = SkillFactory.create()
self.skill3 = SkillFactory.create()
self.skill4 = SkillFactory.create()

self.task_url = '/api/bb_tasks/'
self.task_preview_url = '/api/bb_tasks/previews/'
self.task_members_url = '/api/bb_tasks/members/'
self.task_url = reverse('task-list')
self.task_preview_url = reverse('task_preview_list')
self.task_members_url = reverse('task-member-list')
self.task_detail_url = reverse('task_detail', args=(self.task.pk, ))

def test_create_task(self):
# Get the list of tasks for some project should return none (count = 0)
Expand All @@ -50,7 +53,7 @@ def test_create_task(self):
token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEquals(response.data['count'], 0)
self.assertEquals(response.data['count'], 1)

future_date = timezone.now() + timezone.timedelta(days=30)

Expand Down Expand Up @@ -96,7 +99,7 @@ def test_create_task(self):
token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEquals(response.data['count'], 1)
self.assertEquals(response.data['count'], 2)

# Another user that owns another project can create a task for that.
another_task_data = {
Expand Down Expand Up @@ -156,7 +159,7 @@ def test_create_task_incorrect_deadline(self):
'time_needed': 5,
'skill': '{0}'.format(self.skill1.id),
'location': 'Overthere',
'deadline': str(self.some_project.deadline + timedelta(hours=1)),
'deadline': str(self.some_project.campaign_started + timedelta(days=400)),
'deadline_to_apply': str(self.some_project.deadline + timedelta(minutes=1))
}
response = self.client.post(self.task_url, some_task_data,
Expand All @@ -166,6 +169,79 @@ def test_create_task_incorrect_deadline(self):
response.data)
self.assertTrue('deadline' in response.data)

def test_create_task_after_project_deadline(self):
self.some_project.project_type = 'sourcing'
self.some_project.save()

# Create a task with an later deadline
some_task_data = {
'project': self.some_project.slug,
'title': 'A nice task!',
'description': 'Well, this is nice',
'time_needed': 5,
'skill': '{0}'.format(self.skill1.id),
'location': 'Overthere',
'deadline': str(self.some_project.deadline + timedelta(days=1)),
'deadline_to_apply': str(self.some_project.deadline + timedelta(minutes=1))
}
response = self.client.post(self.task_url, some_task_data,
token=self.some_token)

self.assertEqual(response.status_code, status.HTTP_201_CREATED,
response.data)
self.assertEqual(
Task.objects.get(
pk=response.data['id']
).deadline.strftime("%Y-%m-%d %H:%M:%S"),
Project.objects.get(
pk=self.some_project.pk
).deadline.strftime("%Y-%m-%d %H:%M:%S"),
)

def test_create_task_after_funding_project_deadline(self):
self.some_project.project_type = 'funding'
self.some_project.save()

# Create a task with an later deadline
some_task_data = {
'project': self.some_project.slug,
'title': 'A nice task!',
'description': 'Well, this is nice',
'time_needed': 5,
'skill': '{0}'.format(self.skill1.id),
'location': 'Overthere',
'deadline': str(self.some_project.deadline + timedelta(days=1)),
'deadline_to_apply': str(self.some_project.deadline + timedelta(minutes=1))
}
response = self.client.post(self.task_url, some_task_data,
token=self.some_token)

# This should fail because
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST,
response.data)

def test_create_task_project_not_started(self):
self.some_project.status = ProjectPhase.objects.get(slug='plan-submitted')
self.some_project.project_type = 'sourcing'
self.some_project.save()

# Create a task with an invalid deadline
some_task_data = {
'project': self.some_project.slug,
'title': 'A nice task!',
'description': 'Well, this is nice',
'time_needed': 5,
'skill': '{0}'.format(self.skill1.id),
'location': 'Overthere',
'deadline': str(self.some_project.deadline + timedelta(days=1)),
'deadline_to_apply': str(self.some_project.deadline + timedelta(minutes=1))
}
response = self.client.post(self.task_url, some_task_data,
token=self.some_token)

self.assertEqual(response.status_code, status.HTTP_201_CREATED,
response.data)

def test_create_task_closed_project(self):
self.some_project.status = ProjectPhase.objects.get(slug='closed')
self.some_project.save()
Expand All @@ -191,6 +267,35 @@ def test_create_task_closed_project(self):
response.data)
self.assertTrue('closed projects' in response.data[0])

def test_update_deadline(self):
self.some_project.project_type = 'sourcing'
self.some_project.save()
future_date = timezone.now() + timezone.timedelta(days=30)
task_data = {
'project': self.some_project.slug,
'title': 'Some new title',
'description': 'Well, this is nice',
'time_needed': 5,
'skill': '{0}'.format(self.skill1.id),
'location': 'Overthere',
'deadline': str(self.some_project.deadline + timedelta(days=1)),
'deadline_to_apply': str(future_date - timezone.timedelta(days=1))
}

response = self.client.put(
self.task_detail_url, task_data, token=self.some_token
)
self.task = Task.objects.get(pk=self.task.pk)
self.some_project = Project.objects.get(pk=self.some_project.pk)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
self.task.deadline.strftime("%Y-%m-%d %H:%M:%S"),
self.some_project.deadline.strftime("%Y-%m-%d %H:%M:%S"),
)
self.assertEqual(
self.task.title, task_data['title']
)

def test_apply_for_task(self):
future_date = timezone.now() + timezone.timedelta(days=60)

Expand Down Expand Up @@ -231,14 +336,14 @@ def test_task_search_by_status(self):
project=self.another_project,
)

self.assertEqual(2, Task.objects.count())
self.assertEqual(3, Task.objects.count())

# Test as a different user
response = self.client.get(self.task_url, {'status': 'open'},
token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['count'], 2)

response = self.client.get(self.task_url, {'status': 'in progress'},
token=self.some_token)
Expand Down Expand Up @@ -271,7 +376,7 @@ def test_task_preview_search(self):
)

self.assertEqual(2, Project.objects.count())
self.assertEqual(2, Task.objects.count())
self.assertEqual(3, Task.objects.count())

api_url = self.task_preview_url

Expand All @@ -280,7 +385,7 @@ def test_task_preview_search(self):
response = self.client.get(api_url, token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['count'], 2)

response = self.client.get(api_url, {'status': 'in progress'},
token=self.some_token)
Expand All @@ -292,7 +397,7 @@ def test_task_preview_search(self):
token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEqual(response.data['count'], 0)
self.assertEqual(response.data['count'], 1)

skill = task1.skill
response = self.client.get(api_url, {'skill': skill.id},
Expand All @@ -313,7 +418,7 @@ def test_task_preview_search(self):
token=self.some_token)
self.assertEqual(response.status_code, status.HTTP_200_OK,
response.data)
self.assertEqual(response.data['count'], 0)
self.assertEqual(response.data['count'], 1)

def test_withdraw_task_member(self):
task = TaskFactory.create()
Expand Down
2 changes: 1 addition & 1 deletion bluebottle/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ def get_list_filter(self, request):

def get_list_display(self, request):
fields = ['get_title_display', 'get_owner_display', 'created', 'status', 'deadline', 'donated_percentage',
'amount_extra', 'expertise_based']
'campaign_edited', 'amount_extra', 'expertise_based']

if request.user.has_perm('projects.approve_payout'):
fields.insert(4, 'payout_status')
Expand Down
24 changes: 24 additions & 0 deletions bluebottle/projects/migrations/0054_auto_20171122_1415.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2017-11-22 13:15
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0053_merge_20171122_1001'),
]

operations = [
migrations.AlterModelOptions(
name='project',
options={'ordering': ['title'], 'permissions': (('approve_payout', 'Can approve payouts for projects'), ('api_read_project', 'Can view projects through the API'), ('api_add_project', 'Can add projects through the API'), ('api_change_project', 'Can change projects through the API'), ('api_delete_project', 'Can delete projects through the API'), ('api_read_own_project', 'Can view own projects through the API'), ('api_add_own_project', 'Can add own projects through the API'), ('api_change_own_project', 'Can change own projects through the API'), ('api_change_own_running_project', 'Can change own running projects through the API'), ('api_delete_own_project', 'Can delete own projects through the API'), ('api_read_projectdocument', 'Can view project documents through the API'), ('api_add_projectdocument', 'Can add project documents through the API'), ('api_change_projectdocument', 'Can change project documents through the API'), ('api_delete_projectdocument', 'Can delete project documents through the API'), ('api_read_own_projectdocument', 'Can view project own documents through the API'), ('api_add_own_projectdocument', 'Can add own project documents through the API'), ('api_change_own_projectdocument', 'Can change own project documents through the API'), ('api_delete_own_projectdocument', 'Can delete own project documents through the API'), ('api_read_projectbudgetline', 'Can view project budget lines through the API'), ('api_add_projectbudgetline', 'Can add project budget lines through the API'), ('api_change_projectbudgetline', 'Can change project budget lines through the API'), ('api_delete_projectbudgetline', 'Can delete project budget lines through the API'), ('api_read_own_projectbudgetline', 'Can view own project budget lines through the API'), ('api_add_own_projectbudgetline', 'Can add own project budget lines through the API'), ('api_change_own_projectbudgetline', 'Can change own project budget lines through the API'), ('api_delete_own_projectbudgetline', 'Can delete own project budget lines through the API')), 'verbose_name': 'project', 'verbose_name_plural': 'projects'},
),
migrations.AlterField(
model_name='projectsearchfilter',
name='name',
field=models.CharField(choices=[(b'location', 'Location'), (b'theme', 'Theme'), (b'skills', 'Skill'), (b'date', 'Date'), (b'status', 'Status'), (b'type', 'Type'), (b'category', 'Category')], max_length=100),
),
]
20 changes: 20 additions & 0 deletions bluebottle/projects/migrations/0055_project_campaign_edited.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2017-11-22 14:09
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0054_auto_20171122_1415'),
]

operations = [
migrations.AddField(
model_name='project',
name='campaign_edited',
field=models.DateTimeField(blank=True, null=True, verbose_name='Campaign edited'),
),
]
16 changes: 16 additions & 0 deletions bluebottle/projects/migrations/0058_merge_20171220_1342.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2017-12-20 12:42
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('projects', '0057_merge_20171205_1236'),
('projects', '0055_project_campaign_edited'),
]

operations = [
]
3 changes: 3 additions & 0 deletions bluebottle/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ class Project(BaseProject, PreviousStatusMixin):
blank=True)
campaign_ended = models.DateTimeField(_('Campaign Ended'), null=True,
blank=True)
campaign_edited = models.DateTimeField(_('Campaign edited'), null=True,
blank=True)
campaign_funded = models.DateTimeField(_('Campaign Funded'), null=True,
blank=True)
campaign_paid_out = models.DateTimeField(_('Campaign Paid Out'), null=True,
Expand Down Expand Up @@ -561,6 +563,7 @@ class Meta(BaseProject.Meta):
('api_read_own_project', 'Can view own projects through the API'),
('api_add_own_project', 'Can add own projects through the API'),
('api_change_own_project', 'Can change own projects through the API'),
('api_change_own_running_project', 'Can change own running projects through the API'),
('api_delete_own_project', 'Can delete own projects through the API'),

('api_read_projectdocument', 'Can view project documents through the API'),
Expand Down
25 changes: 24 additions & 1 deletion bluebottle/projects/permissions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from rest_framework import permissions

from bluebottle.utils.permissions import BasePermission, RelatedResourceOwnerPermission
from bluebottle.utils.permissions import (
BasePermission, RelatedResourceOwnerPermission, ResourceOwnerPermission
)


class RelatedProjectTaskManagerPermission(RelatedResourceOwnerPermission):
Expand All @@ -22,3 +24,24 @@ def has_object_action_permission(self, action, user, obj):

def has_action_permission(self, action, user, model_cls):
return True

def has_parent_permission(self, method, user, parent, model=None):
return self.has_object_action_permission(method, user, parent)


class CanEditOwnRunningProjects(ResourceOwnerPermission):
""" Allows access only to obj owner. """
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': [],
'PUT': ['%(app_label)s.api_change_own_running_%(model_name)s'],
'PATCH': [],
'DELETE': [],
}

def has_object_action_permission(self, action, user, obj):
return super(CanEditOwnRunningProjects, self).has_object_action_permission(
action, user, obj
) and obj.status.slug in ('campaign', 'voting')
Loading

0 comments on commit 0153740

Please sign in to comment.