fix merge
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
# environment variables
|
||||
.env
|
||||
|
||||
|
||||
# virtual env
|
||||
venv/
|
||||
|
||||
|
||||
191
README.md
191
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,172 @@ 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/<int:pk>/`
|
||||
|
||||
Permissions: only accessible for your own user instance
|
||||
|
||||
|
||||
### UpdateUserView
|
||||
|
||||
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/`
|
||||
|
||||
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`
|
||||
|
||||
### activate_user
|
||||
|
||||
Endpoint: `/activate/<uidb64>/<token>/`
|
||||
|
||||
This endpoint is reached from the URL sent to the user after their registration
|
||||
|
||||
### User Management
|
||||
|
||||
Creation:
|
||||
|
||||
- endpoint: /api/v1/users/
|
||||
- endpoint: `/api/v1/users/`
|
||||
- method: GET
|
||||
- payload:
|
||||
```json
|
||||
@@ -149,32 +300,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:
|
||||
|
||||
@@ -183,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
|
||||
|
||||
|
||||
@@ -35,6 +35,23 @@ 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):
|
||||
if request.user.is_authenticated:
|
||||
return request.user.role == self.admin_role
|
||||
return False
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.user.is_authenticated:
|
||||
return request.user.role == self.admin_role
|
||||
return False
|
||||
|
||||
|
||||
class ReadOnly(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
@@ -68,3 +85,4 @@ class YourOwnUserPermissions(permissions.BasePermission):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@@ -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, AdminCompanyViewSet
|
||||
from products.views import ProductViewSet, MyProductsViewSet, AdminProductsViewSet
|
||||
from history.views import HistorySyncViewSet
|
||||
from stats.views import StatsLogViewSet
|
||||
|
||||
@@ -13,7 +13,10 @@ router = routers.DefaultRouter()
|
||||
|
||||
router.register('users', CustomUserViewSet, basename='users')
|
||||
router.register('companies', CompanyViewSet, basename='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-products')
|
||||
router.register('history', HistorySyncViewSet, basename='history')
|
||||
router.register('stats', StatsLogViewSet, basename='stats')
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -57,6 +56,7 @@ INSTALLED_APPS = [
|
||||
'anymail',
|
||||
'storages',
|
||||
'mapwidgets',
|
||||
'django_admin_listfilter_dropdown',
|
||||
|
||||
# local apps
|
||||
'core',
|
||||
@@ -170,3 +170,6 @@ MAP_WIDGETS = {
|
||||
),
|
||||
"GOOGLE_MAP_API_KEY": os.getenv('GOOGLE_MAP_API_KEY')
|
||||
}
|
||||
|
||||
# ACTIVATION_REDIRECT URL
|
||||
ACTIVATION_REDIRECT = os.getenv('ACTIVATION_REDIRECT')
|
||||
|
||||
@@ -40,7 +40,9 @@ 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 = "no-reply@latienda.com"
|
||||
DEFAULT_FROM_EMAIL = "info@latienda.coop"
|
||||
|
||||
SERVER_EMAIL = "mail-server@latienda.com"
|
||||
|
||||
ANYMAIL = {
|
||||
|
||||
@@ -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),
|
||||
@@ -34,6 +35,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/<int:pk>/', 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'),
|
||||
@@ -41,7 +43,8 @@ urlpatterns = [
|
||||
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/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)),
|
||||
|
||||
@@ -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
|
||||
from mapwidgets.widgets import GooglePointFieldWidget
|
||||
|
||||
from . import models
|
||||
@@ -8,9 +9,9 @@ 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_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city')
|
||||
search_fields = ('short_name', 'company_name', 'email', 'url')
|
||||
list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active', 'link')
|
||||
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}
|
||||
|
||||
@@ -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,8 +52,21 @@ 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='comp_hist')
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
link.boolean = True
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
@@ -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
|
||||
@@ -313,7 +314,9 @@ class MyCompanyViewTest(APITestCase):
|
||||
|
||||
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)
|
||||
@@ -321,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))
|
||||
self.assertEquals(payload['company']['id'], company.id)
|
||||
|
||||
def test_anon_user_cannot_access(self):
|
||||
# send in request
|
||||
@@ -404,3 +385,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)
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ 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 rest_framework import filters
|
||||
from back_latienda.permissions import IsCreator, IsSiteAdmin
|
||||
|
||||
from utils import woocommerce
|
||||
|
||||
@@ -69,11 +69,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 = {
|
||||
@@ -102,10 +104,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
|
||||
@@ -159,23 +163,37 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
||||
return Response(message)
|
||||
|
||||
|
||||
@api_view(['GET',])
|
||||
@permission_classes([IsAuthenticated,])
|
||||
@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)
|
||||
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
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(company=self.request.user.company)
|
||||
|
||||
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
|
||||
"""
|
||||
queryset = Company.objects.all()
|
||||
serializer_class = CompanySerializer
|
||||
permission_classes = [IsSiteAdmin]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(creator=self.request.user)
|
||||
|
||||
|
||||
@api_view(['GET',])
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
from . import models
|
||||
# Register your models here.
|
||||
|
||||
|
||||
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')
|
||||
|
||||
42
core/apps.py
42
core/apps.py
@@ -1,5 +1,47 @@
|
||||
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'),
|
||||
)
|
||||
|
||||
def ready(self):
|
||||
super(SuitConfig, self).ready()
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'core'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
@@ -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
|
||||
|
||||
381
core/tests.py
381
core/tests.py
@@ -7,11 +7,14 @@ 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 django.conf import settings
|
||||
|
||||
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
|
||||
|
||||
@@ -39,7 +42,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 = {
|
||||
@@ -55,7 +58,11 @@ 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
|
||||
self.assertTrue(len(mail.outbox) == 1)
|
||||
|
||||
def test_anon_user_cannot_modify_existing_instance(self):
|
||||
"""Not logged-in user cannot modify existing instance
|
||||
@@ -155,6 +162,30 @@ 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",
|
||||
'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)
|
||||
|
||||
# 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
|
||||
@@ -182,6 +213,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
|
||||
@@ -324,99 +357,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):
|
||||
@@ -533,3 +473,252 @@ class MyUserViewTest(APITestCase):
|
||||
# check response
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class ActivateUserTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.endpoint = 'activate/<uidb64>/<token>/'
|
||||
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, 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))
|
||||
|
||||
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())
|
||||
|
||||
|
||||
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 = "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()
|
||||
|
||||
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',
|
||||
'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
|
||||
}
|
||||
}
|
||||
|
||||
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_creation_error(self):
|
||||
|
||||
response = self.client.post(self.endpoint, data={}, format='json')
|
||||
|
||||
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)
|
||||
|
||||
# admin user
|
||||
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', 'companies_timeline', 'products_timeline', 'users_timeline', 'contact_timeline', 'shopping_timeline']
|
||||
for name in expected_entries:
|
||||
self.assertTrue(name in payload)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
164
core/views.py
164
core/views.py
@@ -10,7 +10,9 @@ 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
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets
|
||||
@@ -20,7 +22,10 @@ from rest_framework.generics import UpdateAPIView
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
|
||||
from companies.models import Company
|
||||
from geo.models import City
|
||||
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
|
||||
@@ -43,9 +48,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
|
||||
@@ -76,9 +81,11 @@ 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
|
||||
utils.send_verification_email(request, instance)
|
||||
|
||||
return Response(self.read_serializer_class(
|
||||
instance, many=False, context={'request': request}).data,
|
||||
@@ -98,63 +105,47 @@ 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,])
|
||||
@permission_classes([AllowAny])
|
||||
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']
|
||||
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_serializer.errors}, 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.objects.create_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_serializer.errors}, 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',])
|
||||
@@ -168,7 +159,6 @@ def my_user(request):
|
||||
return Response({'error': {str(type(e))}}, status=500)
|
||||
|
||||
|
||||
|
||||
@api_view(['POST',])
|
||||
@permission_classes([IsAdminUser,])
|
||||
def load_coop_managers(request):
|
||||
@@ -200,10 +190,66 @@ 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")
|
||||
if settings.ACTIVATION_REDIRECT:
|
||||
return redirect(settings.ACTIVATION_REDIRECT)
|
||||
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)
|
||||
|
||||
|
||||
@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__within=region.geo).count()
|
||||
companies_per_region[region.name] = count
|
||||
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)
|
||||
|
||||
@@ -15,3 +15,5 @@ WC_KEY = ''
|
||||
WC_SECRET = ''
|
||||
# GOOGLE MAPS
|
||||
GOOGLE_MAP_API_KEY = ''
|
||||
# USER ACTIVATION REDIRECTION
|
||||
ACTIVATION_REDIRECT = ''
|
||||
@@ -73,7 +73,10 @@ class City(models.Model):
|
||||
updated = models.DateTimeField('date last update', auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.province:
|
||||
return f'{self.name} [{self.province}]'
|
||||
else:
|
||||
return f'{self.name}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Municipio"
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter, ChoiceDropdownFilter
|
||||
|
||||
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', RelatedDropdownFilter),
|
||||
)
|
||||
|
||||
def company_name(self, instance):
|
||||
if instance.company and instance.company.company_name:
|
||||
return instance.company.company_name
|
||||
else:
|
||||
return 'NULL'
|
||||
|
||||
|
||||
admin.site.register(models.HistorySync, HistoryAdmin)
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from django_admin_listfilter_dropdown.filters import DropdownFilter, RelatedDropdownFilter, ChoiceDropdownFilter
|
||||
|
||||
from . import models
|
||||
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_filter = ('company', 'tags', 'category', 'attributes')
|
||||
list_display = ('name', 'category', 'sourcing_date', 'company', 'active' )
|
||||
list_filter = (
|
||||
('company', RelatedDropdownFilter),
|
||||
('tags', RelatedDropdownFilter),
|
||||
('category', RelatedDropdownFilter),
|
||||
('attributes', RelatedDropdownFilter)
|
||||
)
|
||||
|
||||
search_fields = ('name', 'sku', 'description')
|
||||
|
||||
|
||||
'''
|
||||
class ProductInline(admin.TabularInline):
|
||||
model = models.Product
|
||||
form = forms.ProductTagForm
|
||||
'''
|
||||
|
||||
admin.site.register(models.Product, ProductAdmin)
|
||||
admin.site.register(models.TreeTag)
|
||||
admin.site.register(models.CategoryTag)
|
||||
|
||||
@@ -64,13 +64,13 @@ 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)
|
||||
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}"
|
||||
|
||||
@@ -6,14 +6,14 @@ 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
|
||||
|
||||
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.models import Product, CategoryTag
|
||||
|
||||
from core.factories import CustomUserFactory
|
||||
from core.utils import get_tokens_for_user
|
||||
@@ -194,6 +194,25 @@ class ProductViewSetTest(APITestCase):
|
||||
# Assert number of instnaces in response
|
||||
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()
|
||||
|
||||
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) <= 10)
|
||||
|
||||
# authenticated user
|
||||
def test_auth_user_can_paginate_instances(self):
|
||||
"""authenticated user can paginate instances
|
||||
@@ -374,7 +393,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)
|
||||
|
||||
@@ -934,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
|
||||
@@ -946,11 +968,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
|
||||
@@ -960,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"
|
||||
@@ -970,8 +994,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
|
||||
@@ -980,41 +1004,291 @@ 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']}")
|
||||
|
||||
class FindRelatedProductsTest(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)
|
||||
|
||||
|
||||
class AdminProductViewSetTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Tests setup
|
||||
"""
|
||||
self.factory = ActiveProductFactory
|
||||
self.endpoint = '/api/v1/admin_products/'
|
||||
self.factory = ProductFactory
|
||||
self.model = Product
|
||||
# clear table
|
||||
self.model.objects.all().delete()
|
||||
# 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_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),
|
||||
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 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.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 = {
|
||||
'email': self.email,
|
||||
'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, 200)
|
||||
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',
|
||||
'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))
|
||||
|
||||
def test_anon_user_bad_email(self):
|
||||
company = CompanyFactory()
|
||||
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'])
|
||||
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
unexpected_instances = [
|
||||
self.factory(tags="notcool"), # shouldn't catch it
|
||||
self.factory(tags="azules"),
|
||||
]
|
||||
response = self.client.get(self.endpoint)
|
||||
|
||||
# searh for it
|
||||
results = find_related_products_v3(tag)
|
||||
# assertions
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
# assert result
|
||||
self.assertTrue(len(results) == len(expected_instances))
|
||||
payload = response.json()
|
||||
|
||||
self.assertEquals(len(instances), len(payload))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,25 +86,45 @@ def extract_search_filters(result_set):
|
||||
return filter_dict
|
||||
|
||||
|
||||
def find_related_products_v3(keyword):
|
||||
def get_related_products(product):
|
||||
"""Make different db searches until you get 10 instances to return
|
||||
"""
|
||||
Ranked product search
|
||||
total_results = []
|
||||
|
||||
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)
|
||||
# search by category
|
||||
category_qs = Product.objects.filter(category=product.category)[:10]
|
||||
# add to results
|
||||
for item in category_qs:
|
||||
total_results.append(item)
|
||||
|
||||
products_qs = Product.objects.annotate(
|
||||
rank=SearchRank(vector, query)
|
||||
).filter(rank__gt=0.05) # removed order_by because its lost in casting
|
||||
# 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)
|
||||
|
||||
return set(products_qs)
|
||||
# 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 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
|
||||
|
||||
@@ -153,9 +174,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'))
|
||||
@@ -183,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:
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
import logging
|
||||
import csv
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from django.db.models import Q
|
||||
from django.core import serializers
|
||||
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
|
||||
|
||||
# Create your views here.
|
||||
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
|
||||
|
||||
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 .utils import extract_search_filters, find_related_products_v3, find_related_products_v6, product_loader
|
||||
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, ranked_product_search, product_loader, get_related_products
|
||||
from utils.tag_serializers import TaggitSerializer
|
||||
from utils.tag_filters import ProductTagFilter, ProductOrderFilter
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
logging.basicConfig(
|
||||
filename='logs/product-load.log',
|
||||
@@ -47,32 +54,37 @@ class ProductViewSet(viewsets.ModelViewSet):
|
||||
serializer.save(creator=self.request.user, company=self.request.user.company)
|
||||
|
||||
@action(detail=True, methods=['GET',])
|
||||
def related(request):
|
||||
def related(self, request, pk=None):
|
||||
"""Find instances similar to the one referenced
|
||||
"""
|
||||
# TODO: find the most similar products
|
||||
return Response(data=[])
|
||||
product = self.get_object()
|
||||
qs = get_related_products(product)
|
||||
serializer = self.serializer_class(qs, many=True)
|
||||
return Response(data=serializer.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):
|
||||
""" Allows user to get all products where user is product creator
|
||||
"""
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(company=self.request.user.company).order_by('-created')
|
||||
|
||||
|
||||
|
||||
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',])
|
||||
@@ -169,7 +181,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:
|
||||
@@ -181,7 +193,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']:
|
||||
@@ -235,3 +247,98 @@ class CategoryTagAutocomplete(autocomplete.Select2QuerySetView):
|
||||
qs = qs.filter(name__icontains=self.q)
|
||||
|
||||
return qs # [x.label for x in qs]
|
||||
|
||||
|
||||
@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 ('telephone', 'company', 'product', 'comment'):
|
||||
assert(param in data.keys())
|
||||
except:
|
||||
return Response({"error": "Required parameters for anonymous user: telephone, company, product, comment"}, status=status.HTTP_406_NOT_ACCEPTABLE)
|
||||
|
||||
if request.user.is_anonymous:
|
||||
user_email = data.get('email')
|
||||
else:
|
||||
user_email = request.user.email
|
||||
telephone = data.get('telephone')
|
||||
# validate email
|
||||
try:
|
||||
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:
|
||||
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)
|
||||
try:
|
||||
# send email to company
|
||||
company_message = render_to_string('purchase_notification_v2.html', {
|
||||
'company': company,
|
||||
'user': request.user,
|
||||
'product': product,
|
||||
'telephone': data['telephone'],
|
||||
'comment': comment,
|
||||
})
|
||||
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
|
||||
user_message = render_to_string('purchase_contact_confirmation_v2.html', {
|
||||
'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])
|
||||
email.content_subtype = "html"
|
||||
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 = {
|
||||
'action_object': product,
|
||||
'user': request.user if request.user.is_authenticated else None,
|
||||
'anonymous': request.user.is_anonymous,
|
||||
'contact': True,
|
||||
'shop': company.shop,
|
||||
}
|
||||
StatsLog.objects.create(**stats_data)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
2
static/.gitignore
vendored
Normal file
2
static/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -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
|
||||
|
||||
@@ -52,13 +52,18 @@ def track_user(request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
|
||||
if data.get('geo'):
|
||||
coordinates = (data['geo'].get('latitude'), data['geo'].get('longitude'))
|
||||
else:
|
||||
coordinates = None
|
||||
|
||||
# gather instance data
|
||||
instance_data = {
|
||||
'action_object': data.get('action_object'),
|
||||
'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(coordinates),
|
||||
}
|
||||
|
||||
if data['action_object'].get('model') == 'product':
|
||||
@@ -76,4 +81,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)
|
||||
|
||||
@@ -1,5 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Interés en copra de producto</title>
|
||||
<style>
|
||||
.body {
|
||||
color: #374493;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #8cead8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mail {
|
||||
margin: 50px auto;
|
||||
width: 60%;
|
||||
padding: 30px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
}
|
||||
.latienda-logo {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
.product-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.text {
|
||||
text-align: justify;
|
||||
font-size: 1.2rem;
|
||||
padding: 3px;
|
||||
margin-top: 40px;
|
||||
max-width: 300px;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.coop-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 250px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.product-link {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.product-price {
|
||||
font-weight: 700;
|
||||
}
|
||||
.coop-info {
|
||||
max-width: fit-content;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.coop-data {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 20px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
align-self: left;
|
||||
}
|
||||
.coop-image {
|
||||
max-width: 120px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
display: inline;
|
||||
}
|
||||
.value {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
font-weight: 400;
|
||||
}
|
||||
@media all and (min-width:780px) {
|
||||
.mail {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 780px) {
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.value, .name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body">
|
||||
<div class="mail">
|
||||
<header class="header">
|
||||
<a href="https://latienda.coop/" target="_blank">
|
||||
<img
|
||||
src="https://latienda.coop/_nuxt/img/latienda-logo.3e53761.svg"
|
||||
alt=""
|
||||
class="latienda-logo"
|
||||
/>
|
||||
</a>
|
||||
<h1 class="title">
|
||||
<span>Confirmación de</span>
|
||||
<span>solicitud enviada</span>
|
||||
</h1>
|
||||
</header>
|
||||
<main class="main">
|
||||
<p class="text">
|
||||
Hola {{company.creator.full_name}}.
|
||||
Estamos contactando contigo como usuario gerente de la compañina {{company.company_name}}.
|
||||
Estamos contactando contigo como usuario gerente de la compañía {{company.company_name}}.
|
||||
|
||||
Datos de usuario:
|
||||
|
||||
@@ -8,3 +164,11 @@ Datos de usuario:
|
||||
|
||||
Contenido del mensaje:
|
||||
{{data}}
|
||||
</p>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>2021 La Tienda.Coop</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,2 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Interés en copra de producto</title>
|
||||
<style>
|
||||
.body {
|
||||
color: #374493;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #8cead8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mail {
|
||||
margin: 50px auto;
|
||||
width: 60%;
|
||||
padding: 30px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
}
|
||||
.latienda-logo {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
.product-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.text {
|
||||
text-align: justify;
|
||||
font-size: 1.2rem;
|
||||
padding: 3px;
|
||||
margin-top: 40px;
|
||||
max-width: 300px;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.coop-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 250px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.product-link {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.product-price {
|
||||
font-weight: 700;
|
||||
}
|
||||
.coop-info {
|
||||
max-width: fit-content;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.coop-data {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 20px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
align-self: left;
|
||||
}
|
||||
.coop-image {
|
||||
max-width: 120px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
display: inline;
|
||||
}
|
||||
.value {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
font-weight: 400;
|
||||
}
|
||||
@media all and (min-width:780px) {
|
||||
.mail {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 780px) {
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.value, .name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body">
|
||||
<div class="mail">
|
||||
<header class="header">
|
||||
<a href="https://latienda.coop/" target="_blank">
|
||||
<img
|
||||
src="https://latienda.coop/_nuxt/img/latienda-logo.3e53761.svg"
|
||||
alt=""
|
||||
class="latienda-logo"
|
||||
/>
|
||||
</a>
|
||||
<h1 class="title">
|
||||
<span>Confirmación de</span>
|
||||
<span>solicitud enviada</span>
|
||||
</h1>
|
||||
</header>
|
||||
<main class="main">
|
||||
<p class="text">
|
||||
Hola {{user.full_name}}.
|
||||
Hemos enviado un email a {{company.company_name}} sobre tu petición.
|
||||
</p>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>2021 La Tienda.Coop</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,168 @@
|
||||
{% autoescape off %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Interés en copra de producto</title>
|
||||
<style>
|
||||
.body {
|
||||
color: #374493;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #8cead8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mail {
|
||||
margin: 50px auto;
|
||||
width: 60%;
|
||||
padding: 30px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
}
|
||||
.latienda-logo {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
.product-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.text {
|
||||
text-align: justify;
|
||||
font-size: 1.2rem;
|
||||
padding: 3px;
|
||||
margin-top: 40px;
|
||||
max-width: 300px;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.coop-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 250px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.product-link {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.product-price {
|
||||
font-weight: 700;
|
||||
}
|
||||
.coop-info {
|
||||
max-width: fit-content;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.coop-data {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 20px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
align-self: left;
|
||||
}
|
||||
.coop-image {
|
||||
max-width: 120px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
display: inline;
|
||||
}
|
||||
.value {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
font-weight: 400;
|
||||
}
|
||||
@media all and (min-width:780px) {
|
||||
.mail {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 780px) {
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.value, .name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body">
|
||||
<div class="mail">
|
||||
<header class="header">
|
||||
<a href="https://latienda.coop/" target="_blank">
|
||||
<img
|
||||
src="https://latienda.coop/_nuxt/img/latienda-logo.3e53761.svg"
|
||||
alt=""
|
||||
class="latienda-logo"
|
||||
/>
|
||||
</a>
|
||||
<h1 class="title">
|
||||
<span>Confirmación de</span>
|
||||
<span>solicitud enviada</span>
|
||||
</h1>
|
||||
</header>
|
||||
<main class="main">
|
||||
<p class="text">
|
||||
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 %}
|
||||
|
||||
{% endautoescape %}
|
||||
</p>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>2021 La Tienda.Coop</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
197
templates/purchase_contact_confirmation.html
Executable file
197
templates/purchase_contact_confirmation.html
Executable file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Interés en copra de producto</title>
|
||||
<style>
|
||||
.body {
|
||||
color: #374493;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #8cead8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mail {
|
||||
margin: 50px auto;
|
||||
width: 60%;
|
||||
padding: 30px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
}
|
||||
.latienda-logo {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
.product-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.text {
|
||||
text-align: justify;
|
||||
font-size: 1.2rem;
|
||||
padding: 3px;
|
||||
margin-top: 40px;
|
||||
max-width: 300px;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.coop-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
max-width: 250px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.product-link {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.product-price {
|
||||
font-weight: 700;
|
||||
}
|
||||
.coop-info {
|
||||
max-width: fit-content;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.coop-data {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 20px;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
align-self: left;
|
||||
}
|
||||
.coop-image {
|
||||
max-width: 120px;
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
display: inline;
|
||||
}
|
||||
.value {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
font-weight: 400;
|
||||
}
|
||||
@media all and (min-width:780px) {
|
||||
.mail {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 780px) {
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.value, .name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body">
|
||||
<div class="mail">
|
||||
<header class="header">
|
||||
<a href="https://latienda.coop/" target="_blank">
|
||||
<img
|
||||
src="https://latienda.coop/_nuxt/img/latienda-logo.3e53761.svg"
|
||||
alt=""
|
||||
class="latienda-logo"
|
||||
/>
|
||||
</a>
|
||||
<h1 class="title">
|
||||
<span>Confirmación de</span>
|
||||
<span>solicitud enviada</span>
|
||||
</h1>
|
||||
</header>
|
||||
<main class="main">
|
||||
|
||||
<div class="product-data">
|
||||
<a
|
||||
href="https://latiendacoop.s3.amazonaws.com/products/0101UNADEGATOBERMEJO.jpeg"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://latiendacoop.s3.amazonaws.com/products/0101UNADEGATOBERMEJO.jpeg"
|
||||
alt=""
|
||||
class="product-image"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://latienda.coop/productos/356"
|
||||
class="product-link"
|
||||
>Enlace al producto</a
|
||||
>
|
||||
<span class="product-name">Nombre del producto: {{product.name}}</span>
|
||||
<span class="product-id-sku">{{product.sku}}</span>
|
||||
<span class="product-price">{{product.price}}</span>
|
||||
<p class="text">¡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 <span class="coop-name">{{company.short_name}}</span>.</p>
|
||||
</div>
|
||||
<div class="coop-info">
|
||||
<h2>Contacto cooperativa</h2>
|
||||
<dl class="coop-data">
|
||||
<dt class="name">Nombre:</dt>
|
||||
<dd class="value">{{manager.full_name}}<br /></dd>
|
||||
<dt class="name">Email:</dt>
|
||||
<dd class="value">{{company.email}}<br /></dd>
|
||||
</dd>
|
||||
</dl>
|
||||
<a href="https://pol-len.cat/" target="_blank">
|
||||
<img class="coop-image" src="https://latiendacoop.s3.amazonaws.com/logos/pollen-logo.png" target="_blank" alt="">
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<span>2021 La Tienda.Coop</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1043
templates/purchase_contact_confirmation_v2.html
Normal file
1043
templates/purchase_contact_confirmation_v2.html
Normal file
File diff suppressed because it is too large
Load Diff
1033
templates/purchase_notification_v2.html
Executable file
1033
templates/purchase_notification_v2.html
Executable file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user