Merge branch 'development' of https://bitbucket.org/enreda/back-latienda into diego

This commit is contained in:
Diego Calvo
2021-02-11 11:53:55 +01:00
19 changed files with 11085 additions and 38 deletions

View File

@@ -8,6 +8,7 @@ This README aims to document functionality of backend as well as required steps
- [Location Data](#location-data) - [Location Data](#location-data)
- [Endpoints](#endpoints) - [Endpoints](#endpoints)
- [Data Load](#data-load) - [Data Load](#data-load)
- [GeoIP Setup](#geoip-setup)
- [Development Utils](#development-utils) - [Development Utils](#development-utils)
## First Steps ## First Steps
@@ -177,6 +178,22 @@ CSV headers: `id,nombre-producto,descripcion,imagen,url,precio,gastos-envio,cond
Only admin users have access to endoint Only admin users have access to endoint
## GeoIP Setup
Module: `geoip2`
- Download the `GeoLite2 City` and `GeoLite2 Country` binary datasets from maxmind.com
- Unzip files into `datasets/` folder
- Set `settings.GEOIP_PATH` to datasets folder
Optional:
- install `libmaxminddb` C library for improved performance:
`sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin`
## Development Utils ## Development Utils
### Fake product load ### Fake product load

View File

@@ -144,3 +144,5 @@ USE_TZ = True
# https://docs.djangoproject.com/en/2.2/howto/static-files/ # https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
TAXONOMY_FILE = 'shop-taxonomy.es-ES.txt'

View File

@@ -21,8 +21,9 @@ DATABASES = {
}, },
} }
MEDIA_ROOT = BASE_DIR + '/media/' MEDIA_ROOT = BASE_DIR + '/../media/'
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
GEOIP_PATH = BASE_DIR + '/../datasets/'
# JWT SETTINGS # JWT SETTINGS
SIMPLE_JWT = { SIMPLE_JWT = {

View File

@@ -26,7 +26,7 @@ class Company(models.Model):
platform = models.CharField('Plataforma de tienda online', choices=PLATFORMS, max_length=25, null=True, blank=True) platform = models.CharField('Plataforma de tienda online', choices=PLATFORMS, max_length=25, null=True, blank=True)
email = models.EmailField('Email', null=True, blank=True) email = models.EmailField('Email', null=True, blank=True)
logo = models.ImageField('Logo', upload_to='logos/', null=True, blank=True) logo = models.ImageField('Logo', upload_to='logos/', null=True, blank=True)
city = models.ForeignKey('geo.City', null=True, blank=True, on_delete=models.DO_NOTHING) city = models.CharField('Municipio', max_length=1000, null=True, blank=True)
address = models.CharField('Dirección', max_length=1000, null=True, blank=True) address = models.CharField('Dirección', max_length=1000, null=True, blank=True)
geo = models.PointField('Coordenadas', null=True, blank=True ) geo = models.PointField('Coordenadas', null=True, blank=True )
phone = models.CharField('Teléfono', max_length=25, null=True, blank=True) phone = models.CharField('Teléfono', max_length=25, null=True, blank=True)

View File

@@ -1,8 +1,9 @@
import logging import logging
from django.shortcuts import render from django.shortcuts import render, get_object_or_404
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.contrib.gis.geoip2 import GeoIP2
# Create your views here. # Create your views here.
from rest_framework import viewsets from rest_framework import viewsets
@@ -10,6 +11,9 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
from rest_framework.decorators import api_view, permission_classes, action from rest_framework.decorators import api_view, permission_classes, action
from ipware import get_client_ip
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
@@ -29,31 +33,92 @@ class CompanyViewSet(viewsets.ModelViewSet):
""" """
Send email to company.creator Send email to company.creator
""" """
try:
queryset = self.get_custom_queryset(request) queryset = self.get_custom_queryset(request)
instance = queryset.filter(pk=kwargs['pk']).first() instance = queryset.filter(pk=kwargs['pk']).first()
if instance: if instance:
# IP stuff
client_ip, is_routable = get_client_ip(request)
g = GeoIP2()
# deserialize payload
data = json.loads(request.body) data = json.loads(request.body)
if request.user.is_authenticated:
# send email to manager # send email to manager
message = render_to_string('company_contact.html', { company_message = render_to_string('company_contact.html', {
'user': request.user, 'company': instance,
'email': request.user.email,
'full_name': request.user.full_name,
'quantity': data['quantity'],
'phone_number': data.get('phone_number'),
'comments': data['comments'],
'product_info': data['product_info'],
})
user_message = render_to_string('confirm_company_contact.html', {
'company': instance,
'username': request.user.full_name,
'data': data, 'data': data,
}) })
email = EmailMessage(subject, message, to=[instance.creator.email]) # send email to company
subject = "Contacto de usuario"
email = EmailMessage(subject, company_message, to=[instance.creator.email])
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
message = render_to_string('confirm_company_contact.html', { subject = 'Confirmación de contacto'
'user': request.user,
})
email = EmailMessage(subject, message, to=[request.user.email]) email = EmailMessage(subject, message, to=[request.user.email])
email.send() email.send()
logging.info(f"Confirmation email sent to {request.user.email}") logging.info(f"Contact confirmation email sent to {request.user.email}")
stats_data = {
'action_object': instance,
'user': None,
'anonymous': True,
'ip_address': client_ip,
'geo': g.geos(client_ip),
'contact': True,
'shop': instance.shop,
}
else:
# for unauthenticated users
company_message = render_to_string('company_contact.html', {
'company': instance,
'email': data['email'],
'full_name': data['full_name'],
'quantity': data['quantity'],
'phone_number': data.get('phone_number'),
'comments': data['comments'],
'product_info': data['product_info'],
})
user_message = render_to_string('confirm_company_contact.html', {
'company': instance,
'username': data['full_name'],
})
# send email to company
email = EmailMessage(subject, company_message, to=[instance.creator.email])
email.send()
logging.info(f"Email sent to {instance.creator.email} as manager of {instance.name}")
# send confirmation email to user
email = EmailMessage(subject, user_message, to=[data['email']])
email.send()
logging.info(f"Contact confirmation email sent to anonymous user {data['email']}")
# statslog data to register interaction
stats_data = {
'action_object': instance,
'user': request.user,
'anonymous': False,
'ip_address': client_ip,
'geo': g.geos(client_ip),
'contact': True,
'shop': instance.shop,
}
# create statslog instance to register interaction
StatsLog.objects.create(**stats_data)
return Response(data=data) return Response(data=data)
else: else:
return Response({"errors":{"details": f"No instance of company with id {kwargs['pk']}",}}) return Response({"errors":{"details": f"No instance of company with id {kwargs['pk']}",}})
except Exception as e:
return Response({"errors":{"details": str(e),}}, status=500)
@api_view(['GET',]) @api_view(['GET',])

View File

@@ -15,6 +15,8 @@ logging.basicConfig(
class Command(BaseCommand): class Command(BaseCommand):
'help' = 'Load geographic dataset'
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
print('Deleting all instances of Country, Region, Province, City') print('Deleting all instances of Country, Region, Province, City')
logging.info('Deleting all instances of Country, Region, Province, City') logging.info('Deleting all instances of Country, Region, Province, City')

View File

@@ -0,0 +1,32 @@
import logging
from django.core.management.base import BaseCommand
from django.conf import settings
from core.models import TreeTag
class Command(BaseCommand):
help = 'Load taxonomy terms into Tags'
def handle(self, *args, **kwargs):
print(self.help)
print("Deleting existing instances")
TreeTag.objects.all().delete()
file_path = settings.BASE_DIR + '/../datasets/' + settings.TAXONOMY_FILE
counter = 0
with open(file_path, 'rt') as data_file:
print(f"Reading from {settings.TAXONOMY_FILE}")
for line in data_file.readlines():
try:
tag = TreeTag.objects.create(name=line)
counter += 1
print('.', end='')
logging.debug(f"{tag} created from {line}")
except Exception as e:
logging.error(f"{type(e)} while creating tags from {settings.TAXONOMY_FILE}")
print(f"\n{counter} new TreeTag instances created")
print('Shutting down\n')

View File

@@ -9,7 +9,9 @@ from django.contrib.gis.geos import GEOSGeometry, MultiPolygon
from faker import Faker from faker import Faker
from companies.factories import CompanyFactory from companies.factories import CompanyFactory
from companies.models import Company
from products.factories import ProductFactory from products.factories import ProductFactory
from products.models import Product
logging.basicConfig( logging.basicConfig(
@@ -22,10 +24,14 @@ logging.basicConfig(
class Command(BaseCommand): class Command(BaseCommand):
logo_url = "https://picsum.photos/200/300" logo_url = "https://picsum.photos/200/300"
help = 'Creates fake companies and related products in database'
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
print("Creating fake data to populate database") print("Create fake data to populate database\n")
print("Deleting existing Product and Company instances")
Product.objects.all().delete()
Company.objects.all().delete()
# start faker # start faker
fake = Faker() fake = Faker()
@@ -40,31 +46,34 @@ class Command(BaseCommand):
company = CompanyFactory(company_name=name) company = CompanyFactory(company_name=name)
new_companies.append(company) new_companies.append(company)
logging.debug(f"New Company {company.company_name} created") logging.debug(f"New Company {company.company_name} created")
print(".", end = '') print(f"\t- {name}")
print('') print('')
logging.info("Creating fake products") logging.info("Creating fake products")
print("Creating fake products")
# create and assign products to companies # create and assign products to companies
for company in new_companies: for company in new_companies:
logging.info(f"Products for {company.company_name}") print("Creating fake products for {company.company_name}")
logging.info(f"Creating Products for {company.company_name}")
for i in range(100): for i in range(100):
name = fake.last_name_nonbinary() name = fake.last_name_nonbinary()
description = fake.paragraph(nb_sentences=5) description = fake.paragraph(nb_sentences=5)
# TODO: apply tags from tag list # TODO: apply tags from tag list
image= None
""" """
# TODO: add dynamic image to product # TODO: write image to S3 storage
response = requests.get(self.logo_url) response = requests.get(self.logo_url)
if response.status_code == 200: if response.status_code == 200:
response.raw.decode_content = True response.raw.decode_content = True
image = response.raw image = response.raw.read()
else: else:
image= None logging.warning(f"Got {response.status_code} querying {self.logo_url}")
""" """
product = ProductFactory(name=name, description=description,)
product = ProductFactory(name=name, description=description, image=image)
logging.debug(f"New Product {product.name} created") logging.debug(f"New Product {product.name} created")
print(".", end = '') print("*", end = '.')
print('') print('')
print("Dataset creation finished") print("Dataset creation finished")

View File

@@ -2,6 +2,8 @@ from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.models import PermissionsMixin
from tagulous.models import TagTreeModel
from companies.models import Company from companies.models import Company
# Create your models here. # Create your models here.
@@ -71,3 +73,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
verbose_name = 'Usuario' verbose_name = 'Usuario'
verbose_name_plural = 'Usuarios' verbose_name_plural = 'Usuarios'
class TreeTag(TagTreeModel):
class TagMeta:
# initial = "food/eating, food/cooking, gaming/football"
force_lowercase = True
# autocomplete_view = 'myapp.views.hobbies_autocomplete'

View File

@@ -7,9 +7,12 @@ from django.utils.http import urlsafe_base64_encode
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.conf import settings
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from tagulous.models import TagModel
User = get_user_model() User = get_user_model()
@@ -17,8 +20,10 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp): def _make_hash_value(self, user, timestamp):
return f"{user.pk}{timestamp}{user.full_name}" return f"{user.pk}{timestamp}{user.full_name}"
account_activation_token = AccountActivationTokenGenerator() account_activation_token = AccountActivationTokenGenerator()
def get_tokens_for_user(user): def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user) refresh = RefreshToken.for_user(user)
@@ -39,12 +44,14 @@ def get_auth_token(client, email, password):
# logging.error(f"User {email} was refused a token: {response.content}") # logging.error(f"User {email} was refused a token: {response.content}")
return None return None
def create_active_user(email, password): def create_active_user(email, password):
user = User.objects.create_user(email=email, password=password) user = User.objects.create_user(email=email, password=password)
user.is_active = True user.is_active = True
user.save() user.save()
return user return user
def create_admin_user(email, password): def create_admin_user(email, password):
user = User.objects.create_user(email=email, password=password) user = User.objects.create_user(email=email, password=password)
user.is_staff = True user.is_staff = True
@@ -52,6 +59,7 @@ def create_admin_user(email, password):
user.save() user.save()
return user return user
def send_verification_email(request, user): def send_verification_email(request, user):
try: try:
current_site = get_current_site(request) current_site = get_current_site(request)
@@ -69,3 +77,23 @@ def send_verification_email(request, user):
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:
logging.error(f"Could not sent verification email to: {user.email}") logging.error(f"Could not sent verification email to: {user.email}")
def reformat_google_taxonomy(file_name):
"""
Read from flat text file
Create Herarchical Tag for each line
"""
base = settings.BASE_DIR + '/../datasets/'
counter = 0
source_file_path = base + file_name
destination_file_path = base + 'ALT' + file_name
source_file = open(source_file_path, 'rt')
destination_file = open(destination_file_path, 'wt')
for line in source_file.readlines():
line = line.replace(' > ', '/')
destination_file.write(line)

BIN
datasets/GeoLite2-City.mmdb Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 MiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ 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
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes, action
import requests import requests
@@ -42,6 +42,11 @@ class ProductViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(creator=self.request.user) serializer.save(creator=self.request.user)
@action(detail=True, methods=['GET',])
def related(request):
# find the most similar products
return Response(data=[])
@api_view(['GET',]) @api_view(['GET',])
@permission_classes([IsAuthenticated,]) @permission_classes([IsAuthenticated,])

View File

@@ -11,3 +11,5 @@ django-taggit-serializer==0.1.7
django-tagulous==1.1.0 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
geoip2==4.1.0

View File

@@ -1,5 +1,6 @@
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
@@ -7,7 +8,14 @@ User = get_user_model()
class StatsLog(models.Model): class StatsLog(models.Model):
model = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING, null=True) action_object_content_type = models.ForeignKey(
ContentType, blank=True,
null=True,
related_name='statslogs',
on_delete=models.CASCADE
)
action_object_object_id = models.CharField(max_length=255, blank=True, null=True)
action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id')
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True) user = models.ForeignKey(User, on_delete=models.DO_NOTHING, null=True)
anonymous = models.BooleanField('Usuario no registrado', null=True) anonymous = models.BooleanField('Usuario no registrado', null=True)
ip_address = models.GenericIPAddressField('IP usuario', null=True, blank=True) ip_address = models.GenericIPAddressField('IP usuario', null=True, blank=True)

View File

@@ -0,0 +1,10 @@
Hola {{company.creator.full_name}}.
Estamos contactando contigo como usuario gerente de la compañina {{company.company_name}}.
Datos de usuario:
- {{user.full_name}}
- {{user.email}}
Contenido del mensaje:
{{data}}

View File

@@ -0,0 +1,2 @@
Hola {{user.full_name}}.
Hemos enviado un email a {{company.company_name}} sobre tu petición.