diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html index 5dce52086caa022b6ce8326272a9cac600468630..ff5e7d815ddf3cb7629883f0c302f8ef11976b64 100644 --- a/opentech/apply/activity/templates/activity/include/listing_base.html +++ b/opentech/apply/activity/templates/activity/include/listing_base.html @@ -36,7 +36,7 @@ {% endif %} {% if editable %} - <div class="feed__comment js-comment" data-id="{{activity.id}}" data-comment="{{activity|display_for:request.user|to_markdown}}" data-edit-url="{% url 'funds:api:comments:edit' pk=activity.pk %}"> + <div class="feed__comment js-comment" data-id="{{activity.id}}" data-comment="{{activity|display_for:request.user|to_markdown}}" data-edit-url="{% url 'api:v1:comments:edit' pk=activity.pk %}"> {{ activity|display_for:request.user|submission_links|markdown|bleach }} </div> diff --git a/opentech/api/__init__.py b/opentech/apply/api/__init__.py similarity index 100% rename from opentech/api/__init__.py rename to opentech/apply/api/__init__.py diff --git a/opentech/apply/api/urls.py b/opentech/apply/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..52f7c206e9b288a4582041c19bdfac655c795555 --- /dev/null +++ b/opentech/apply/api/urls.py @@ -0,0 +1,10 @@ +from django.urls import include, path + +from .v1 import urls as v1_urls + +app_name = 'api' + + +urlpatterns = [ + path('v1/', include(v1_urls)), +] diff --git a/opentech/apply/api/v1/__init__.py b/opentech/apply/api/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/api/pagination.py b/opentech/apply/api/v1/pagination.py similarity index 100% rename from opentech/api/pagination.py rename to opentech/apply/api/v1/pagination.py diff --git a/opentech/apply/api/v1/permissions.py b/opentech/apply/api/v1/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..c8769166655a2898844116ba0bbc724296e10578 --- /dev/null +++ b/opentech/apply/api/v1/permissions.py @@ -0,0 +1,18 @@ +from rest_framework import permissions + + +class IsAuthor(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.user == request.user + + +class IsApplyStaffUser(permissions.BasePermission): + """ + Custom permission to only allow organisation Staff or higher + """ + + def has_permission(self, request, view): + return request.user.is_apply_staff + + def has_object_permission(self, request, view, obj): + return request.user.is_apply_staff diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/api/v1/serializers.py similarity index 95% rename from opentech/apply/funds/serializers.py rename to opentech/apply/api/v1/serializers.py index cd828247a3e32144b6a4d6066e2f76d7de0b8b80..87886264cba6e9a2baf82ad024aa77cb4317c65e 100644 --- a/opentech/apply/funds/serializers.py +++ b/opentech/apply/api/v1/serializers.py @@ -7,10 +7,10 @@ from rest_framework import serializers from opentech.apply.activity.models import Activity from opentech.apply.determinations.views import DeterminationCreateOrUpdateView +from opentech.apply.funds.models import ApplicationSubmission, RoundsAndLabs from opentech.apply.review.models import Review, ReviewOpinion from opentech.apply.review.options import RECOMMENDATION_CHOICES from opentech.apply.users.groups import PARTNER_GROUP_NAME, STAFF_GROUP_NAME -from .models import ApplicationSubmission, RoundsAndLabs User = get_user_model() @@ -112,7 +112,7 @@ class TimestampField(serializers.Field): class SubmissionListSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='funds:api:submissions:detail') + url = serializers.HyperlinkedIdentityField(view_name='api:v1:submissions:detail') round = serializers.SerializerMethodField() last_update = TimestampField() @@ -201,7 +201,7 @@ class RoundLabSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer): user = serializers.StringRelatedField() message = serializers.SerializerMethodField() - edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit') + edit_url = serializers.HyperlinkedIdentityField(view_name='api:v1:comments:edit') editable = serializers.SerializerMethodField() timestamp = TimestampField(read_only=True) edited = TimestampField(read_only=True) @@ -219,7 +219,7 @@ class CommentSerializer(serializers.ModelSerializer): class CommentCreateSerializer(serializers.ModelSerializer): user = serializers.StringRelatedField() - edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit') + edit_url = serializers.HyperlinkedIdentityField(view_name='api:v1:comments:edit') editable = serializers.SerializerMethodField() timestamp = TimestampField(read_only=True) edited = TimestampField(read_only=True) diff --git a/opentech/apply/api/v1/tests/__init__.py b/opentech/apply/api/v1/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/api/v1/tests/test_serializers.py b/opentech/apply/api/v1/tests/test_serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..0f5ea6c76ff9eb09c4c6778029ae144de119aa5c --- /dev/null +++ b/opentech/apply/api/v1/tests/test_serializers.py @@ -0,0 +1,28 @@ +from django.test import override_settings, TestCase + +from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory +from opentech.apply.review.tests.factories import ReviewFactory + +from ..serializers import ReviewSummarySerializer + + +@override_settings(ROOT_URLCONF='opentech.apply.urls') +class TestReviewSummarySerializer(TestCase): + def test_handles_no_reviews(self): + submission = ApplicationSubmissionFactory() + data = ReviewSummarySerializer(submission).data + self.assertEqual(data['count'], 0) + self.assertEqual(data['score'], None) + self.assertEqual(data['recommendation'], {'value': -1, 'display': None}) + self.assertEqual(data['assigned'], []) + self.assertEqual(data['reviews'], []) + + def test_handles_negative_reviews(self): + submission = ApplicationSubmissionFactory() + ReviewFactory(submission=submission) + data = ReviewSummarySerializer(submission).data + self.assertEqual(data['count'], 1) + self.assertEqual(data['score'], 0) + self.assertEqual(data['recommendation'], {'value': 0, 'display': 'No'}) + self.assertEqual(len(data['assigned']), 1) + self.assertEqual(len(data['reviews']), 1) diff --git a/opentech/apply/funds/tests/test_api_views.py b/opentech/apply/api/v1/tests/test_views.py similarity index 94% rename from opentech/apply/funds/tests/test_api_views.py rename to opentech/apply/api/v1/tests/test_views.py index 10083d15fcff039ab2481dd6f427513e56016aeb..38858d56a1871638c5fca7a0b80ad2ee3046b981 100644 --- a/opentech/apply/funds/tests/test_api_views.py +++ b/opentech/apply/api/v1/tests/test_views.py @@ -11,7 +11,7 @@ from opentech.apply.users.tests.factories import UserFactory class TestCommentEdit(TestCase): def post_to_edit(self, comment_pk, message='my message'): return self.client.post( - reverse_lazy('funds:api:comments:edit', kwargs={'pk': comment_pk}), + reverse_lazy('api:v1:comments:edit', kwargs={'pk': comment_pk}), secure=True, data={'message': message}, ) @@ -59,7 +59,7 @@ class TestCommentEdit(TestCase): self.client.force_login(user) response = self.client.post( - reverse_lazy('funds:api:comments:edit', kwargs={'pk': comment.pk}), + reverse_lazy('api:v1:comments:edit', kwargs={'pk': comment.pk}), secure=True, data={ 'message': 'the new message', diff --git a/opentech/apply/api/v1/urls.py b/opentech/apply/api/v1/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..df7f62c6de314c09b97f47534f16ad12ea6e9768 --- /dev/null +++ b/opentech/apply/api/v1/urls.py @@ -0,0 +1,31 @@ +from django.urls import include, path + +from .views import ( + CommentEdit, + CommentList, + CommentListCreate, + RoundLabDetail, + RoundLabList, + SubmissionAction, + SubmissionList, + SubmissionDetail, +) + +app_name = 'v1' + +urlpatterns = [ + path('submissions/', include(([ + path('', SubmissionList.as_view(), name='list'), + path('<int:pk>/', SubmissionDetail.as_view(), name='detail'), + path('<int:pk>/actions/', SubmissionAction.as_view(), name='actions'), + path('<int:pk>/comments/', CommentListCreate.as_view(), name='comments'), + ], 'submissions'))), + path('rounds/', include(([ + path('', RoundLabList.as_view(), name='list'), + path('<int:pk>/', RoundLabDetail.as_view(), name='detail'), + ], 'rounds'))), + path('comments/', include(([ + path('', CommentList.as_view(), name='list'), + path('<int:pk>/edit/', CommentEdit.as_view(), name='edit'), + ], 'comments'))) +] diff --git a/opentech/apply/funds/api_views.py b/opentech/apply/api/v1/views.py similarity index 97% rename from opentech/apply/funds/api_views.py rename to opentech/apply/api/v1/views.py index 75d31ef1c336ecf530bb79fc760384b152b22ec1..1953515fdbdb6b1e9c1cdac9421d52dc310be9cf 100644 --- a/opentech/apply/funds/api_views.py +++ b/opentech/apply/api/v1/views.py @@ -12,14 +12,17 @@ from rest_framework.exceptions import (NotFound, PermissionDenied, from rest_framework_api_key.permissions import HasAPIKey from django_filters import rest_framework as filters -from opentech.api.pagination import StandardResultsSetPagination +from opentech.apply.funds.models import ApplicationSubmission, RoundsAndLabs +from opentech.apply.funds.workflow import PHASES + from opentech.apply.activity.models import Activity, COMMENT from opentech.apply.activity.messaging import messenger, MESSAGES from opentech.apply.determinations.views import DeterminationCreateOrUpdateView from opentech.apply.review.models import Review from opentech.apply.funds.models import FundType, LabType -from .models import ApplicationSubmission, RoundsAndLabs +from .pagination import StandardResultsSetPagination +from .permissions import IsApplyStaffUser, IsAuthor from .serializers import ( CommentSerializer, CommentCreateSerializer, @@ -30,8 +33,6 @@ from .serializers import ( SubmissionListSerializer, SubmissionDetailSerializer, ) -from .permissions import IsApplyStaffUser, IsAuthor -from .workflow import PHASES class RoundLabFilter(filters.ModelChoiceFilter): diff --git a/opentech/apply/funds/permissions.py b/opentech/apply/funds/permissions.py index 3a623b13adac2e9fe126ec88a3ef869aabb900dc..816e1cf0b13c236f9d2f070f4cee7eaf4027cb4b 100644 --- a/opentech/apply/funds/permissions.py +++ b/opentech/apply/funds/permissions.py @@ -1,23 +1,3 @@ -from rest_framework import permissions - - -class IsAuthor(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return obj.user == request.user - - -class IsApplyStaffUser(permissions.BasePermission): - """ - Custom permission to only allow organisation Staff or higher - """ - - def has_permission(self, request, view): - return request.user.is_apply_staff - - def has_object_permission(self, request, view, obj): - return request.user.is_apply_staff - - def is_user_has_access_to_view_submission(user, submission): has_access = False diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py index 82e31476aa4bf6312cac9d9bac97da4ca8f6cd57..677f98cfd5ea2ca74e1fab2aa5741b30be33af0d 100644 --- a/opentech/apply/funds/urls.py +++ b/opentech/apply/funds/urls.py @@ -20,16 +20,6 @@ from .views import ( SubmissionUserFlaggedView, SubmissionStaffFlaggedView, ) -from .api_views import ( - CommentEdit, - CommentList, - CommentListCreate, - RoundLabDetail, - RoundLabList, - SubmissionAction, - SubmissionList, - SubmissionDetail, -) revision_urls = ([ @@ -68,23 +58,6 @@ submission_urls = ([ path('<slug:status>/', SubmissionsByStatus.as_view(), name='status'), ], 'submissions') -api_urls = ([ - path('submissions/', include(([ - path('', SubmissionList.as_view(), name='list'), - path('<int:pk>/', SubmissionDetail.as_view(), name='detail'), - path('<int:pk>/actions/', SubmissionAction.as_view(), name='actions'), - path('<int:pk>/comments/', CommentListCreate.as_view(), name='comments'), - ], 'submissions'))), - path('rounds/', include(([ - path('', RoundLabList.as_view(), name='list'), - path('<int:pk>/', RoundLabDetail.as_view(), name='detail'), - ], 'rounds'))), - path('comments/', include(([ - path('', CommentList.as_view(), name='list'), - path('<int:pk>/edit/', CommentEdit.as_view(), name='edit'), - ], 'comments'))) -], 'api') - rounds_urls = ([ path('', RoundListView.as_view(), name="list"), path('<int:pk>/', SubmissionsByRound.as_view(), name="detail"), @@ -95,5 +68,4 @@ urlpatterns = [ path('submissions/', include(submission_urls)), path('rounds/', include(rounds_urls)), path('projects/', include(projects_urls)), - path('api/', include(api_urls)), ] diff --git a/opentech/apply/urls.py b/opentech/apply/urls.py index 6138117fab04d25e24a59884ba270f81a3b7f2ec..9b1c25c11b2acbc2718da9fff12caa1c3e0ab3f4 100644 --- a/opentech/apply/urls.py +++ b/opentech/apply/urls.py @@ -6,6 +6,7 @@ from two_factor.urls import urlpatterns as tf_urls from .utils import views from .users import urls as users_urls from .dashboard import urls as dashboard_urls +from .api import urls as api_urls from opentech.urls import base_urlpatterns @@ -14,6 +15,7 @@ urlpatterns = [ path('apply/', include('opentech.apply.funds.urls', 'apply')), path('activity/', include('opentech.apply.activity.urls', 'activity')), path('', include(users_urls)), + path('api/', include(api_urls)), path('dashboard/', include(dashboard_urls)), path('hijack/', include('hijack.urls', 'hijack')), path('', include(tf_urls, 'two_factor')), diff --git a/opentech/static_src/src/app/src/api/notes.js b/opentech/static_src/src/app/src/api/notes.js index 7dd077494a5c963a905e0663fe5dae565215d041..1035f1e3f6c80207d4b7813123eb0a10361b93dc 100644 --- a/opentech/static_src/src/app/src/api/notes.js +++ b/opentech/static_src/src/app/src/api/notes.js @@ -1,6 +1,6 @@ export function fetchNotesForSubmission(submissionID, visibility = 'team') { return { - path: `/apply/api/submissions/${submissionID}/comments/`, + path: `/v1/submissions/${submissionID}/comments/`, params: { visibility, page_size: 1000, @@ -11,7 +11,7 @@ export function fetchNotesForSubmission(submissionID, visibility = 'team') { export function fetchNewNotesForSubmission(submissionID, latestID, visibility = 'team') { return { - path: `/apply/api/submissions/${submissionID}/comments/`, + path: `/v1/submissions/${submissionID}/comments/`, params: { visibility, newer: latestID, @@ -23,7 +23,7 @@ export function fetchNewNotesForSubmission(submissionID, latestID, visibility = export function createNoteForSubmission(submissionID, note) { return { - path: `/apply/api/submissions/${submissionID}/comments/`, + path: `/v1/submissions/${submissionID}/comments/`, method: 'POST', options: { body: note, @@ -33,7 +33,7 @@ export function createNoteForSubmission(submissionID, note) { export function editNoteForSubmission(note) { return { - path: `/apply/api/comments/${note.id}/edit/`, + path: `/v1/comments/${note.id}/edit/`, method: 'POST', options: { body: JSON.stringify({ message: note.message }), diff --git a/opentech/static_src/src/app/src/api/rounds.js b/opentech/static_src/src/app/src/api/rounds.js index c7ef0a75be9a4e641419fa1e2227d6fe8e55464b..c0520203ac1884994ffa09206a9f2109d2c0c4c7 100644 --- a/opentech/static_src/src/app/src/api/rounds.js +++ b/opentech/static_src/src/app/src/api/rounds.js @@ -1,12 +1,12 @@ export function fetchRound(id) { return { - path:`/apply/api/rounds/${id}/`, + path:`/v1/rounds/${id}/`, }; } export function fetchRounds() { return { - path:`/apply/api/rounds/`, + path:`/v1/rounds/`, params: { page_size: 1000, }, diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js index 88fdd2758424b915fee9b5c76d38607574030fef..efbf81653c9673092715bb7fc9cd9f4773069f50 100644 --- a/opentech/static_src/src/app/src/api/submissions.js +++ b/opentech/static_src/src/app/src/api/submissions.js @@ -1,6 +1,6 @@ export function fetchSubmissionsByRound(id) { return { - path:'/apply/api/submissions/', + path:'/v1/submissions/', params: { round: id, page_size: 1000, @@ -10,7 +10,7 @@ export function fetchSubmissionsByRound(id) { export function fetchSubmission(id) { return { - path: `/apply/api/submissions/${id}/`, + path: `/v1/submissions/${id}/`, }; } @@ -20,14 +20,14 @@ export function fetchSubmissionsByStatuses(statuses) { statuses.forEach(v => params.append('status', v)); return { - path:'/apply/api/submissions/', + path:'/v1/submissions/', params, }; } export function executeSubmissionAction(submissionID, action) { return { - path: `/apply/api/submissions/${submissionID}/actions/`, + path: `/v1/submissions/${submissionID}/actions/`, method: 'POST', options: { body: { diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js index 29e4a3ef537aaa4054ba4738a0eb1fb989c288a8..703bf2d5b7f31f2fa8a54178f88165fe2bcefdd9 100644 --- a/opentech/static_src/src/app/src/api/utils.js +++ b/opentech/static_src/src/app/src/api/utils.js @@ -6,7 +6,7 @@ const getBaseUrl = () => { export function apiFetch({path, method = 'GET', params = new URLSearchParams, options = {}}) { const url = new URL(getBaseUrl()); - url.pathname = path; + url.pathname = url.pathname + path; for (const [paramKey, paramValue] of getIteratorForParams(params)) { url.searchParams.append(paramKey, paramValue); diff --git a/opentech/static_src/src/app/webpack.dev.config.js b/opentech/static_src/src/app/webpack.dev.config.js index f86535ada9080507d6fed9941b029213df88656b..3aac5c995417b371daa9ed33c41ba549060f128c 100644 --- a/opentech/static_src/src/app/webpack.dev.config.js +++ b/opentech/static_src/src/app/webpack.dev.config.js @@ -15,7 +15,7 @@ devConfig.plugins = devConfig.plugins.concat([ new webpack.NoEmitOnErrorsPlugin(), new BundleTracker({filename: './opentech/static_compiled/app/webpack-stats.json'}), new webpack.EnvironmentPlugin({ - API_BASE_URL: 'http://apply.localhost:8000/', + API_BASE_URL: 'http://apply.localhost:8000/api', }), ])