Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion news/tests/test_news_project_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@

from core.models import Like, View
from news.models import News
from partner_programs.models import PartnerProgramProject, PartnerProgramUserProfile

from .helpers import create_news_for, create_project, create_user, create_user_file
from .helpers import (
create_news_for,
create_partner_program,
create_project,
create_user,
create_user_file,
)


class ProjectScopedNewsAPITests(TestCase):
Expand Down Expand Up @@ -99,6 +106,50 @@ def test_project_news_detail_can_be_updated_and_deleted_by_leader(self):
self.assertEqual(delete_response.status_code, 204)
self.assertFalse(News.objects.filter(pk=news.id).exists())

def test_submitted_project_copy_preserves_news_and_original_can_publish(self):
self.client.force_authenticate(self.leader)
program = create_partner_program()
program.is_competitive = True
program.save(update_fields=["is_competitive"])
project_copy = create_project(leader=self.leader)
PartnerProgramUserProfile.objects.create(
partner_program=program,
user=self.leader,
project=project_copy,
partner_program_data={},
)
news = create_news_for(project_copy, text="News saved before submission")
PartnerProgramProject.objects.create(
partner_program=program,
project=project_copy,
source_project=self.project,
submitted=True,
)
feed_url = f"/projects/{project_copy.id}/news/"
entry_url = f"{feed_url}{news.id}/"

self.assertEqual(self.client.get(feed_url).status_code, 200)
self.assertEqual(self.client.get(entry_url).status_code, 200)
self.assertEqual(
self.client.post(feed_url, {"text": "New post"}, format="json").status_code,
403,
)
self.assertEqual(
self.client.patch(entry_url, {"text": "Changed"}, format="json").status_code,
403,
)
self.assertEqual(self.client.delete(entry_url).status_code, 403)
news.refresh_from_db()
self.assertEqual(news.text, "News saved before submission")
self.assertEqual(
self.client.post(
f"/projects/{self.project.id}/news/",
{"text": "New post on the original"},
format="json",
).status_code,
201,
)

def test_project_news_update_rejects_file_of_another_user(self):
self.client.force_authenticate(self.leader)
leader_file = create_user_file(self.leader, name="leader-file")
Expand Down
63 changes: 43 additions & 20 deletions news/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from contextlib import contextmanager

from django.contrib.auth import get_user_model
from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
Expand Down Expand Up @@ -27,7 +30,7 @@
)
from partner_programs.models import PartnerProgram
from projects.models import Project
from projects.permissions import ProjectVisibilityPermission
from projects.permissions import ProjectVisibilityPermission, require_editable_submission

User = get_user_model()

Expand All @@ -46,6 +49,21 @@


class ContextNewsAPIView:
@contextmanager
def editable_project_news(self):
project_id = self.kwargs.get("project_pk")
if project_id is None:
yield None
return

# Submission takes the same Project lock before setting submitted.
with transaction.atomic():
project = get_object_or_404(
Project.objects.select_for_update(), pk=project_id
)
require_editable_submission(project)
yield project

def get_queryset(self):
return get_news_queryset_for_context(self.kwargs)

Expand Down Expand Up @@ -82,12 +100,12 @@ def post(self, request: Request, *args, **kwargs) -> Response:
data = serializer.validated_data

if kwargs.get("project_pk"):
project = get_object_or_404(Project, pk=kwargs["project_pk"])
news = create_project_news(project, request.user, data)
return Response(
self.get_detail_response_serializer_class()(news).data,
status=status.HTTP_201_CREATED,
)
with self.editable_project_news() as project:
news = create_project_news(project, request.user, data)
return Response(
self.get_detail_response_serializer_class()(news).data,
status=status.HTTP_201_CREATED,
)
if kwargs.get("user_pk"):
user = get_object_or_404(User, pk=kwargs["user_pk"])
news = create_user_news(user, request.user, data)
Expand Down Expand Up @@ -128,19 +146,24 @@ def get(self, request: Request, *args, **kwargs) -> Response:
)

def update(self, request: Request, *args, **kwargs) -> Response:
news = self.get_news_object()
context = {"user": request.user}
serializer = NewsUpdateSerializer(
news,
data=request.data,
context={"request": request},
partial=kwargs.get("partial", False),
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
self.get_detail_response_serializer_class()(news, context=context).data
)
with self.editable_project_news():
news = self.get_news_object()
context = {"user": request.user}
serializer = NewsUpdateSerializer(
news,
data=request.data,
context={"request": request},
partial=kwargs.get("partial", False),
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(
self.get_detail_response_serializer_class()(news, context=context).data
)

def destroy(self, request: Request, *args, **kwargs) -> Response:
with self.editable_project_news():
return super().destroy(request, *args, **kwargs)


class NewsDetailSetViewed(ContextNewsAPIView, generics.CreateAPIView):
Expand Down
Loading