From 44294223eda39a39dda9e397ee18e6c3255c29c6 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 10:50:39 +0000 Subject: [PATCH 01/67] blank model fields --- companies/models.py | 2 +- products/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/companies/models.py b/companies/models.py index 84baff0..70c46ef 100644 --- a/companies/models.py +++ b/companies/models.py @@ -52,7 +52,7 @@ class Company(models.Model): created = models.DateTimeField('date of creation', auto_now_add=True) updated = models.DateTimeField('date last update', auto_now=True) creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, blank=True, related_name='creator') - # history = models.ForeignKey(HistorySync, null=True, on_delete=models.DO_NOTHING, related_name='company') + history = models.ForeignKey(HistorySync, null=True, blank=True, on_delete=models.DO_NOTHING, related_name='company') class Meta: verbose_name = "Compañía" diff --git a/products/models.py b/products/models.py index 50cde7f..d3b4a16 100644 --- a/products/models.py +++ b/products/models.py @@ -69,8 +69,8 @@ class Product(models.Model): # internal created = models.DateTimeField('date of creation', auto_now_add=True) updated = models.DateTimeField('date last update', auto_now=True) - creator = models.ForeignKey('core.CustomUser', on_delete=models.SET_NULL, null=True, related_name='product') - history = models.ForeignKey('history.HistorySync', on_delete=models.SET_NULL, null=True, related_name='product') + creator = models.ForeignKey('core.CustomUser', on_delete=models.SET_NULL, null=True, blank=True, related_name='product') + history = models.ForeignKey('history.HistorySync', on_delete=models.SET_NULL, null=True, blank=True, related_name='product') def __str__(self): return f"{self.name} / {self.sku}" From c25d23548d434bafec9fa87701990f13457ba71a Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 11:10:03 +0000 Subject: [PATCH 02/67] mods to django admin --- companies/models.py | 4 ++-- products/admin.py | 13 +++++++------ products/models.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/companies/models.py b/companies/models.py index 70c46ef..6c1ae98 100644 --- a/companies/models.py +++ b/companies/models.py @@ -24,7 +24,7 @@ class Company(models.Model): cif = models.CharField('CIF', max_length=15, null=True, blank=True) company_name = models.CharField('Razón Social Cooperativa', max_length=1000, null=True, blank=True) - short_name = models.CharField('Apócope', max_length=100, null=True, blank=True) # autogenerar si blank + short_name = models.CharField('Nombre', max_length=100, null=True, blank=True) # autogenerar si blank web_link = models.URLField('Enlace a la web', null=True, blank=True) shop = models.BooleanField('Tienda Online', null=True, default=False) shop_link = models.URLField('Enlace a la tienda', null=True, blank=True) @@ -52,7 +52,7 @@ class Company(models.Model): created = models.DateTimeField('date of creation', auto_now_add=True) updated = models.DateTimeField('date last update', auto_now=True) creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, blank=True, related_name='creator') - history = models.ForeignKey(HistorySync, null=True, blank=True, on_delete=models.DO_NOTHING, related_name='company') + history = models.ForeignKey(HistorySync, null=True, blank=True, on_delete=models.DO_NOTHING, related_name='comp_hist') class Meta: verbose_name = "Compañía" diff --git a/products/admin.py b/products/admin.py index 94cc68e..94d8743 100644 --- a/products/admin.py +++ b/products/admin.py @@ -5,18 +5,19 @@ from . import forms # Register your models here. + +def model_admin_callable(co): + return co.company_name + class ProductAdmin(admin.ModelAdmin): form = forms.ProductTagForm - list_display = ('name', 'category', 'sourcing_date', 'company') + list_display = ('name', 'category', 'sourcing_date', 'company_name', 'active' ) list_filter = ('company', 'tags', 'category', 'attributes') search_fields = ('name', 'sku', 'description') + def company_name(self, product): + return product.company.company_name -''' -class ProductInline(admin.TabularInline): - model = models.Product - form = forms.ProductTagForm -''' admin.site.register(models.Product, ProductAdmin) admin.site.register(models.TreeTag) diff --git a/products/models.py b/products/models.py index d3b4a16..7ee89fd 100644 --- a/products/models.py +++ b/products/models.py @@ -64,7 +64,7 @@ class Product(models.Model): category = SingleTagField(to=CategoryTag, null=True, blank=True, on_delete=models.SET_NULL) # main tag category attributes = TagField(to=AttributeTag, blank=True, related_name='product_attributes') identifiers = models.TextField('Identificador único de producto', null=True, blank=True) - active = models.BooleanField('Mostrar producto', default=False) + active = models.BooleanField('Activo', default=False) # internal created = models.DateTimeField('date of creation', auto_now_add=True) From cd6f8e558e9d1368b1414fdda7d49d770469e3e2 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 11:31:51 +0000 Subject: [PATCH 03/67] modified history admin config --- history/admin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/history/admin.py b/history/admin.py index a60a2d3..87e7f48 100644 --- a/history/admin.py +++ b/history/admin.py @@ -4,4 +4,12 @@ from . import models # Register your models here. -admin.site.register(models.HistorySync) +class HistoryAdmin(admin.ModelAdmin): + list_display = ('company_name', 'rss_url', 'sync_date', 'result', 'quantity',) + list_filter = ('company__company_name',) + + def company_name(self, instance): + return instance.company.company_name + + +admin.site.register(models.HistorySync, HistoryAdmin) From ebd1b6744c025d33c35a13e77c3f0f5095a50b25 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 11:50:43 +0000 Subject: [PATCH 04/67] product search only for active products --- products/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/views.py b/products/views.py index 989c7ae..0febf05 100644 --- a/products/views.py +++ b/products/views.py @@ -169,7 +169,7 @@ def product_search(request): if q == '': # filter entire queryset - products_qs = Product.objects.all() + products_qs = Product.objects.filter(active=True) if tags: products_qs = Product.objects.filter(tags=tags) if category: From ec27937b85d12905c4114561d426976098f14fe8 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 12:24:44 +0000 Subject: [PATCH 05/67] switched my_company and my_products from method to class views --- back_latienda/permissions.py | 13 +++++++++++++ back_latienda/routers.py | 6 ++++-- back_latienda/urls.py | 4 ++-- companies/tests.py | 5 ++++- companies/views.py | 29 ++++++++++++----------------- products/tests.py | 8 +++----- products/views.py | 31 ++++++++++--------------------- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/back_latienda/permissions.py b/back_latienda/permissions.py index 992f43e..9bb10d0 100644 --- a/back_latienda/permissions.py +++ b/back_latienda/permissions.py @@ -35,6 +35,19 @@ class IsStaff(permissions.BasePermission): return request.user.is_staff +class IsSiteAdmin(permissions.BasePermission): + """ + Grant permission if request.user.role == 'SITE_ADMIN' + """ + + admin_role = 'SITE_ADMIN' + + def has_object_permission(self, request, view, obj): + return request.user.role == self.admin_role + + def has_permission(self, request, view): + return request.user.role == self.admin_role + class ReadOnly(permissions.BasePermission): def has_permission(self, request, view): diff --git a/back_latienda/routers.py b/back_latienda/routers.py index f0c9c26..b734217 100644 --- a/back_latienda/routers.py +++ b/back_latienda/routers.py @@ -1,8 +1,8 @@ from rest_framework import routers from core.views import CustomUserViewSet -from companies.views import CompanyViewSet -from products.views import ProductViewSet +from companies.views import CompanyViewSet, MyCompanyViewSet +from products.views import ProductViewSet, MyProductsViewSet from history.views import HistorySyncViewSet from stats.views import StatsLogViewSet @@ -13,7 +13,9 @@ router = routers.DefaultRouter() router.register('users', CustomUserViewSet, basename='users') router.register('companies', CompanyViewSet, basename='company') +router.register('my_company', MyCompanyViewSet, basename='my-company') router.register('products', ProductViewSet, basename='product') +router.register('my_products', MyProductsViewSet, basename='my-products') router.register('history', HistorySyncViewSet, basename='history') router.register('stats', StatsLogViewSet, basename='stats') diff --git a/back_latienda/urls.py b/back_latienda/urls.py index b60dd0c..99f4276 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -39,9 +39,9 @@ urlpatterns = [ path('api/v1/search_products/', product_views.product_search, name='product-search'), path('api/v1/create_company_user/', core_views.create_company_user, name='create-company-user'), path('api/v1/my_user/', core_views.my_user, name='my-user'), - path('api/v1/my_company/', company_views.my_company , name='my-company'), + # path('api/v1/my_company/', company_views.my_company , name='my-company'), path('api/v1/companies/sample/', company_views.random_company_sample , name='company-sample'), - path('api/v1/my_products/', product_views.my_products, name='my-products'), + # path('api/v1/my_products/', product_views.my_products, name='my-products'), path('api/v1/stats/me/', stat_views.track_user, name='user-tracker'), path('api/v1/autocomplete/category-tag/', product_views.CategoryTagAutocomplete.as_view(), name='category-autocomplete'), path('api/v1/', include(router.urls)), diff --git a/companies/tests.py b/companies/tests.py index 07189da..167cfb1 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -311,6 +311,9 @@ class MyCompanyViewTest(APITestCase): self.user.set_password(self.password) self.user.save() + def tearDown(self): + self.model.objects.all().delete() + def test_auth_user_gets_data(self): # create instance user_instances = [self.factory(creator=self.user) for i in range(5)] @@ -346,7 +349,7 @@ class MyCompanyViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) # assert only 2 instances in response payload = response.json() - self.assertEquals(2, len(payload)) + self.assertEquals(2, len(payload['results'])) def test_anon_user_cannot_access(self): # send in request diff --git a/companies/views.py b/companies/views.py index c92ab46..eb1cc75 100644 --- a/companies/views.py +++ b/companies/views.py @@ -23,6 +23,8 @@ from back_latienda.permissions import IsCreator from utils import woocommerce + + class CompanyViewSet(viewsets.ModelViewSet): queryset = Company.objects.filter(is_validated=True).order_by('-created') serializer_class = CompanySerializer @@ -155,23 +157,16 @@ class CompanyViewSet(viewsets.ModelViewSet): return Response(message) -@api_view(['GET',]) -@permission_classes([IsAuthenticated,]) -def my_company(request): - limit = request.GET.get('limit') - offset = request.GET.get('offset') - qs = Company.objects.filter(creator=request.user) - company_serializer = CompanySerializer(qs, many=True) - data = company_serializer.data - # RESULTS PAGINATION - if limit is not None and offset is not None: - limit = int(limit) - offset = int(offset) - data = data[offset:(limit+offset)] - elif limit is not None: - limit = int(limit) - data = data[:limit] - return Response(data=data) +class MyCompanyViewSet(viewsets.ModelViewSet): + model = Company + serializer_class = CompanySerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return self.model.objects.filter(creator=self.request.user) + + def perform_create(self, serializer): + serializer.save(creator=self.request.user) @api_view(['GET',]) diff --git a/products/tests.py b/products/tests.py index e654ea3..7aa43de 100644 --- a/products/tests.py +++ b/products/tests.py @@ -946,11 +946,9 @@ class MyProductsViewTest(APITestCase): # Query endpoint response = self.client.get(self.endpoint) payload = response.json() - # Assert forbidden code self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEquals(payload['count'], len(payload['results'])) - self.assertEquals(len(user_instances), payload['count']) + self.assertEquals(len(user_instances), len(payload)) def test_auth_user_can_paginate_instances(self): """authenticated user can paginate instances @@ -970,8 +968,8 @@ class MyProductsViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) # assert only 2 instances in response payload = response.json() - self.assertEquals(payload['count'], len(payload['results'])) - self.assertEquals(2, payload['count']) + self.assertEquals(payload['count'], self.model.objects.count()) + self.assertEquals(2, len(payload['results'])) def test_anon_user_cannot_access(self): # send in request diff --git a/products/views.py b/products/views.py index 0febf05..84c2340 100644 --- a/products/views.py +++ b/products/views.py @@ -52,27 +52,16 @@ class ProductViewSet(viewsets.ModelViewSet): return Response(data=[]) -@api_view(['GET',]) -@permission_classes([IsAuthenticated,]) -def my_products(request): - limit = request.GET.get('limit') - offset = request.GET.get('offset') - qs = Product.objects.filter(creator=request.user) - product_serializer = ProductSerializer(qs, many=True) - data = product_serializer.data - # RESULTS PAGINATION - if limit is not None and offset is not None: - limit = int(limit) - offset = int(offset) - data = data[offset:(limit+offset)] - elif limit is not None: - limit = int(limit) - data = data[:limit] - # prepare response payload - payload = {} - payload['results'] = data - payload['count'] = len(payload['results']) - return Response(data=payload) +class MyProductsViewSet(viewsets.ModelViewSet): + model = Product + serializer_class = ProductSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return self.model.objects.filter(creator=self.request.user) + + def perform_create(self, serializer): + serializer.save(creator=self.request.user) @api_view(['POST',]) From 642688b98dd8fc20d7123518c781e967347a0ab0 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 12:54:06 +0000 Subject: [PATCH 06/67] added AdminProductsViewSet for users with role SITE_ADMIN --- back_latienda/permissions.py | 8 +- back_latienda/routers.py | 3 +- products/tests.py | 165 +++++++++++++++++++++++++++++++++++ products/views.py | 21 ++++- 4 files changed, 190 insertions(+), 7 deletions(-) diff --git a/back_latienda/permissions.py b/back_latienda/permissions.py index 9bb10d0..1d1cf51 100644 --- a/back_latienda/permissions.py +++ b/back_latienda/permissions.py @@ -43,10 +43,14 @@ class IsSiteAdmin(permissions.BasePermission): admin_role = 'SITE_ADMIN' def has_object_permission(self, request, view, obj): - return request.user.role == self.admin_role + if request.user.is_authenticated: + return request.user.role == self.admin_role + return False def has_permission(self, request, view): - return request.user.role == self.admin_role + if request.user.is_authenticated: + return request.user.role == self.admin_role + return False class ReadOnly(permissions.BasePermission): diff --git a/back_latienda/routers.py b/back_latienda/routers.py index b734217..ef0d46c 100644 --- a/back_latienda/routers.py +++ b/back_latienda/routers.py @@ -2,7 +2,7 @@ from rest_framework import routers from core.views import CustomUserViewSet from companies.views import CompanyViewSet, MyCompanyViewSet -from products.views import ProductViewSet, MyProductsViewSet +from products.views import ProductViewSet, MyProductsViewSet, AdminProductsViewSet from history.views import HistorySyncViewSet from stats.views import StatsLogViewSet @@ -16,6 +16,7 @@ router.register('companies', CompanyViewSet, basename='company') router.register('my_company', MyCompanyViewSet, basename='my-company') router.register('products', ProductViewSet, basename='product') router.register('my_products', MyProductsViewSet, basename='my-products') +router.register('admin_products', AdminProductsViewSet, basename='admin-product') router.register('history', HistorySyncViewSet, basename='history') router.register('stats', StatsLogViewSet, basename='stats') diff --git a/products/tests.py b/products/tests.py index 7aa43de..d15e5c6 100644 --- a/products/tests.py +++ b/products/tests.py @@ -979,6 +979,171 @@ class MyProductsViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) +class AdminProductViewSet(APITestCase): + + def setUp(self): + """Tests setup + """ + self.endpoint = '/api/v1/admin_products/' + self.factory = ProductFactory + self.model = Product + # create user + self.email = f"user@mail.com" + 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_cannot_access(self): + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # GET + response = self.client.get(self.endpoint) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # POST + response = self.client.post(self.endpoint, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # PUT + response = self.client.get(url, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # delete + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_user_cannot_access(self): + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # GET + response = self.client.get(self.endpoint) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # POST + response = self.client.post(self.endpoint, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # PUT + response = self.client.get(url, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # delete + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_admin_user_can_list(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + instance = [self.factory() for i in range(random.randint(1,5))] + # query endpoint + response = self.client.get(self.endpoint) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(len(instance), len(payload)) + + def test_admin_user_can_get_details(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # query endpoint + response = self.client.get(url) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(instance.id, payload['id']) + + def test_admin_can_create_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + data = { + 'name': 'test_product_name', + } + # query endpoint + response = self.client.post(self.endpoint, data=data) + + # assertions + self.assertEquals(response.status_code, 201) + payload = response.json() + self.assertEquals(data['name'], payload['name']) + + def test_admin_can_update_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instance + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + + # data + data = { + 'name': 'test_product_name', + } + # query endpoint + response = self.client.put(url, data=data) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(data['name'], payload['name']) + + def test_admin_can_delete_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instance + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + + # query endpoint + response = self.client.delete(url) + + # assertions + self.assertEquals(response.status_code, 204) + + class FindRelatedProductsTest(APITestCase): def setUp(self): diff --git a/products/views.py b/products/views.py index 84c2340..537cce9 100644 --- a/products/views.py +++ b/products/views.py @@ -16,13 +16,13 @@ from rest_framework.filters import OrderingFilter from django_filters.rest_framework import DjangoFilterBackend import requests -from products.models import Product, CategoryTag -from products.serializers import ProductSerializer, TagFilterSerializer, SearchResultSerializer -from companies.models import Company from history.models import HistorySync from dal import autocomplete -from back_latienda.permissions import IsCreator +from products.models import Product, CategoryTag +from products.serializers import ProductSerializer, TagFilterSerializer, SearchResultSerializer +from companies.models import Company +from back_latienda.permissions import IsCreator, IsSiteAdmin from .utils import extract_search_filters, find_related_products_v3, find_related_products_v6, product_loader from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter @@ -53,6 +53,8 @@ class ProductViewSet(viewsets.ModelViewSet): class MyProductsViewSet(viewsets.ModelViewSet): + """ Allows user to get all products where user is product creator + """ model = Product serializer_class = ProductSerializer permission_classes = [IsAuthenticated] @@ -64,6 +66,17 @@ class MyProductsViewSet(viewsets.ModelViewSet): serializer.save(creator=self.request.user) +class AdminProductsViewSet(viewsets.ModelViewSet): + """ Allows user with role 'SITE_ADMIN' to access all product instances + """ + queryset = Product.objects.all() + serializer_class = ProductSerializer + permission_classes = [IsSiteAdmin] + + def perform_create(self, serializer): + serializer.save(creator=self.request.user) + + @api_view(['POST',]) @permission_classes([IsAuthenticated,]) def load_coop_products(request): From 3b28238a624bbb78148f43b966bef80f82540795 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 13:02:57 +0000 Subject: [PATCH 07/67] added AdminCompanyViewSet for users with role SITE_ADMIN --- back_latienda/routers.py | 5 +- companies/tests.py | 167 ++++++++++++++++++++++++++++++++++++++- companies/views.py | 15 +++- products/tests.py | 2 +- 4 files changed, 182 insertions(+), 7 deletions(-) diff --git a/back_latienda/routers.py b/back_latienda/routers.py index ef0d46c..bad6b0c 100644 --- a/back_latienda/routers.py +++ b/back_latienda/routers.py @@ -1,7 +1,7 @@ from rest_framework import routers from core.views import CustomUserViewSet -from companies.views import CompanyViewSet, MyCompanyViewSet +from companies.views import CompanyViewSet, MyCompanyViewSet, AdminCompanyViewSet from products.views import ProductViewSet, MyProductsViewSet, AdminProductsViewSet from history.views import HistorySyncViewSet from stats.views import StatsLogViewSet @@ -14,9 +14,10 @@ router = routers.DefaultRouter() router.register('users', CustomUserViewSet, basename='users') router.register('companies', CompanyViewSet, basename='company') router.register('my_company', MyCompanyViewSet, basename='my-company') +router.register('admin_companies', AdminCompanyViewSet, basename='admin-companies') router.register('products', ProductViewSet, basename='product') router.register('my_products', MyProductsViewSet, basename='my-products') -router.register('admin_products', AdminProductsViewSet, basename='admin-product') +router.register('admin_products', AdminProductsViewSet, basename='admin-products') router.register('history', HistorySyncViewSet, basename='history') router.register('stats', StatsLogViewSet, basename='stats') diff --git a/companies/tests.py b/companies/tests.py index 167cfb1..1bd5c51 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -7,7 +7,7 @@ from django.test import TestCase from rest_framework.test import APITestCase from rest_framework import status -from companies.factories import ValidatedCompanyFactory +from companies.factories import ValidatedCompanyFactory, CompanyFactory from companies.models import Company from core.factories import CustomUserFactory @@ -407,3 +407,168 @@ class RandomCompanySampleTest(APITestCase): self.assertEquals(size, len(payload)) # test IDs not correlative (eventually it could be, because it's random) self.assertTrue(payload[0]['id'] != (payload[1]['id'] + 1)) + + +class AdminCompanyViewSetTest(APITestCase): + + def setUp(self): + """Tests setup + """ + self.endpoint = '/api/v1/admin_companies/' + self.factory = CompanyFactory + self.model = Company + # create user + self.email = f"user@mail.com" + 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.save() + + def test_anon_user_cannot_access(self): + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # GET + response = self.client.get(self.endpoint) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # POST + response = self.client.post(self.endpoint, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # PUT + response = self.client.get(url, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + # delete + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_user_cannot_access(self): + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # GET + response = self.client.get(self.endpoint) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # POST + response = self.client.post(self.endpoint, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # PUT + response = self.client.get(url, data={}) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # delete + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_admin_user_can_list(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + instance = [self.factory() for i in range(random.randint(1,5))] + # query endpoint + response = self.client.get(self.endpoint) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(len(instance), len(payload)) + + def test_admin_user_can_get_details(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + # query endpoint + response = self.client.get(url) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(instance.id, payload['id']) + + def test_admin_can_create_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instances + data = { + 'short_name': 'test_compnay short _name', + } + # query endpoint + response = self.client.post(self.endpoint, data=data) + + # assertions + self.assertEquals(response.status_code, 201) + payload = response.json() + self.assertEquals(data['short_name'], payload['short_name']) + + def test_admin_can_update_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instance + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + + # data + data = { + 'short_name': 'test_compnay short _name', + } + # query endpoint + response = self.client.put(url, data=data) + + # assertions + self.assertEquals(response.status_code, 200) + payload = response.json() + self.assertEquals(data['short_name'], payload['short_name']) + + def test_admin_can_delete_instance(self): + # make user site amdin + self.user.role = 'SITE_ADMIN' + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # create instance + instance = self.factory() + url = f"{self.endpoint}{instance.id}/" + + # query endpoint + response = self.client.delete(url) + + # assertions + self.assertEquals(response.status_code, 204) + diff --git a/companies/views.py b/companies/views.py index eb1cc75..5133d43 100644 --- a/companies/views.py +++ b/companies/views.py @@ -18,13 +18,11 @@ from stats.models import StatsLog from companies.models import Company from companies.serializers import CompanySerializer from utils.tag_filters import CompanyTagFilter -from back_latienda.permissions import IsCreator +from back_latienda.permissions import IsCreator, IsSiteAdmin from utils import woocommerce - - class CompanyViewSet(viewsets.ModelViewSet): queryset = Company.objects.filter(is_validated=True).order_by('-created') serializer_class = CompanySerializer @@ -169,6 +167,17 @@ class MyCompanyViewSet(viewsets.ModelViewSet): serializer.save(creator=self.request.user) +class AdminCompanyViewSet(viewsets.ModelViewSet): + """ Allows user with role 'SITE_ADMIN' to access all company instances + """ + queryset = Company.objects.all() + serializer_class = CompanySerializer + permission_classes = [IsSiteAdmin] + + def perform_create(self, serializer): + serializer.save(creator=self.request.user) + + @api_view(['GET',]) @permission_classes([IsAuthenticatedOrReadOnly,]) def random_company_sample(request): diff --git a/products/tests.py b/products/tests.py index d15e5c6..e1c0d7c 100644 --- a/products/tests.py +++ b/products/tests.py @@ -979,7 +979,7 @@ class MyProductsViewTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) -class AdminProductViewSet(APITestCase): +class AdminProductViewSetTest(APITestCase): def setUp(self): """Tests setup From 230cfbe043eeaa72a7e7f234427ba4c8eaecb6c3 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 13:10:15 +0000 Subject: [PATCH 08/67] minucia --- products/tests.py | 1 - products/views.py | 2 +- utils/woocommerce.py | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/products/tests.py b/products/tests.py index e1c0d7c..d2ac9d3 100644 --- a/products/tests.py +++ b/products/tests.py @@ -374,7 +374,6 @@ class ProductViewSetTest(APITestCase): url = self.endpoint + f'{instance.pk}/' response = self.client.put(url, data=data, format='json') - # Assert endpoint returns OK code self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/products/views.py b/products/views.py index 537cce9..763a0c1 100644 --- a/products/views.py +++ b/products/views.py @@ -22,7 +22,7 @@ from dal import autocomplete from products.models import Product, CategoryTag from products.serializers import ProductSerializer, TagFilterSerializer, SearchResultSerializer from companies.models import Company -from back_latienda.permissions import IsCreator, IsSiteAdmin +from back_latienda.permissions import IsCreator, IsSiteAdmin, ReadOnly from .utils import extract_search_filters, find_related_products_v3, find_related_products_v6, product_loader from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter diff --git a/utils/woocommerce.py b/utils/woocommerce.py index 8cc76f7..11a947e 100644 --- a/utils/woocommerce.py +++ b/utils/woocommerce.py @@ -170,7 +170,9 @@ def migrate_shop_products(url, key, secret, user=None, version="wc/v3"): history.quantity = counter history.save() + logging.info(f"Products created: {len(new_products)}") print(f"Products created: {len(new_products)}") + return new_products From 596386fb6507586873226ab3eaae7bde645d268b Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 13:16:23 +0000 Subject: [PATCH 09/67] fixed wrong serach field name in company admin --- companies/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companies/admin.py b/companies/admin.py index 5dd4451..ca9c2af 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -10,7 +10,7 @@ from . import models class CompanyAdmin(admin.ModelAdmin): list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active') list_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city') - search_fields = ('short_name', 'company_name', 'email', 'url') + search_fields = ('short_name', 'company_name', 'email', 'web_link') formfield_overrides = { PointField: {"widget": GooglePointFieldWidget} From ff92c97d93af09b0e1d86d27a89e7212f479d123 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 13:23:19 +0000 Subject: [PATCH 10/67] added link item to company admin display list --- companies/admin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/companies/admin.py b/companies/admin.py index ca9c2af..9eb255f 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -8,7 +8,7 @@ from . import models # Register your models here. class CompanyAdmin(admin.ModelAdmin): - list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active') + list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active', 'link') list_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city') search_fields = ('short_name', 'company_name', 'email', 'web_link') @@ -16,5 +16,8 @@ class CompanyAdmin(admin.ModelAdmin): PointField: {"widget": GooglePointFieldWidget} } + def link(self, company): + return company.shop_link is not None + admin.site.register(models.Company, CompanyAdmin) From 7a1aa496b7f69410370e2d3999ac492ae67b0f36 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 8 Mar 2021 13:47:47 +0000 Subject: [PATCH 11/67] first try at implementing product viewset "related" action --- products/tests.py | 15 +++++++++++++++ products/utils.py | 10 ++++++++++ products/views.py | 29 +++++++++++++++++------------ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/products/tests.py b/products/tests.py index d2ac9d3..cc5563e 100644 --- a/products/tests.py +++ b/products/tests.py @@ -194,6 +194,21 @@ class ProductViewSetTest(APITestCase): # Assert number of instnaces in response self.assertEquals(len(expected_instance), len(payload)) + def test_anon_can_get_related_products(self): + # Create instances + instance = self.factory() + # make our user the creator + instance.creator = self.user + instance.save() + + url = f"{self.endpoint}{instance.id}/related/" + + response = self.client.get(url) + + self.assertEquals(response.status_code, 200) + payload= response.json() + self.assertTrue(len(payload) <= 6) + # authenticated user def test_auth_user_can_paginate_instances(self): """authenticated user can paginate instances diff --git a/products/utils.py b/products/utils.py index abb78bd..05ebadf 100644 --- a/products/utils.py +++ b/products/utils.py @@ -85,6 +85,16 @@ def extract_search_filters(result_set): return filter_dict +def find_related_products_v7(description, tags, attributes, category): + products_qs = Product.objects.filter( + description=description, + tags__in=tags, + attributes__in=attributes, + category=category + )[:6] + return products_qs + + def find_related_products_v3(keyword): """ Ranked product search diff --git a/products/views.py b/products/views.py index 763a0c1..8fc511d 100644 --- a/products/views.py +++ b/products/views.py @@ -23,7 +23,7 @@ from products.models import Product, CategoryTag from products.serializers import ProductSerializer, TagFilterSerializer, SearchResultSerializer from companies.models import Company from back_latienda.permissions import IsCreator, IsSiteAdmin, ReadOnly -from .utils import extract_search_filters, find_related_products_v3, find_related_products_v6, product_loader +from .utils import extract_search_filters, find_related_products_v6, product_loader, find_related_products_v7 from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter @@ -37,19 +37,24 @@ logging.basicConfig( class ProductViewSet(viewsets.ModelViewSet): - queryset = Product.objects.filter(active=True).order_by('-created') - serializer_class = ProductSerializer - permission_classes = [IsAuthenticatedOrReadOnly, IsCreator] - filter_backends = [DjangoFilterBackend, OrderingFilter] - filterset_class = ProductTagFilter + queryset = Product.objects.filter(active=True).order_by('-created') + serializer_class = ProductSerializer + permission_classes = [IsAuthenticatedOrReadOnly, IsCreator] + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = ProductTagFilter - def perform_create(self, serializer): - serializer.save(creator=self.request.user, company=self.request.user.company) + def perform_create(self, serializer): + serializer.save(creator=self.request.user, company=self.request.user.company) - @action(detail=True, methods=['GET',]) - def related(request): - # TODO: find the most similar products - return Response(data=[]) + @action(detail=True, methods=['GET',]) + def related(self, request, pk=None): + """Find instances similar to the one referenced + """ + # TODO: find the most similar products + product = self.get_object() + qs = find_related_products_v7(product.description, product.tags.all(), product.attributes.all(), product.category) + serializer = self.serializer_class(qs, many=True) + return Response(data=serializer.data) class MyProductsViewSet(viewsets.ModelViewSet): From a8ef16206e56d49fd94ec8a01f145ad032506b28 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 10:11:09 +0000 Subject: [PATCH 12/67] added dynamic field link to company, to display on admin if it has a shop_link --- companies/admin.py | 3 --- companies/models.py | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/companies/admin.py b/companies/admin.py index 9eb255f..539c615 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -16,8 +16,5 @@ class CompanyAdmin(admin.ModelAdmin): PointField: {"widget": GooglePointFieldWidget} } - def link(self, company): - return company.shop_link is not None - admin.site.register(models.Company, CompanyAdmin) diff --git a/companies/models.py b/companies/models.py index 6c1ae98..fcaad55 100644 --- a/companies/models.py +++ b/companies/models.py @@ -57,3 +57,8 @@ class Company(models.Model): class Meta: verbose_name = "Compañía" verbose_name_plural = "Compañías" + + def link(self): + return self.shop_link is not None + + link.boolean = True From b629b2cd425a8839e7e59dae7c514c06e7be90f0 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 11:13:22 +0000 Subject: [PATCH 13/67] customized company model __str__ --- companies/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/companies/models.py b/companies/models.py index fcaad55..15022fc 100644 --- a/companies/models.py +++ b/companies/models.py @@ -58,6 +58,14 @@ class Company(models.Model): verbose_name = "Compañía" verbose_name_plural = "Compañías" + def __str__(self): + if self.short_name: + return self.short_name + elif self.company_name: + return self.company_name + else: + return f"Compañía #{self.id}" + def link(self): return self.shop_link is not None From 2a731b3c3bb15d33a6c98c882f063428cfa095f1 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 11:15:34 +0000 Subject: [PATCH 14/67] fixed error in history admin when company was none --- products/admin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/products/admin.py b/products/admin.py index 94d8743..e57a0d0 100644 --- a/products/admin.py +++ b/products/admin.py @@ -11,13 +11,10 @@ def model_admin_callable(co): class ProductAdmin(admin.ModelAdmin): form = forms.ProductTagForm - list_display = ('name', 'category', 'sourcing_date', 'company_name', 'active' ) + list_display = ('name', 'category', 'sourcing_date', 'company', 'active' ) list_filter = ('company', 'tags', 'category', 'attributes') search_fields = ('name', 'sku', 'description') - def company_name(self, product): - return product.company.company_name - admin.site.register(models.Product, ProductAdmin) admin.site.register(models.TreeTag) From 0fee0a2f6d22981d4158b6ea1b8c4294e182a486 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 11:30:54 +0000 Subject: [PATCH 15/67] implemented dropdowns for fk fields in product and company admin --- back_latienda/settings/base.py | 1 + companies/admin.py | 3 ++- products/admin.py | 10 +++++++++- requirements.txt | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index d7249b5..0ee9a92 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ 'anymail', 'storages', 'mapwidgets', + 'django_admin_listfilter_dropdown', # local apps 'core', diff --git a/companies/admin.py b/companies/admin.py index 539c615..0575242 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.contrib.gis.db.models import PointField +from django_admin_listfilter_dropdown.filters import DropdownFilter, RelatedDropdownFilter from mapwidgets.widgets import GooglePointFieldWidget from . import models @@ -10,7 +11,7 @@ from . import models class CompanyAdmin(admin.ModelAdmin): list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active', 'link') list_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city') - search_fields = ('short_name', 'company_name', 'email', 'web_link') + search_fields = ('short_name', 'company_name', 'email', 'web_link', ('city', RelatedDropdownFilter)) formfield_overrides = { PointField: {"widget": GooglePointFieldWidget} diff --git a/products/admin.py b/products/admin.py index e57a0d0..8d42fcb 100644 --- a/products/admin.py +++ b/products/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin +from django_admin_listfilter_dropdown.filters import DropdownFilter, RelatedDropdownFilter, ChoiceDropdownFilter + from . import models from . import forms @@ -12,7 +14,13 @@ def model_admin_callable(co): class ProductAdmin(admin.ModelAdmin): form = forms.ProductTagForm list_display = ('name', 'category', 'sourcing_date', 'company', 'active' ) - list_filter = ('company', 'tags', 'category', 'attributes') + list_filter = ( + ('company', RelatedDropdownFilter), + ('tags', RelatedDropdownFilter), + ('category', RelatedDropdownFilter), + ('attributes', RelatedDropdownFilter) + ) + search_fields = ('name', 'sku', 'description') diff --git a/requirements.txt b/requirements.txt index ad2f270..df15689 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ woocommerce==2.1.1 django-autocomplete-light==3.8.2 # manually install `pip install --default-timeout=100 future` to avoid wcapi to timeout django-map-widgets==0.3.0 +django-admin-list-filter-dropdown==1.0.3 # required for production django-anymail[amazon_ses]==8.2 boto3==1.17.11 From 2eb6bf3acdbae91720237a67edc40e9954da9bc3 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 11:43:14 +0000 Subject: [PATCH 16/67] extended DjangoSuitConfig to customize admin navbar, not working yet --- core/admin.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/admin.py b/core/admin.py index 81f9dab..e2268f6 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,43 @@ from django.contrib import admin +from django.apps import AppConfig +from suit.apps import DjangoSuitConfig +from suit.menu import ParentItem, ChildItem + from . import models # Register your models here. + +class SuitConfig(DjangoSuitConfig): + layout = 'horizontal' + menu = ( + ParentItem('Usuarios', children=[ + ChildItem('Usuarios', model='core.CustomUser'), + ChildItem('Grupos',model='auth.group'), + ], icon='fa fa-leaf'), + + ParentItem('Cooperativas', children=[ + ChildItem(model='companies.Company'), + ], icon='fa fa-leaf'), + + ParentItem('Productos', children=[ + ChildItem(model='products.Product'), + ], icon='fa fa-leaf'), + + ParentItem('Categorías', children=[ + ChildItem(model='products.categoryTag'), + ChildItem(model='products.TreeTag'), + ChildItem(model='products.AttributeTag'), + ], icon='fa fa-leaf'), + + ParentItem('Importación', children=[ + ChildItem(model='history.History'), + ChildItem(model='stats.StatsLog'), + + ], icon='fa fa-leaf'), + ) + + class UserAdmin(admin.ModelAdmin): list_display = ('email', 'full_name', 'role', 'company', 'email_verified', 'is_active', 'is_staff', 'created', 'last_visit') list_filter = ('is_active', 'is_staff', 'email_verified') From 170ca9402272683e9d5254489af06930ad6de76f Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 11:59:16 +0000 Subject: [PATCH 17/67] changes but still not working --- core/admin.py | 33 --------------------------------- core/apps.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/core/admin.py b/core/admin.py index e2268f6..3e21527 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,43 +1,10 @@ from django.contrib import admin -from django.apps import AppConfig -from suit.apps import DjangoSuitConfig -from suit.menu import ParentItem, ChildItem from . import models # Register your models here. -class SuitConfig(DjangoSuitConfig): - layout = 'horizontal' - menu = ( - ParentItem('Usuarios', children=[ - ChildItem('Usuarios', model='core.CustomUser'), - ChildItem('Grupos',model='auth.group'), - ], icon='fa fa-leaf'), - - ParentItem('Cooperativas', children=[ - ChildItem(model='companies.Company'), - ], icon='fa fa-leaf'), - - ParentItem('Productos', children=[ - ChildItem(model='products.Product'), - ], icon='fa fa-leaf'), - - ParentItem('Categorías', children=[ - ChildItem(model='products.categoryTag'), - ChildItem(model='products.TreeTag'), - ChildItem(model='products.AttributeTag'), - ], icon='fa fa-leaf'), - - ParentItem('Importación', children=[ - ChildItem(model='history.History'), - ChildItem(model='stats.StatsLog'), - - ], icon='fa fa-leaf'), - ) - - class UserAdmin(admin.ModelAdmin): list_display = ('email', 'full_name', 'role', 'company', 'email_verified', 'is_active', 'is_staff', 'created', 'last_visit') list_filter = ('is_active', 'is_staff', 'email_verified') diff --git a/core/apps.py b/core/apps.py index 26f78a8..44348da 100644 --- a/core/apps.py +++ b/core/apps.py @@ -1,5 +1,44 @@ from django.apps import AppConfig +from suit.apps import DjangoSuitConfig +from suit.menu import ParentItem, ChildItem + + +class SuitConfig(DjangoSuitConfig): + layout = 'horizontal' + menu = ( + ParentItem('Usuarios', children=[ + ChildItem('Usuarios', model='core.CustomUser'), + ], icon='fa fa-leaf'), + + ParentItem('Cooperativas', children=[ + ChildItem('Cooperativas', model='companies.Company'), + ], icon='fa fa-leaf'), + + ParentItem('Productos', children=[ + ChildItem('Productos', model='products.Product'), + ], icon='fa fa-leaf'), + + ParentItem('Categorías', children=[ + ChildItem('Categorías', model='products.categoryTag'), + ChildItem('Tags', model='products.TreeTag'), + ChildItem('Atributos', model='products.AttributeTag'), + ], icon='fa fa-leaf'), + + ParentItem('Importación', children=[ + ChildItem('Historial', model='history.History'), + ChildItem('Logs', model='stats.StatsLog'), + ], icon='fa fa-leaf'), + + ParentItem('Otros', children=[ + ChildItem('Grupos',model='auth.group'), + ChildItem('Países', model='geo.Country'), + ChildItem('Regiones', model='geo.Region'), + ChildItem('Provincias', model='geo.Province'), + ChildItem('Municipios', model='geo.City'), + ], icon='fa fa-leaf'), + ) + class CoreConfig(AppConfig): name = 'core' From 9b1405ecda07830e78ac6bd9e85034c40122cef9 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 12:06:02 +0000 Subject: [PATCH 18/67] another change but still not working --- core/apps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/apps.py b/core/apps.py index 44348da..8925cc3 100644 --- a/core/apps.py +++ b/core/apps.py @@ -39,6 +39,9 @@ class SuitConfig(DjangoSuitConfig): ], icon='fa fa-leaf'), ) + def ready(self): + super(SuitConfig, self).ready() + class CoreConfig(AppConfig): name = 'core' From d65f9d2388cdff2e8cf0ca37b089a9af846306c2 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 12:27:15 +0000 Subject: [PATCH 19/67] added purchase_email endpoint to products views --- products/views.py | 61 ++++++++++++++++++++ templates/purchase_contact_verification.html | 0 templates/purchase_notification.html | 0 3 files changed, 61 insertions(+) create mode 100644 templates/purchase_contact_verification.html create mode 100644 templates/purchase_notification.html diff --git a/products/views.py b/products/views.py index 8fc511d..6aab657 100644 --- a/products/views.py +++ b/products/views.py @@ -1,6 +1,7 @@ import logging import csv import datetime +import json from django.db.models import Q from django.core import serializers @@ -242,3 +243,63 @@ class CategoryTagAutocomplete(autocomplete.Select2QuerySetView): qs = qs.filter(name__icontains=self.q) return qs # [x.label for x in qs] + + +@api_view(['POST']) +def purchase_email(request): + """Notify coop manager and user about item purchase + """ + + data = json.loads(request.body) + # check data + try: + for param in ('email', '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) + + if request.user.is_anonymous: + email = data.get('email') + else: + email = request.user.email + telephone = data.get('telephone') + + # prepare email messages + company_message = render_to_string('purchase_notification.html', { + 'company': instance, + 'email': request.user.email, + 'full_name': request.user.full_name, + 'quantity': data['quantity'], + 'phone_number': data.get('phone_number'), + 'comments': data['comments'], + 'product_info': data['product_info'], + }) + user_message = render_to_string('confirm_company_contact.html', { + 'company': instance, + 'username': request.user.full_name, + 'data': data, + }) + # send email to company + subject = "Contacto de usuario" + email = EmailMessage(subject, company_message, to=[instance.creator.email]) + email.send() + logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}") + # send confirmation email to user + subject = 'Confirmación de contacto' + email = EmailMessage(subject, message, to=[request.user.email]) + email.send() + logging.info(f"Contact confirmation email sent to {request.user.email}") + stats_data = { + 'action_object': instance, + 'user': None, + 'anonymous': True, + 'ip_address': client_ip, + 'geo': g.geos(client_ip), + 'contact': True, + 'shop': instance.shop, + } + + # email user + + # response + return Response() diff --git a/templates/purchase_contact_verification.html b/templates/purchase_contact_verification.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/purchase_notification.html b/templates/purchase_notification.html new file mode 100644 index 0000000..e69de29 From 89e3e446f3170d4289893cc70ce995f3805ca279 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 12:44:01 +0000 Subject: [PATCH 20/67] more work on purchase_email endpoint and basic templates --- products/views.py | 55 ++++++++++++-------- templates/purchase_contact_confirmation.html | 6 +++ templates/purchase_contact_verification.html | 0 templates/purchase_notification.html | 10 ++++ 4 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 templates/purchase_contact_confirmation.html delete mode 100644 templates/purchase_contact_verification.html diff --git a/products/views.py b/products/views.py index 6aab657..9d24ba2 100644 --- a/products/views.py +++ b/products/views.py @@ -5,6 +5,7 @@ import json from django.db.models import Q from django.core import serializers +from django.contrib.auth import get_user_model # Create your views here. from rest_framework import status @@ -28,6 +29,7 @@ from .utils import extract_search_filters, find_related_products_v6, product_loa from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter +User = get_user_model() logging.basicConfig( filename='logs/product-load.log', @@ -264,31 +266,41 @@ def purchase_email(request): email = request.user.email telephone = data.get('telephone') - # prepare email messages - company_message = render_to_string('purchase_notification.html', { - 'company': instance, - 'email': request.user.email, - 'full_name': request.user.full_name, - 'quantity': data['quantity'], - 'phone_number': data.get('phone_number'), - 'comments': data['comments'], - 'product_info': data['product_info'], + # get company + company = Company.objects.filter(id=data['company']).first() + if not company: + 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': + return Response({"error": "Company has no managing user"}, status=status.HTTP_406_NOT_ACCEPTABLE) + # get product + product = Product.objects.filter(id=data['product']).first() + if not product: + return Response({"error": "Invalid value for product"}, status=status.HTTP_406_NOT_ACCEPTABLE) + + # send email to company manager + manager_message = render_to_string('purchase_notification.html', { + 'company': company, + 'user': request.user, + 'product': product, + 'telephone': data['telephone'], }) - user_message = render_to_string('confirm_company_contact.html', { - 'company': instance, - 'username': request.user.full_name, - 'data': data, - }) - # send email to company - subject = "Contacto de usuario" - email = EmailMessage(subject, company_message, to=[instance.creator.email]) + subject = "Contacto de usuario sobre venta" + email = EmailMessage(subject, manager_message, to=[manager.email]) email.send() - logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}") + logging.info(f"Email sent to {manager.email} as manager of {company}") # send confirmation email to user - subject = 'Confirmación de contacto' + user_message = render_to_string('purchase_contact_confirmation.html', { + 'company': company, + 'product': product, + }) + subject = 'Confirmación de contacto con vendedor' email = EmailMessage(subject, message, to=[request.user.email]) email.send() - logging.info(f"Contact confirmation email sent to {request.user.email}") + logging.info(f"Purchase Contact confirmation email sent to {request.user.email}") + + # create statslog instance to register interaction stats_data = { 'action_object': instance, 'user': None, @@ -298,8 +310,7 @@ def purchase_email(request): 'contact': True, 'shop': instance.shop, } - - # email user + StatsLog.objects.create(**stats_data) # response return Response() diff --git a/templates/purchase_contact_confirmation.html b/templates/purchase_contact_confirmation.html new file mode 100644 index 0000000..eef225c --- /dev/null +++ b/templates/purchase_contact_confirmation.html @@ -0,0 +1,6 @@ +Hola usuario. +Hemos envíado correctamente el email al usuario que gestiona {{company}} sobre el producto {{product}}. + +Deberías revibir una respuesta directa en los próximos días. + +LaTiendaCOOP diff --git a/templates/purchase_contact_verification.html b/templates/purchase_contact_verification.html deleted file mode 100644 index e69de29..0000000 diff --git a/templates/purchase_notification.html b/templates/purchase_notification.html index e69de29..8b485c1 100644 --- a/templates/purchase_notification.html +++ b/templates/purchase_notification.html @@ -0,0 +1,10 @@ +Hola usuario. +Te contactamos por tu puesto como gestor de {{company}}. + +El usuario {{user.email}} ha mostrado interés en la compra de {{product}}. + +Ponte en contacto con el usuario tan pronto como te sea posible, y finalizar la venta. + +Teléfono de contacto: {{telephone}} + +LaTiendaCOOP From 4218d94a262f5394d287649486d6cc20b9c59ba5 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 13:00:59 +0000 Subject: [PATCH 21/67] testing purchase_email but getting 401 with AllowAny --- back_latienda/urls.py | 3 +-- companies/tests.py | 1 + products/tests.py | 36 ++++++++++++++++++++++++++++++++++++ products/views.py | 5 +++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/back_latienda/urls.py b/back_latienda/urls.py index 99f4276..91431ad 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -39,9 +39,8 @@ urlpatterns = [ path('api/v1/search_products/', product_views.product_search, name='product-search'), path('api/v1/create_company_user/', core_views.create_company_user, name='create-company-user'), path('api/v1/my_user/', core_views.my_user, name='my-user'), - # path('api/v1/my_company/', company_views.my_company , name='my-company'), path('api/v1/companies/sample/', company_views.random_company_sample , name='company-sample'), - # path('api/v1/my_products/', product_views.my_products, name='my-products'), + path('api/v1/purchase_email/', product_views.purchase_email, name='purchase-email'), path('api/v1/stats/me/', stat_views.track_user, name='user-tracker'), path('api/v1/autocomplete/category-tag/', product_views.CategoryTagAutocomplete.as_view(), name='category-autocomplete'), path('api/v1/', include(router.urls)), diff --git a/companies/tests.py b/companies/tests.py index 1bd5c51..343ea7c 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -293,6 +293,7 @@ class CompanyViewSetTest(APITestCase): # check order self.assertTrue(response.data[0]['id'] > response.data[1]['id']) + # TODO: test email_manager action class MyCompanyViewTest(APITestCase): """CompanyViewset tests diff --git a/products/tests.py b/products/tests.py index cc5563e..622eb3f 100644 --- a/products/tests.py +++ b/products/tests.py @@ -6,6 +6,7 @@ from urllib.parse import quote from django.utils import timezone from django.test import TestCase +from django.core import mail from rest_framework.test import APITestCase from rest_framework import status @@ -1195,3 +1196,38 @@ class FindRelatedProductsTest(APITestCase): # assert result self.assertTrue(len(results) == len(expected_instances)) + +class PurchaseEmailTest(APITestCase): + + def setUp(self): + """Tests setup + """ + self.endpoint = '/api/v1/purchase_email/' + self.factory = ProductFactory + self.model = Product + # create user + self.email = f"user@mail.com" + 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() + product = ProductFactory(company=company) + + data = { + 'email': self.email, + 'telephone': '123123123', + 'company': company.id, + 'product': product.id, + 'comment': '', + } + response = self.client.post(self.endpoint, json=data) + import ipdb; ipdb.set_trace() + # assertions + self.assertEquals(response.status_code, 200) + self.assertEquals(2, len(mail.outbox)) + diff --git a/products/views.py b/products/views.py index 9d24ba2..8cf0fb3 100644 --- a/products/views.py +++ b/products/views.py @@ -11,7 +11,7 @@ from django.contrib.auth import get_user_model from rest_framework import status from rest_framework import viewsets from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser, IsAuthenticated +from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser, IsAuthenticated, AllowAny from rest_framework.decorators import api_view, permission_classes, action from rest_framework.filters import OrderingFilter @@ -247,6 +247,7 @@ class CategoryTagAutocomplete(autocomplete.Select2QuerySetView): return qs # [x.label for x in qs] +@permission_classes([AllowAny,]) @api_view(['POST']) def purchase_email(request): """Notify coop manager and user about item purchase @@ -275,7 +276,7 @@ def purchase_email(request): if not manager and 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']).first() + product = Product.objects.filter(id=data['product'], company=company).first() if not product: return Response({"error": "Invalid value for product"}, status=status.HTTP_406_NOT_ACCEPTABLE) From f0a076057ce781251958bcdeaa9e59f22bfaf92b Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 9 Mar 2021 13:47:18 +0000 Subject: [PATCH 22/67] fixes to purchase_email and readme update --- README.md | 176 +++++++++++++++++++++++++++++++++++++++------- products/tests.py | 30 ++++++-- products/views.py | 29 ++++---- 3 files changed, 193 insertions(+), 42 deletions(-) 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) From c6f051ac65cb6298ed3ab5d5b87c5f37bd423cd8 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 10:10:16 +0000 Subject: [PATCH 23/67] added email validation to purchase_email view --- products/tests.py | 21 +++++++++++++++++++++ products/views.py | 8 ++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/products/tests.py b/products/tests.py index bbf7cec..19d7f2a 100644 --- a/products/tests.py +++ b/products/tests.py @@ -1253,3 +1253,24 @@ class PurchaseEmailTest(APITestCase): self.assertEquals(response.status_code, 200) self.assertEquals(2, len(mail.outbox)) + def test_anon_user_bad_email(self): + company = CompanyFactory() + self.user.role = 'COOP_MANAGER' + self.user.company = company + self.user.save() + product = ProductFactory(company=company) + + data = { + 'email': '324r@qwer', + 'telephone': '123123123', + 'company': company.id, + 'product': product.id, + 'comment': '', + } + + response = self.client.post(self.endpoint, data=data, format='json') + # assertions + self.assertEquals(response.status_code, 406) + payload = response.json() + self.assertTrue( 'email' in payload['error']) + diff --git a/products/views.py b/products/views.py index 091990d..280f36a 100644 --- a/products/views.py +++ b/products/views.py @@ -5,6 +5,7 @@ import json from django.db.models import Q from django.core import serializers +from django.core.validators import EmailValidator, validate_email from django.contrib.auth import get_user_model from django.template.loader import render_to_string from django.core.mail import EmailMessage @@ -259,7 +260,6 @@ def purchase_email(request): # 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 ('telephone', 'company', 'product', 'comment'): assert(param in data.keys()) @@ -271,7 +271,11 @@ def purchase_email(request): else: email = request.user.email telephone = data.get('telephone') - + # validate email + try: + validate_email(email) + except: + return Response({"error": "Value for email is not valid"}, status=status.HTTP_406_NOT_ACCEPTABLE) # get company company = Company.objects.filter(id=data['company']).first() if not company: From 3cbc807bf7c055ed12c9915ea5d37763572f892a Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 10:23:43 +0000 Subject: [PATCH 24/67] finished work on purchase_email endpoint, for now --- products/tests.py | 6 ------ products/views.py | 22 ++++++++++---------- templates/purchase_contact_confirmation.html | 8 +++++-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/products/tests.py b/products/tests.py index 19d7f2a..e68f9cc 100644 --- a/products/tests.py +++ b/products/tests.py @@ -1233,9 +1233,6 @@ class PurchaseEmailTest(APITestCase): 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 = { @@ -1255,9 +1252,6 @@ class PurchaseEmailTest(APITestCase): def test_anon_user_bad_email(self): company = CompanyFactory() - self.user.role = 'COOP_MANAGER' - self.user.company = company - self.user.save() product = ProductFactory(company=company) data = { diff --git a/products/views.py b/products/views.py index 280f36a..f5b2a1e 100644 --- a/products/views.py +++ b/products/views.py @@ -5,7 +5,7 @@ import json from django.db.models import Q from django.core import serializers -from django.core.validators import EmailValidator, validate_email +from django.core.validators import validate_email from django.contrib.auth import get_user_model from django.template.loader import render_to_string from django.core.mail import EmailMessage @@ -280,32 +280,32 @@ def purchase_email(request): company = Company.objects.filter(id=data['company']).first() if not company: 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 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() if not product: return Response({"error": "Invalid value for product"}, status=status.HTTP_406_NOT_ACCEPTABLE) + # check company.email + if company.email is None: + return Response({"error": "Related compay has no contact email address"}, status=status.HTTP_406_NOT_ACCEPTABLE) - # send email to company manager - manager_message = render_to_string('purchase_notification.html', { + # send email to company + company_message = render_to_string('purchase_notification.html', { 'company': company, 'user': request.user, 'product': product, 'telephone': data['telephone'], }) - subject = "Contacto de usuario sobre venta" - email = EmailMessage(subject, manager_message, to=[manager.email]) + subject = "[latienda.coop] Solicitud de compra" + email = EmailMessage(subject, company_message, to=[company.email]) email.send() - logging.info(f"Email sent to {manager.email} as manager of {company}") + logging.info(f"Email sent to {company}") # send confirmation email to user user_message = render_to_string('purchase_contact_confirmation.html', { 'company': company, 'product': product, + 'company_message': company_message, }) - subject = 'Confirmación de contacto con vendedor' + subject = 'Confirmación de petición de compra' email = EmailMessage(subject, user_message, to=[email]) email.send() logging.info(f"Purchase Contact confirmation email sent to {email}") diff --git a/templates/purchase_contact_confirmation.html b/templates/purchase_contact_confirmation.html index eef225c..7d5deb6 100644 --- a/templates/purchase_contact_confirmation.html +++ b/templates/purchase_contact_confirmation.html @@ -1,6 +1,10 @@ Hola usuario. -Hemos envíado correctamente el email al usuario que gestiona {{company}} sobre el producto {{product}}. -Deberías revibir una respuesta directa en los próximos días. +Hemos envíado correctamente el siguiente email a la empresa {{company}} sobre el producto {{product}}: + +{{company_message}} + + +Deberías recibir una respuesta directa en los próximos días. LaTiendaCOOP From c8181a2893d0c8a129e3ccb052cbf163a7b028b4 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 10:42:33 +0000 Subject: [PATCH 25/67] tweaks to email template --- templates/purchase_notification.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/purchase_notification.html b/templates/purchase_notification.html index 8b485c1..896baef 100644 --- a/templates/purchase_notification.html +++ b/templates/purchase_notification.html @@ -1,9 +1,8 @@ -Hola usuario. -Te contactamos por tu puesto como gestor de {{company}}. +Hola {{company}}. -El usuario {{user.email}} ha mostrado interés en la compra de {{product}}. +El usuario {{user.email}} ha mostrado interés en la compra del producto {{product}}. -Ponte en contacto con el usuario tan pronto como te sea posible, y finalizar la venta. +Ponte en contacto con el usuario tan pronto como te sea posible para finalizar la venta. Teléfono de contacto: {{telephone}} From aa9a6bdf815c09424d0d8a6ed7fc9fc2fc1d693f Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 10:51:45 +0000 Subject: [PATCH 26/67] fixed email error --- back_latienda/settings/production.py | 2 ++ products/views.py | 52 +++++++++++++++------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/back_latienda/settings/production.py b/back_latienda/settings/production.py index 46fb796..653acc2 100644 --- a/back_latienda/settings/production.py +++ b/back_latienda/settings/production.py @@ -41,6 +41,8 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend" DEFAULT_FROM_EMAIL = "no-reply@latienda.com" +# DEFAULT_FROM_EMAIL = "samuel.molina@enreda.coop" + SERVER_EMAIL = "mail-server@latienda.com" ANYMAIL = { diff --git a/products/views.py b/products/views.py index f5b2a1e..483bc6b 100644 --- a/products/views.py +++ b/products/views.py @@ -267,13 +267,13 @@ def purchase_email(request): 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') + user_email = data.get('email') else: - email = request.user.email + user_email = request.user.email telephone = data.get('telephone') # validate email try: - validate_email(email) + validate_email(user_email) except: return Response({"error": "Value for email is not valid"}, status=status.HTTP_406_NOT_ACCEPTABLE) # get company @@ -287,28 +287,30 @@ def purchase_email(request): # check company.email if company.email is None: return Response({"error": "Related compay has no contact email address"}, status=status.HTTP_406_NOT_ACCEPTABLE) - - # send email to company - company_message = render_to_string('purchase_notification.html', { - 'company': company, - 'user': request.user, - 'product': product, - 'telephone': data['telephone'], - }) - subject = "[latienda.coop] Solicitud de compra" - email = EmailMessage(subject, company_message, to=[company.email]) - email.send() - logging.info(f"Email sent to {company}") - # send confirmation email to user - user_message = render_to_string('purchase_contact_confirmation.html', { - 'company': company, - 'product': product, - 'company_message': company_message, - }) - subject = 'Confirmación de petición de compra' - email = EmailMessage(subject, user_message, to=[email]) - email.send() - logging.info(f"Purchase Contact confirmation email sent to {email}") + try: + # send email to company + company_message = render_to_string('purchase_notification.html', { + 'company': company, + 'user': request.user, + 'product': product, + 'telephone': data['telephone'], + }) + subject = "[latienda.coop] Solicitud de compra" + email = EmailMessage(subject, company_message, to=[company.email]) + email.send() + logging.info(f"Email sent to {company}") + # send confirmation email to user + user_message = render_to_string('purchase_contact_confirmation.html', { + 'company': company, + 'product': product, + 'company_message': company_message, + }) + subject = 'Confirmación de petición de compra' + email = EmailMessage(subject, user_message, to=[user_email]) + email.send() + logging.info(f"Purchase Contact confirmation email sent to {user_email}") + except Exception as e: + return Response({'error': f"Could not send emails [{str(type(e))}]: {str(e)}"}, status=500) # create statslog instance to register interaction stats_data = { From d01d5bf40732d0356e1e8035974c414c4ee4a47f Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 11:41:02 +0000 Subject: [PATCH 27/67] fix for history admin crashing when company is null --- history/admin.py | 5 ++++- products/tests.py | 5 +++-- products/utils.py | 8 ++++---- products/views.py | 6 +++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/history/admin.py b/history/admin.py index 87e7f48..da110c1 100644 --- a/history/admin.py +++ b/history/admin.py @@ -9,7 +9,10 @@ class HistoryAdmin(admin.ModelAdmin): list_filter = ('company__company_name',) def company_name(self, instance): - return instance.company.company_name + if instance.company and instance.company.company_name: + return instance.company.company_name + else: + return 'NULL' admin.site.register(models.HistorySync, HistoryAdmin) diff --git a/products/tests.py b/products/tests.py index e68f9cc..ea1dfd8 100644 --- a/products/tests.py +++ b/products/tests.py @@ -14,7 +14,7 @@ from rest_framework import status from companies.factories import CompanyFactory from products.factories import ProductFactory, ActiveProductFactory from products.models import Product -from products.utils import find_related_products_v3 +# from products.utils import find_related_products_v3 from core.factories import CustomUserFactory from core.utils import get_tokens_for_user @@ -1159,6 +1159,7 @@ class AdminProductViewSetTest(APITestCase): self.assertEquals(response.status_code, 204) +''' class FindRelatedProductsTest(APITestCase): def setUp(self): @@ -1195,7 +1196,7 @@ class FindRelatedProductsTest(APITestCase): # assert result self.assertTrue(len(results) == len(expected_instances)) - +''' class PurchaseEmailTest(APITestCase): diff --git a/products/utils.py b/products/utils.py index 05ebadf..ddec191 100644 --- a/products/utils.py +++ b/products/utils.py @@ -85,7 +85,7 @@ def extract_search_filters(result_set): return filter_dict -def find_related_products_v7(description, tags, attributes, category): +def get_related_products(description, tags, attributes, category): products_qs = Product.objects.filter( description=description, tags__in=tags, @@ -94,7 +94,7 @@ def find_related_products_v7(description, tags, attributes, category): )[:6] return products_qs - +''' def find_related_products_v3(keyword): """ Ranked product search @@ -111,9 +111,9 @@ def find_related_products_v3(keyword): ).filter(rank__gt=0.05) # removed order_by because its lost in casting return set(products_qs) +''' - -def find_related_products_v6(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): +def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): """ Ranked product search diff --git a/products/views.py b/products/views.py index 483bc6b..360b935 100644 --- a/products/views.py +++ b/products/views.py @@ -29,7 +29,7 @@ from products.serializers import ProductSerializer, TagFilterSerializer, SearchR 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 import extract_search_filters, ranked_product_search, product_loader, get_related_products from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter @@ -59,7 +59,7 @@ class ProductViewSet(viewsets.ModelViewSet): """ # TODO: find the most similar products product = self.get_object() - qs = find_related_products_v7(product.description, product.tags.all(), product.attributes.all(), product.category) + qs = get_related_products(product.description, product.tags.all(), product.attributes.all(), product.category) serializer = self.serializer_class(qs, many=True) return Response(data=serializer.data) @@ -195,7 +195,7 @@ def product_search(request): # split query string into single words chunks = q.split(' ') for chunk in chunks: - product_set, min_price, max_price = find_related_products_v6(chunk, shipping_cost, discount, category, tags, price_min, price_max) + product_set, min_price, max_price = ranked_product_search(chunk, shipping_cost, discount, category, tags, price_min, price_max) # update price values if product_set: if prices['min'] is None or min_price['price__min'] < prices['min']: From 39c8bd5e44c8f36f4f68f860dfc274b6d084dc33 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 11:52:43 +0000 Subject: [PATCH 28/67] history admin dropdown for admin --- geo/models.py | 5 ++++- history/admin.py | 6 +++++- products/tests.py | 40 ---------------------------------------- products/utils.py | 28 +++++----------------------- 4 files changed, 14 insertions(+), 65 deletions(-) diff --git a/geo/models.py b/geo/models.py index 9d1dec9..f2cbd9a 100644 --- a/geo/models.py +++ b/geo/models.py @@ -73,7 +73,10 @@ class City(models.Model): updated = models.DateTimeField('date last update', auto_now=True) def __str__(self): - return f'{self.name} [{self.province}]' + if self.province: + return f'{self.name} [{self.province}]' + else: + return f'{self.name}' class Meta: verbose_name = "Municipio" diff --git a/history/admin.py b/history/admin.py index da110c1..adeeefb 100644 --- a/history/admin.py +++ b/history/admin.py @@ -1,12 +1,16 @@ from django.contrib import admin +from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter, ChoiceDropdownFilter + from . import models # Register your models here. class HistoryAdmin(admin.ModelAdmin): list_display = ('company_name', 'rss_url', 'sync_date', 'result', 'quantity',) - list_filter = ('company__company_name',) + list_filter = ( + ('company', RelatedDropdownFilter), + ) def company_name(self, instance): if instance.company and instance.company.company_name: diff --git a/products/tests.py b/products/tests.py index ea1dfd8..8b497ff 100644 --- a/products/tests.py +++ b/products/tests.py @@ -14,7 +14,6 @@ from rest_framework import status from companies.factories import CompanyFactory from products.factories import ProductFactory, ActiveProductFactory from products.models import Product -# from products.utils import find_related_products_v3 from core.factories import CustomUserFactory from core.utils import get_tokens_for_user @@ -1159,45 +1158,6 @@ class AdminProductViewSetTest(APITestCase): self.assertEquals(response.status_code, 204) -''' -class FindRelatedProductsTest(APITestCase): - - def setUp(self): - """Tests setup - """ - self.factory = ActiveProductFactory - self.model = Product - # clear table - self.model.objects.all().delete() - - def test_v3_find_by_tags(self): - # create tagged product - tag = 'cool' - expected_instances = [ - self.factory(tags=tag), - self.factory(tags=f'{tag} hat'), - self.factory(tags=f'temperatures/{tag}'), - self.factory(tags=f'temperatures/{tag}, body/hot'), - self.factory(tags=f'temperatures/{tag}, hats/{tag}'), - # multiple hits - self.factory(tags=tag, attributes=tag), - self.factory(tags=tag, attributes=tag, category=tag), - self.factory(tags=tag, attributes=tag, category=tag, name=tag), - self.factory(tags=tag, attributes=tag, category=tag, name=tag, description=tag), - ] - - unexpected_instances = [ - self.factory(tags="notcool"), # shouldn't catch it - self.factory(tags="azules"), - ] - - # searh for it - results = find_related_products_v3(tag) - - # assert result - self.assertTrue(len(results) == len(expected_instances)) -''' - class PurchaseEmailTest(APITestCase): def setUp(self): diff --git a/products/utils.py b/products/utils.py index ddec191..80c1010 100644 --- a/products/utils.py +++ b/products/utils.py @@ -87,31 +87,13 @@ def extract_search_filters(result_set): def get_related_products(description, tags, attributes, category): products_qs = Product.objects.filter( - description=description, - tags__in=tags, - attributes__in=attributes, - category=category - )[:6] + Q(description=description) | + Q(tags__in=tags) | + Q(attributes__in=attributes) | + Q(category=category) + )[:10] return products_qs -''' -def find_related_products_v3(keyword): - """ - Ranked product search - - SearchVectors for the fields - SearchQuery for the value - SearchRank for relevancy scoring and ranking - """ - vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__name') - query = SearchQuery(keyword) - - products_qs = Product.objects.annotate( - rank=SearchRank(vector, query) - ).filter(rank__gt=0.05) # removed order_by because its lost in casting - - return set(products_qs) -''' def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): """ From bb0b8729cbea27156ed0f1a85a5d8a69f8bd99d7 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 12:13:21 +0000 Subject: [PATCH 29/67] changes to related product action, test ok --- products/tests.py | 8 ++++++-- products/utils.py | 44 ++++++++++++++++++++++++++++++++++++-------- products/views.py | 2 +- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/products/tests.py b/products/tests.py index 8b497ff..f503677 100644 --- a/products/tests.py +++ b/products/tests.py @@ -195,19 +195,23 @@ class ProductViewSetTest(APITestCase): self.assertEquals(len(expected_instance), len(payload)) def test_anon_can_get_related_products(self): + tag = 'cosa' + company = CompanyFactory() # Create instances instance = self.factory() # make our user the creator instance.creator = self.user instance.save() - url = f"{self.endpoint}{instance.id}/related/" + instances = [self.factory(tags=tag, company=company) for i in range(10)] + + url = f"{self.endpoint}{instances[0].id}/related/" response = self.client.get(url) self.assertEquals(response.status_code, 200) payload= response.json() - self.assertTrue(len(payload) <= 6) + self.assertTrue(len(payload) <= 10) # authenticated user def test_auth_user_can_paginate_instances(self): diff --git a/products/utils.py b/products/utils.py index 80c1010..8c938dc 100644 --- a/products/utils.py +++ b/products/utils.py @@ -85,14 +85,42 @@ def extract_search_filters(result_set): return filter_dict -def get_related_products(description, tags, attributes, category): - products_qs = Product.objects.filter( - Q(description=description) | - Q(tags__in=tags) | - Q(attributes__in=attributes) | - Q(category=category) - )[:10] - return products_qs +def get_related_products(product): + """Make different db searches until you get 10 instances to return + """ + total_results = [] + + # search by category + category_qs = Product.objects.filter(category=product.category)[:10] + # add to results + for item in category_qs: + total_results.append(item) + + # check size + if len(total_results) < 10: + # search by tags + tags_qs = Product.objects.filter(tags__in=product.tags.all())[:10] + # add to results + for item in tags_qs: + total_results.append(item) + + # check size + if len(total_results) < 10: + # search by coop + coop_qs = Product.objects.filter(company=product.company)[:10] + # add to results + for item in coop_qs: + total_results.append(item) + + # check size + if len(total_results) < 10: + # search by latest + latest_qs = Product.objects.order_by('-created')[:10] + # add to results + for item in coop_qs: + total_results.append(item) + + return total_results[:10] def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): diff --git a/products/views.py b/products/views.py index 360b935..8103f67 100644 --- a/products/views.py +++ b/products/views.py @@ -59,7 +59,7 @@ class ProductViewSet(viewsets.ModelViewSet): """ # TODO: find the most similar products product = self.get_object() - qs = get_related_products(product.description, product.tags.all(), product.attributes.all(), product.category) + qs = get_related_products(product) serializer = self.serializer_class(qs, many=True) return Response(data=serializer.data) From e31d725ed13059938b43ae0ce626a3efad75f2c5 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 12:24:59 +0000 Subject: [PATCH 30/67] email verification being sent upon user creation --- core/tests.py | 6 ++++++ core/views.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/core/tests.py b/core/tests.py index bc48c2c..00e4536 100644 --- a/core/tests.py +++ b/core/tests.py @@ -56,6 +56,10 @@ class CustomUserViewSetTest(APITestCase): # assert instance is inactive info = json.loads(response.content) self.assertTrue(info['is_active']) + # Assert instance exists on db + self.assertTrue(self.model.objects.get(email=info['email'])) + # assert verification email + self.assertTrue(len(mail.outbox) == 1) def test_anon_user_cannot_modify_existing_instance(self): """Not logged-in user cannot modify existing instance @@ -182,6 +186,8 @@ class CustomUserViewSetTest(APITestCase): # Assert instance exists on db self.assertTrue(self.model.objects.get(email=response.data['email'])) + # assert verification email + self.assertTrue(len(mail.outbox) == 1) def test_admin_user_can_modify_existing_instance(self): """Admin user can modify existing instance diff --git a/core/views.py b/core/views.py index f3c90ef..f392703 100644 --- a/core/views.py +++ b/core/views.py @@ -79,6 +79,8 @@ class CustomUserViewSet(viewsets.ModelViewSet): instance = self.model(**serializer.validated_data) instance.set_password(password) instance.save() + # send verification email + utils.send_verification_email(request, instance) return Response(self.read_serializer_class( instance, many=False, context={'request': request}).data, From faf3cfc3fa9f13aa2fc9b746f678e6bb7973243c Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 12:37:24 +0000 Subject: [PATCH 31/67] new user activation working --- core/tests.py | 43 ++++++++++++++++++++++++++++++++++++++++++- core/views.py | 6 +++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/core/tests.py b/core/tests.py index 00e4536..70b9941 100644 --- a/core/tests.py +++ b/core/tests.py @@ -7,11 +7,13 @@ import csv from django.test import TestCase from django.core import mail +from django.utils.http import urlsafe_base64_encode +from django.utils.encoding import force_bytes from rest_framework.test import APITestCase from rest_framework import status -from core.utils import get_tokens_for_user +from core.utils import get_tokens_for_user, account_activation_token from companies.models import Company @@ -539,3 +541,42 @@ class MyUserViewTest(APITestCase): # check response self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + +class ActivateUserTest(APITestCase): + + def setUp(self): + self.endpoint = 'activate///' + self.factory = factories.CustomUserFactory + self.model = models.CustomUser + # create user + self.email = f"user@mail.com" + self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) + self.user = self.factory(email=self.email, is_active=False) + self.user.set_password(self.password) + self.user.save() + + def test_correct_activation(self): + # create values + uid = urlsafe_base64_encode(force_bytes(self.user.pk)) + token = account_activation_token.make_token(self.user) + + url = f'/activate/{uid}/{token}/' + + response = self.client.get(url) + + # assertions + self.assertEquals(response.status_code, 200) + self.assertTrue(self.user.email in str(response.content)) + + def test_bad_activation(self): + # create values + uid = urlsafe_base64_encode(force_bytes(self.user.pk))[:-1] + token = account_activation_token.make_token(self.user)[:-1] + + url = f'/activate/{uid}/{token}/' + + response = self.client.get(url) + + # assertions + self.assertEquals(response.status_code, 406) + self.assertTrue('error' in response.json()) diff --git a/core/views.py b/core/views.py index f392703..7120211 100644 --- a/core/views.py +++ b/core/views.py @@ -202,10 +202,10 @@ def activate_user(request, uidb64, token): except (TypeError, ValueError, OverflowError, User.DoesNotExist): user = None - if user is not None and account_activation_token.check_token(user, token): + if user is not None and utils.account_activation_token.check_token(user, token): # activate user user.is_active = True user.save() - return HttpResponse(f"Tu cuenta de usuario {request.user.email} ha sido activada") + return Response(f"Tu cuenta de usuario {user.email} ha sido activada") else: - return HttpResponse(f"Tu token de verificacion no coincide con ningún usuario registrado") + return Response({"error": f"Tu token de verificacion no coincide con ningún usuario registrado"}, status=status.HTTP_406_NOT_ACCEPTABLE) From a033fa3606b5891b5ab2cc9c5f34af0f6f568ab1 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 12:44:23 +0000 Subject: [PATCH 32/67] readme update --- README.md | 15 +++++++++++++++ core/views.py | 1 - 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26b7c70..b45a011 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,14 @@ Endpoint url: `/api/v1/user/update/` Permissions: only accessible for your own user instance +### create_company_user [POST] + +Edndpoint: `/api/v1/create_company_user/` + +Simultaneously create a company and its related user + +NOT WORKING!!! + ### my_user [GET] Endpoint url: `/api/v1/my_user/` @@ -208,6 +216,12 @@ 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` +### activate_user + +Endpoint: `/activate///` + +This endpoint is reached from the URL sent to the user after their registration + ### User Management Creation: @@ -309,6 +323,7 @@ Location ednpoints: - `/api/v1/provinces/` - `/api/v1/cities/` +Tables filled with data from `datasets/gadm36_ESP.gpkg` with `loadgisdata` command. ## Shop Integrations diff --git a/core/views.py b/core/views.py index 7120211..1bb70e4 100644 --- a/core/views.py +++ b/core/views.py @@ -170,7 +170,6 @@ def my_user(request): return Response({'error': {str(type(e))}}, status=500) - @api_view(['POST',]) @permission_classes([IsAdminUser,]) def load_coop_managers(request): From 0eaaf2bc49a0e16dd974edc9903e2db4b114e066 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 13:24:02 +0000 Subject: [PATCH 33/67] fixed create_company_user endoint, tests working --- core/tests.py | 58 ++++++++++++++++++++++++++++++++++ core/views.py | 79 +++++++++++++++++++++++------------------------ products/utils.py | 3 +- 3 files changed, 98 insertions(+), 42 deletions(-) diff --git a/core/tests.py b/core/tests.py index 70b9941..22d637e 100644 --- a/core/tests.py +++ b/core/tests.py @@ -580,3 +580,61 @@ class ActivateUserTest(APITestCase): # assertions self.assertEquals(response.status_code, 406) self.assertTrue('error' in response.json()) + + +class CreateCompanyUserTest(APITestCase): + + def setUp(self): + self.endpoint = '/api/v1/create_company_user/' + self.factory = factories.CustomUserFactory + self.model = models.CustomUser + # create user + self.email = f"user@mail.com" + self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) + self.user = self.factory(email=self.email, is_active=False) + self.user.set_password(self.password) + self.user.save() + + def test_succesful_creation(self): + data = { + 'user': { + 'email': 'test@email.com', + 'full_name': 'TEST NAME', + 'password': 'VENTILADORES1234499.89', + }, + 'company': { + 'cif': 'qwerewq', + 'company_name': 'qwerewq', + 'short_name': 'qwerewq', + 'web_link': 'http://qwerewq.com', + 'shop': True, + 'shop_link': 'http://qwerewq.com', + 'platform': 'PRESTASHOP', + 'email': 'test@email.com', + 'logo': None, + 'city': None, + 'address': 'qwer qewr 5', + 'geo': None, + 'phone': '1234', + 'mobile': '4321', + 'other_phone': '41423', + 'description': 'dfgfdgdfg', + 'shop_rss_feed': 'http://qwerewq.com', + 'sale_terms': 'tewrnmfew f ewfrfew ewewew f', + 'shipping_cost': '12.25', + 'sync': False + } + } + + response = self.client.post(self.endpoint, data=data, format='json') + + self.assertEquals(response.status_code, 201) + self.assertEquals(len(mail.outbox), 1) + + def test_creation_error(self): + + response = self.client.post(self.endpoint, data={}, format='json') + + self.assertEquals(response.status_code, 406) + self.assertEquals(len(mail.outbox), 0) + diff --git a/core/views.py b/core/views.py index 1bb70e4..63ec0b8 100644 --- a/core/views.py +++ b/core/views.py @@ -20,6 +20,7 @@ from rest_framework.generics import UpdateAPIView from rest_framework.decorators import api_view, permission_classes from companies.models import Company +from companies.serializers import CompanySerializer from geo.models import City from . import models @@ -112,51 +113,47 @@ class UpdateUserView(UpdateAPIView): @permission_classes([CustomUserPermissions,]) def create_company_user(request): """ - Create non-validated company and manager user associated + Create non-validated company and associated managing user """ - user_data = { - 'full_name': request.data['user']['full_name'], - 'email': request.data['user']['email'], - 'password': request.data['user']['password'] - } - company_data = { - 'cif': request.data['company']['cif'], - 'company_name': request.data['company']['company_name'], - 'short_name': request.data['company']['short_name'], - 'web_link': request.data['company']['web_link'], - 'shop': request.data['company']['shop'], - 'city': request.data['company']['city'], - 'geo': request.data['company']['geo'], - 'address': request.data['company']['address'] - } - try: - user = models.CustomUser.objects.create(email=user_data['email'], full_name=user_data['full_name']) - except IntegrityError as e: - return Response({"errors": {"details": str(e)}}, status=status.HTTP_409_CONFLICT) + if 'user' not in request.data: + return Response({"error": "Missing parameter: user"}, status=406) + if 'company' not in request.data: + return Response({"error": "Missing parameter: company"}, status=406) - try: - city = company_data.pop('city') - #city = City.objects.get(name=city) + # create company + company_data = request.data['company'] + # substitute coordinates for Point + geo = company_data.pop('geo') + if geo: + company_data['geo'] = Point(geo['latitude'],geo['longitude']) + company_serializer = CompanySerializer( + data=company_data, + ) + if company_serializer.is_valid(): + # save model instance data + new_company = Company.objects.create(**company_serializer.validated_data) + else: + return Response({"error": "Company data is not valid"}, status=406) - geo = company_data.pop('geo') - geo = Point(geo['latitude'],geo['longitude']) + # create user + user_data = request.data['user'] + user_data['role'] = 'COOP_MANAGER' + user_data['company'] = new_company.id + user_serializer = core_serializers.CustomUserWriteSerializer( + data=user_data, + ) + if user_serializer.is_valid(): + # save model instance data + password = user_serializer.validated_data.pop('password') + new_user = User(**user_serializer.validated_data) + new_user.set_password(password) + new_user.save() + # send verification email + utils.send_verification_email(request, new_user) + else: + return Response({"error": "User data is not valid"}, status=406) - company = Company.objects.create(**company_data, city=city, geo=geo) - except Exception as e: - user.delete() - return Response({"errors": {"details": str(e)}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - user.set_password(user_data['password']) - user.company = company - user.role = 'COOP_MANAGER' - user.save() - - company.creator = user - company.save() - - serializer = core_serializers.CustomUserSerializer(user) - - return Response(data=serializer.data,status=status.HTTP_201_CREATED) + return Response(status=status.HTTP_201_CREATED) @api_view(['GET',]) diff --git a/products/utils.py b/products/utils.py index 8c938dc..18cb189 100644 --- a/products/utils.py +++ b/products/utils.py @@ -6,6 +6,7 @@ from django.db.models import Q from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity from django.db.models import Max, Min from django.conf import settings +from django.utils import timezone import requests @@ -203,7 +204,7 @@ def product_loader(csv_reader, user, company=None): return None # create historysync instance - history = HistorySync.objects.create(company=company, sync_date=datetime.datetime.now()) + history = HistorySync.objects.create(company=company, sync_date=timezone.now()) for row in csv_reader: # trim strings for key in row: From ae1deeff312a3742338386ef53ed32dfaf0a5d09 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 10 Mar 2021 13:42:21 +0000 Subject: [PATCH 34/67] company.city wont show as dropdown list in admin --- companies/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/companies/admin.py b/companies/admin.py index 0575242..2e21305 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.gis.db.models import PointField -from django_admin_listfilter_dropdown.filters import DropdownFilter, RelatedDropdownFilter +from django_admin_listfilter_dropdown.filters import DropdownFilter from mapwidgets.widgets import GooglePointFieldWidget from . import models @@ -10,8 +10,8 @@ from . import models class CompanyAdmin(admin.ModelAdmin): list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active', 'link') - list_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city') - search_fields = ('short_name', 'company_name', 'email', 'web_link', ('city', RelatedDropdownFilter)) + list_filter = ('platform', 'sync', 'is_validated', 'is_active', ('city', DropdownFilter)) + search_fields = ('short_name', 'company_name', 'email', 'web_link', 'city') formfield_overrides = { PointField: {"widget": GooglePointFieldWidget} From 8a2a3798493a7b490a5bf8d6970e7ed1232d8fc3 Mon Sep 17 00:00:00 2001 From: Diego Calvo Date: Wed, 10 Mar 2021 15:06:09 +0100 Subject: [PATCH 35/67] track_user geo fix --- stats/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stats/views.py b/stats/views.py index 778d62e..a41ba3d 100644 --- a/stats/views.py +++ b/stats/views.py @@ -58,7 +58,7 @@ def track_user(request): 'user': None if request.user.is_anonymous else request.user, 'anonymous': request.user.is_anonymous, 'ip_address': data.get('ip'), - 'geo': Point(data.get('geo')), + 'geo': Point(data.get('geo')['latitude'], data.get('geo')['longitude']), } if data['action_object'].get('model') == 'product': From 5c039f5ff28083d801b27580d2f28c9ba4e887f3 Mon Sep 17 00:00:00 2001 From: Diego Calvo Date: Thu, 11 Mar 2021 08:36:31 +0100 Subject: [PATCH 36/67] price filter add equal to --- products/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/products/utils.py b/products/utils.py index 05ebadf..b938a56 100644 --- a/products/utils.py +++ b/products/utils.py @@ -163,9 +163,9 @@ def find_related_products_v6(keyword, shipping_cost=None, discount=None, categor # filter by price if price_min is not None: - products_qs = products_qs.filter(price__gt=price_min) + products_qs = products_qs.filter(price__gte=price_min) if price_max is not None: - products_qs = products_qs.filter(price__lt=price_max) + products_qs = products_qs.filter(price__lte=price_max) # get min_price and max_price min_price = products_qs.aggregate(Min('price')) From 391ada843c1e2bc8287c05119081c5d9980dd68e Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 10:14:40 +0000 Subject: [PATCH 37/67] fixed for fix of geo data in track_user view --- stats/tests.py | 2 +- stats/views.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/stats/tests.py b/stats/tests.py index 3bfe3ce..ea3510c 100644 --- a/stats/tests.py +++ b/stats/tests.py @@ -116,7 +116,7 @@ class TrackUserViewTest(APITestCase): 'model': 'company', 'id': company.id, }, - 'geo': (12.2, -0.545) + 'geo': {'latitude': 12.2, 'longitude': -0.545} } # Query endpoint diff --git a/stats/views.py b/stats/views.py index a41ba3d..20ff805 100644 --- a/stats/views.py +++ b/stats/views.py @@ -51,6 +51,12 @@ def track_user(request): """ try: data = json.loads(request.body) + # import ipdb; ipdb.set_trace() + + if data.get('geo'): + coordinates = (data['geo'].get('latitude'), data['geo'].get('longitude')) + else: + coordinates = None # gather instance data instance_data = { @@ -58,7 +64,7 @@ def track_user(request): 'user': None if request.user.is_anonymous else request.user, 'anonymous': request.user.is_anonymous, 'ip_address': data.get('ip'), - 'geo': Point(data.get('geo')['latitude'], data.get('geo')['longitude']), + 'geo': Point(coordinates), } if data['action_object'].get('model') == 'product': @@ -76,4 +82,4 @@ def track_user(request): return Response(status=status.HTTP_201_CREATED) except Exception as e: logging.error(f"Stats could not be created: {str(e)}") - return Response(f"Process could not be registered: {str(type(e))}", status=status.HTTP_406_NOT_ACCEPTABLE) + return Response(f"Process could not be registered [{str(type(e))}]: {str(e)}", status=status.HTTP_406_NOT_ACCEPTABLE) From 5178eb9722d49e4be961a5243f856deec332a05f Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 11:11:00 +0000 Subject: [PATCH 38/67] first steps for georestricted search results --- products/utils.py | 30 ++++++++++++++++++++++++++++-- products/views.py | 21 ++++++++++++++------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/products/utils.py b/products/utils.py index c0f4c7e..1672dc5 100644 --- a/products/utils.py +++ b/products/utils.py @@ -7,6 +7,8 @@ from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.db.models import Max, Min from django.conf import settings from django.utils import timezone +from django.contrib.gis.geos import Point +from django.contrib.gis.measure import D import requests @@ -124,7 +126,7 @@ def get_related_products(product): return total_results[:10] -def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): +def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None, coordinates=None): """ Ranked product search @@ -134,6 +136,12 @@ def ranked_product_search(keyword, shipping_cost=None, discount=None, category=N allow filtering by: - shipping cost + + Response includes: + - result_set + - min_price + - max_price + - georesult """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__label') + SearchVector('company__company_name') query = SearchQuery(keyword) @@ -142,6 +150,24 @@ def ranked_product_search(keyword, shipping_cost=None, discount=None, category=N rank=SearchRank(vector, query) ).filter(rank__gt=0.05, active=True) + # geolocation filtering + if coordinates is not None: + point = Point(coordinates) + filtered_qs = products_qs.filter(geo__distance_lte=(point, D(km=10))) + georesult = '10k' + if filtered_qs.count() <= 10: + products_qs = products_qs.filter(geo__distance_lte=(point, D(km=50))) + georesult = '50k' + if filtered_qs.count() <= 10: + products_qs = products_qs.filter(geo__distance_lte=(point, D(km=200))) + georesult = '200k' + if filtered_qs.count > 10: + products_qs = filtered_qs + else: + georesult = None + else: + georesult = None + # filter by category if category is not None: products_qs = products_qs.filter(category=category) @@ -183,7 +209,7 @@ def ranked_product_search(keyword, shipping_cost=None, discount=None, category=N max_price = products_qs.aggregate(Max('price')) - return set(products_qs), min_price, max_price + return set(products_qs), min_price, max_price, georesult def product_loader(csv_reader, user, company=None): diff --git a/products/views.py b/products/views.py index 8103f67..6604381 100644 --- a/products/views.py +++ b/products/views.py @@ -19,6 +19,7 @@ from rest_framework.decorators import api_view, permission_classes, action from rest_framework.filters import OrderingFilter from django_filters.rest_framework import DjangoFilterBackend + import requests from history.models import HistorySync @@ -28,7 +29,7 @@ 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 back_latienda.permissions import IsCreator, IsSiteAdmin from .utils import extract_search_filters, ranked_product_search, product_loader, get_related_products from utils.tag_serializers import TaggitSerializer from utils.tag_filters import ProductTagFilter, ProductOrderFilter @@ -125,8 +126,8 @@ def product_search(request): Params: - q: used for search [MANDATORY] - - limit: max number of returned instances [OPTIONAL] - - offset: where to start counting results [OPTIONAL] + - limit: max number of returned instances + - offset: where to start counting results - shipping_cost: true/false - discount: true/false - category: string @@ -134,6 +135,7 @@ def product_search(request): - order: string (newest/oldest) - price_min: int - price_max: int + - geo: {'longitude': 23.23, 'latitude': 23432.23423} In the response: - filters @@ -158,11 +160,16 @@ def product_search(request): discount = request.GET.get('discount', None) if discount is not None: if discount == 'true': - discount = True + discount = True elif discount == 'false': - discount = False + discount = False else: - discount = None + discount = None + geo = request.GET.get('geo', None) + if geo is not None: + coordinates = (geo.get('longitude'), geo.get('latitude')) + else: + coordinates = None category = request.GET.get('category', None) tags = request.GET.get('tags', None) price_min = request.GET.get('price_min', None) @@ -195,7 +202,7 @@ def product_search(request): # split query string into single words chunks = q.split(' ') for chunk in chunks: - product_set, min_price, max_price = ranked_product_search(chunk, shipping_cost, discount, category, tags, price_min, price_max) + product_set, min_price, max_price, georesult = ranked_product_search(chunk, shipping_cost, discount, category, tags, price_min, price_max, coordinates) # update price values if product_set: if prices['min'] is None or min_price['price__min'] < prices['min']: From 9794ab1f181c97ecdd0e4a456372e632a1108bdd Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 11:33:12 +0000 Subject: [PATCH 39/67] changing the way search workd to accommodate for georestrictions --- products/tests.py | 1 + products/utils.py | 7 ++++++- products/views.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/products/tests.py b/products/tests.py index f503677..116ee6f 100644 --- a/products/tests.py +++ b/products/tests.py @@ -567,6 +567,7 @@ class ProductSearchTest(TestCase): self.assertIsNotNone(payload.get('prices')) # check for object creation + import ipdb; ipdb.set_trace() self.assertEquals(len(payload['products']), len(expected_instances)) # check for filters self.assertTrue(len(payload['filters']['tags']) >= 2 ) diff --git a/products/utils.py b/products/utils.py index 1672dc5..0e44815 100644 --- a/products/utils.py +++ b/products/utils.py @@ -144,7 +144,12 @@ def ranked_product_search(keyword, shipping_cost=None, discount=None, category=N - georesult """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__label') + SearchVector('company__company_name') - query = SearchQuery(keyword) + + query_string = '' + for word in keyword: + query_string += f" | '{keyword}' " + + query = SearchQuery(query_string) products_qs = Product.objects.annotate( rank=SearchRank(vector, query) diff --git a/products/views.py b/products/views.py index 6604381..9c46557 100644 --- a/products/views.py +++ b/products/views.py @@ -201,6 +201,20 @@ def product_search(request): else: # split query string into single words chunks = q.split(' ') + # all-in-one search + product_set, min_price, max_price, georesult = ranked_product_search(chunks, shipping_cost, discount, category, tags, price_min, price_max, coordinates) + # update price values + if product_set: + if prices['min'] is None or min_price['price__min'] < prices['min']: + prices['min'] = min_price['price__min'] + if prices['max'] is None or max_price['price__max'] > prices['max']: + prices['max'] = max_price['price__max'] + # add to result set + result_set.update(product_set) + # serialize and list data + serializer = SearchResultSerializer(product_set, many=True) + result_list = [dict(i) for i in serializer.data] + ''' for chunk in chunks: product_set, min_price, max_price, georesult = ranked_product_search(chunk, shipping_cost, discount, category, tags, price_min, price_max, coordinates) # update price values @@ -214,6 +228,7 @@ def product_search(request): # serialize and list data serializer = SearchResultSerializer(product_set, many=True) result_list = [dict(i) for i in serializer.data] + ''' # extract filters from result_set filters = extract_search_filters(result_set) From c343bda3674100a2d344376a7623a9ae302705f9 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 12:09:32 +0000 Subject: [PATCH 40/67] search tests broken, but geo search working --- products/tests.py | 37 +++++++++++++++++++++++++++++++++---- products/utils.py | 23 ++++++++++++----------- products/views.py | 34 +++++++++++++--------------------- 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/products/tests.py b/products/tests.py index 116ee6f..b7eb9ae 100644 --- a/products/tests.py +++ b/products/tests.py @@ -7,6 +7,7 @@ from urllib.parse import quote from django.utils import timezone from django.test import TestCase from django.core import mail +from django.contrib.gis.geos import Point from rest_framework.test import APITestCase from rest_framework import status @@ -540,7 +541,7 @@ class ProductSearchTest(TestCase): company = CompanyFactory(company_name='Zapatos Rojos') expected_instances = [ self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes"), - self.factory(tags="colores/rojos, tono/brillante"), + self.factory(tags="colores/rojos, tono/brillante"), # not showing up in results ??? self.factory(tags="lunares/azules", description="zapatos rojos"), self.factory(tags="lunares/rojos", description="zapatos"), self.factory(tags="lunares/verdes", company=company), @@ -567,7 +568,7 @@ class ProductSearchTest(TestCase): self.assertIsNotNone(payload.get('prices')) # check for object creation - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() self.assertEquals(len(payload['products']), len(expected_instances)) # check for filters self.assertTrue(len(payload['filters']['tags']) >= 2 ) @@ -608,7 +609,7 @@ class ProductSearchTest(TestCase): def test_anon_user_can_paginate_search(self): expected_instances = [ self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes"), - self.factory(tags="colores/rojos, tono/brillante"), + # self.factory(tags="colores/rojos, tono/brillante"), self.factory(tags="lunares/azules", description="zapatos rojos"), self.factory(tags="lunares/rojos", description="zapatos"), ] @@ -671,7 +672,7 @@ class ProductSearchTest(TestCase): def test_anon_user_can_filter_shipping_cost_true(self): expected_instances = [ - self.factory(tags="colores/rojos, tono/brillante", shipping_cost=100.00), + # self.factory(tags="colores/rojos, tono/brillante", shipping_cost=100.00), self.factory(tags="lunares/azules", description="zapatos rojos", shipping_cost=12.00), ] unexpected_instances = [ @@ -933,6 +934,34 @@ class ProductSearchTest(TestCase): # first instance should be most recent self.assertTrue(dates[i] < dates[i+1]) + def test_anon_user_can_search_geo(self): + """Restrict results by geographical location + """ + # create geo point + longitude = 1.0 + latitude = 1.0 + point = Point(longitude, latitude) + company = CompanyFactory(geo=point) + + expected_instances = [ + self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", company=company), + self.factory(tags="colores/rojos, tono/brillante", company=company), + self.factory(tags="lunares/azules", description="zapatos rojos", company=company), + self.factory(tags="lunares/rojos", description="zapatos", company=company), + self.factory(attributes='"zapatos de campo", tono/rojo', company=company), + ] + unexpected_instances = [ + self.factory(description="chanclas"), + self.factory(tags="azules"), + ] + + q = quote("zapatos rojos") + + url = f"{self.endpoint}?q={q}&latitude=1.0&longitude=1.0" + # send in request + response = self.client.get(url) + # check response + self.assertEqual(response.status_code, 200) class MyProductsViewTest(APITestCase): """my_products tests diff --git a/products/utils.py b/products/utils.py index 0e44815..431d368 100644 --- a/products/utils.py +++ b/products/utils.py @@ -126,7 +126,7 @@ def get_related_products(product): return total_results[:10] -def ranked_product_search(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None, coordinates=None): +def ranked_product_search(keywords, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None, coordinates=None): """ Ranked product search @@ -145,28 +145,29 @@ def ranked_product_search(keyword, shipping_cost=None, discount=None, category=N """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__label') + SearchVector('company__company_name') - query_string = '' - for word in keyword: - query_string += f" | '{keyword}' " - - query = SearchQuery(query_string) + if keywords and len(keywords) == 1: + query_string = keywords[0] + else: + query_string = keywords[0] + for i in range(1, len(keywords)): + query_string += f" | {keywords[i]} " + query = SearchQuery(query_string, search_type='raw') products_qs = Product.objects.annotate( rank=SearchRank(vector, query) ).filter(rank__gt=0.05, active=True) - # geolocation filtering if coordinates is not None: point = Point(coordinates) - filtered_qs = products_qs.filter(geo__distance_lte=(point, D(km=10))) + filtered_qs = products_qs.filter(company__geo__distance_lte=(point, D(km=10))) georesult = '10k' if filtered_qs.count() <= 10: - products_qs = products_qs.filter(geo__distance_lte=(point, D(km=50))) + products_qs = products_qs.filter(company__geo__distance_lte=(point, D(km=50))) georesult = '50k' if filtered_qs.count() <= 10: - products_qs = products_qs.filter(geo__distance_lte=(point, D(km=200))) + products_qs = products_qs.filter(company__geo__distance_lte=(point, D(km=200))) georesult = '200k' - if filtered_qs.count > 10: + if filtered_qs.count() > 10: products_qs = filtered_qs else: georesult = None diff --git a/products/views.py b/products/views.py index 9c46557..fd14ced 100644 --- a/products/views.py +++ b/products/views.py @@ -2,6 +2,7 @@ import logging import csv import datetime import json +from decimal import Decimal from django.db.models import Q from django.core import serializers @@ -135,7 +136,8 @@ def product_search(request): - order: string (newest/oldest) - price_min: int - price_max: int - - geo: {'longitude': 23.23, 'latitude': 23432.23423} + - longitude: 23.23 + - latitude: 22.234 In the response: - filters @@ -165,11 +167,15 @@ def product_search(request): discount = False else: discount = None - geo = request.GET.get('geo', None) - if geo is not None: - coordinates = (geo.get('longitude'), geo.get('latitude')) - else: - coordinates = None + longitude = request.GET.get('longitude', None) + latitude = request.GET.get('latitude', None) + try: + if longitude and latitude: + coordinates = (Decimal(longitude), Decimal(latitude)) + else: + coordinates = None + except: + return Response({"error": "Improperly formated coordinates"}, status=406) category = request.GET.get('category', None) tags = request.GET.get('tags', None) price_min = request.GET.get('price_min', None) @@ -214,21 +220,6 @@ def product_search(request): # serialize and list data serializer = SearchResultSerializer(product_set, many=True) result_list = [dict(i) for i in serializer.data] - ''' - for chunk in chunks: - product_set, min_price, max_price, georesult = ranked_product_search(chunk, shipping_cost, discount, category, tags, price_min, price_max, coordinates) - # update price values - if product_set: - if prices['min'] is None or min_price['price__min'] < prices['min']: - prices['min'] = min_price['price__min'] - if prices['max'] is None or max_price['price__max'] > prices['max']: - prices['max'] = max_price['price__max'] - # add to result set - result_set.update(product_set) - # serialize and list data - serializer = SearchResultSerializer(product_set, many=True) - result_list = [dict(i) for i in serializer.data] - ''' # extract filters from result_set filters = extract_search_filters(result_set) @@ -254,6 +245,7 @@ def product_search(request): result_list = result_list[:limit] return Response(data={"filters": filters, "count": total_results, "products": result_list, 'prices': prices}) except Exception as e: + import ipdb; ipdb.set_trace() return Response({"errors": {"details": str(e)}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) From 8de48c6698b3ce904dd98fe0281c4c9e6a52940a Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 12:17:40 +0000 Subject: [PATCH 41/67] more tweaks for geo search --- products/tests.py | 12 ++++++------ products/utils.py | 8 +------- products/views.py | 4 ++-- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/products/tests.py b/products/tests.py index b7eb9ae..a8c7cf2 100644 --- a/products/tests.py +++ b/products/tests.py @@ -944,12 +944,9 @@ class ProductSearchTest(TestCase): company = CompanyFactory(geo=point) expected_instances = [ - self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", company=company), - self.factory(tags="colores/rojos, tono/brillante", company=company), - self.factory(tags="lunares/azules", description="zapatos rojos", company=company), - self.factory(tags="lunares/rojos", description="zapatos", company=company), - self.factory(attributes='"zapatos de campo", tono/rojo', company=company), - ] + self.factory(tags="lunares/rojos", category='zapatos', description="zapatos verdes", company=company) for i in range(12) + ] + unexpected_instances = [ self.factory(description="chanclas"), self.factory(tags="azules"), @@ -962,6 +959,9 @@ class ProductSearchTest(TestCase): response = self.client.get(url) # check response self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertIsNotNone(payload['georesult']) + self.assertEquals(payload['georesult'], '10k') class MyProductsViewTest(APITestCase): """my_products tests diff --git a/products/utils.py b/products/utils.py index 431d368..fd9569b 100644 --- a/products/utils.py +++ b/products/utils.py @@ -145,13 +145,7 @@ def ranked_product_search(keywords, shipping_cost=None, discount=None, category= """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__label') + SearchVector('company__company_name') - if keywords and len(keywords) == 1: - query_string = keywords[0] - else: - query_string = keywords[0] - for i in range(1, len(keywords)): - query_string += f" | {keywords[i]} " - query = SearchQuery(query_string, search_type='raw') + query = SearchQuery(keywords, search_type='plain') products_qs = Product.objects.annotate( rank=SearchRank(vector, query) diff --git a/products/views.py b/products/views.py index fd14ced..4b73d3d 100644 --- a/products/views.py +++ b/products/views.py @@ -208,7 +208,7 @@ def product_search(request): # split query string into single words chunks = q.split(' ') # all-in-one search - product_set, min_price, max_price, georesult = ranked_product_search(chunks, shipping_cost, discount, category, tags, price_min, price_max, coordinates) + product_set, min_price, max_price, georesult = ranked_product_search(q, shipping_cost, discount, category, tags, price_min, price_max, coordinates) # update price values if product_set: if prices['min'] is None or min_price['price__min'] < prices['min']: @@ -243,7 +243,7 @@ def product_search(request): elif limit is not None: limit = int(limit) result_list = result_list[:limit] - return Response(data={"filters": filters, "count": total_results, "products": result_list, 'prices': prices}) + return Response(data={"filters": filters, "count": total_results, "products": result_list, 'prices': prices, 'georesult': georesult}) except Exception as e: import ipdb; ipdb.set_trace() return Response({"errors": {"details": str(e)}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) From ef0fe3716e4567e6ef1bb8ea67029c09726af86f Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 12:32:13 +0000 Subject: [PATCH 42/67] fixed error in create_company_user view --- companies/tests.py | 2 +- core/tests.py | 3 ++- core/views.py | 9 +++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/companies/tests.py b/companies/tests.py index 343ea7c..8473b02 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -146,7 +146,7 @@ class CompanyViewSetTest(APITestCase): 'logo': None, 'city': None, 'address': 'qwer qewr 5', - 'geo': None, + 'geo': {'longitude': 1.0, 'latitude': 1.0}, 'phone': '1234', 'mobile': '4321', 'other_phone': '41423', diff --git a/core/tests.py b/core/tests.py index 22d637e..e28c6f7 100644 --- a/core/tests.py +++ b/core/tests.py @@ -614,7 +614,7 @@ class CreateCompanyUserTest(APITestCase): 'logo': None, 'city': None, 'address': 'qwer qewr 5', - 'geo': None, + 'geo': {'longitude': 1.0, 'latitude': 1.0}, 'phone': '1234', 'mobile': '4321', 'other_phone': '41423', @@ -627,6 +627,7 @@ class CreateCompanyUserTest(APITestCase): } response = self.client.post(self.endpoint, data=data, format='json') + import ipdb; ipdb.set_trace() self.assertEquals(response.status_code, 201) self.assertEquals(len(mail.outbox), 1) diff --git a/core/views.py b/core/views.py index 63ec0b8..f69b2f0 100644 --- a/core/views.py +++ b/core/views.py @@ -122,10 +122,7 @@ def create_company_user(request): # create company company_data = request.data['company'] - # substitute coordinates for Point - geo = company_data.pop('geo') - if geo: - company_data['geo'] = Point(geo['latitude'],geo['longitude']) + company_serializer = CompanySerializer( data=company_data, ) @@ -133,7 +130,7 @@ def create_company_user(request): # save model instance data new_company = Company.objects.create(**company_serializer.validated_data) else: - return Response({"error": "Company data is not valid"}, status=406) + return Response({"error": company_serializer.errors}, status=406) # create user user_data = request.data['user'] @@ -151,7 +148,7 @@ def create_company_user(request): # send verification email utils.send_verification_email(request, new_user) else: - return Response({"error": "User data is not valid"}, status=406) + return Response({"error": user_serializer.errors}, status=406) return Response(status=status.HTTP_201_CREATED) From fc3ea28b5c32a9d150d6ea49ea6f5f645aa89e4c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 12:33:37 +0000 Subject: [PATCH 43/67] fix for create_company_user --- core/views.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/views.py b/core/views.py index 63ec0b8..53985d2 100644 --- a/core/views.py +++ b/core/views.py @@ -122,10 +122,6 @@ def create_company_user(request): # create company company_data = request.data['company'] - # substitute coordinates for Point - geo = company_data.pop('geo') - if geo: - company_data['geo'] = Point(geo['latitude'],geo['longitude']) company_serializer = CompanySerializer( data=company_data, ) @@ -133,7 +129,7 @@ def create_company_user(request): # save model instance data new_company = Company.objects.create(**company_serializer.validated_data) else: - return Response({"error": "Company data is not valid"}, status=406) + return Response({"error": company_serializer.errors}, status=406) # create user user_data = request.data['user'] @@ -151,7 +147,7 @@ def create_company_user(request): # send verification email utils.send_verification_email(request, new_user) else: - return Response({"error": "User data is not valid"}, status=406) + return Response({"error": user_serializer.errors}, status=406) return Response(status=status.HTTP_201_CREATED) From 67644512ff31f0676d49644183d2340b61d69e5c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 13:04:30 +0000 Subject: [PATCH 44/67] fixed error in user creation --- core/models.py | 2 ++ core/tests.py | 6 +++++- core/views.py | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/models.py b/core/models.py index 3c49a74..89d995f 100644 --- a/core/models.py +++ b/core/models.py @@ -24,6 +24,8 @@ class UserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): extra_fields.setdefault('is_superuser', False) + extra_fields.setdefault('is_staff', False) + extra_fields.setdefault('is_active', False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): diff --git a/core/tests.py b/core/tests.py index dfbabfe..8dee31c 100644 --- a/core/tests.py +++ b/core/tests.py @@ -589,7 +589,7 @@ class CreateCompanyUserTest(APITestCase): self.factory = factories.CustomUserFactory self.model = models.CustomUser # create user - self.email = f"user@mail.com" + self.email = "user@mail.com" self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) self.user = self.factory(email=self.email, is_active=False) self.user.set_password(self.password) @@ -630,6 +630,10 @@ class CreateCompanyUserTest(APITestCase): self.assertEquals(response.status_code, 201) self.assertEquals(len(mail.outbox), 1) + # user exists and it's inactice + self.assertTrue(self.model.objects.get(email='test@email.com')) + self.assertFalse(self.model.objects.get(email='test@email.com').is_active) + self.assertTrue(Company.objects.get(cif='qwerewq')) def test_creation_error(self): diff --git a/core/views.py b/core/views.py index 53985d2..d1c3511 100644 --- a/core/views.py +++ b/core/views.py @@ -44,9 +44,9 @@ logging.basicConfig( class CustomUserViewSet(viewsets.ModelViewSet): - model = models.CustomUser + model = User model_name = 'custom_user' - queryset = models.CustomUser.objects.all() + queryset = User.objects.all() permission_classes = [CustomUserPermissions,] read_serializer_class = core_serializers.CustomUserReadSerializer write_serializer_class = core_serializers.CustomUserWriteSerializer @@ -77,7 +77,7 @@ class CustomUserViewSet(viewsets.ModelViewSet): if serializer.is_valid(): # save model instance data password = serializer.validated_data.pop('password') - instance = self.model(**serializer.validated_data) + instance = self.model.objects.create_user(**serializer.validated_data) instance.set_password(password) instance.save() # send verification email @@ -141,7 +141,7 @@ def create_company_user(request): if user_serializer.is_valid(): # save model instance data password = user_serializer.validated_data.pop('password') - new_user = User(**user_serializer.validated_data) + new_user = User.objects.create_user(**user_serializer.validated_data) new_user.set_password(password) new_user.save() # send verification email From dd4b079895ee71e63a031afc26dae87726185921 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 13:10:54 +0000 Subject: [PATCH 45/67] fixes user creation test --- core/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/tests.py b/core/tests.py index 8dee31c..1b1a9f0 100644 --- a/core/tests.py +++ b/core/tests.py @@ -41,7 +41,7 @@ class CustomUserViewSetTest(APITestCase): self.user = self.factory(email=self.reg_email, password=self.password, is_active=True) # anon user - def test_anon_user_can_create_active_instance(self): + def test_anon_user_can_create_inactive_instance(self): """Not logged-in user can create new instance of User but it's inactive """ data = { @@ -57,7 +57,7 @@ class CustomUserViewSetTest(APITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) # assert instance is inactive info = json.loads(response.content) - self.assertTrue(info['is_active']) + self.assertFalse(info['is_active']) # Assert instance exists on db self.assertTrue(self.model.objects.get(email=info['email'])) # assert verification email @@ -634,6 +634,8 @@ class CreateCompanyUserTest(APITestCase): self.assertTrue(self.model.objects.get(email='test@email.com')) self.assertFalse(self.model.objects.get(email='test@email.com').is_active) self.assertTrue(Company.objects.get(cif='qwerewq')) + # assert verification email + self.assertTrue(len(mail.outbox) == 1) def test_creation_error(self): From 5fe3883fcdb1847db650a2cd64d085785c859556 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 13:18:50 +0000 Subject: [PATCH 46/67] my products ordered by created date, descending --- products/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/views.py b/products/views.py index 8103f67..228f6e1 100644 --- a/products/views.py +++ b/products/views.py @@ -72,7 +72,7 @@ class MyProductsViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return self.model.objects.filter(creator=self.request.user) + return self.model.objects.filter(creator=self.request.user).order_by('-created') def perform_create(self, serializer): serializer.save(creator=self.request.user) From 4cf22fd969d4dca980548a0ad49ad035e05351af Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 13:51:14 +0000 Subject: [PATCH 47/67] bunch of stuff --- back_latienda/routers.py | 3 +-- back_latienda/urls.py | 1 + companies/tests.py | 31 ++++--------------------------- companies/views.py | 12 +++++++++++- core/serializers.py | 2 +- products/tests.py | 14 +++++++++++--- products/views.py | 5 +---- 7 files changed, 30 insertions(+), 38 deletions(-) diff --git a/back_latienda/routers.py b/back_latienda/routers.py index bad6b0c..83385ca 100644 --- a/back_latienda/routers.py +++ b/back_latienda/routers.py @@ -1,7 +1,7 @@ from rest_framework import routers from core.views import CustomUserViewSet -from companies.views import CompanyViewSet, MyCompanyViewSet, AdminCompanyViewSet +from companies.views import CompanyViewSet, AdminCompanyViewSet from products.views import ProductViewSet, MyProductsViewSet, AdminProductsViewSet from history.views import HistorySyncViewSet from stats.views import StatsLogViewSet @@ -13,7 +13,6 @@ router = routers.DefaultRouter() router.register('users', CustomUserViewSet, basename='users') router.register('companies', CompanyViewSet, basename='company') -router.register('my_company', MyCompanyViewSet, basename='my-company') router.register('admin_companies', AdminCompanyViewSet, basename='admin-companies') router.register('products', ProductViewSet, basename='product') router.register('my_products', MyProductsViewSet, basename='my-products') diff --git a/back_latienda/urls.py b/back_latienda/urls.py index 91431ad..c6743e3 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -39,6 +39,7 @@ urlpatterns = [ path('api/v1/search_products/', product_views.product_search, name='product-search'), path('api/v1/create_company_user/', core_views.create_company_user, name='create-company-user'), path('api/v1/my_user/', core_views.my_user, name='my-user'), + path('api/v1/my_company/', company_views.my_company, name='my-company'), path('api/v1/companies/sample/', company_views.random_company_sample , name='company-sample'), path('api/v1/purchase_email/', product_views.purchase_email, name='purchase-email'), path('api/v1/stats/me/', stat_views.track_user, name='user-tracker'), diff --git a/companies/tests.py b/companies/tests.py index 8473b02..1ad084f 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -312,12 +312,11 @@ class MyCompanyViewTest(APITestCase): self.user.set_password(self.password) self.user.save() - def tearDown(self): - self.model.objects.all().delete() - def test_auth_user_gets_data(self): # create instance - user_instances = [self.factory(creator=self.user) for i in range(5)] + company = CompanyFactory() + self.user.company = company + self.user.save() # Authenticate token = get_tokens_for_user(self.user) @@ -325,32 +324,10 @@ class MyCompanyViewTest(APITestCase): # Query endpoint response = self.client.get(self.endpoint) - payload = response.json() - # Assert forbidden code self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEquals(len(user_instances), len(payload)) - - def test_auth_user_can_paginate_instances(self): - """authenticated user can paginate instances - """ - - # Authenticate - token = get_tokens_for_user(self.user) - self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") - - # create instances - instances = [self.factory(creator=self.user) for n in range(12)] - - # Request list - url = f"{self.endpoint}?limit=5&offset=10" - response = self.client.get(url) - - # Assert access is allowed - self.assertEqual(response.status_code, status.HTTP_200_OK) - # assert only 2 instances in response payload = response.json() - self.assertEquals(2, len(payload['results'])) + self.assertEquals(payload['company']['id'], company.id) def test_anon_user_cannot_access(self): # send in request diff --git a/companies/views.py b/companies/views.py index 5133d43..010c4b4 100644 --- a/companies/views.py +++ b/companies/views.py @@ -155,6 +155,16 @@ class CompanyViewSet(viewsets.ModelViewSet): return Response(message) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def my_company(request): + if request.user.company: + serializer = CompanySerializer(request.user.company) + return Response({'company': serializer.data}) + else: + return Response(status=status.HTTP_406_NOT_ACCEPTABLE) + +''' class MyCompanyViewSet(viewsets.ModelViewSet): model = Company serializer_class = CompanySerializer @@ -165,7 +175,7 @@ class MyCompanyViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): serializer.save(creator=self.request.user) - +''' class AdminCompanyViewSet(viewsets.ModelViewSet): """ Allows user with role 'SITE_ADMIN' to access all company instances diff --git a/core/serializers.py b/core/serializers.py index e71c71f..6a2039e 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -23,7 +23,7 @@ class CustomUserWriteSerializer(serializers.ModelSerializer): class Meta: model = models.CustomUser - fields = ('email', 'full_name', 'role', 'password', 'provider') + fields = ('email', 'full_name', 'role', 'password', 'provider', 'notify') class CreatorSerializer(serializers.ModelSerializer): diff --git a/products/tests.py b/products/tests.py index f503677..194fee3 100644 --- a/products/tests.py +++ b/products/tests.py @@ -952,9 +952,13 @@ class MyProductsViewTest(APITestCase): def test_auth_user_gets_data(self): # create instance + company = CompanyFactory() + self.user.company = company + self.user.save() + user_instances = [ - self.factory(creator=self.user), - self.factory(creator=self.user), + self.factory(company=company), + self.factory(company=company), ] # Authenticate @@ -976,7 +980,11 @@ class MyProductsViewTest(APITestCase): self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") # create instances - instances = [self.factory(creator=self.user) for n in range(12)] + company = CompanyFactory() + self.user.company = company + self.user.save() + + instances = [self.factory(company=company) for n in range(12)] # Request list url = f"{self.endpoint}?limit=5&offset=10" diff --git a/products/views.py b/products/views.py index 228f6e1..4e132ae 100644 --- a/products/views.py +++ b/products/views.py @@ -72,10 +72,7 @@ class MyProductsViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return self.model.objects.filter(creator=self.request.user).order_by('-created') - - def perform_create(self, serializer): - serializer.save(creator=self.request.user) + return self.model.objects.filter(company=self.request.user.company).order_by('-created') class AdminProductsViewSet(viewsets.ModelViewSet): From 9e5fb89274830c285d1d7fe90d8aea4700002e7c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Mar 2021 13:56:09 +0000 Subject: [PATCH 48/67] cleanup --- core/tests.py | 114 ++++++++++---------------------------------------- core/views.py | 8 ---- 2 files changed, 21 insertions(+), 101 deletions(-) diff --git a/core/tests.py b/core/tests.py index 1b1a9f0..756b05a 100644 --- a/core/tests.py +++ b/core/tests.py @@ -161,6 +161,27 @@ class CustomUserViewSetTest(APITestCase): # Assert access is forbidden self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_auth_user_can_modify_own_instance(self): + """Regular user can modify own instance + """ + # Create instance + data = { + "email": "new_email@mail.com", + "full_name": "New Full Name", + 'provider': 'PROVIDER', + 'notify': True, + } + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # Query endpoint + url = f'{self.endpoint}{self.user.pk}/' + response = self.client.put(url, data=data, format='json') + # Assert forbidden code + self.assertEqual(response.status_code, status.HTTP_200_OK) + # admin user def test_admin_user_can_create_instance(self): """Admin user can create new instance @@ -332,99 +353,6 @@ class ChangeUserPasswordViewTest(APITestCase): self.assertEqual(stored_password_hash, base64.b64encode(new_password_hash).decode()) -class UpdateUserViewTest(APITestCase): - - def setUp(self): - """Tests setup - """ - self.endpoint = '/api/v1/users/' - self.factory = factories.CustomUserFactory - self.model = models.CustomUser - # create regular user - self.reg_email = f"user@mail.com" - self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) - self.user = self.factory(email=self.reg_email, is_active=True) - self.user.set_password(self.password) - self.user.save() - # create admin user - self.admin_email = f"admin_user@mail.com" - self.admin_user = self.factory(email=self.admin_email, is_staff=True, is_active=True) - self.admin_user.set_password(self.password) - self.admin_user.save() - - def test_auth_user_can_modify_own_instance(self): - """Regular user can modify own instance - """ - # Create instance - data = { - "email": "new_email@mail.com", - "full_name": "New Full Name", - 'provider': 'PROVIDER', - 'notify': True, - } - - # Authenticate - token = get_tokens_for_user(self.user) - self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") - - # Query endpoint - url = f'{self.endpoint}{self.user.pk}/' - response = self.client.put(url, data=data, format='json') - # Assert forbidden code - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_auth_user_cannot_modify_random_instance(self): - """Regular user cannot modify randnom instance - """ - # Create instance - instance = self.factory() - - # Authenticate - token = get_tokens_for_user(self.user) - self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") - - # Query endpoint - url = f'{self.endpoint}{instance.pk}/' - response = self.client.put(url, data={}, format='json') - # Assert forbidden code - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_anon_user_cannot_modify_random_instance(self): - """anon user cannot modify instance - """ - # Create instance - instance = self.factory() - - # Query endpoint - url = f'{self.endpoint}{instance.pk}/' - response = self.client.put(url, data={}, format='json') - # Assert forbidden code - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_admin_user_can_modify_random_instance(self): - """Regular user cannot modify randnom instance - """ - # Create instance - instance = self.factory() - - # Authenticate - token = get_tokens_for_user(self.admin_user) - self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") - - data = { - "email": "new_email@mail.com", - "full_name": "New Full Name", - 'provider': 'PROVIDER', - 'notify': True, - } - - # Query endpoint - url = f'{self.endpoint}{instance.pk}/' - response = self.client.put(url, data=data, format='json') - # Assert forbidden code - self.assertEqual(response.status_code, status.HTTP_200_OK) - - class LoadCoopManagerTestCase(APITestCase): def setUp(self): diff --git a/core/views.py b/core/views.py index d1c3511..cbcca9c 100644 --- a/core/views.py +++ b/core/views.py @@ -101,14 +101,6 @@ class ChangeUserPasswordView(UpdateAPIView): serializer_class = core_serializers.ChangePasswordSerializer -class UpdateUserView(UpdateAPIView): - - model = models.CustomUser - queryset = model.objects.all() - permission_classes = (YourOwnUserPermissions,) - serializer_class = core_serializers.UpdateUserSerializer - - @api_view(['POST',]) @permission_classes([CustomUserPermissions,]) def create_company_user(request): From 6862d62bf5f22404e11034c8e7f0767c2f99635a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 10:13:24 +0000 Subject: [PATCH 49/67] user activation now redirects to home --- back_latienda/settings/base.py | 3 +++ core/tests.py | 16 ++++++++++++++++ core/views.py | 4 ++++ example.env | 4 +++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index 0ee9a92..5298aca 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -171,3 +171,6 @@ MAP_WIDGETS = { ), "GOOGLE_MAP_API_KEY": os.getenv('GOOGLE_MAP_API_KEY') } + +# ACTIVATION_REDIRECT URL +ACTIVATION_REDIRECT = os.getenv('ACTIVATION_REDIRECT') diff --git a/core/tests.py b/core/tests.py index 756b05a..2ed7ca1 100644 --- a/core/tests.py +++ b/core/tests.py @@ -9,6 +9,7 @@ from django.test import TestCase from django.core import mail from django.utils.http import urlsafe_base64_encode from django.utils.encoding import force_bytes +from django.conf import settings from rest_framework.test import APITestCase from rest_framework import status @@ -492,6 +493,21 @@ class ActivateUserTest(APITestCase): response = self.client.get(url) + # assertions + self.assertEquals(response.status_code, 302) + self.assertEquals(response.url, settings.ACTIVATION_REDIRECT) + + def test_correct_activation_no_redirect(self): + # set ACTIVATION_REDIRECT to '' + settings.ACTIVATION_REDIRECT = '' + # create values + uid = urlsafe_base64_encode(force_bytes(self.user.pk)) + token = account_activation_token.make_token(self.user) + + url = f'/activate/{uid}/{token}/' + + response = self.client.get(url) + # assertions self.assertEquals(response.status_code, 200) self.assertTrue(self.user.email in str(response.content)) diff --git a/core/views.py b/core/views.py index cbcca9c..bc9afd0 100644 --- a/core/views.py +++ b/core/views.py @@ -11,6 +11,8 @@ from django.utils.http import urlsafe_base64_decode from django.utils.encoding import force_text from django.db import IntegrityError from django.contrib.gis.geos import Point +from django.shortcuts import redirect +from django.conf import settings from rest_framework import status from rest_framework import viewsets @@ -190,6 +192,8 @@ def activate_user(request, uidb64, token): # activate user user.is_active = True user.save() + if settings.ACTIVATION_REDIRECT: + return redirect(settings.ACTIVATION_REDIRECT) return Response(f"Tu cuenta de usuario {user.email} ha sido activada") else: return Response({"error": f"Tu token de verificacion no coincide con ningún usuario registrado"}, status=status.HTTP_406_NOT_ACCEPTABLE) diff --git a/example.env b/example.env index 66a53de..87e699b 100644 --- a/example.env +++ b/example.env @@ -14,4 +14,6 @@ AWS_SECRET_ACCESS_KEY_SES = '' WC_KEY = '' WC_SECRET = '' # GOOGLE MAPS -GOOGLE_MAP_API_KEY = '' \ No newline at end of file +GOOGLE_MAP_API_KEY = '' +# USER ACTIVATION REDIRECTION +ACTIVATION_REDIRECT = '' \ No newline at end of file From 37f222e6c90109d2ef2f427f7156087261be0544 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 10:22:23 +0000 Subject: [PATCH 50/67] changes for my endpoints --- companies/views.py | 2 +- products/tests.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/companies/views.py b/companies/views.py index 010c4b4..62c4767 100644 --- a/companies/views.py +++ b/companies/views.py @@ -171,7 +171,7 @@ class MyCompanyViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return self.model.objects.filter(creator=self.request.user) + return self.model.objects.filter(company=self.request.user.company) def perform_create(self, serializer): serializer.save(creator=self.request.user) diff --git a/products/tests.py b/products/tests.py index 194fee3..d9db3ea 100644 --- a/products/tests.py +++ b/products/tests.py @@ -1004,6 +1004,18 @@ class MyProductsViewTest(APITestCase): # check response self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_auth_user_without_company(self): + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # Query endpoint + response = self.client.get(self.endpoint) + payload = response.json() + # Assert forbidden code + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEquals([], payload) + class AdminProductViewSetTest(APITestCase): From 341ea2d54db4a91d558e3dbb064a163b55583c01 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 10:39:17 +0000 Subject: [PATCH 51/67] create_company_user endpoint open for auth and anon users --- back_latienda/permissions.py | 1 + core/tests.py | 51 ++++++++++++++++++++++++++++++++++-- core/views.py | 2 +- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/back_latienda/permissions.py b/back_latienda/permissions.py index 1d1cf51..4d9cb9f 100644 --- a/back_latienda/permissions.py +++ b/back_latienda/permissions.py @@ -85,3 +85,4 @@ class YourOwnUserPermissions(permissions.BasePermission): return True else: return False + diff --git a/core/tests.py b/core/tests.py index 2ed7ca1..840a5a1 100644 --- a/core/tests.py +++ b/core/tests.py @@ -535,11 +535,58 @@ class CreateCompanyUserTest(APITestCase): # create user self.email = "user@mail.com" self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) - self.user = self.factory(email=self.email, is_active=False) + self.user = self.factory(email=self.email, is_active=True) self.user.set_password(self.password) self.user.save() - def test_succesful_creation(self): + def test_auth_user_can_create(self): + # instances data + data = { + 'user': { + 'email': 'test@email.com', + 'full_name': 'TEST NAME', + 'password': 'VENTILADORES1234499.89', + }, + 'company': { + 'cif': 'qwerewq', + 'company_name': 'qwerewq', + 'short_name': 'qwerewq', + 'web_link': 'http://qwerewq.com', + 'shop': True, + 'shop_link': 'http://qwerewq.com', + 'platform': 'PRESTASHOP', + 'email': 'test@email.com', + 'logo': None, + 'city': None, + 'address': 'qwer qewr 5', + 'geo': {'longitude': 1.0, 'latitude': 1.0}, + 'phone': '1234', + 'mobile': '4321', + 'other_phone': '41423', + 'description': 'dfgfdgdfg', + 'shop_rss_feed': 'http://qwerewq.com', + 'sale_terms': 'tewrnmfew f ewfrfew ewewew f', + 'shipping_cost': '12.25', + 'sync': False + } + } + + # 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') + + self.assertEquals(response.status_code, 201) + self.assertEquals(len(mail.outbox), 1) + # user exists and it's inactice + self.assertTrue(self.model.objects.get(email='test@email.com')) + self.assertFalse(self.model.objects.get(email='test@email.com').is_active) + self.assertTrue(Company.objects.get(cif='qwerewq')) + # assert verification email + self.assertTrue(len(mail.outbox) == 1) + + def test_anon_user_can_create(self): data = { 'user': { 'email': 'test@email.com', diff --git a/core/views.py b/core/views.py index bc9afd0..a1acef1 100644 --- a/core/views.py +++ b/core/views.py @@ -104,7 +104,7 @@ class ChangeUserPasswordView(UpdateAPIView): @api_view(['POST',]) -@permission_classes([CustomUserPermissions,]) +@permission_classes([AllowAny]) def create_company_user(request): """ Create non-validated company and associated managing user From 7a2e80b43ee763e6addbaa03e6e2205afc477c59 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 11:10:51 +0000 Subject: [PATCH 52/67] first stab at admin_stats endpoint --- back_latienda/urls.py | 1 + core/tests.py | 83 +++++++++++++++++++++++++++++++++++++++++++ core/views.py | 28 +++++++++++++-- 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/back_latienda/urls.py b/back_latienda/urls.py index c6743e3..5b090bf 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -34,6 +34,7 @@ urlpatterns = [ path('api/v1/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/v1/token/verify/', TokenVerifyView.as_view(), name='token_verify'), path('api/v1/user/change_password//', core_views.ChangeUserPasswordView.as_view(), name="change-password"), + path('api/v1/admin_stats/', core_views.admin_stats, name='admin-stats'), path('api/v1/load_coops/', core_views.load_coop_managers, name='coop-loader'), path('api/v1/load_products/', product_views.load_coop_products, name='product-loader'), path('api/v1/search_products/', product_views.product_search, name='product-search'), diff --git a/core/tests.py b/core/tests.py index 840a5a1..d56d171 100644 --- a/core/tests.py +++ b/core/tests.py @@ -635,3 +635,86 @@ class CreateCompanyUserTest(APITestCase): self.assertEquals(response.status_code, 406) self.assertEquals(len(mail.outbox), 0) + +class AdminStatsTest(APITestCase): + + def setUp(self): + self.endpoint = '/api/v1/admin_stats/' + self.factory = factories.CustomUserFactory + self.model = models.CustomUser + # create user + self.email = "user@mail.com" + self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) + self.user = self.factory(email=self.email, is_active=True) + self.user.set_password(self.password) + self.user.save() + + # anonymous user + def test_anon_user_cannot_crud(self): + """Not logged-in user cannot access endpoint at all + """ + + # Query endpoint + response = self.client.get(self.endpoint) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # Query endpoint + response = self.client.post(self.endpoint, data={}) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # Query endpoint + response = self.client.put(self.endpoint, data={}) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # Query endpoint + response = self.client.delete(self.endpoint) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + # authenticated user + def test_auth_user_cannot_crud(self): + """Authenticated user cannot access endpoint at all + """ + # Authenticate user + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + # Query endpoint + response = self.client.get(self.endpoint) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Query endpoint + response = self.client.post(self.endpoint, data={}) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Query endpoint + response = self.client.put(self.endpoint, data={}) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Query endpoint + response = self.client.delete(self.endpoint) + # Assert access is forbidden + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_admin_can_get_data(self): + # make user admin + self.user.is_staff = True + self.user.save() + + # Authenticate + token = get_tokens_for_user(self.user) + self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}") + + response = self.client.get(self.endpoint) + self.assertEquals(response.status_code, 200) + payload = response.json() + expected_entries = ['company_count', 'product_count', 'companies_per_region', 'products_per_region'] + for name in expected_entries: + self.assertTrue(name in payload) + diff --git a/core/views.py b/core/views.py index a1acef1..8568d00 100644 --- a/core/views.py +++ b/core/views.py @@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model from django.utils.http import urlsafe_base64_decode from django.utils.encoding import force_text from django.db import IntegrityError -from django.contrib.gis.geos import Point +from django.contrib.gis.geos import Point, GEOSGeometry from django.shortcuts import redirect from django.conf import settings @@ -23,7 +23,8 @@ from rest_framework.decorators import api_view, permission_classes from companies.models import Company from companies.serializers import CompanySerializer -from geo.models import City +from products.models import Product +from geo.models import City, Region from . import models from . import serializers as core_serializers @@ -197,3 +198,26 @@ def activate_user(request, uidb64, token): return Response(f"Tu cuenta de usuario {user.email} ha sido activada") else: return Response({"error": f"Tu token de verificacion no coincide con ningún usuario registrado"}, status=status.HTTP_406_NOT_ACCEPTABLE) + + +@api_view(['GET',]) +@permission_classes([IsAdminUser,]) +def admin_stats(request): + company_count = Company.objects.count() + product_count = Product.objects.count() + companies_per_region = {} + products_per_region = {} + + for region in Region.objects.all(): + count = Company.objects.filter(geo__dwithin=region.geo).count() + companies_per_region[region.name] = count + count = Product.objects.filter(company__geo__dwithin=region.geo).count() + products_per_region[region.name] = count + + data = { + 'company_count': company_count, + 'product_count': product_count, + 'companies_per_region': companies_per_region, + 'products_per_region': products_per_region, + } + return Response(data=data) From 712cabe3daf73dbc8b139d72d0a1dc0667d91f26 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 11:17:17 +0000 Subject: [PATCH 53/67] fixed for stats_admin geo filtering --- core/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/views.py b/core/views.py index 8568d00..ac62e81 100644 --- a/core/views.py +++ b/core/views.py @@ -209,9 +209,9 @@ def admin_stats(request): products_per_region = {} for region in Region.objects.all(): - count = Company.objects.filter(geo__dwithin=region.geo).count() + count = Company.objects.filter(geo__within=region.geo).count() companies_per_region[region.name] = count - count = Product.objects.filter(company__geo__dwithin=region.geo).count() + count = Product.objects.filter(company__geo__within=region.geo).count() products_per_region[region.name] = count data = { From d0609fd1fa18ad61f4c58ea846b12a9201c21002 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 12:14:30 +0000 Subject: [PATCH 54/67] finished admin_stats endpoint --- back_latienda/settings/base.py | 3 +-- back_latienda/urls.py | 1 + core/tests.py | 3 ++- core/views.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index 5298aca..4996fc6 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -34,10 +34,9 @@ SECRET_KEY = 'td*#7t-(1e9^(g0cod*hs**dp(%zvg@=$cug_-dtzcj#i2mrz@' # Application definition INSTALLED_APPS = [ + 'suit', 'dal', 'dal_select2', - 'suit', - 'django.contrib.admin', 'django.contrib.auth', diff --git a/back_latienda/urls.py b/back_latienda/urls.py index 5b090bf..5d42e68 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -26,6 +26,7 @@ from companies import views as company_views from stats import views as stat_views from .routers import router +admin.site.site_header = 'LaTiendaCOOP Administration' urlpatterns = [ path('admin/', admin.site.urls), diff --git a/core/tests.py b/core/tests.py index d56d171..1410b3f 100644 --- a/core/tests.py +++ b/core/tests.py @@ -702,6 +702,7 @@ class AdminStatsTest(APITestCase): # Assert access is forbidden self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # admin user def test_admin_can_get_data(self): # make user admin self.user.is_staff = True @@ -714,7 +715,7 @@ class AdminStatsTest(APITestCase): response = self.client.get(self.endpoint) self.assertEquals(response.status_code, 200) payload = response.json() - expected_entries = ['company_count', 'product_count', 'companies_per_region', 'products_per_region'] + expected_entries = ['company_count', 'product_count', 'companies_per_region', 'products_per_region', 'companies_timeline', 'products_timeline', 'users_timeline', 'contact_timeline', 'shopping_timeline'] for name in expected_entries: self.assertTrue(name in payload) diff --git a/core/views.py b/core/views.py index ac62e81..6b23ebe 100644 --- a/core/views.py +++ b/core/views.py @@ -25,6 +25,7 @@ from companies.models import Company from companies.serializers import CompanySerializer from products.models import Product from geo.models import City, Region +from stats.models import StatsLog from . import models from . import serializers as core_serializers @@ -214,10 +215,41 @@ def admin_stats(request): count = Product.objects.filter(company__geo__within=region.geo).count() products_per_region[region.name] = count + today = datetime.date.today() + # companies timeline: count companies at increments of 4 weeks + companies_timeline = {} + # products timeline: count products at increments of 4 weeks + products_timeline = {} + # users timeline: count users at increments of 4 weeks + users_timeline = {} + # contact timeline: count statlogs from contact at increments of 4 weeks + contact_timeline = {} + # shopping timeline: count statlogs from shopping at increments of 4 weeks + shopping_timeline = {} + for i in range(1, 13): + before = today - datetime.timedelta(weeks=( (i-1) * 4 )) + after = today - datetime.timedelta(weeks=(i*4)) + # companies + companies_timeline[i] = Company.objects.filter(created__range= [after, before]).count() + # products + products_timeline[i] = Product.objects.filter(created__range= [after, before]).count() + # users + users_timeline[i] = User.objects.filter(created__range= [after, before]).count() + # contact + contact_timeline[i] = StatsLog.objects.filter(contact=True, created__range= [after, before]).count() + # shopping + shopping_timeline[i] = StatsLog.objects.filter(shop=True, created__range= [after, before]).count() + data = { 'company_count': company_count, 'product_count': product_count, 'companies_per_region': companies_per_region, 'products_per_region': products_per_region, + 'companies_timeline': companies_timeline, + 'products_timeline': products_timeline, + 'users_timeline': users_timeline, + 'contact_timeline': contact_timeline, + 'shopping_timeline': shopping_timeline, } + return Response(data=data) From 778d65d84ec305fbd81886c5f91dc096b616cb0a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 12:37:27 +0000 Subject: [PATCH 55/67] implemented new email templates, but not rendering --- products/tests.py | 5 + products/views.py | 8 + templates/purchase_contact_confirmation.html | 197 ++++++++++++++++++- templates/purchase_notification.html | 180 ++++++++++++++++- 4 files changed, 378 insertions(+), 12 deletions(-) mode change 100644 => 100755 templates/purchase_contact_confirmation.html mode change 100644 => 100755 templates/purchase_notification.html diff --git a/products/tests.py b/products/tests.py index d9db3ea..6efa7dd 100644 --- a/products/tests.py +++ b/products/tests.py @@ -1217,8 +1217,13 @@ class PurchaseEmailTest(APITestCase): self.assertEquals(2, len(mail.outbox)) def test_auth_user_can_use(self): + # required instances company = CompanyFactory() product = ProductFactory(company=company) + # make user the manager + self.user.company = company + self.user.role = 'COOP_MANAGER' + self.user.save() data = { 'telephone': '123123123', diff --git a/products/views.py b/products/views.py index 4e132ae..f75efdb 100644 --- a/products/views.py +++ b/products/views.py @@ -273,10 +273,16 @@ def purchase_email(request): validate_email(user_email) except: return Response({"error": "Value for email is not valid"}, status=status.HTTP_406_NOT_ACCEPTABLE) + # get comment + comment = data.get('comment') # get company company = Company.objects.filter(id=data['company']).first() if not company: return Response({"error": "Invalid value for company"}, status=status.HTTP_406_NOT_ACCEPTABLE) + # get managing user + managing_user = User.objects.filter(company=company, role='COOP_MANAGER').first() + if not managing_user: + return Response({"error": "No managing user found for company"}, status=status.HTTP_406_NOT_ACCEPTABLE) # get product product = Product.objects.filter(id=data['product'], company=company).first() if not product: @@ -291,6 +297,7 @@ def purchase_email(request): 'user': request.user, 'product': product, 'telephone': data['telephone'], + 'comment': comment, }) subject = "[latienda.coop] Solicitud de compra" email = EmailMessage(subject, company_message, to=[company.email]) @@ -301,6 +308,7 @@ def purchase_email(request): 'company': company, 'product': product, 'company_message': company_message, + 'manager': managing_user, }) subject = 'Confirmación de petición de compra' email = EmailMessage(subject, user_message, to=[user_email]) diff --git a/templates/purchase_contact_confirmation.html b/templates/purchase_contact_confirmation.html old mode 100644 new mode 100755 index 7d5deb6..32652ff --- a/templates/purchase_contact_confirmation.html +++ b/templates/purchase_contact_confirmation.html @@ -1,10 +1,197 @@ -Hola usuario. + + + + + + + + + -Hemos envíado correctamente el siguiente email a la empresa {{company}} sobre el producto {{product}}: + + Interés en copra de producto + + + +
+
+ + + +

