Merge branch 'development' into diego
This commit is contained in:
43
README.md
43
README.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,])
|
||||
|
||||
31
core/management/commands/extractparenttags.py
Normal file
31
core/management/commands/extractparenttags.py
Normal 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}")
|
||||
@@ -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
21
datasets/top_tags.txt
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
115
utils/woocommerce.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user