diff --git a/backend/apps/volontulo/tests/views/api/offers/test_join.py b/backend/apps/volontulo/tests/views/api/offers/test_join.py new file mode 100644 index 00000000..e0b4443f --- /dev/null +++ b/backend/apps/volontulo/tests/views/api/offers/test_join.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +""" +.. module:: test_join +""" + +from rest_framework import status +from rest_framework.test import APITestCase + +from apps.volontulo.tests.views.offers.commons import TestOffersCommons + + +class TestAuthenticatedUserJoinOffer(TestOffersCommons, APITestCase): + + """Tests for REST API's join offer view for authenticated user.""" + + def setUp(self): + """Set up each test.""" + super(TestAuthenticatedUserJoinOffer, self).setUp() + self.client.login(username='admin@example.com', password='123admin') + + def test_user_join_offer_authenticated(self): + """Test offer's created status for admin user.""" + response = self.client.post('/api/offers/1/join/') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_user_join_not_existing_offer(self): + """Test offer's 404 status for non existing offer.""" + response = self.client.post('/api/offers/1999999999999/join/') + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +class TestNotAuthenticatedUserJoinOffer(TestOffersCommons, APITestCase): + + """Tests for REST API's join offer view for not authenitcated user""" + + def test_user_join_offer_not_authenticated(self): + """Test offer's forbidden status for not logged in user.""" + response = self.client.post('/api/offers/1/join/') + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/apps/volontulo/views/api.py b/backend/apps/volontulo/views/api.py index f8773828..368afb56 100644 --- a/backend/apps/volontulo/views/api.py +++ b/backend/apps/volontulo/views/api.py @@ -225,6 +225,18 @@ def get_queryset(self): ) return qs.filter(offer_status='published') + @detail_route(methods=['POST'], permission_classes=(IsAuthenticated,)) + # pylint: disable=invalid-name + def join(self, request, pk): + """Endpoint to join offer by current user""" + offer = get_object_or_404(self.get_queryset(), id=pk) + offer.volunteers.add(request.user) + + return Response(self.serializer_class( + offer, + context={'request': request} + ).data, status=201) + class OrganizationViewSet(viewsets.ModelViewSet): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8e06c608..8f99ab0b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5035,7 +5035,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "dev": true, "requires": { "brace-expansion": "1.1.8" diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index e498b652..5cd4d50c 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -54,6 +54,7 @@ import { ContactResolver } from './resolvers'; import { FormErrorComponent } from './form-error/form-error.component'; import { ContactService } from './contact.service'; import { UserProfileComponent } from './user-profile/user-profile.component'; +import { OfferJoinFormComponent } from './offers/offer-join-form/offer-join-form.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; Raven.config(environment.sentryDSN).install(); @@ -166,6 +167,11 @@ const appRoutes: Routes = [ component: UserProfileComponent, canActivate: [LoggedInGuard], }, + { + path: 'offers/:offerSlug/:offerId/join', + component: OfferJoinFormComponent, + canActivate: [LoggedInGuard], + }, { path: '**', component: RedirectComponent @@ -212,7 +218,8 @@ registerLocaleData(localePl); ContactComponent, FormErrorComponent, OrganizationsListComponent, - UserProfileComponent + UserProfileComponent, + OfferJoinFormComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'volontulo' }), diff --git a/frontend/src/app/homepage-offer/offers.service.ts b/frontend/src/app/homepage-offer/offers.service.ts index 5a506a6d..37b9bc04 100644 --- a/frontend/src/app/homepage-offer/offers.service.ts +++ b/frontend/src/app/homepage-offer/offers.service.ts @@ -35,4 +35,8 @@ export class OffersService { return this.http.put(`${environment.apiRoot}/offers/${id}/`, offer); } + joinOffer(id: number, message: string) { + return this.http.post(`${environment.apiRoot}/offers/${id}/join/`, {message}, {observe: 'response'}); + } + } diff --git a/frontend/src/app/offers/offer-detail/offer-detail.component.html b/frontend/src/app/offers/offer-detail/offer-detail.component.html index 51e2d40c..348d424b 100644 --- a/frontend/src/app/offers/offer-detail/offer-detail.component.html +++ b/frontend/src/app/offers/offer-detail/offer-detail.component.html @@ -68,7 +68,7 @@