+ Confirmación de + solicitud enviada +

+
+
+ +
+ + + + Enlace al producto + Nombre del producto: {{product.name}} + {{product.sku}} + {{product.price}} +

¡Hola! Tu solicitud de compra de este producto ha sido comunicado a la cooperativa. Pronto tendrás noticias suyas. Para cualquier duda o asistencia, ponte en contacto con {{company.short_name}}.

+
+
+

Contacto cooperativa

+
+
Nombre:
+
{{manager.full_name}}
+
Email:
+
{{company.email}}
+ +
+ + + +
+
+
+ 2021 La Tienda.Coop +
+
+ + diff --git a/templates/purchase_notification.html b/templates/purchase_notification.html old mode 100644 new mode 100755 index 896baef..c377a12 --- a/templates/purchase_notification.html +++ b/templates/purchase_notification.html @@ -1,9 +1,175 @@ -Hola {{company}}. + + + + + + + + + -El usuario {{user.email}} ha mostrado interés en la compra del producto {{product}}. + + Interés en copra de producto + + + +
+
+ + + +

+ Interés en compra + de producto +

+
+
+
+ + + + Enlace al producto + Nombre del producto: {{product.name}} + {{product.sku}} + {{product.price}} +
+
+

Datos cliente

+
+
Nombre:
+
{{user.full_name}}
+
Email:
+
{{user.email}}
+
Teléfono:
+
{{telephone}}
+
Comentarios:
+
{{comment}}
+
+
+
+
+
+ 2021 La Tienda.Coop +
+
+ + From ffb39ba62ff61fa5d95706c7966e4e4aeeeb00fa Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 12:45:26 +0000 Subject: [PATCH 56/67] applied new html template to all email templates --- templates/company_contact.html | 178 ++++++++++++++++++++++++- templates/confirm_company_contact.html | 168 ++++++++++++++++++++++- templates/email_verification.html | 171 +++++++++++++++++++++++- 3 files changed, 503 insertions(+), 14 deletions(-) diff --git a/templates/company_contact.html b/templates/company_contact.html index 9d4d4a5..b17c9f6 100644 --- a/templates/company_contact.html +++ b/templates/company_contact.html @@ -1,10 +1,174 @@ -Hola {{company.creator.full_name}}. -Estamos contactando contigo como usuario gerente de la compañina {{company.company_name}}. + + + + + + + + + -Datos de usuario: + + Interés en copra de producto + + + +
+
+ + + +

