diff --git a/README.md b/README.md index c7546df..26b7c70 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ This README aims to document functionality of backend as well as required steps - [First Steps](#first-steps) - [Load location data](#load-location-data) - [Load taxonomy data](#load-taxonomy-data) -- [Endpoints](#endpoints) +- [Company Endpoints](#company-endpoints) +- [Product Endpoints](#product-endpoints) +- [Core Endpoints](#core-endpoints) +- [History Endpoints](#history-endpoints) +- [Stats Endpoints](#stats-endpoints) - [Shop Integrations](#shop-integrations) - [WooCommerce](#woocommerce) - [Product Search](#product-search) @@ -57,25 +61,158 @@ This data serves as initial Tags To load initial set of tags: `python manage.py addtaxonomy` -## Endpoints +## Company Endpoints + +### CompanyViewSet + +Queryset: validated Company instances only + +Permissions: + +- anon user: safe methods +- auth user: full access where user is company creator + +### MyCompanyViewSet + +Queryset: Company instances where user is creator + +Permissions: + +- anon user: no access +- auth user: full access + +### AdminCompanyViewSet + +Queryset: all Company instances, validated or not + +Permissions: only accesible to authenticated users with role `SITE_ADMIN` -## Pagination +### random_company_sample -By default a `LimitOffsetPagination` pagination is enabled +Method view that returns a randome sample of companies -Examples: `http://127.0.0.1:8000/api/v1/products/?limit=10&offset=0` +By default it returns 6 instances, but can be customized through parameter `size` -The response data has the following keys: -``` -dict_keys(['count', 'next', 'previous', 'results']) -``` + +## Product Endpoints + +### ProductViewSet + +Endpoint url: `/api/v1/products/` + +Queryset: active Product instances only + +Permissions: + +- anon user: safe methods +- auth user: full access where user is product creator + +### MyProductsViewSet + +Endpoint url: `/api/v1/my_products/` + +Queryset: Product instances where user is creator + +Permissions: + +- anon user: no access +- auth user: full access + + +### AdminProductsViewSet + +Endpoint url: `/api/v1/admin_products/` + +Queryset: all Product instances, acgtive or not + +Permissions: only accesible to authenticated users with role `SITE_ADMIN` + +### load_coop_products [POST] + +Endpoint url: `/api/v1/load_products/` + +Method view that reads a CSV file. + +### product_search [GET] + +Endpoint url: `/api/v1/search_products/` + +Allows searching of Products to all users + +Parameters: + +- q: used for search [MANDATORY] +- limit: max number of returned instances [OPTIONAL] +- offset: where to start counting results [OPTIONAL] +- shipping_cost: true/false +- discount: true/false +- category: string +- tags: string +- order: string (newest/oldest) +- price_min: int +- price_max: int + + +### purchase_email [POST] + +Endpoint url: `/api/v1/purchase_email/` + +Sends email to company manager about the product that the user wants to purchase, and sends confirmation email to user. + +Parameters: + +- email: mandatory for anonymous users +- telephone +- company +- product +- comment + +## Core Endpoints + +### CustomUserViewSet + +Endpoint url: `/api/v1/users/` + +Queryset: all CustomUser instances + +Permissions: + +- anon user: only POST to register new user +- auth user: no access +- admin user: full access + +### ChangeUserPasswordView + +Ednpoint url: `/api/v1/user/change_password//` + +Permissions: only accessible for your own user instance + + +### UpdateUserView + +Endpoint url: `/api/v1/user/update/` + +Permissions: only accessible for your own user instance + + +### my_user [GET] + +Endpoint url: `/api/v1/my_user/` + +Returns instance of authenticated user + +### load_coop_managers [POST] + +Ednpoint url: `/api/v1/load_coops/` + +For each row it creates a Company instance, and a user instance linked to the company, with role `COOP_MANAGER` ### User Management Creation: -- endpoint: /api/v1/users/ +- endpoint: `/api/v1/users/` - method: GET - payload: ```json @@ -149,32 +286,21 @@ To create user: ``` -### Companies - -Endpoint url: `/api/v1/companies/` - -To get company linked to authenticated user: `/api/v1/my_company/` - -### Products - -Endpoint url: `/api/v1/products/` - -To get products linked to authenticated user: `/api/v1/my_products/` - -### History +## History Endpoints Endpoint url: `/api/v1/history/`: Historical records about product importation -### Stats +## Stats Endpoints + Endpoint url: `/api/v1/stats/` logs about user interaction with products links -### Locations +## Location Endpoints Location ednpoints: diff --git a/products/tests.py b/products/tests.py index 622eb3f..bbf7cec 100644 --- a/products/tests.py +++ b/products/tests.py @@ -1210,12 +1210,13 @@ class PurchaseEmailTest(APITestCase): self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) self.user = CustomUserFactory(email=self.email, is_active=True) self.user.set_password(self.password) - # self.user.role = 'SITE_ADMIN' self.user.save() def test_anon_user_can_use(self): - company = CompanyFactory() + self.user.role = 'COOP_MANAGER' + self.user.company = company + self.user.save() product = ProductFactory(company=company) data = { @@ -1225,8 +1226,29 @@ class PurchaseEmailTest(APITestCase): 'product': product.id, 'comment': '', } - response = self.client.post(self.endpoint, json=data) - import ipdb; ipdb.set_trace() + response = self.client.post(self.endpoint, data=data, format='json') + # assertions + self.assertEquals(response.status_code, 200) + self.assertEquals(2, len(mail.outbox)) + + def test_auth_user_can_use(self): + company = CompanyFactory() + self.user.role = 'COOP_MANAGER' + self.user.company = company + self.user.save() + product = ProductFactory(company=company) + + data = { + 'telephone': '123123123', + 'company': company.id, + 'product': product.id, + 'comment': '', + } + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + response = self.client.post(self.endpoint, data=data, format='json') # assertions self.assertEquals(response.status_code, 200) self.assertEquals(2, len(mail.outbox)) diff --git a/products/views.py b/products/views.py index 8cf0fb3..091990d 100644 --- a/products/views.py +++ b/products/views.py @@ -6,6 +6,8 @@ import json from django.db.models import Q from django.core import serializers from django.contrib.auth import get_user_model +from django.template.loader import render_to_string +from django.core.mail import EmailMessage # Create your views here. from rest_framework import status @@ -24,6 +26,7 @@ from dal import autocomplete from products.models import Product, CategoryTag from products.serializers import ProductSerializer, TagFilterSerializer, SearchResultSerializer from companies.models import Company +from stats.models import StatsLog from back_latienda.permissions import IsCreator, IsSiteAdmin, ReadOnly from .utils import extract_search_filters, find_related_products_v6, product_loader, find_related_products_v7 from utils.tag_serializers import TaggitSerializer @@ -247,19 +250,21 @@ class CategoryTagAutocomplete(autocomplete.Select2QuerySetView): return qs # [x.label for x in qs] -@permission_classes([AllowAny,]) @api_view(['POST']) +@permission_classes([AllowAny,]) def purchase_email(request): """Notify coop manager and user about item purchase """ - data = json.loads(request.body) # check data + if request.user.is_anonymous and 'email' not in data: + return Response({"error": "Anonymous users must include an email parameter value"}, status=status.HTTP_406_NOT_ACCEPTABLE) + try: - for param in ('email', 'telephone', 'company', 'product', 'comment'): + for param in ('telephone', 'company', 'product', 'comment'): assert(param in data.keys()) except: - return Response({"error": "Required parameters for anonymous user: email, telephone"}, status=status.HTTP_406_NOT_ACCEPTABLE) + return Response({"error": "Required parameters for anonymous user: telephone, company, product, comment"}, status=status.HTTP_406_NOT_ACCEPTABLE) if request.user.is_anonymous: email = data.get('email') @@ -273,7 +278,7 @@ def purchase_email(request): return Response({"error": "Invalid value for company"}, status=status.HTTP_406_NOT_ACCEPTABLE) # get company manager manager = User.objects.filter(company=company).first() - if not manager and manager.role != 'COOP_MANAGER': + if not manager or manager.role != 'COOP_MANAGER': return Response({"error": "Company has no managing user"}, status=status.HTTP_406_NOT_ACCEPTABLE) # get product product = Product.objects.filter(id=data['product'], company=company).first() @@ -297,19 +302,17 @@ def purchase_email(request): 'product': product, }) subject = 'Confirmación de contacto con vendedor' - email = EmailMessage(subject, message, to=[request.user.email]) + email = EmailMessage(subject, user_message, to=[email]) email.send() - logging.info(f"Purchase Contact confirmation email sent to {request.user.email}") + logging.info(f"Purchase Contact confirmation email sent to {email}") # create statslog instance to register interaction stats_data = { - 'action_object': instance, - 'user': None, - 'anonymous': True, - 'ip_address': client_ip, - 'geo': g.geos(client_ip), + 'action_object': product, + 'user': request.user if request.user.is_authenticated else None, + 'anonymous': request.user.is_anonymous, 'contact': True, - 'shop': instance.shop, + 'shop': company.shop, } StatsLog.objects.create(**stats_data)