fix merge
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
|||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
|
||||||
# virtual env
|
# virtual env
|
||||||
venv/
|
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)
|
- [First Steps](#first-steps)
|
||||||
- [Load location data](#load-location-data)
|
- [Load location data](#load-location-data)
|
||||||
- [Load taxonomy data](#load-taxonomy-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)
|
- [Shop Integrations](#shop-integrations)
|
||||||
- [WooCommerce](#woocommerce)
|
- [WooCommerce](#woocommerce)
|
||||||
- [Product Search](#product-search)
|
- [Product Search](#product-search)
|
||||||
@@ -57,25 +61,172 @@ This data serves as initial Tags
|
|||||||
To load initial set of tags: `python manage.py addtaxonomy`
|
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:
|
|
||||||
```
|
## Product Endpoints
|
||||||
dict_keys(['count', 'next', 'previous', 'results'])
|
|
||||||
```
|
### 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
|
### User Management
|
||||||
|
|
||||||
Creation:
|
Creation:
|
||||||
|
|
||||||
- endpoint: /api/v1/users/
|
- endpoint: `/api/v1/users/`
|
||||||
- method: GET
|
- method: GET
|
||||||
- payload:
|
- payload:
|
||||||
```json
|
```json
|
||||||
@@ -149,32 +300,21 @@ To create user:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Companies
|
## History Endpoints
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Endpoint url: `/api/v1/history/`:
|
Endpoint url: `/api/v1/history/`:
|
||||||
|
|
||||||
Historical records about product importation
|
Historical records about product importation
|
||||||
|
|
||||||
|
|
||||||
### Stats
|
## Stats Endpoints
|
||||||
|
|
||||||
Endpoint url: `/api/v1/stats/`
|
Endpoint url: `/api/v1/stats/`
|
||||||
|
|
||||||
logs about user interaction with products links
|
logs about user interaction with products links
|
||||||
|
|
||||||
|
|
||||||
### Locations
|
## Location Endpoints
|
||||||
|
|
||||||
Location ednpoints:
|
Location ednpoints:
|
||||||
|
|
||||||
@@ -183,6 +323,7 @@ Location ednpoints:
|
|||||||
- `/api/v1/provinces/`
|
- `/api/v1/provinces/`
|
||||||
- `/api/v1/cities/`
|
- `/api/v1/cities/`
|
||||||
|
|
||||||
|
Tables filled with data from `datasets/gadm36_ESP.gpkg` with `loadgisdata` command.
|
||||||
|
|
||||||
## Shop Integrations
|
## Shop Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,23 @@ class IsStaff(permissions.BasePermission):
|
|||||||
return request.user.is_staff
|
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):
|
class ReadOnly(permissions.BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
@@ -68,3 +85,4 @@ class YourOwnUserPermissions(permissions.BasePermission):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from core.views import CustomUserViewSet
|
from core.views import CustomUserViewSet
|
||||||
from companies.views import CompanyViewSet
|
from companies.views import CompanyViewSet, AdminCompanyViewSet
|
||||||
from products.views import ProductViewSet
|
from products.views import ProductViewSet, MyProductsViewSet, AdminProductsViewSet
|
||||||
from history.views import HistorySyncViewSet
|
from history.views import HistorySyncViewSet
|
||||||
from stats.views import StatsLogViewSet
|
from stats.views import StatsLogViewSet
|
||||||
|
|
||||||
@@ -13,7 +13,10 @@ router = routers.DefaultRouter()
|
|||||||
|
|
||||||
router.register('users', CustomUserViewSet, basename='users')
|
router.register('users', CustomUserViewSet, basename='users')
|
||||||
router.register('companies', CompanyViewSet, basename='company')
|
router.register('companies', CompanyViewSet, basename='company')
|
||||||
|
router.register('admin_companies', AdminCompanyViewSet, basename='admin-companies')
|
||||||
router.register('products', ProductViewSet, basename='product')
|
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('history', HistorySyncViewSet, basename='history')
|
||||||
router.register('stats', StatsLogViewSet, basename='stats')
|
router.register('stats', StatsLogViewSet, basename='stats')
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,9 @@ SECRET_KEY = 'td*#7t-(1e9^(g0cod*hs**dp(%zvg@=$cug_-dtzcj#i2mrz@'
|
|||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'suit',
|
||||||
'dal',
|
'dal',
|
||||||
'dal_select2',
|
'dal_select2',
|
||||||
'suit',
|
|
||||||
|
|
||||||
|
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@@ -57,6 +56,7 @@ INSTALLED_APPS = [
|
|||||||
'anymail',
|
'anymail',
|
||||||
'storages',
|
'storages',
|
||||||
'mapwidgets',
|
'mapwidgets',
|
||||||
|
'django_admin_listfilter_dropdown',
|
||||||
|
|
||||||
# local apps
|
# local apps
|
||||||
'core',
|
'core',
|
||||||
@@ -170,3 +170,6 @@ MAP_WIDGETS = {
|
|||||||
),
|
),
|
||||||
"GOOGLE_MAP_API_KEY": os.getenv('GOOGLE_MAP_API_KEY')
|
"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'
|
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
|
||||||
|
|
||||||
EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend"
|
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"
|
SERVER_EMAIL = "mail-server@latienda.com"
|
||||||
|
|
||||||
ANYMAIL = {
|
ANYMAIL = {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from companies import views as company_views
|
|||||||
from stats import views as stat_views
|
from stats import views as stat_views
|
||||||
from .routers import router
|
from .routers import router
|
||||||
|
|
||||||
|
admin.site.site_header = 'LaTiendaCOOP Administration'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
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/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('api/v1/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
|
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/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_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/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/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/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_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/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/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/autocomplete/category-tag/', product_views.CategoryTagAutocomplete.as_view(), name='category-autocomplete'),
|
||||||
path('api/v1/', include(router.urls)),
|
path('api/v1/', include(router.urls)),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.gis.db.models import PointField
|
from django.contrib.gis.db.models import PointField
|
||||||
|
|
||||||
|
from django_admin_listfilter_dropdown.filters import DropdownFilter
|
||||||
from mapwidgets.widgets import GooglePointFieldWidget
|
from mapwidgets.widgets import GooglePointFieldWidget
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
@@ -8,9 +9,9 @@ from . import models
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
class CompanyAdmin(admin.ModelAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active')
|
list_display = ('short_name', 'city', 'email', 'shop', 'platform', 'sync', 'is_validated', 'is_active', 'link')
|
||||||
list_filter = ('platform', 'sync', 'is_validated', 'is_active', 'city')
|
list_filter = ('platform', 'sync', 'is_validated', 'is_active', ('city', DropdownFilter))
|
||||||
search_fields = ('short_name', 'company_name', 'email', 'url')
|
search_fields = ('short_name', 'company_name', 'email', 'web_link', 'city')
|
||||||
|
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
PointField: {"widget": GooglePointFieldWidget}
|
PointField: {"widget": GooglePointFieldWidget}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Company(models.Model):
|
|||||||
|
|
||||||
cif = models.CharField('CIF', max_length=15, null=True, blank=True)
|
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)
|
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)
|
web_link = models.URLField('Enlace a la web', null=True, blank=True)
|
||||||
shop = models.BooleanField('Tienda Online', null=True, default=False)
|
shop = models.BooleanField('Tienda Online', null=True, default=False)
|
||||||
shop_link = models.URLField('Enlace a la tienda', null=True, blank=True)
|
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)
|
created = models.DateTimeField('date of creation', auto_now_add=True)
|
||||||
updated = models.DateTimeField('date last update', auto_now=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')
|
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:
|
class Meta:
|
||||||
verbose_name = "Compañía"
|
verbose_name = "Compañía"
|
||||||
verbose_name_plural = "Compañías"
|
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.test import APITestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from companies.factories import ValidatedCompanyFactory
|
from companies.factories import ValidatedCompanyFactory, CompanyFactory
|
||||||
from companies.models import Company
|
from companies.models import Company
|
||||||
|
|
||||||
from core.factories import CustomUserFactory
|
from core.factories import CustomUserFactory
|
||||||
@@ -146,7 +146,7 @@ class CompanyViewSetTest(APITestCase):
|
|||||||
'logo': None,
|
'logo': None,
|
||||||
'city': None,
|
'city': None,
|
||||||
'address': 'qwer qewr 5',
|
'address': 'qwer qewr 5',
|
||||||
'geo': None,
|
'geo': {'longitude': 1.0, 'latitude': 1.0},
|
||||||
'phone': '1234',
|
'phone': '1234',
|
||||||
'mobile': '4321',
|
'mobile': '4321',
|
||||||
'other_phone': '41423',
|
'other_phone': '41423',
|
||||||
@@ -293,6 +293,7 @@ class CompanyViewSetTest(APITestCase):
|
|||||||
# check order
|
# check order
|
||||||
self.assertTrue(response.data[0]['id'] > response.data[1]['id'])
|
self.assertTrue(response.data[0]['id'] > response.data[1]['id'])
|
||||||
|
|
||||||
|
# TODO: test email_manager action
|
||||||
|
|
||||||
class MyCompanyViewTest(APITestCase):
|
class MyCompanyViewTest(APITestCase):
|
||||||
"""CompanyViewset tests
|
"""CompanyViewset tests
|
||||||
@@ -313,7 +314,9 @@ class MyCompanyViewTest(APITestCase):
|
|||||||
|
|
||||||
def test_auth_user_gets_data(self):
|
def test_auth_user_gets_data(self):
|
||||||
# create instance
|
# create instance
|
||||||
user_instances = [self.factory(creator=self.user) for i in range(5)]
|
company = CompanyFactory()
|
||||||
|
self.user.company = company
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
# Authenticate
|
# Authenticate
|
||||||
token = get_tokens_for_user(self.user)
|
token = get_tokens_for_user(self.user)
|
||||||
@@ -321,32 +324,10 @@ class MyCompanyViewTest(APITestCase):
|
|||||||
|
|
||||||
# Query endpoint
|
# Query endpoint
|
||||||
response = self.client.get(self.endpoint)
|
response = self.client.get(self.endpoint)
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Assert forbidden code
|
# Assert forbidden code
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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()
|
payload = response.json()
|
||||||
self.assertEquals(2, len(payload))
|
self.assertEquals(payload['company']['id'], company.id)
|
||||||
|
|
||||||
def test_anon_user_cannot_access(self):
|
def test_anon_user_cannot_access(self):
|
||||||
# send in request
|
# send in request
|
||||||
@@ -404,3 +385,168 @@ class RandomCompanySampleTest(APITestCase):
|
|||||||
self.assertEquals(size, len(payload))
|
self.assertEquals(size, len(payload))
|
||||||
# test IDs not correlative (eventually it could be, because it's random)
|
# test IDs not correlative (eventually it could be, because it's random)
|
||||||
self.assertTrue(payload[0]['id'] != (payload[1]['id'] + 1))
|
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.models import Company
|
||||||
from companies.serializers import CompanySerializer
|
from companies.serializers import CompanySerializer
|
||||||
from utils.tag_filters import CompanyTagFilter
|
from utils.tag_filters import CompanyTagFilter
|
||||||
from back_latienda.permissions import IsCreator
|
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
|
from back_latienda.permissions import IsCreator, IsSiteAdmin
|
||||||
|
|
||||||
from utils import woocommerce
|
from utils import woocommerce
|
||||||
|
|
||||||
@@ -69,11 +69,13 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
|||||||
# send email to company
|
# send email to company
|
||||||
subject = "Contacto de usuario"
|
subject = "Contacto de usuario"
|
||||||
email = EmailMessage(subject, company_message, to=[instance.creator.email])
|
email = EmailMessage(subject, company_message, to=[instance.creator.email])
|
||||||
|
email.content_subtype = "html"
|
||||||
email.send()
|
email.send()
|
||||||
logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}")
|
logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}")
|
||||||
# send confirmation email to user
|
# send confirmation email to user
|
||||||
subject = 'Confirmación de contacto'
|
subject = 'Confirmación de contacto'
|
||||||
email = EmailMessage(subject, message, to=[request.user.email])
|
email = EmailMessage(subject, message, to=[request.user.email])
|
||||||
|
email.content_subtype = "html"
|
||||||
email.send()
|
email.send()
|
||||||
logging.info(f"Contact confirmation email sent to {request.user.email}")
|
logging.info(f"Contact confirmation email sent to {request.user.email}")
|
||||||
stats_data = {
|
stats_data = {
|
||||||
@@ -102,10 +104,12 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
|||||||
})
|
})
|
||||||
# send email to company
|
# send email to company
|
||||||
email = EmailMessage(subject, company_message, to=[instance.creator.email])
|
email = EmailMessage(subject, company_message, to=[instance.creator.email])
|
||||||
|
email.content_subtype = "html"
|
||||||
email.send()
|
email.send()
|
||||||
logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}")
|
logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}")
|
||||||
# send confirmation email to user
|
# send confirmation email to user
|
||||||
email = EmailMessage(subject, user_message, to=[data['email']])
|
email = EmailMessage(subject, user_message, to=[data['email']])
|
||||||
|
email.content_subtype = "html"
|
||||||
email.send()
|
email.send()
|
||||||
logging.info(f"Contact confirmation email sent to anonymous user {data['email']}")
|
logging.info(f"Contact confirmation email sent to anonymous user {data['email']}")
|
||||||
# statslog data to register interaction
|
# statslog data to register interaction
|
||||||
@@ -159,23 +163,37 @@ class CompanyViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(message)
|
return Response(message)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET',])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated,])
|
@permission_classes([IsAuthenticated])
|
||||||
def my_company(request):
|
def my_company(request):
|
||||||
limit = request.GET.get('limit')
|
if request.user.company:
|
||||||
offset = request.GET.get('offset')
|
serializer = CompanySerializer(request.user.company)
|
||||||
qs = Company.objects.filter(creator=request.user)
|
return Response({'company': serializer.data})
|
||||||
company_serializer = CompanySerializer(qs, many=True)
|
else:
|
||||||
data = company_serializer.data
|
return Response(status=status.HTTP_406_NOT_ACCEPTABLE)
|
||||||
# RESULTS PAGINATION
|
|
||||||
if limit is not None and offset is not None:
|
'''
|
||||||
limit = int(limit)
|
class MyCompanyViewSet(viewsets.ModelViewSet):
|
||||||
offset = int(offset)
|
model = Company
|
||||||
data = data[offset:(limit+offset)]
|
serializer_class = CompanySerializer
|
||||||
elif limit is not None:
|
permission_classes = [IsAuthenticated]
|
||||||
limit = int(limit)
|
|
||||||
data = data[:limit]
|
def get_queryset(self):
|
||||||
return Response(data=data)
|
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',])
|
@api_view(['GET',])
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
list_display = ('email', 'full_name', 'role', 'company', 'email_verified', 'is_active', 'is_staff', 'created', 'last_visit')
|
list_display = ('email', 'full_name', 'role', 'company', 'email_verified', 'is_active', 'is_staff', 'created', 'last_visit')
|
||||||
list_filter = ('is_active', 'is_staff', 'email_verified')
|
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 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):
|
class CoreConfig(AppConfig):
|
||||||
name = 'core'
|
name = 'core'
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class UserManager(BaseUserManager):
|
|||||||
|
|
||||||
def create_user(self, email, password=None, **extra_fields):
|
def create_user(self, email, password=None, **extra_fields):
|
||||||
extra_fields.setdefault('is_superuser', False)
|
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)
|
return self._create_user(email, password, **extra_fields)
|
||||||
|
|
||||||
def create_superuser(self, email, password, **extra_fields):
|
def create_superuser(self, email, password, **extra_fields):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class CustomUserWriteSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CustomUser
|
model = models.CustomUser
|
||||||
fields = ('email', 'full_name', 'role', 'password', 'provider')
|
fields = ('email', 'full_name', 'role', 'password', 'provider', 'notify')
|
||||||
|
|
||||||
|
|
||||||
class CreatorSerializer(serializers.ModelSerializer):
|
class CreatorSerializer(serializers.ModelSerializer):
|
||||||
@@ -85,7 +85,7 @@ class UpdateUserSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CustomUser
|
model = models.CustomUser
|
||||||
fields = ('full_name', 'email')
|
fields = ('full_name', 'email', 'notify')
|
||||||
|
|
||||||
def validate_email(self, value):
|
def validate_email(self, value):
|
||||||
user = self.context['request'].user
|
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.test import TestCase
|
||||||
from django.core import mail
|
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.test import APITestCase
|
||||||
from rest_framework import status
|
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
|
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)
|
self.user = self.factory(email=self.reg_email, password=self.password, is_active=True)
|
||||||
|
|
||||||
# anon user
|
# 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
|
"""Not logged-in user can create new instance of User but it's inactive
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
@@ -55,7 +58,11 @@ class CustomUserViewSetTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
# assert instance is inactive
|
# assert instance is inactive
|
||||||
info = json.loads(response.content)
|
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):
|
def test_anon_user_cannot_modify_existing_instance(self):
|
||||||
"""Not logged-in user cannot modify existing instance
|
"""Not logged-in user cannot modify existing instance
|
||||||
@@ -155,6 +162,30 @@ class CustomUserViewSetTest(APITestCase):
|
|||||||
# Assert access is forbidden
|
# Assert access is forbidden
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
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
|
# admin user
|
||||||
def test_admin_user_can_create_instance(self):
|
def test_admin_user_can_create_instance(self):
|
||||||
"""Admin user can create new instance
|
"""Admin user can create new instance
|
||||||
@@ -182,6 +213,8 @@ class CustomUserViewSetTest(APITestCase):
|
|||||||
|
|
||||||
# Assert instance exists on db
|
# Assert instance exists on db
|
||||||
self.assertTrue(self.model.objects.get(email=response.data['email']))
|
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):
|
def test_admin_user_can_modify_existing_instance(self):
|
||||||
"""Admin user can modify existing instance
|
"""Admin user can modify existing instance
|
||||||
@@ -324,99 +357,6 @@ class ChangeUserPasswordViewTest(APITestCase):
|
|||||||
self.assertEqual(stored_password_hash, base64.b64encode(new_password_hash).decode())
|
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):
|
class LoadCoopManagerTestCase(APITestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -533,3 +473,252 @@ class MyUserViewTest(APITestCase):
|
|||||||
# check response
|
# check response
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
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(
|
email = EmailMessage(
|
||||||
subject, message, to=[user.email]
|
subject, message, to=[user.email]
|
||||||
)
|
)
|
||||||
|
email.content_subtype = "html"
|
||||||
email.send()
|
email.send()
|
||||||
logging.info(f"Verification email sent to {user.email}")
|
logging.info(f"Verification email sent to {user.email}")
|
||||||
except Exception as e:
|
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.http import urlsafe_base64_decode
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.db import IntegrityError
|
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 status
|
||||||
from rest_framework import viewsets
|
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 rest_framework.decorators import api_view, permission_classes
|
||||||
|
|
||||||
from companies.models import Company
|
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 models
|
||||||
from . import serializers as core_serializers
|
from . import serializers as core_serializers
|
||||||
@@ -43,9 +48,9 @@ logging.basicConfig(
|
|||||||
|
|
||||||
class CustomUserViewSet(viewsets.ModelViewSet):
|
class CustomUserViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
model = models.CustomUser
|
model = User
|
||||||
model_name = 'custom_user'
|
model_name = 'custom_user'
|
||||||
queryset = models.CustomUser.objects.all()
|
queryset = User.objects.all()
|
||||||
permission_classes = [CustomUserPermissions,]
|
permission_classes = [CustomUserPermissions,]
|
||||||
read_serializer_class = core_serializers.CustomUserReadSerializer
|
read_serializer_class = core_serializers.CustomUserReadSerializer
|
||||||
write_serializer_class = core_serializers.CustomUserWriteSerializer
|
write_serializer_class = core_serializers.CustomUserWriteSerializer
|
||||||
@@ -76,9 +81,11 @@ class CustomUserViewSet(viewsets.ModelViewSet):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
# save model instance data
|
# save model instance data
|
||||||
password = serializer.validated_data.pop('password')
|
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.set_password(password)
|
||||||
instance.save()
|
instance.save()
|
||||||
|
# send verification email
|
||||||
|
utils.send_verification_email(request, instance)
|
||||||
|
|
||||||
return Response(self.read_serializer_class(
|
return Response(self.read_serializer_class(
|
||||||
instance, many=False, context={'request': request}).data,
|
instance, many=False, context={'request': request}).data,
|
||||||
@@ -98,63 +105,47 @@ class ChangeUserPasswordView(UpdateAPIView):
|
|||||||
serializer_class = core_serializers.ChangePasswordSerializer
|
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',])
|
@api_view(['POST',])
|
||||||
@permission_classes([CustomUserPermissions,])
|
@permission_classes([AllowAny])
|
||||||
def create_company_user(request):
|
def create_company_user(request):
|
||||||
"""
|
"""
|
||||||
Create non-validated company and manager user associated
|
Create non-validated company and associated managing user
|
||||||
"""
|
"""
|
||||||
user_data = {
|
if 'user' not in request.data:
|
||||||
'full_name': request.data['user']['full_name'],
|
return Response({"error": "Missing parameter: user"}, status=406)
|
||||||
'email': request.data['user']['email'],
|
if 'company' not in request.data:
|
||||||
'password': request.data['user']['password']
|
return Response({"error": "Missing parameter: company"}, status=406)
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
try:
|
# create company
|
||||||
city = company_data.pop('city')
|
company_data = request.data['company']
|
||||||
#city = City.objects.get(name=city)
|
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')
|
# create user
|
||||||
geo = Point(geo['latitude'],geo['longitude'])
|
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)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET',])
|
@api_view(['GET',])
|
||||||
@@ -168,7 +159,6 @@ def my_user(request):
|
|||||||
return Response({'error': {str(type(e))}}, status=500)
|
return Response({'error': {str(type(e))}}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST',])
|
@api_view(['POST',])
|
||||||
@permission_classes([IsAdminUser,])
|
@permission_classes([IsAdminUser,])
|
||||||
def load_coop_managers(request):
|
def load_coop_managers(request):
|
||||||
@@ -200,10 +190,66 @@ def activate_user(request, uidb64, token):
|
|||||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
||||||
user = None
|
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
|
# activate user
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
user.save()
|
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:
|
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 = ''
|
WC_SECRET = ''
|
||||||
# GOOGLE MAPS
|
# GOOGLE MAPS
|
||||||
GOOGLE_MAP_API_KEY = ''
|
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)
|
updated = models.DateTimeField('date last update', auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.province:
|
||||||
return f'{self.name} [{self.province}]'
|
return f'{self.name} [{self.province}]'
|
||||||
|
else:
|
||||||
|
return f'{self.name}'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Municipio"
|
verbose_name = "Municipio"
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter, ChoiceDropdownFilter
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
# Register your models here.
|
# 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.contrib import admin
|
||||||
|
|
||||||
|
from django_admin_listfilter_dropdown.filters import DropdownFilter, RelatedDropdownFilter, ChoiceDropdownFilter
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import forms
|
from . import forms
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
|
def model_admin_callable(co):
|
||||||
|
return co.company_name
|
||||||
|
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
form = forms.ProductTagForm
|
form = forms.ProductTagForm
|
||||||
list_display = ('name', 'category', 'sourcing_date', 'company')
|
list_display = ('name', 'category', 'sourcing_date', 'company', 'active' )
|
||||||
list_filter = ('company', 'tags', 'category', 'attributes')
|
list_filter = (
|
||||||
|
('company', RelatedDropdownFilter),
|
||||||
|
('tags', RelatedDropdownFilter),
|
||||||
|
('category', RelatedDropdownFilter),
|
||||||
|
('attributes', RelatedDropdownFilter)
|
||||||
|
)
|
||||||
|
|
||||||
search_fields = ('name', 'sku', 'description')
|
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.Product, ProductAdmin)
|
||||||
admin.site.register(models.TreeTag)
|
admin.site.register(models.TreeTag)
|
||||||
admin.site.register(models.CategoryTag)
|
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
|
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')
|
attributes = TagField(to=AttributeTag, blank=True, related_name='product_attributes')
|
||||||
identifiers = models.TextField('Identificador único de producto', null=True, blank=True)
|
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
|
# internal
|
||||||
created = models.DateTimeField('date of creation', auto_now_add=True)
|
created = models.DateTimeField('date of creation', auto_now_add=True)
|
||||||
updated = models.DateTimeField('date last update', auto_now=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')
|
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, related_name='product')
|
history = models.ForeignKey('history.HistorySync', on_delete=models.SET_NULL, null=True, blank=True, related_name='product')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} / {self.sku}"
|
return f"{self.name} / {self.sku}"
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.core import mail
|
||||||
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from companies.factories import CompanyFactory
|
from companies.factories import CompanyFactory
|
||||||
from products.factories import ProductFactory, ActiveProductFactory
|
from products.factories import ProductFactory, ActiveProductFactory
|
||||||
from products.models import Product
|
from products.models import Product, CategoryTag
|
||||||
from products.utils import find_related_products_v3
|
|
||||||
|
|
||||||
from core.factories import CustomUserFactory
|
from core.factories import CustomUserFactory
|
||||||
from core.utils import get_tokens_for_user
|
from core.utils import get_tokens_for_user
|
||||||
@@ -194,6 +194,25 @@ class ProductViewSetTest(APITestCase):
|
|||||||
# Assert number of instnaces in response
|
# Assert number of instnaces in response
|
||||||
self.assertEquals(len(expected_instance), len(payload))
|
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
|
# authenticated user
|
||||||
def test_auth_user_can_paginate_instances(self):
|
def test_auth_user_can_paginate_instances(self):
|
||||||
"""authenticated user can paginate instances
|
"""authenticated user can paginate instances
|
||||||
@@ -374,7 +393,6 @@ class ProductViewSetTest(APITestCase):
|
|||||||
url = self.endpoint + f'{instance.pk}/'
|
url = self.endpoint + f'{instance.pk}/'
|
||||||
response = self.client.put(url, data=data, format='json')
|
response = self.client.put(url, data=data, format='json')
|
||||||
|
|
||||||
|
|
||||||
# Assert endpoint returns OK code
|
# Assert endpoint returns OK code
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
@@ -934,9 +952,13 @@ class MyProductsViewTest(APITestCase):
|
|||||||
|
|
||||||
def test_auth_user_gets_data(self):
|
def test_auth_user_gets_data(self):
|
||||||
# create instance
|
# create instance
|
||||||
|
company = CompanyFactory()
|
||||||
|
self.user.company = company
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
user_instances = [
|
user_instances = [
|
||||||
self.factory(creator=self.user),
|
self.factory(company=company),
|
||||||
self.factory(creator=self.user),
|
self.factory(company=company),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Authenticate
|
# Authenticate
|
||||||
@@ -946,11 +968,9 @@ class MyProductsViewTest(APITestCase):
|
|||||||
# Query endpoint
|
# Query endpoint
|
||||||
response = self.client.get(self.endpoint)
|
response = self.client.get(self.endpoint)
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
|
|
||||||
# Assert forbidden code
|
# Assert forbidden code
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEquals(payload['count'], len(payload['results']))
|
self.assertEquals(len(user_instances), len(payload))
|
||||||
self.assertEquals(len(user_instances), payload['count'])
|
|
||||||
|
|
||||||
def test_auth_user_can_paginate_instances(self):
|
def test_auth_user_can_paginate_instances(self):
|
||||||
"""authenticated user can paginate instances
|
"""authenticated user can paginate instances
|
||||||
@@ -960,7 +980,11 @@ class MyProductsViewTest(APITestCase):
|
|||||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token['access']}")
|
||||||
|
|
||||||
# create instances
|
# 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
|
# Request list
|
||||||
url = f"{self.endpoint}?limit=5&offset=10"
|
url = f"{self.endpoint}?limit=5&offset=10"
|
||||||
@@ -970,8 +994,8 @@ class MyProductsViewTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
# assert only 2 instances in response
|
# assert only 2 instances in response
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
self.assertEquals(payload['count'], len(payload['results']))
|
self.assertEquals(payload['count'], self.model.objects.count())
|
||||||
self.assertEquals(2, payload['count'])
|
self.assertEquals(2, len(payload['results']))
|
||||||
|
|
||||||
def test_anon_user_cannot_access(self):
|
def test_anon_user_cannot_access(self):
|
||||||
# send in request
|
# send in request
|
||||||
@@ -980,41 +1004,291 @@ class MyProductsViewTest(APITestCase):
|
|||||||
# check response
|
# check response
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
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):
|
def setUp(self):
|
||||||
"""Tests setup
|
"""Tests setup
|
||||||
"""
|
"""
|
||||||
self.factory = ActiveProductFactory
|
self.endpoint = '/api/v1/admin_products/'
|
||||||
|
self.factory = ProductFactory
|
||||||
self.model = Product
|
self.model = Product
|
||||||
# clear table
|
# create user
|
||||||
self.model.objects.all().delete()
|
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):
|
def test_anon_user_cannot_access(self):
|
||||||
# create tagged product
|
instance = self.factory()
|
||||||
tag = 'cool'
|
url = f"{self.endpoint}{instance.id}/"
|
||||||
expected_instances = [
|
# GET
|
||||||
self.factory(tags=tag),
|
response = self.client.get(self.endpoint)
|
||||||
self.factory(tags=f'{tag} hat'),
|
# check response
|
||||||
self.factory(tags=f'temperatures/{tag}'),
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
self.factory(tags=f'temperatures/{tag}, body/hot'),
|
# POST
|
||||||
self.factory(tags=f'temperatures/{tag}, hats/{tag}'),
|
response = self.client.post(self.endpoint, data={})
|
||||||
# multiple hits
|
# check response
|
||||||
self.factory(tags=tag, attributes=tag),
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
self.factory(tags=tag, attributes=tag, category=tag),
|
# PUT
|
||||||
self.factory(tags=tag, attributes=tag, category=tag, name=tag),
|
response = self.client.get(url, data={})
|
||||||
self.factory(tags=tag, attributes=tag, category=tag, name=tag, description=tag),
|
# 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 = [
|
response = self.client.get(self.endpoint)
|
||||||
self.factory(tags="notcool"), # shouldn't catch it
|
|
||||||
self.factory(tags="azules"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# searh for it
|
# assertions
|
||||||
results = find_related_products_v3(tag)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
# assert result
|
payload = response.json()
|
||||||
self.assertTrue(len(results) == len(expected_instances))
|
|
||||||
|
|
||||||
|
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.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||||
from django.db.models import Max, Min
|
from django.db.models import Max, Min
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -85,25 +86,45 @@ def extract_search_filters(result_set):
|
|||||||
return filter_dict
|
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
|
# search by category
|
||||||
SearchQuery for the value
|
category_qs = Product.objects.filter(category=product.category)[:10]
|
||||||
SearchRank for relevancy scoring and ranking
|
# add to results
|
||||||
"""
|
for item in category_qs:
|
||||||
vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__name')
|
total_results.append(item)
|
||||||
query = SearchQuery(keyword)
|
|
||||||
|
|
||||||
products_qs = Product.objects.annotate(
|
# check size
|
||||||
rank=SearchRank(vector, query)
|
if len(total_results) < 10:
|
||||||
).filter(rank__gt=0.05) # removed order_by because its lost in casting
|
# 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
|
Ranked product search
|
||||||
|
|
||||||
@@ -153,9 +174,9 @@ def find_related_products_v6(keyword, shipping_cost=None, discount=None, categor
|
|||||||
|
|
||||||
# filter by price
|
# filter by price
|
||||||
if price_min is not None:
|
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:
|
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
|
# get min_price and max_price
|
||||||
min_price = products_qs.aggregate(Min('price'))
|
min_price = products_qs.aggregate(Min('price'))
|
||||||
@@ -183,7 +204,7 @@ def product_loader(csv_reader, user, company=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# create historysync instance
|
# 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:
|
for row in csv_reader:
|
||||||
# trim strings
|
# trim strings
|
||||||
for key in row:
|
for key in row:
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
import logging
|
import logging
|
||||||
import csv
|
import csv
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core import serializers
|
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.
|
# Create your views here.
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
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.decorators import api_view, permission_classes, action
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
import requests
|
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 history.models import HistorySync
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
|
||||||
from back_latienda.permissions import IsCreator
|
from products.models import Product, CategoryTag
|
||||||
from .utils import extract_search_filters, find_related_products_v3, find_related_products_v6, product_loader
|
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_serializers import TaggitSerializer
|
||||||
from utils.tag_filters import ProductTagFilter, ProductOrderFilter
|
from utils.tag_filters import ProductTagFilter, ProductOrderFilter
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
filename='logs/product-load.log',
|
filename='logs/product-load.log',
|
||||||
@@ -47,32 +54,37 @@ class ProductViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.save(creator=self.request.user, company=self.request.user.company)
|
serializer.save(creator=self.request.user, company=self.request.user.company)
|
||||||
|
|
||||||
@action(detail=True, methods=['GET',])
|
@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
|
# 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',])
|
class MyProductsViewSet(viewsets.ModelViewSet):
|
||||||
@permission_classes([IsAuthenticated,])
|
""" Allows user to get all products where user is product creator
|
||||||
def my_products(request):
|
"""
|
||||||
limit = request.GET.get('limit')
|
model = Product
|
||||||
offset = request.GET.get('offset')
|
serializer_class = ProductSerializer
|
||||||
qs = Product.objects.filter(creator=request.user)
|
permission_classes = [IsAuthenticated]
|
||||||
product_serializer = ProductSerializer(qs, many=True)
|
|
||||||
data = product_serializer.data
|
def get_queryset(self):
|
||||||
# RESULTS PAGINATION
|
return self.model.objects.filter(company=self.request.user.company).order_by('-created')
|
||||||
if limit is not None and offset is not None:
|
|
||||||
limit = int(limit)
|
|
||||||
offset = int(offset)
|
|
||||||
data = data[offset:(limit+offset)]
|
class AdminProductsViewSet(viewsets.ModelViewSet):
|
||||||
elif limit is not None:
|
""" Allows user with role 'SITE_ADMIN' to access all product instances
|
||||||
limit = int(limit)
|
"""
|
||||||
data = data[:limit]
|
queryset = Product.objects.all()
|
||||||
# prepare response payload
|
serializer_class = ProductSerializer
|
||||||
payload = {}
|
permission_classes = [IsSiteAdmin]
|
||||||
payload['results'] = data
|
|
||||||
payload['count'] = len(payload['results'])
|
def perform_create(self, serializer):
|
||||||
return Response(data=payload)
|
serializer.save(creator=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST',])
|
@api_view(['POST',])
|
||||||
@@ -169,7 +181,7 @@ def product_search(request):
|
|||||||
|
|
||||||
if q == '':
|
if q == '':
|
||||||
# filter entire queryset
|
# filter entire queryset
|
||||||
products_qs = Product.objects.all()
|
products_qs = Product.objects.filter(active=True)
|
||||||
if tags:
|
if tags:
|
||||||
products_qs = Product.objects.filter(tags=tags)
|
products_qs = Product.objects.filter(tags=tags)
|
||||||
if category:
|
if category:
|
||||||
@@ -181,7 +193,7 @@ def product_search(request):
|
|||||||
# split query string into single words
|
# split query string into single words
|
||||||
chunks = q.split(' ')
|
chunks = q.split(' ')
|
||||||
for chunk in chunks:
|
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
|
# update price values
|
||||||
if product_set:
|
if product_set:
|
||||||
if prices['min'] is None or min_price['price__min'] < prices['min']:
|
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)
|
qs = qs.filter(name__icontains=self.q)
|
||||||
|
|
||||||
return qs # [x.label for x in qs]
|
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
|
django-autocomplete-light==3.8.2
|
||||||
# manually install `pip install --default-timeout=100 future` to avoid wcapi to timeout
|
# manually install `pip install --default-timeout=100 future` to avoid wcapi to timeout
|
||||||
django-map-widgets==0.3.0
|
django-map-widgets==0.3.0
|
||||||
|
django-admin-list-filter-dropdown==1.0.3
|
||||||
# required for production
|
# required for production
|
||||||
django-anymail[amazon_ses]==8.2
|
django-anymail[amazon_ses]==8.2
|
||||||
boto3==1.17.11
|
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',
|
'model': 'company',
|
||||||
'id': company.id,
|
'id': company.id,
|
||||||
},
|
},
|
||||||
'geo': (12.2, -0.545)
|
'geo': {'latitude': 12.2, 'longitude': -0.545}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Query endpoint
|
# Query endpoint
|
||||||
|
|||||||
@@ -52,13 +52,18 @@ def track_user(request):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
|
|
||||||
|
if data.get('geo'):
|
||||||
|
coordinates = (data['geo'].get('latitude'), data['geo'].get('longitude'))
|
||||||
|
else:
|
||||||
|
coordinates = None
|
||||||
|
|
||||||
# gather instance data
|
# gather instance data
|
||||||
instance_data = {
|
instance_data = {
|
||||||
'action_object': data.get('action_object'),
|
'action_object': data.get('action_object'),
|
||||||
'user': None if request.user.is_anonymous else request.user,
|
'user': None if request.user.is_anonymous else request.user,
|
||||||
'anonymous': request.user.is_anonymous,
|
'anonymous': request.user.is_anonymous,
|
||||||
'ip_address': data.get('ip'),
|
'ip_address': data.get('ip'),
|
||||||
'geo': Point(data.get('geo')),
|
'geo': Point(coordinates),
|
||||||
}
|
}
|
||||||
|
|
||||||
if data['action_object'].get('model') == 'product':
|
if data['action_object'].get('model') == 'product':
|
||||||
@@ -76,4 +81,4 @@ def track_user(request):
|
|||||||
return Response(status=status.HTTP_201_CREATED)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Stats could not be created: {str(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,10 +1,174 @@
|
|||||||
Hola {{company.creator.full_name}}.
|
<!DOCTYPE html>
|
||||||
Estamos contactando contigo como usuario gerente de la compañina {{company.company_name}}.
|
<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}}
|
.header {
|
||||||
- {{user.email}}
|
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:
|
.coop-name {
|
||||||
{{data}}
|
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>
|
||||||
|
|||||||
@@ -1,2 +1,166 @@
|
|||||||
Hola {{user.full_name}}.
|
<!DOCTYPE html>
|
||||||
Hemos enviado un email a {{company.company_name}} sobre tu petición.
|
<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>
|
||||||
Hola {{ user.full_name }},
|
<html lang="en">
|
||||||
Por favor, verifica tu registro en LaTiendaCOOP haciendo click en el siguiente enlace:
|
<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>
|
||||||
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.quantity = counter
|
||||||
history.save()
|
history.save()
|
||||||
|
|
||||||
|
logging.info(f"Products created: {len(new_products)}")
|
||||||
print(f"Products created: {len(new_products)}")
|
print(f"Products created: {len(new_products)}")
|
||||||
|
|
||||||
return new_products
|
return new_products
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user