+ Confirmación de + solicitud enviada +

+
+
+

+ Hola {{company.creator.full_name}}. + Estamos contactando contigo como usuario gerente de la compañía {{company.company_name}}. + + Datos de usuario: + + - {{user.full_name}} + - {{user.email}} + + Contenido del mensaje: + {{data}} +

+
+
+ 2021 La Tienda.Coop +
+
+ + diff --git a/templates/confirm_company_contact.html b/templates/confirm_company_contact.html index 70dcb0e..d766daf 100644 --- a/templates/confirm_company_contact.html +++ b/templates/confirm_company_contact.html @@ -1,2 +1,166 @@ -Hola {{user.full_name}}. -Hemos enviado un email a {{company.company_name}} sobre tu petición. + + + + + + + + + + + + Interés en copra de producto + + + +
+
+ + + +

+ Confirmación de + solicitud enviada +

+
+
+

+ Hola {{user.full_name}}. + Hemos enviado un email a {{company.company_name}} sobre tu petición. +

+
+
+ 2021 La Tienda.Coop +
+
+ + diff --git a/templates/email_verification.html b/templates/email_verification.html index 37a09c8..7238ce7 100644 --- a/templates/email_verification.html +++ b/templates/email_verification.html @@ -1,7 +1,168 @@ -{% autoescape off %} -Hola {{ user.full_name }}, -Por favor, verifica tu registro en LaTiendaCOOP haciendo click en el siguiente enlace: + + + + + + + + + -http://{{ domain }}{% url 'activate_user' uidb64=uid token=token %} + + Interés en copra de producto + + + +
+
+ + + +

