fix merge

This commit is contained in:
pablogg
2021-03-16 13:09:26 +01:00
37 changed files with 4192 additions and 361 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
# environment variables
.env
# virtual env
venv/

191
README.md
View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -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 = {

View File

@@ -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,14 +35,16 @@ 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'),
path('api/v1/create_company_user/', core_views.create_company_user, name='create-company-user'),
path('api/v1/my_user/', core_views.my_user, name='my-user'),
path('api/v1/my_company/', company_views.my_company , name='my-company'),
path('api/v1/my_company/', company_views.my_company, name='my-company'),
path('api/v1/companies/sample/', company_views.random_company_sample , name='company-sample'),
path('api/v1/my_products/', product_views.my_products, name='my-products'),
path('api/v1/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)),

View File

@@ -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}

View File

@@ -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

View File

@@ -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)

View File

@@ -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',])

View File

@@ -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')

View File

@@ -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'

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -14,4 +14,6 @@ AWS_SECRET_ACCESS_KEY_SES = ''
WC_KEY = ''
WC_SECRET = ''
# GOOGLE MAPS
GOOGLE_MAP_API_KEY = ''
GOOGLE_MAP_API_KEY = ''
# USER ACTIVATION REDIRECTION
ACTIVATION_REDIRECT = ''

View File

@@ -73,7 +73,10 @@ class City(models.Model):
updated = models.DateTimeField('date last update', auto_now=True)
def __str__(self):
return f'{self.name} [{self.province}]'
if self.province:
return f'{self.name} [{self.province}]'
else:
return f'{self.name}'
class Meta:
verbose_name = "Municipio"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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}"

View File

@@ -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))

View File

@@ -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:

View File

@@ -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',
@@ -37,42 +44,47 @@ logging.basicConfig(
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.filter(active=True).order_by('-created')
serializer_class = ProductSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCreator]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_class = ProductTagFilter
queryset = Product.objects.filter(active=True).order_by('-created')
serializer_class = ProductSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCreator]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_class = ProductTagFilter
def perform_create(self, serializer):
serializer.save(creator=self.request.user, company=self.request.user.company)
def perform_create(self, serializer):
serializer.save(creator=self.request.user, company=self.request.user.company)
@action(detail=True, methods=['GET',])
def related(request):
# TODO: find the most similar products
return Response(data=[])
@action(detail=True, methods=['GET',])
def related(self, request, pk=None):
"""Find instances similar to the one referenced
"""
# TODO: find the most similar products
product = self.get_object()
qs = 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)

View File

@@ -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
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -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

View File

@@ -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)

View File

@@ -1,10 +1,174 @@
Hola {{company.creator.full_name}}.
Estamos contactando contigo como usuario gerente de la compañina {{company.company_name}}.
<!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" />
Datos de usuario:
<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;
}
- {{user.full_name}}
- {{user.email}}
.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;
}
Contenido del mensaje:
{{data}}
.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ñía {{company.company_name}}.
Datos de usuario:
- {{user.full_name}}
- {{user.email}}
Contenido del mensaje:
{{data}}
</p>
</main>
<footer class="footer">
<span>2021 La Tienda.Coop</span>
</footer>
</div>
</body>
</html>

View File

@@ -1,2 +1,166 @@
Hola {{user.full_name}}.
Hemos enviado un email a {{company.company_name}} sobre tu petición.
<!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>

View File

@@ -1,7 +1,168 @@
{% autoescape off %}
Hola {{ user.full_name }},
Por favor, verifica tu registro en LaTiendaCOOP haciendo click en el siguiente enlace:
<!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" />
http://{{ domain }}{% url 'activate_user' uidb64=uid token=token %}
<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;
}
{% endautoescape %}
.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 %}
</p>
</main>
<footer class="footer">
<span>2021 La Tienda.Coop</span>
</footer>
</div>
</body>
</html>

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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