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 taxonomy data](#load-taxonomy-data)
- [Endpoints](#endpoints)
- [Shop Integrations](#shop-integrations)
- [WooCommerce](#woocommerce)
- [Product Search](#product-search)
- [Massive Data Load Endpoints](#massive-data-load-endpoints)
- [COOP and Managing User Data Load](#coop-and-managing-user-data-load)
- [Product Data Load](#product-data-load)
- [GeoIP Setup](#geoip-setup)
- [Tags](#tags)
- [Development Utils](#development-utils)
- [Fake product data generation](#fake-product-data-generation)
@@ -169,6 +172,30 @@ Location ednpoints:
- `/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
Endpoint: `/api/v1/product_search/`
@@ -231,6 +258,22 @@ Optional:
`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
### Fake product data generation

View File

@@ -1,12 +1,15 @@
# from django.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 core.models import TreeTag
from history.models import HistorySync
# Create your models here.
# User = get_user_model()
class Company(models.Model):
WOO_COMMERCE = 'WOO_COMMERCE'
@@ -42,14 +45,13 @@ class Company(models.Model):
sync = models.BooleanField('Sincronizar tienda', 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)
credentials = JSONField(null=True)
# internal
created = models.DateTimeField('date of creation', auto_now_add=True)
updated = models.DateTimeField('date last update', auto_now=True)
creator = models.ForeignKey('core.CustomUser', on_delete=models.DO_NOTHING, null=True, related_name='creator')
def __str__(self):
return self.company_name
# history = models.ForeignKey(HistorySync, null=True, on_delete=models.DO_NOTHING, related_name='company')
class Meta:
verbose_name = "Compañía"

View File

@@ -19,6 +19,8 @@ from companies.serializers import CompanySerializer
from utils.tag_filters import CompanyTagFilter
from back_latienda.permissions import IsCreator
from utils import woocommerce
class CompanyViewSet(viewsets.ModelViewSet):
queryset = Company.objects.all()
@@ -35,8 +37,7 @@ class CompanyViewSet(viewsets.ModelViewSet):
Send email to company.creator
"""
try:
queryset = self.get_custom_queryset(request)
instance = queryset.filter(pk=kwargs['pk']).first()
instance = self.queryset.filter(pk=kwargs['pk']).first()
if instance:
# IP stuff
client_ip, is_routable = get_client_ip(request)
@@ -121,6 +122,35 @@ class CompanyViewSet(viewsets.ModelViewSet):
except Exception as e:
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',])
@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/Alimentos
Alimentación, bebida y tabaco/Alimentos/Aliños y especias

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)
stock = models.PositiveIntegerField('Stock', null=True)
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')
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):
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)
category = SingleTagSerializerField(required=False) # main tag category
attributes = TagListSerializerField(required=False)

View File

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

View File

@@ -13,3 +13,4 @@ Pillow==8.1.0
drf-extra-fields==3.0.4
django-ipware==3.0.2
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