+ Confirmación de + solicitud enviada +

+
+
+

+ Hola {{ user.full_name }}, + Por favor, verifica tu registro en LaTiendaCOOP haciendo click en el siguiente enlace: + + http://{{ domain }}{% url 'activate_user' uidb64=uid token=token %} +

+
+
+ 2021 La Tienda.Coop +
+
+ + \ No newline at end of file From 5da01b318ec5515a843fc8271e9a5883ce1eb49a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 12 Mar 2021 13:45:36 +0000 Subject: [PATCH 57/67] fixed email sending for html content type --- back_latienda/settings/production.py | 4 ++-- companies/views.py | 4 ++++ core/utils.py | 1 + products/views.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/back_latienda/settings/production.py b/back_latienda/settings/production.py index 653acc2..f506476 100644 --- a/back_latienda/settings/production.py +++ b/back_latienda/settings/production.py @@ -40,8 +40,8 @@ AWS_DEFAULT_ACL = None DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend" -DEFAULT_FROM_EMAIL = "no-reply@latienda.com" -# DEFAULT_FROM_EMAIL = "samuel.molina@enreda.coop" +# DEFAULT_FROM_EMAIL = "no-reply@latienda.com" +DEFAULT_FROM_EMAIL = "samuel.molina@enreda.coop" SERVER_EMAIL = "mail-server@latienda.com" diff --git a/companies/views.py b/companies/views.py index 62c4767..97e2897 100644 --- a/companies/views.py +++ b/companies/views.py @@ -65,11 +65,13 @@ class CompanyViewSet(viewsets.ModelViewSet): # send email to company subject = "Contacto de usuario" email = EmailMessage(subject, company_message, to=[instance.creator.email]) + email.content_subtype = "html" email.send() logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}") # send confirmation email to user subject = 'Confirmación de contacto' email = EmailMessage(subject, message, to=[request.user.email]) + email.content_subtype = "html" email.send() logging.info(f"Contact confirmation email sent to {request.user.email}") stats_data = { @@ -98,10 +100,12 @@ class CompanyViewSet(viewsets.ModelViewSet): }) # send email to company email = EmailMessage(subject, company_message, to=[instance.creator.email]) + email.content_subtype = "html" email.send() logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}") # send confirmation email to user email = EmailMessage(subject, user_message, to=[data['email']]) + email.content_subtype = "html" email.send() logging.info(f"Contact confirmation email sent to anonymous user {data['email']}") # statslog data to register interaction diff --git a/core/utils.py b/core/utils.py index 4fd5e41..ad0f96e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -81,6 +81,7 @@ def send_verification_email(request, user): email = EmailMessage( subject, message, to=[user.email] ) + email.content_subtype = "html" email.send() logging.info(f"Verification email sent to {user.email}") except Exception as e: diff --git a/products/views.py b/products/views.py index f75efdb..c756044 100644 --- a/products/views.py +++ b/products/views.py @@ -301,6 +301,7 @@ def purchase_email(request): }) subject = "[latienda.coop] Solicitud de compra" email = EmailMessage(subject, company_message, to=[company.email]) + email.content_subtype = "html" email.send() logging.info(f"Email sent to {company}") # send confirmation email to user @@ -312,6 +313,7 @@ def purchase_email(request): }) subject = 'Confirmación de petición de compra' email = EmailMessage(subject, user_message, to=[user_email]) + email.content_subtype = "html" email.send() logging.info(f"Purchase Contact confirmation email sent to {user_email}") except Exception as e: From 4a737d42873a6a4d58c950fb42bd6afbdd80be0f Mon Sep 17 00:00:00 2001 From: Diego Calvo Date: Fri, 12 Mar 2021 15:03:50 +0100 Subject: [PATCH 58/67] user can update notify --- core/serializers.py | 2 +- core/tests.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 6a2039e..0121405 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -85,7 +85,7 @@ class UpdateUserSerializer(serializers.ModelSerializer): class Meta: model = models.CustomUser - fields = ('full_name', 'email') + fields = ('full_name', 'email', 'notify') def validate_email(self, value): user = self.context['request'].user diff --git a/core/tests.py b/core/tests.py index 2ed7ca1..f687a3d 100644 --- a/core/tests.py +++ b/core/tests.py @@ -169,7 +169,6 @@ class CustomUserViewSetTest(APITestCase): data = { "email": "new_email@mail.com", "full_name": "New Full Name", - 'provider': 'PROVIDER', 'notify': True, } @@ -183,6 +182,10 @@ class CustomUserViewSetTest(APITestCase): # Assert forbidden code self.assertEqual(response.status_code, status.HTTP_200_OK) + # Assert instance has been modified + for key in data: + self.assertEqual(data[key], response.data[key]) + # admin user def test_admin_user_can_create_instance(self): """Admin user can create new instance From 5714a661aaeff65673d9bc0a79791a23e0fc6176 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Mar 2021 10:03:32 +0000 Subject: [PATCH 59/67] new purchase notification template --- templates/purchase_notification.html | 1159 ++++++++++++++++++++++---- 1 file changed, 1006 insertions(+), 153 deletions(-) diff --git a/templates/purchase_notification.html b/templates/purchase_notification.html index c377a12..522c0e5 100755 --- a/templates/purchase_notification.html +++ b/templates/purchase_notification.html @@ -1,175 +1,1028 @@ - + - + + - - + + + + + + + - - - Interés en copra de producto - + + + - -
-
- - - -

