Merge branch 'development' into diego

This commit is contained in:
Diego Calvo
2021-02-17 14:19:29 +01:00
11 changed files with 269 additions and 15 deletions

View File

@@ -8,11 +8,14 @@ This README aims to document functionality of backend as well as required 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) - [Endpoints](#endpoints)
- [Shop Integrations](#shop-integrations)
- [WooCommerce](#woocommerce)
- [Product Search](#product-search) - [Product Search](#product-search)
- [Massive Data Load Endpoints](#massive-data-load-endpoints) - [Massive Data Load Endpoints](#massive-data-load-endpoints)
- [COOP and Managing User Data Load](#coop-and-managing-user-data-load) - [COOP and Managing User Data Load](#coop-and-managing-user-data-load)
- [Product Data Load](#product-data-load) - [Product Data Load](#product-data-load)
- [GeoIP Setup](#geoip-setup) - [GeoIP Setup](#geoip-setup)
- [Tags](#tags)
- [Development Utils](#development-utils) - [Development Utils](#development-utils)
- [Fake product data generation](#fake-product-data-generation) - [Fake product data generation](#fake-product-data-generation)
@@ -169,6 +172,30 @@ Location ednpoints:
- `/api/v1/cities/` - `/api/v1/cities/`
## Shop Integrations
We provide integrations with online shop platforms
It requires the json field `Company.credentials` to have the appropiate format and values
Endoint: `/api/v1/companies/{PK}/import_products/`
The software to handle different platform imports can be found in `utils`
### WooCommerce
Credential format:
```json
{
"key": "qwerweqr",
"secret": "asdfsa",
}
```
Method: `utils.woocommerce.migrate_shop_products`
## Product Search ## Product Search
Endpoint: `/api/v1/product_search/` Endpoint: `/api/v1/product_search/`
@@ -231,6 +258,22 @@ Optional:
`sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin` `sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin`
## Tags
Both `Company` and `Product` models make use of tags.
### Load shopping taxonomy
To create the initial set of tags, we can use the `addtaxonomy` management command.
Reads the data from `datasets/shop-taxonomy.es-ES.txt` which is from google shopping
### Top-level tags
In order to extract the top level tags for use as categories, we can use the `extractparenttas` management command.
It saves the results to `datasets/top_tags.txt`
## Development Utils ## Development Utils
### Fake product data generation ### Fake product data generation

View File

@@ -1,12 +1,15 @@
# from django.db import models # from django.db import models
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.fields import JSONField
from django.contrib.auth import get_user_model
from tagulous.models import TagField from tagulous.models import TagField
# from core.models import TreeTag
from history.models import HistorySync
# Create your models here. # Create your models here.
# User = get_user_model()
class Company(models.Model): class Company(models.Model):
WOO_COMMERCE = 'WOO_COMMERCE' WOO_COMMERCE = 'WOO_COMMERCE'
@@ -42,14 +45,13 @@ class Company(models.Model):
sync = models.BooleanField('Sincronizar tienda', default=False, null=True, blank=True) sync = models.BooleanField('Sincronizar tienda', default=False, null=True, blank=True)
is_validated = models.BooleanField('Validado', default=False, null=True, blank=True) is_validated = models.BooleanField('Validado', default=False, null=True, blank=True)
is_active = models.BooleanField('Activado', default=False, null=True, blank=True) is_active = models.BooleanField('Activado', default=False, null=True, blank=True)
credentials = JSONField(null=True)
# 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.DO_NOTHING, null=True, related_name='creator') creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, related_name='creator')
# history = models.ForeignKey(HistorySync, null=True, on_delete=models.DO_NOTHING, related_name='company')
def __str__(self):
return self.company_name
class Meta: class Meta:
verbose_name = "Compañía" verbose_name = "Compañía"

View File

@@ -19,6 +19,8 @@ from companies.serializers import CompanySerializer
from utils.tag_filters import CompanyTagFilter from utils.tag_filters import CompanyTagFilter
from back_latienda.permissions import IsCreator from back_latienda.permissions import IsCreator
from utils import woocommerce
class CompanyViewSet(viewsets.ModelViewSet): class CompanyViewSet(viewsets.ModelViewSet):
queryset = Company.objects.all() queryset = Company.objects.all()
@@ -35,8 +37,7 @@ class CompanyViewSet(viewsets.ModelViewSet):
Send email to company.creator Send email to company.creator
""" """
try: try:
queryset = self.get_custom_queryset(request) instance = self.queryset.filter(pk=kwargs['pk']).first()
instance = queryset.filter(pk=kwargs['pk']).first()
if instance: if instance:
# IP stuff # IP stuff
client_ip, is_routable = get_client_ip(request) client_ip, is_routable = get_client_ip(request)
@@ -121,6 +122,35 @@ class CompanyViewSet(viewsets.ModelViewSet):
except Exception as e: except Exception as e:
return Response({"errors":{"details": str(e),}}, status=500) return Response({"errors":{"details": str(e),}}, status=500)
@action(detail=True, methods=['GET', ])
def import_products(self, request, **kwargs):
instance = self.queryset.filter(pk=kwargs['pk']).first()
# check if it's a shop
if instance.shop is not True:
return Response({'error': 'This company is not a shop'})
# check required credentials
credentials = instance.credentials
if credentials is None or credentials == {}:
return Response({'error': 'This company has no registered credentials'})
# check what platform
platform = instance.platform
if platform is None:
message = {'error': 'This company is not registered with any platforms'}
elif platform == 'WOO_COMMERCE':
# recheck credentials
if 'key' in credentials.keys() and 'secret' in credentials.keys():
# execute import
products = woocommerce.migrate_shop_products(
instance.web_link,
credentials['key'],
credentials['secret'])
message = {'details': f'{len(products)} products added for {instance.company_name}'}
else:
message = {"error": 'Credentials have wrong format'}
else:
message = {'error': f'Platform {plaform} not registered'}
return Response(message)
@api_view(['GET',]) @api_view(['GET',])
@permission_classes([IsAuthenticated,]) @permission_classes([IsAuthenticated,])

View File

@@ -0,0 +1,31 @@
import logging
from django.core.management.base import BaseCommand
from django.conf import settings
from core.models import TreeTag
class Command(BaseCommand):
help = 'Extract top level tags'
def handle(self, *args, **kwargs):
# get all instances
tags = TreeTag.objects.all()
top_tags = []
print("Extracting top-level tags from TreeTag instances")
# extract tags with no ancestor
for tag in tags:
if not tag.get_ancestors():
top_tags.append(tag.name)
print("Saving top-level tags to file")
# save results to dataset/top_tags.txt
path = f"{settings.BASE_DIR}/../datasets/top_tags.txt"
with open(path, 'wt') as f:
f.writelines([tag + '\n' for tag in top_tags])
# print out results
logging.info(f"Extracted {len(top_tags)} to {path}")

View File

@@ -1,4 +1,3 @@
# Google_Product_Taxonomy_Version: 2015-02-19
Alimentación, bebida y tabaco Alimentación, bebida y tabaco
Alimentación, bebida y tabaco/Alimentos Alimentación, bebida y tabaco/Alimentos
Alimentación, bebida y tabaco/Alimentos/Aliños y especias Alimentación, bebida y tabaco/Alimentos/Aliños y especias
@@ -3786,7 +3785,7 @@ Equipamiento deportivo/Equipamiento para atletismo/Gimnasia/Trampolines
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Conjuntos de equipamiento para lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Conjuntos de equipamiento para lacrosse
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Almohadillas de hombros para lacrosse y hockey sobre hierba Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Almohadillas de hombros para lacrosse y hockey sobre hierba
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Cascos para hockey sobre hierba y lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Cascos para hockey sobre hierba y lacrosse
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Guantes para hockey sobre hierba y lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Guantes para hockey sobre hierba y lacrosse
Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Máscaras y gafas protectoras para hockey sobre hierba y lacrosse Equipamiento deportivo/Equipamiento para atletismo/Hockey sobre hierba y lacrosse/Equipamiento de protección para hockey sobre hierba y lacrosse/Máscaras y gafas protectoras para hockey sobre hierba y lacrosse

21
datasets/top_tags.txt Normal file
View File

@@ -0,0 +1,21 @@
Alimentación, bebida y tabaco
Arte y ocio
Bebés y niños pequeños
Bricolaje
Cámaras y ópticas
Casa y jardín
Economía e industria
Electrónica
Elementos religiosos y ceremoniales
Equipamiento deportivo
Juegos y juguetes
Maletas y bolsos de viaje
Material de oficina
Mobiliario
Multimedia
Productos para adultos
Productos para mascotas y animales
Ropa y accesorios
Salud y belleza
Software
Vehículos y recambios

View File

@@ -35,7 +35,7 @@ class Product(models.Model):
discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True) discount = models.DecimalField('Descuento', max_digits=5, decimal_places=2, null=True, blank=True)
stock = models.PositiveIntegerField('Stock', null=True) stock = models.PositiveIntegerField('Stock', null=True)
tags = TagField(to=TreeTag) tags = TagField(to=TreeTag)
category = SingleTagField(null=True) # main tag category category = SingleTagField(blank=True, null=True) # main tag category
attributes = TagField(to=TreeTag, related_name='product_attributes') attributes = TagField(to=TreeTag, 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)

View File

@@ -7,6 +7,18 @@ from utils.tag_serializers import TagListSerializerField, TaggitSerializer, Sing
class ProductSerializer(TaggitSerializer, serializers.ModelSerializer): class ProductSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField(required=False)
category = SingleTagSerializerField(required=False) # main tag category
attributes = TagListSerializerField(required=False)
# image = serializers.SerializerMethodField()
class Meta:
model = Product
exclude = ['created', 'updated', 'creator']
class ProductSearchSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
category = SingleTagSerializerField(required=False) # main tag category category = SingleTagSerializerField(required=False) # main tag category
attributes = TagListSerializerField(required=False) attributes = TagListSerializerField(required=False)

View File

@@ -18,7 +18,7 @@ from rest_framework.decorators import api_view, permission_classes, action
import requests import requests
from products.models import Product from products.models import Product
from products.serializers import ProductSerializer, TagFilterSerializer from products.serializers import ProductSerializer, TagFilterSerializer, ProductSearchSerializer
from companies.models import Company from companies.models import Company
from history.models import HistorySync from history.models import HistorySync
@@ -48,7 +48,7 @@ class ProductViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['GET',]) @action(detail=True, methods=['GET',])
def related(request): def related(request):
# find the most similar products # TODO: find the most similar products
return Response(data=[]) return Response(data=[])
@@ -175,7 +175,7 @@ def product_search(request):
# extract filters from result_set # extract filters from result_set
filters = extract_search_filters(result_set) filters = extract_search_filters(result_set)
# serialize and respond # serialize and respond
product_serializer = ProductSerializer(result_set, many=True) product_serializer = ProductSearchSerializer(result_set, many=True, context={'request': request})
return Response(data={"filters": filters, "products": product_serializer.data}) return Response(data={"filters": filters, "products": product_serializer.data})
except Exception as e: except Exception as e:
return Response({"errors": {"details": str(type(e))}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({"errors": {"details": str(type(e))}}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -12,4 +12,5 @@ django-tagulous==1.1.0
Pillow==8.1.0 Pillow==8.1.0
drf-extra-fields==3.0.4 drf-extra-fields==3.0.4
django-ipware==3.0.2 django-ipware==3.0.2
geoip2==4.1.0 geoip2==4.1.0
woocommerce==2.1.1

115
utils/woocommerce.py Normal file
View File

@@ -0,0 +1,115 @@
"""
This file holds the functions necesary to:
- Connect to seller's shop API
- Load information on seller [??]
- Load product information
"""
import logging
from woocommerce import API
from companies.models import Company
from products.models import Product
from products.serializers import ProductSerializer
def get_wcapi_instance(url, key, secret, version="wc/v3"):
wcapi = API(
url=url,
consumer_key=key,
consumer_secret=secret,
wp_api=True,
version=version
)
return wcapi
def migrate_shop_products(url, key, secret, version="wc/v3"):
# get wcapi
wcapi = get_wcapi_instance(url, key, secret, version)
consumer_key = 'ck_565539bb25b472b1ff7a209eb157ca11c0a26397'
consumer_secret = 'cs_9c1690ba5da0dd70f51d61c395628fa14d1a104c'
# get company fom url
company = Company.objects.filter(web_link=url).first()
if not company:
# logging.error(f"Could not find Company with URL: {url}")
# print(f"Could not find Company with URL: {url}")
# return None
# TODO: ELIMINATE THIS AFTER DEBUGGING
company = Company.objects.create(web_link=url)
logging.error(f"Created Company for testing: {url}")
# list products
response = wcapi.get('/products/')
if response.status_code == 200:
products = response.json()
elif response.status_code == 401:
logging.error(f"{response.status_code} [{response.url}]: {response.json()}")
return None
else:
logging.error(f"Could not load products from {url}: [{response.status_code}]")
print(f"Could not load products fom {url}: [{response.status_code}]")
return None
product_fields = [f.name for f in Product._meta.get_fields()]
counter = 0
products_created = []
for product in products:
instance_data = {'company':company.id}
# parse the product info
for key in product:
if key in product_fields:
instance_data[key] = product[key]
# remove unwanted fields
instance_data.pop('id')
# extract m2m field data
tags = instance_data.pop('tags')
attributes = instance_data.pop('attributes')
# create instance with serializer
'''
serializer = ProductSerializer(data=instance_data)
if serializer.is_valid():
new = serializer.save()
if tags:
new.tags.set(tags)
if attributes:
new.attributes.set(attributes)
new.save()
else:
logging.error(f"{serializer.errors}")
continue
'''
# alternative method
serializer = ProductSerializer(data=instance_data)
if serializer.is_valid():
try:
new = Product.objects.create(**serializer.validated_data)
if tags:
new.tags.set(tags)
if attributes:
new.attributes.set(attributes)
new.save()
except Exception as e:
logging.error(f"Could not create product instance: {str(e)}")
else:
logging.error(f"{serializer.errors}")
continue
products_created.append(new)
counter += 1
logging.info(f"Product {instance_data.get('name')} created")
print(f"Product {instance_data.get('name')} created")
print(f"Products created: {counter}")
return products_created