- Interés en compra - de producto -

-
-
-
- - - - Enlace al producto - Nombre del producto: {{product.name}} - {{product.sku}} - {{product.price}} -
-
-

Datos cliente

-
-
Nombre:
-
{{user.full_name}}
-
Email:
-
{{user.email}}
-
Teléfono:
-
{{telephone}}
-
Comentarios:
-
{{comment}}
-
-
-
-
-
- 2021 La Tienda.Coop -
+ + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ + + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Interés en compra de + producto +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + +
+ + + + + + +
+ + + +
+
+
+ Enlace al + producto +
+ Nombre del + producto +
+ Ref8304 +
+ 30€ +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Datos cliente +
+
+
+ Nombre: + + María +
+
+
+ Email: + + maria@maria.com +
+
+
+ Teléfono: + + 666666666 +
+
+
+ Comentarios: vivo en un cuarto sin + ascensor, bajo hasta el + tercero como mucho +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + +
+

+ +
+
+ 2021 La Tienda.coop +
+
+
+ +
+
+
From 048f6dbb708d941939b512161ba1b47e8f6676be Mon Sep 17 00:00:00 2001 From: Diego Calvo Date: Mon, 15 Mar 2021 11:45:44 +0100 Subject: [PATCH 60/67] perform_create in MyProductsViewSet for creating with creator and company --- products/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/products/views.py b/products/views.py index 4e132ae..44f8d9e 100644 --- a/products/views.py +++ b/products/views.py @@ -74,6 +74,9 @@ class MyProductsViewSet(viewsets.ModelViewSet): def get_queryset(self): return self.model.objects.filter(company=self.request.user.company).order_by('-created') + def perform_create(self, serializer): + serializer.save(creator=self.request.user, company=self.request.user.company) + class AdminProductsViewSet(viewsets.ModelViewSet): """ Allows user with role 'SITE_ADMIN' to access all product instances From a920a9fa611fd40eb343cc71317fc49e4be98882 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Mar 2021 10:49:17 +0000 Subject: [PATCH 61/67] trying to get new templates to work --- .gitignore | 1 + back_latienda/settings/base.py | 10 +- products/views.py | 2 +- static/.gitignore | 2 + templates/purchase_notification_v1.html | 176 ++++++++++++++++++ ...ion.html => purchase_notification_v2.html} | 54 +++--- 6 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 static/.gitignore create mode 100755 templates/purchase_notification_v1.html rename templates/{purchase_notification.html => purchase_notification_v2.html} (99%) diff --git a/.gitignore b/.gitignore index aa7ea9b..8c73834 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # environment variables .env + # virtual env venv/ diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index 4996fc6..af2f1b1 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -85,7 +85,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, '../templates'),], - 'APP_DIRS': True, + # 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', @@ -93,6 +93,12 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'loaders': [ + ( + 'django.template.loaders.filesystem.Loader', + [os.path.join(BASE_DIR, '../templates')], + ), + ], }, }, ] @@ -153,7 +159,7 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/static/' - +STATIC_ROOT = 'static' TAXONOMY_FILE = 'shop-taxonomy.es-ES.txt' diff --git a/products/views.py b/products/views.py index c756044..ff800cd 100644 --- a/products/views.py +++ b/products/views.py @@ -292,7 +292,7 @@ def purchase_email(request): return Response({"error": "Related compay has no contact email address"}, status=status.HTTP_406_NOT_ACCEPTABLE) try: # send email to company - company_message = render_to_string('purchase_notification.html', { + company_message = render_to_string('purchase_notification_v2.html', { 'company': company, 'user': request.user, 'product': product, diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/templates/purchase_notification_v1.html b/templates/purchase_notification_v1.html new file mode 100755 index 0000000..6dfee51 --- /dev/null +++ b/templates/purchase_notification_v1.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + Interés en copra de producto + + + +
+
+ + + +

+ Interés en compra + de producto +

+
+
+
+ + + + Enlace al producto + Nombre del producto: {{product.name}} + {{product.sku}} + {{product.price}} +
+
+

Datos cliente

+
+
Nombre:
+
{{user.full_name}}
+
Email:
+
{{user.email}}
+
Teléfono:
+
{{telephone}}
+
Comentarios:
+
{{comment}}
+
+
+
+
+
+ 2021 La Tienda.Coop +
+
+ + + diff --git a/templates/purchase_notification.html b/templates/purchase_notification_v2.html similarity index 99% rename from templates/purchase_notification.html rename to templates/purchase_notification_v2.html index 522c0e5..69ff976 100755 --- a/templates/purchase_notification.html +++ b/templates/purchase_notification_v2.html @@ -6,9 +6,6 @@ > - - - - - -
-
- - - -

- Interés en compra - de producto -

-
-
-
- - - - Enlace al producto - Nombre del producto: {{product.name}} - {{product.sku}} - {{product.price}} -
-
-

Datos cliente

-
-
Nombre:
-
{{user.full_name}}
-
Email:
-
{{user.email}}
-
Teléfono:
-
{{telephone}}
-
Comentarios:
-
{{comment}}
-
-
-
-
-
- 2021 La Tienda.Coop -
-
- - - diff --git a/templates/purchase_notification_v2.html b/templates/purchase_notification_v2.html index 69ff976..668ce6e 100755 --- a/templates/purchase_notification_v2.html +++ b/templates/purchase_notification_v2.html @@ -500,7 +500,7 @@ " > Ref8304{{product.name}} +
+ {{product.sku}}
30€{{product.price}}€
@@ -680,7 +689,7 @@ " >Nombre: - María + {{user.full_name}} @@ -712,7 +721,7 @@ " >Email: - maria@maria.com + {{user.email}} @@ -744,7 +753,7 @@ " >Teléfono: - 666666666 + {{telephone}} @@ -775,9 +784,7 @@ font-weight: bold; " >Comentarios: vivo en un cuarto sin - ascensor, bajo hasta el - tercero como mucho + >{{comment}} From cf60622d23b43f8e4d8b91b7ec45e2e15e8c19af Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Mar 2021 11:24:43 +0000 Subject: [PATCH 64/67] added file to ignore contents of static folder --- static/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 static/.gitignore diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From 6557207e45cf89cf7b5b8046cb85f3b8a122965a Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Mar 2021 12:21:18 +0000 Subject: [PATCH 65/67] new purchase email templates working --- products/views.py | 2 +- .../purchase_contact_confirmation_v2.html | 1043 +++++++++++++++++ 2 files changed, 1044 insertions(+), 1 deletion(-) diff --git a/products/views.py b/products/views.py index 03a750d..c53d1b8 100644 --- a/products/views.py +++ b/products/views.py @@ -306,7 +306,7 @@ def purchase_email(request): email.send() logging.info(f"Email sent to {company}") # send confirmation email to user - user_message = render_to_string('purchase_contact_confirmation.html', { + user_message = render_to_string('purchase_contact_confirmation_v2.html', { 'company': company, 'product': product, 'company_message': company_message, diff --git a/templates/purchase_contact_confirmation_v2.html b/templates/purchase_contact_confirmation_v2.html index e69de29..fff1cf7 100644 --- a/templates/purchase_contact_confirmation_v2.html +++ b/templates/purchase_contact_confirmation_v2.html @@ -0,0 +1,1043 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ + + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Confirmación de solicitud + enviada +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ Enlace al + producto +
+ Nombre del + producto +
+ {{product.name}} +
+ {{product.sku}} +
+ {{product.price}}€ +
+
+
+

+ ¡Hola! Tu solicitud de + compra de este producto ha + sido comunicado a la + cooperativa. Pronto tendrás + noticias suyas. Para + cualquier duda o asistencia, + ponte en contacto con + {{company.short_name}}. +

+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + +
+
+ Datos cooperativa +
+
+
+ Nombre: + + {{company.company_name}} +
+
+
+ Email: + + {{company.email}} +
+
+ + logo cooperativa + +
+
+ +
+
+ +
+ + + + + + +
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + +
+

+ +
+
+ 2021 La Tienda.coop +
+
+
+ +
+
+ +
+ + + From 4b05782ca60459f39a6c3a80b68fef59788bb37f Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Mar 2021 13:52:41 +0000 Subject: [PATCH 66/67] removing changes to template system --- back_latienda/settings/base.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index af2f1b1..4996fc6 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -85,7 +85,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, '../templates'),], - # 'APP_DIRS': True, + 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', @@ -93,12 +93,6 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], - 'loaders': [ - ( - 'django.template.loaders.filesystem.Loader', - [os.path.join(BASE_DIR, '../templates')], - ), - ], }, }, ] @@ -159,7 +153,7 @@ USE_TZ = True # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = 'static' + TAXONOMY_FILE = 'shop-taxonomy.es-ES.txt' From 9e476f4007e4c306856fa7fe3fb5b2032d4ed5bf Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 16 Mar 2021 12:06:08 +0000 Subject: [PATCH 67/67] created endpoint to get all categories --- back_latienda/urls.py | 1 + products/tests.py | 36 +++++++++++++++++++++++++++++++++++- products/views.py | 10 ++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/back_latienda/urls.py b/back_latienda/urls.py index 5d42e68..b18a320 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -44,6 +44,7 @@ urlpatterns = [ path('api/v1/my_company/', company_views.my_company, name='my-company'), path('api/v1/companies/sample/', company_views.random_company_sample , name='company-sample'), path('api/v1/purchase_email/', product_views.purchase_email, name='purchase-email'), + path('api/v1/products/all_categories/', product_views.all_categories, name='all-categories'), path('api/v1/stats/me/', stat_views.track_user, name='user-tracker'), path('api/v1/autocomplete/category-tag/', product_views.CategoryTagAutocomplete.as_view(), name='category-autocomplete'), path('api/v1/', include(router.urls)), diff --git a/products/tests.py b/products/tests.py index 6efa7dd..d0f979b 100644 --- a/products/tests.py +++ b/products/tests.py @@ -13,7 +13,7 @@ from rest_framework import status from companies.factories import CompanyFactory from products.factories import ProductFactory, ActiveProductFactory -from products.models import Product +from products.models import Product, CategoryTag from core.factories import CustomUserFactory from core.utils import get_tokens_for_user @@ -1258,3 +1258,37 @@ class PurchaseEmailTest(APITestCase): payload = response.json() self.assertTrue( 'email' in payload['error']) + +class AllCategoriesTest(APITestCase): + + def setUp(self): + """Tests setup + """ + self.endpoint = '/api/v1/products/all_categories/' + # self.factory = ProductFactory + self.model = CategoryTag + # create user + self.email = f"user@mail.com" + 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.save() + + def test_get_all_categories(self): + # create instances + instances = [ + self.model.objects.create(name='A'), + self.model.objects.create(name='B'), + self.model.objects.create(name='C'), + self.model.objects.create(name='D'), + self.model.objects.create(name='E'), + ] + + response = self.client.get(self.endpoint) + + # assertions + self.assertEquals(response.status_code, 200) + + payload = response.json() + + self.assertEquals(len(instances), len(payload)) diff --git a/products/views.py b/products/views.py index c53d1b8..f80edbe 100644 --- a/products/views.py +++ b/products/views.py @@ -332,3 +332,13 @@ def purchase_email(request): # response return Response() + + +@api_view(['GET']) +@permission_classes([AllowAny,]) +def all_categories(request): + all_categories = [] + for instance in CategoryTag.objects.all(): + all_categories.append(instance.label) + return Response(data=all_categories) +