feat: ruff'ed

This commit is contained in:
2025-05-02 08:38:57 +02:00
parent 32b36b81b3
commit 48b53d851a
62 changed files with 2019 additions and 3655 deletions
+1
View File
@@ -0,0 +1 @@
3.13
+1 -1
View File
@@ -2,6 +2,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application() application = get_asgi_application()
+3 -3
View File
@@ -3,15 +3,15 @@ import os
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
app = Celery("config") app = Celery('config')
# Using a string here means the worker doesn't have to serialize # Using a string here means the worker doesn't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys # - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix. # should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
+9 -29
View File
@@ -8,10 +8,10 @@ from django.views.generic import TemplateView
class HTMXFormComponent(TemplateView): class HTMXFormComponent(TemplateView):
form_class: Type[forms.ModelForm] = None form_class: Type[forms.ModelForm] = None
template_name: str = "components/default/form.html" template_name: str = 'components/default/form.html'
hx_trigger = "" hx_trigger = ''
view_name = "" view_name = ''
save_button_text = "" save_button_text = ''
def get_view_name(self): def get_view_name(self):
return self.view_name return self.view_name
@@ -23,9 +23,7 @@ class HTMXFormComponent(TemplateView):
instance = self.get_object() instance = self.get_object()
if initial: if initial:
return self.form_class( return self.form_class(initial=self.get_initial_values(instance), instance=instance)
initial=self.get_initial_values(instance), instance=instance
)
return self.form_class(self.request.POST, instance=instance) return self.form_class(self.request.POST, instance=instance)
@@ -38,11 +36,7 @@ class HTMXFormComponent(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
form = self.get_form() form = self.get_form()
return { return {'form': form, 'url': self.get_view_name(), 'save_button_text': self.save_button_text}
"form": form,
"url": self.get_view_name(),
"save_button_text": self.save_button_text,
}
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form(initial=False) form = self.get_form(initial=False)
@@ -50,22 +44,8 @@ class HTMXFormComponent(TemplateView):
if form.is_valid(): if form.is_valid():
instance = form.save() instance = form.save()
response = render( response = render(request, self.template_name, {'form': form, 'instance': instance})
request, response['HX-Trigger'] = self.hx_trigger
self.template_name,
{
"form": form,
"instance": instance,
},
)
response["HX-Trigger"] = self.hx_trigger
return response return response
return render( return render(request, self.template_name, {'form': form, 'save_button_text': self.save_button_text})
request,
self.template_name,
{
"form": form,
"save_button_text": self.save_button_text,
},
)
+2 -2
View File
@@ -8,7 +8,7 @@ class SingletonModel(models.Model):
abstract = True abstract = True
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
cache_key = f"{self._meta.model_name}_singleton_key" cache_key = f'{self._meta.model_name}_singleton_key'
self.pk = 1 self.pk = 1
super(SingletonModel, self).save(*args, **kwargs) super(SingletonModel, self).save(*args, **kwargs)
@@ -21,7 +21,7 @@ class SingletonModel(models.Model):
@classmethod @classmethod
def load(cls): def load(cls):
cache_key = f"{cls._meta.model_name}_singleton_key" cache_key = f'{cls._meta.model_name}_singleton_key'
data = cache.get(cache_key) data = cache.get(cache_key)
if not data: if not data:
instance, created = cls.objects.get_or_create(pk=1) instance, created = cls.objects.get_or_create(pk=1)
+111 -139
View File
@@ -4,9 +4,9 @@ from django.utils.text import gettext_lazy as _
from config.settings.environ import * # noqa from config.settings.environ import * # noqa
APP_NAME = "Shop" APP_NAME = 'Shop'
DESCRIPTION = "" DESCRIPTION = ''
VERSION = "0.1.0" VERSION = '0.1.0'
CORS_ORIGIN_ALLOW_ALL = DEBUG CORS_ORIGIN_ALLOW_ALL = DEBUG
@@ -17,118 +17,98 @@ if not DEBUG:
DJANGO_APPS = [ DJANGO_APPS = [
"unfold", # before django.contrib.admin 'unfold', # before django.contrib.admin
"django.contrib.admin", 'django.contrib.admin',
"django.contrib.auth", 'django.contrib.auth',
"django.contrib.contenttypes", 'django.contrib.contenttypes',
"django.contrib.sessions", 'django.contrib.sessions',
"django.contrib.messages", 'django.contrib.messages',
"django.contrib.staticfiles", 'django.contrib.staticfiles',
] ]
THIRD_PARTY_APPS = [ THIRD_PARTY_APPS = [
"corsheaders", 'corsheaders',
"django_extensions", 'django_extensions',
"django_filters", 'django_filters',
"watchman", 'watchman',
"drf_spectacular", 'drf_spectacular',
"gonk", 'gonk',
"rest_framework_simplejwt", 'rest_framework_simplejwt',
] ]
PROJECT_APPS = [ PROJECT_APPS = ['shop', 'users', 'web']
"shop",
"users",
"web",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
if DEBUG: if DEBUG:
INSTALLED_APPS += ["django_browser_reload"] INSTALLED_APPS += ['django_browser_reload']
PROJECT_MIDDLEWARE = [] PROJECT_MIDDLEWARE = []
DJANGO_MIDDLEWARE = [ DJANGO_MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", 'django.middleware.security.SecurityMiddleware',
"django.contrib.sessions.middleware.SessionMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware", 'corsheaders.middleware.CorsMiddleware',
"django.middleware.common.CommonMiddleware", 'django.middleware.common.CommonMiddleware',
"django.middleware.csrf.CsrfViewMiddleware", 'django.middleware.csrf.CsrfViewMiddleware',
"django.contrib.auth.middleware.AuthenticationMiddleware", 'django.contrib.auth.middleware.AuthenticationMiddleware',
"django.contrib.messages.middleware.MessageMiddleware", 'django.contrib.messages.middleware.MessageMiddleware',
"django.middleware.clickjacking.XFrameOptionsMiddleware", 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
MIDDLEWARE = DJANGO_MIDDLEWARE + PROJECT_MIDDLEWARE MIDDLEWARE = DJANGO_MIDDLEWARE + PROJECT_MIDDLEWARE
if DEBUG: if DEBUG:
INSTALLED_APPS += ["silk"] INSTALLED_APPS += ['silk']
MIDDLEWARE += [ MIDDLEWARE += ['silk.middleware.SilkyMiddleware', 'django_browser_reload.middleware.BrowserReloadMiddleware']
"silk.middleware.SilkyMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
]
ROOT_URLCONF = "config.urls" ROOT_URLCONF = 'config.urls'
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", 'BACKEND': 'django.template.backends.django.DjangoTemplates',
"DIRS": [], 'DIRS': [],
"APP_DIRS": True, 'APP_DIRS': True,
"OPTIONS": { 'OPTIONS': {
"context_processors": [ 'context_processors': [
"django.template.context_processors.debug", 'django.template.context_processors.debug',
"django.template.context_processors.request", 'django.template.context_processors.request',
"django.contrib.auth.context_processors.auth", 'django.contrib.auth.context_processors.auth',
"django.contrib.messages.context_processors.messages", 'django.contrib.messages.context_processors.messages',
"web.context_processors.web_settings", 'web.context_processors.web_settings',
], ]
},
}, },
}
] ]
WSGI_APPLICATION = "config.wsgi.application" WSGI_APPLICATION = 'config.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases # https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = { DATABASES = {'default': DATABASE_URL}
"default": DATABASE_URL,
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
}, {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/ # https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "es" LANGUAGE_CODE = 'es'
LANGUAGES = ( LANGUAGES = (('en', _('Inglés')), ('es', _('Castellano')))
("en", _("Inglés")),
("es", _("Castellano")),
)
TIME_ZONE = "Europe/Madrid" TIME_ZONE = 'Europe/Madrid'
USE_I18N = True USE_I18N = True
@@ -140,59 +120,51 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "/static/" STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "static" STATIC_ROOT = BASE_DIR / 'static'
MEDIA_URL = "/media/" MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / 'media'
LOGIN_REDIRECT_URL = "/login/" LOGIN_REDIRECT_URL = '/login/'
STORAGES = { STORAGES = {
"default": { 'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
"BACKEND": "django.core.files.storage.FileSystemStorage", 'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage'},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
} }
# Django Rest Framework Configuration # Django Rest Framework Configuration
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
"DEFAULT_AUTHENTICATION_CLASSES": ( 'DEFAULT_AUTHENTICATION_CLASSES': (
"rest_framework_simplejwt.authentication.JWTAuthentication", 'rest_framework_simplejwt.authentication.JWTAuthentication',
"rest_framework.authentication.SessionAuthentication", 'rest_framework.authentication.SessionAuthentication',
"rest_framework.authentication.BasicAuthentication", 'rest_framework.authentication.BasicAuthentication',
), ),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), 'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',),
"DEFAULT_FILTER_BACKENDS": [ 'DEFAULT_FILTER_BACKENDS': [
"rest_framework.filters.OrderingFilter", 'rest_framework.filters.OrderingFilter',
"django_filters.rest_framework.DjangoFilterBackend", 'django_filters.rest_framework.DjangoFilterBackend',
"rest_framework.filters.SearchFilter", 'rest_framework.filters.SearchFilter',
], ],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
"PAGE_SIZE": PAGE_SIZE, 'PAGE_SIZE': PAGE_SIZE,
} }
LOCALE_PATHS = [ LOCALE_PATHS = [BASE_DIR / 'locale']
BASE_DIR / "locale",
]
if S3_ENABLED: if S3_ENABLED:
STORAGES["default"] = { STORAGES['default'] = {'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage'}
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
}
WATCHMAN_STORAGE_PATH = "tmp" WATCHMAN_STORAGE_PATH = 'tmp'
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
"TITLE": APP_NAME, 'TITLE': APP_NAME,
"DESCRIPTION": DESCRIPTION, 'DESCRIPTION': DESCRIPTION,
"VERSION": VERSION, 'VERSION': VERSION,
"SERVE_INCLUDE_SCHEMA": False, 'SERVE_INCLUDE_SCHEMA': False,
} }
@@ -200,42 +172,42 @@ EMAIL_USE_TLS = True
EMAIL_USE_SSL = False EMAIL_USE_SSL = False
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="memory://localhost:8000//") CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', default='memory://localhost:8000//')
CELERY_TIME_ZONE = TIME_ZONE CELERY_TIME_ZONE = TIME_ZONE
CELERY_ALWAYS_EAGER = DEBUG CELERY_ALWAYS_EAGER = DEBUG
SIMPLE_JWT = { SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(hours=12), 'ACCESS_TOKEN_LIFETIME': datetime.timedelta(hours=12),
"REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
"ROTATE_REFRESH_TOKENS": False, 'ROTATE_REFRESH_TOKENS': False,
"BLACKLIST_AFTER_ROTATION": False, 'BLACKLIST_AFTER_ROTATION': False,
"UPDATE_LAST_LOGIN": False, 'UPDATE_LAST_LOGIN': False,
"ALGORITHM": "HS256", 'ALGORITHM': 'HS256',
"SIGNING_KEY": SECRET_KEY, 'SIGNING_KEY': SECRET_KEY,
"VERIFYING_KEY": "", 'VERIFYING_KEY': '',
"AUDIENCE": None, 'AUDIENCE': None,
"ISSUER": None, 'ISSUER': None,
"JSON_ENCODER": None, 'JSON_ENCODER': None,
"JWK_URL": None, 'JWK_URL': None,
"LEEWAY": 0, 'LEEWAY': 0,
"AUTH_HEADER_TYPES": ("Bearer",), 'AUTH_HEADER_TYPES': ('Bearer',),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
"USER_ID_FIELD": "id", 'USER_ID_FIELD': 'id',
"USER_ID_CLAIM": "user_id", 'USER_ID_CLAIM': 'user_id',
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
"TOKEN_TYPE_CLAIM": "token_type", 'TOKEN_TYPE_CLAIM': 'token_type',
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
"JTI_CLAIM": "jti", 'JTI_CLAIM': 'jti',
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
"SLIDING_TOKEN_LIFETIME": datetime.timedelta(minutes=5), 'SLIDING_TOKEN_LIFETIME': datetime.timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": datetime.timedelta(days=1), 'SLIDING_TOKEN_REFRESH_LIFETIME': datetime.timedelta(days=1),
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer", 'TOKEN_OBTAIN_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenObtainPairSerializer',
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer", 'TOKEN_REFRESH_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenRefreshSerializer',
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer", 'TOKEN_VERIFY_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenVerifySerializer',
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer", 'TOKEN_BLACKLIST_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenBlacklistSerializer',
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer", 'SLIDING_TOKEN_OBTAIN_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer',
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", 'SLIDING_TOKEN_REFRESH_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer',
} }
TAILWIND_APP_NAME = "theme" TAILWIND_APP_NAME = 'theme'
+34 -42
View File
@@ -4,64 +4,56 @@ from shutil import which
import environ import environ
BASE_DIR = Path(".") BASE_DIR = Path('.')
env = environ.Env() env = environ.Env()
env_file = BASE_DIR / ".env" env_file = BASE_DIR / '.env'
if os.path.exists(env_file): if os.path.exists(env_file):
environ.Env.read_env(env_file) environ.Env.read_env(env_file)
SECRET_KEY = env.str("SECRET_KEY", "this-is-the-default-secret-key") SECRET_KEY = env.str('SECRET_KEY', 'this-is-the-default-secret-key')
DEBUG = env.bool("DEBUG", False) DEBUG = env.bool('DEBUG', False)
CORS_ORIGIN_WHITELIST = env.str("CORS_ORIGIN_WHITELIST", "http://localhost:8000").split( CORS_ORIGIN_WHITELIST = env.str('CORS_ORIGIN_WHITELIST', 'http://localhost:8000').split(',')
"," CSRF_TRUSTED_ORIGINS = env.str('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000').split(',')
)
CSRF_TRUSTED_ORIGINS = env.str("CSRF_TRUSTED_ORIGINS", "http://localhost:8000").split(
","
)
DATABASE_URL = env.db_url("DATABASE_URL", "sqlite:///db.sqlite3") DATABASE_URL = env.db_url('DATABASE_URL', 'sqlite:///db.sqlite3')
STATIC_ROOT = env.str("STATIC_ROOT", "static") STATIC_ROOT = env.str('STATIC_ROOT', 'static')
MEDIA_ROOT = env.str("MEDIA_ROOT", "media") MEDIA_ROOT = env.str('MEDIA_ROOT', 'media')
REDIS_HOST = env.str("REDIS_HOST", "redis") REDIS_HOST = env.str('REDIS_HOST', 'redis')
REDIS_PORT = env.str("REDIS_PORT", 6379) REDIS_PORT = env.str('REDIS_PORT', 6379)
PAGE_SIZE = env.int("PAGE_SIZE", 20) PAGE_SIZE = env.int('PAGE_SIZE', 20)
ALLOWED_HOSTS = env.str("ALLOWED_HOSTS", "*").split(",") ALLOWED_HOSTS = env.str('ALLOWED_HOSTS', '*').split(',')
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", "redis://172.17.0.1:6379/0") CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', 'redis://172.17.0.1:6379/0')
S3_ENABLED = env.bool("S3_ENABLED", False) S3_ENABLED = env.bool('S3_ENABLED', False)
AWS_S3_HOST = env.str("S3_HOST", "") AWS_S3_HOST = env.str('S3_HOST', '')
AWS_ACCESS_KEY_ID = env.str("S3_ACCESS_KEY_ID", "") AWS_ACCESS_KEY_ID = env.str('S3_ACCESS_KEY_ID', '')
AWS_SECRET_ACCESS_KEY = env.str("S3_SECRET_ACCESS_KEY", "") AWS_SECRET_ACCESS_KEY = env.str('S3_SECRET_ACCESS_KEY', '')
AWS_STORAGE_BUCKET_NAME = env.str("S3_STORAGE_BUCKET_NAME", "") AWS_STORAGE_BUCKET_NAME = env.str('S3_STORAGE_BUCKET_NAME', '')
AWS_S3_ENDPOINT_URL = env.str("S3_ENDPOINT_URL", "") AWS_S3_ENDPOINT_URL = env.str('S3_ENDPOINT_URL', '')
# Redsys # Redsys
# https://pagosonline.redsys.es/conexion-redireccion.html # https://pagosonline.redsys.es/conexion-redireccion.html
REDSYS_DEBUG = env.bool("REDSYS_DEBUG", DEBUG) REDSYS_DEBUG = env.bool('REDSYS_DEBUG', DEBUG)
REDSYS_SHARED_SECRET = env.str( REDSYS_SHARED_SECRET = env.str('REDSYS_SHARED_SECRET', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7')
"REDSYS_SHARED_SECRET", "sq7HjrUOBfKmC576ILgskD5srU870gJ7" REDSYS_MERCHANT_CODE = env.str('REDSYS_MERCHANT_CODE', '999008881')
) REDSYS_TERMINAL = env.str('REDSYS_TERMINAL', '001')
REDSYS_MERCHANT_CODE = env.str("REDSYS_MERCHANT_CODE", "999008881") REDSYS_CURRENCY_CODE = env.str('REDSYS_CURRENCY_CODE', '978')
REDSYS_TERMINAL = env.str("REDSYS_TERMINAL", "001") REDSYS_TPV_DOMAIN = env.str('REDSYS_TPV_DOMAIN', '')
REDSYS_CURRENCY_CODE = env.str("REDSYS_CURRENCY_CODE", "978")
REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "")
ITEMS_PER_PAGE = env.int("ITEMS_PER_PAGE", 20) ITEMS_PER_PAGE = env.int('ITEMS_PER_PAGE', 20)
SHOP_COUNTRY = env.str("SHOP_COUNTRY", "ESPAÑA") SHOP_COUNTRY = env.str('SHOP_COUNTRY', 'ESPAÑA')
NPM_BIN_PATH = which("npm") NPM_BIN_PATH = which('npm')
EMAIL_BACKEND = env.str( EMAIL_BACKEND = env.str('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
"EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend" EMAIL_HOST = env.str('EMAIL_HOST', '')
) EMAIL_HOST_USER = env.str('EMAIL_HOST_USER', '')
EMAIL_HOST = env.str("EMAIL_HOST", "") EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD', '')
EMAIL_HOST_USER = env.str("EMAIL_HOST_USER", "") EMAIL_PORT = env.int('EMAIL_PORT', 587)
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", "")
EMAIL_PORT = env.int("EMAIL_PORT", 587)
+7 -10
View File
@@ -5,18 +5,15 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("web.urls", namespace="web")), path('', include('web.urls', namespace='web')),
path("admin/", admin.site.urls), path('admin/', admin.site.urls),
path("watchman/", include("watchman.urls")), path('watchman/', include('watchman.urls')),
path("auth/", include("users.urls", namespace="users")), path('auth/', include('users.urls', namespace='users')),
path("tpv/", include("shop.urls", namespace="shop")), path('tpv/', include('shop.urls', namespace='shop')),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns.append(path("__debug__/", include(debug_toolbar.urls))) urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [ urlpatterns += [path('silk/', include('silk.urls')), path('__reload__/', include('django_browser_reload.urls'))]
path("silk/", include("silk.urls")),
path("__reload__/", include("django_browser_reload.urls")),
]
+1 -1
View File
@@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application() application = get_wsgi_application()
+5 -4
View File
@@ -1,22 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you " 'available on your PYTHONPATH environment variable? Did you '
"forget to activate a virtual environment?" 'forget to activate a virtual environment?'
) from exc ) from exc
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == "__main__": if __name__ == '__main__':
main() main()
+97
View File
@@ -0,0 +1,97 @@
[project]
name = "shoppy"
version = "0.1.1"
description = ""
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"boto3==1.34.105",
"celery==5.4.0",
"dj-database-url==1.0.0",
"django==5.1.4",
"django-celery-beat==2.7.0",
"django-cors-headers==3.13.0",
"django-cryptography==1.1",
"django-debug-toolbar==4.4.6",
"django-environ==0.10.0",
"django-extensions==3.2.0",
"django-filter==24.3",
"django-silk==5.1.0",
"django-storages==1.13.2",
"django-unfold==0.42.0",
"django-watchman==1.3.0",
"djangorestframework==3.15.2",
"djangorestframework-simplejwt==5.4.0",
"drf-spectacular==0.26.1",
"gonk==0.6.1",
"ipython==8.16.1",
"pillow==11.0.0",
"psycopg2-binary==2.9.10",
"pydes==2.0.1",
"pytailwindcss==0.2.0",
"redis==4.5.4",
"requests==2.32.3",
"uvicorn==0.21.1",
"xmltodict==0.13.0",
]
[tool.ruff]
line-length = 119
indent-width = 4
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
".venv",
]
[tool.ruff.format]
quote-style = "single"
skip-magic-trailing-comma = true
[tool.ruff.lint]
ignore = ["E501", "E741", "F401", "F403", "F405", "F406"]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "main.settings"
addopts = "--ignore=src"
python_files = ["tests.py", "test_*.py", "*_tests.py"]
[tool.pyright]
venvPath = "."
venv = ".venv"
[dependency-groups]
dev = [
"coverage==7.2.3",
"django-browser-reload==1.17.0",
"django-celery-beat==2.7.0",
"django-debug-toolbar==4.4.6",
"django-silk==5.1.0",
"pytest==7.3.0",
"pytest-cov==4.0.0",
"pytest-django==4.8.0",
]
+22 -89
View File
@@ -24,123 +24,71 @@ from shop.models import (
# Register your models here. # Register your models here.
@admin.register(Product) @admin.register(Product)
class ProductAdmin(ModelAdmin): class ProductAdmin(ModelAdmin):
search_fields = ("name",) search_fields = ('name',)
list_display = ( list_display = ('id', 'name', 'stock')
"id",
"name",
"stock",
)
@admin.register(ProductPrice) @admin.register(ProductPrice)
class ProductPriceAdmin(ModelAdmin): class ProductPriceAdmin(ModelAdmin):
search_fields = ("product",) search_fields = ('product',)
autocomplete_fields = ("product",) autocomplete_fields = ('product',)
list_display = ( list_display = ('id', 'product', 'price', 'date', 'tax')
"id",
"product",
"price",
"date",
"tax",
)
@admin.register(Tax) @admin.register(Tax)
class TaxAdmin(ModelAdmin): class TaxAdmin(ModelAdmin):
list_display = ( list_display = ('code', 'value')
"code",
"value",
)
@admin.register(OrderLine) @admin.register(OrderLine)
class OrderLineAdmin(ModelAdmin): class OrderLineAdmin(ModelAdmin):
autocomplete_fields = ("product",) autocomplete_fields = ('product',)
list_display = ( list_display = ('id', 'product', 'price', 'quantity')
"id",
"product",
"price",
"quantity",
)
@admin.register(Order) @admin.register(Order)
class OrderAdmin(ModelAdmin): class OrderAdmin(ModelAdmin):
list_display = ("id",) list_display = ('id',)
@admin.register(Provider) @admin.register(Provider)
class ProviderAdmin(ModelAdmin): class ProviderAdmin(ModelAdmin):
list_display = ( list_display = ('vat_id', 'name')
"vat_id",
"name",
)
@admin.register(Tag) @admin.register(Tag)
class TagAdmin(ModelAdmin): class TagAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'name')
"id",
"name",
)
@admin.register(Brand) @admin.register(Brand)
class BrandAdmin(ModelAdmin): class BrandAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'name')
"id",
"name",
)
@admin.register(ProductBatch) @admin.register(ProductBatch)
class ProductBatchAdmin(ModelAdmin): class ProductBatchAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'product', 'quantity', 'provider')
"id",
"product",
"quantity",
"provider",
)
@admin.register(Cart) @admin.register(Cart)
class ProductBatchAdmin(ModelAdmin): class CartAdmin(ModelAdmin):
list_display = ( list_display = ('uuid', 'user', 'creation_date')
"uuid",
"user",
"creation_date",
)
@admin.register(CartItem) @admin.register(CartItem)
class ProductBatchAdmin(ModelAdmin): class CartItemAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'cart', 'product', 'quantity')
"id",
"cart",
"product",
"quantity",
)
@admin.register(ShippingMethod) @admin.register(ShippingMethod)
class ShippingMethodAdmin(ModelAdmin): class ShippingMethodAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'name', 'description')
"id",
"name",
"description",
)
@admin.register(CustomerAddress) @admin.register(CustomerAddress)
class CustomerAddressAdmin(ModelAdmin): class CustomerAddressAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'user', 'vat_id', 'address', 'address_state', 'address_zip')
"id",
"user",
"vat_id",
"address",
"address_state",
"address_zip",
)
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
@@ -149,29 +97,14 @@ class CustomerAddressAdmin(ModelAdmin):
@admin.register(ProductCategory) @admin.register(ProductCategory)
class ProductCategoryAdmin(ModelAdmin): class ProductCategoryAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'name', 'parent', 'promoted', 'hidden', 'show_in_navbar')
"id",
"name",
"parent",
"promoted",
"hidden",
"show_in_navbar",
)
@admin.register(ShopSettings) @admin.register(ShopSettings)
class ShopSettingsAdmin(ModelAdmin): class ShopSettingsAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'merchant_code', 'currency_code', 'terminal')
"id",
"merchant_code",
"currency_code",
"terminal",
)
@admin.register(ProductImage) @admin.register(ProductImage)
class ProductImageAdmin(ModelAdmin): class ProductImageAdmin(ModelAdmin):
list_display = ( list_display = ('id', 'original')
"id",
"original",
)
+4 -2
View File
@@ -1,6 +1,8 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ShopConfig(AppConfig): class ShopConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = 'django.db.models.BigAutoField'
name = "shop" name = 'shop'
verbose_name = _('Tienda')
+15 -30
View File
@@ -6,44 +6,29 @@ from shop.models import Product, ProductPrice
class ProductFilter(django_filters.FilterSet): class ProductFilter(django_filters.FilterSet):
name = django_filters.CharFilter( name = django_filters.CharFilter(field_name='name', lookup_expr='icontains', label=_('Nombre'))
field_name="name", lookup_expr="icontains", label=_("Nombre") tags = django_filters.BaseInFilter(field_name='tags', label=_('Etiquetas'))
) category = django_filters.BaseInFilter(field_name='categories', label=_('Categorías'))
tags = django_filters.BaseInFilter(field_name="tags", label=_("Etiquetas")) price_lt = django_filters.NumberFilter(method='filter_price_lt')
category = django_filters.BaseInFilter( price_gt = django_filters.NumberFilter(method='filter_price_gt')
field_name="categories", label=_("Categorías") o = OrderingFilter(fields=(('name', 'name'), ('id', 'id')))
)
price_lt = django_filters.NumberFilter(method="filter_price_lt")
price_gt = django_filters.NumberFilter(method="filter_price_gt")
o = OrderingFilter(
fields=(
("name", "name"),
("id", "id"),
)
)
def filter_price_lt(self, queryset, name, value): def filter_price_lt(self, queryset, name, value):
ids = queryset.values("id") ids = queryset.values('id')
product_ids = ProductPrice.objects.filter( product_ids = ProductPrice.objects.filter(product_id__in=ids, current=True, price_with_tax__lte=value).values(
product_id__in=ids, current=True, price_with_tax__lte=value 'product_id'
).values("product_id") )
return queryset.filter(pk__in=product_ids) return queryset.filter(pk__in=product_ids)
def filter_price_gt(self, queryset, name, value): def filter_price_gt(self, queryset, name, value):
ids = queryset.values("id") ids = queryset.values('id')
product_ids = ProductPrice.objects.filter( product_ids = ProductPrice.objects.filter(product_id__in=ids, current=True, price_with_tax__gte=value).values(
product_id__in=ids, current=True, price_with_tax__gte=value 'product_id'
).values("product_id") )
return queryset.filter(pk__in=product_ids) return queryset.filter(pk__in=product_ids)
class Meta: class Meta:
model = Product model = Product
fields = ( fields = ('name', 'stock', 'is_digital_asset', 'tags', 'category')
"name",
"stock",
"is_digital_asset",
"tags",
"category",
)
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -4,15 +4,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [('shop', '0001_initial')]
dependencies = [
("shop", "0001_initial"),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name="shopsettings", model_name='shopsettings',
name="debug", name='debug',
field=models.BooleanField(default=True, verbose_name="Modo depuración"), field=models.BooleanField(default=True, verbose_name='Modo depuración'),
), )
] ]
+205 -466
View File
@@ -19,12 +19,8 @@ User = get_user_model()
class TimestampedModel(models.Model): class TimestampedModel(models.Model):
creation_date = models.DateTimeField( creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('fecha de creación'))
auto_now_add=True, verbose_name=_("fecha de creación") last_modification_date = models.DateTimeField(auto_now=True, verbose_name=_('fecha de última modificación'))
)
last_modification_date = models.DateTimeField(
auto_now=True, verbose_name=_("fecha de última modificación")
)
class Meta: class Meta:
abstract = True abstract = True
@@ -37,33 +33,20 @@ class Tag(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = _("etiqueta") verbose_name = _('etiqueta')
verbose_name_plural = _("etiquetas") verbose_name_plural = _('etiquetas')
ordering = ("name",) ordering = ('name',)
class ProductCategory(models.Model): class ProductCategory(models.Model):
name = models.CharField(max_length=32) name = models.CharField(max_length=32)
parent = models.ForeignKey( parent = models.ForeignKey(
"shop.ProductCategory", 'shop.ProductCategory', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('categoría padre')
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("categoría padre"),
)
promoted = models.BooleanField(default=False, verbose_name=_("promocionada"))
hidden = models.BooleanField(default=False, verbose_name=_("oculta"))
show_in_navbar = models.BooleanField(
default=False, verbose_name=_("mostrar en cabecera")
)
slug = models.SlugField(
max_length=48,
blank=True,
default="",
verbose_name=_("slug"),
unique=True,
db_index=True,
) )
promoted = models.BooleanField(default=False, verbose_name=_('promocionada'))
hidden = models.BooleanField(default=False, verbose_name=_('oculta'))
show_in_navbar = models.BooleanField(default=False, verbose_name=_('mostrar en cabecera'))
slug = models.SlugField(max_length=48, blank=True, default='', verbose_name=_('slug'), unique=True, db_index=True)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -73,58 +56,24 @@ class ProductCategory(models.Model):
super().save() super().save()
class Meta: class Meta:
verbose_name = _("categoría") verbose_name = _('categoría')
verbose_name_plural = _("categorías") verbose_name_plural = _('categorías')
ordering = ("name",) ordering = ('name',)
class Product(TimestampedModel): class Product(TimestampedModel):
sku = models.CharField( sku = models.CharField(max_length=64, blank=False, null=False, unique=True, verbose_name=_('código de referencia'))
max_length=64, name = models.CharField(max_length=96, blank=False, null=False, verbose_name=_('nombre'))
blank=False, description = models.TextField(blank=True, default='', verbose_name=_('descripción'))
null=False, stock = models.DecimalField(max_digits=13, decimal_places=4, default=Decimal('0'), verbose_name=_('stock'))
unique=True, is_digital_asset = models.BooleanField(default=False, verbose_name=_('es un activo digital'))
verbose_name=_("código de referencia"), url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
) tags = models.ManyToManyField('shop.Tag', blank=True, verbose_name=_('etiquetas'))
name = models.CharField( categories = models.ManyToManyField('shop.ProductCategory', blank=True, verbose_name=_('categorías'))
max_length=96, brand = models.ForeignKey('shop.Brand', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('marca'))
blank=False, slug = models.SlugField(default='', blank=True, null=True, max_length=128, verbose_name=_('Slug'))
null=False, hidden = models.BooleanField(default=False, db_index=True, verbose_name=_('oculto'))
verbose_name=_("nombre"), is_shipping_method = models.BooleanField(default=False, verbose_name=_('es forma de envío'))
)
description = models.TextField(
blank=True,
default="",
verbose_name=_("descripción"),
)
stock = models.DecimalField(
max_digits=13,
decimal_places=4,
default=Decimal("0"),
verbose_name=_("stock"),
)
is_digital_asset = models.BooleanField(
default=False, verbose_name=_("es un activo digital")
)
url = models.URLField(blank=True, verbose_name=_("URL de descarga"))
tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas"))
categories = models.ManyToManyField(
"shop.ProductCategory", blank=True, verbose_name=_("categorías")
)
brand = models.ForeignKey(
"shop.Brand",
blank=True,
null=True,
on_delete=models.SET_NULL,
verbose_name=_("marca"),
)
slug = models.SlugField(
default="", blank=True, null=True, max_length=128, verbose_name=_("Slug")
)
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto"))
is_shipping_method = models.BooleanField(
default=False, verbose_name=_("es forma de envío")
)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -139,38 +88,25 @@ class Product(TimestampedModel):
return self.prices.filter(current=True).first() return self.prices.filter(current=True).first()
class Meta: class Meta:
verbose_name = _("producto") verbose_name = _('producto')
verbose_name_plural = _("productos") verbose_name_plural = _('productos')
ordering = ("id",) ordering = ('id',)
class ProductPrice(TimestampedModel): class ProductPrice(TimestampedModel):
price = models.DecimalField( price = models.DecimalField(max_digits=11, decimal_places=2, verbose_name=_('precio'))
max_digits=11, decimal_places=2, verbose_name=_("precio") date = models.DateTimeField(default=timezone.now, verbose_name=_('fecha'))
)
date = models.DateTimeField(default=timezone.now, verbose_name=_("fecha"))
product = models.ForeignKey( product = models.ForeignKey(
"shop.Product", 'shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'), related_name='prices'
on_delete=models.CASCADE,
verbose_name=_("producto"),
related_name="prices",
)
tax = models.ForeignKey(
"shop.Tax",
on_delete=models.PROTECT,
verbose_name=_("impuesto aplicable"),
) )
tax = models.ForeignKey('shop.Tax', on_delete=models.PROTECT, verbose_name=_('impuesto aplicable'))
price_with_tax = models.DecimalField( price_with_tax = models.DecimalField(
max_digits=11, max_digits=11, decimal_places=2, blank=True, null=True, verbose_name=_('precio con impuestos')
decimal_places=2,
blank=True,
null=True,
verbose_name=_("precio con impuestos"),
) )
current = models.BooleanField(default=False, verbose_name=_("es el precio actual")) current = models.BooleanField(default=False, verbose_name=_('es el precio actual'))
def __str__(self): def __str__(self):
return f"{self.price} - {self.tax.code}" return f'{self.price} - {self.tax.code}'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
tax_value = self.price * Decimal(self.tax.value / 100) tax_value = self.price * Decimal(self.tax.value / 100)
@@ -178,63 +114,41 @@ class ProductPrice(TimestampedModel):
super().save() super().save()
class Meta: class Meta:
verbose_name = _("precio de producto") verbose_name = _('precio de producto')
verbose_name_plural = _("precio de producto") verbose_name_plural = _('precio de producto')
ordering = ("-date",) ordering = ('-date',)
class ProductImage(models.Model): class ProductImage(models.Model):
product = models.ForeignKey( product = models.ForeignKey(
"shop.Product", 'shop.Product',
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False, null=False,
verbose_name=_("Producto"), verbose_name=_('Producto'),
related_name="images", related_name='images',
)
original = models.ImageField(
upload_to="uploads", blank=False, null=False, verbose_name=_("Imagen original")
) )
original = models.ImageField(upload_to='uploads', blank=False, null=False, verbose_name=_('Imagen original'))
xl = models.ImageField( xl = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('XL'))
upload_to="uploads", blank=True, null=True, verbose_name=_("XL") xl_dark = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('XL (fondo oscuro)'))
)
xl_dark = models.ImageField(
upload_to="uploads", blank=True, null=True, verbose_name=_("XL (fondo oscuro)")
)
l = models.ImageField( l = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('L'))
upload_to="uploads", blank=True, null=True, verbose_name=_("L") l_dark = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('L (fondo oscuro)'))
)
l_dark = models.ImageField(
upload_to="uploads", blank=True, null=True, verbose_name=_("L (fondo oscuro)")
)
m = models.ImageField( m = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('M'))
upload_to="uploads", blank=True, null=True, verbose_name=_("M") m_dark = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('M (fondo oscuro)'))
)
m_dark = models.ImageField(
upload_to="uploads", blank=True, null=True, verbose_name=_("M (fondo oscuro)")
)
s = models.ImageField( s = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('S'))
upload_to="uploads", blank=True, null=True, verbose_name=_("S") s_dark = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('S (fondo oscuro)'))
)
s_dark = models.ImageField(
upload_to="uploads", blank=True, null=True, verbose_name=_("S (fondo oscuro)")
)
xs = models.ImageField( xs = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('XS'))
upload_to="uploads", blank=True, null=True, verbose_name=_("XS") xs_dark = models.ImageField(upload_to='uploads', blank=True, null=True, verbose_name=_('XS (fondo oscuro)'))
)
xs_dark = models.ImageField(
upload_to="uploads", blank=True, null=True, verbose_name=_("XS (fondo oscuro)")
)
def __str__(self): def __str__(self):
return self.original.url return self.original.url
def make_square(self, image, max_size=512, fill_color="#000000"): def make_square(self, image, max_size=512, fill_color='#000000'):
width, height = image.size width, height = image.size
if width > height: if width > height:
@@ -247,7 +161,7 @@ class ProductImage(models.Model):
resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) resized_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
new_image = Image.new("RGB", (max_size, max_size), fill_color) new_image = Image.new('RGB', (max_size, max_size), fill_color)
position_x = int((max_size - new_width) / 2) position_x = int((max_size - new_width) / 2)
position_y = int((max_size - new_height) / 2) position_y = int((max_size - new_height) / 2)
@@ -256,503 +170,328 @@ class ProductImage(models.Model):
return new_image return new_image
def get_resized_image(self, image, size, fill_color="#000000"): def get_resized_image(self, image, size, fill_color='#000000'):
image = Image.open(image) image = Image.open(image)
image = self.make_square(image, size, fill_color) image = self.make_square(image, size, fill_color)
buffer = BytesIO() buffer = BytesIO()
image.save(fp=buffer, format="WEBP") image.save(fp=buffer, format='WEBP')
return ContentFile(buffer.getvalue(), name=f"{self.product.slug}_{size}.webp") return ContentFile(buffer.getvalue(), name=f'{self.product.slug}_{size}.webp')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.xl = self.get_resized_image(self.original, 1024, fill_color="#FFFFFF") self.xl = self.get_resized_image(self.original, 1024, fill_color='#FFFFFF')
self.xl_dark = self.get_resized_image(self.original, 1024) self.xl_dark = self.get_resized_image(self.original, 1024)
self.l = self.get_resized_image(self.original, 512, fill_color="#FFFFFF") self.l = self.get_resized_image(self.original, 512, fill_color='#FFFFFF')
self.l_dark = self.get_resized_image(self.original, 512) self.l_dark = self.get_resized_image(self.original, 512)
self.m = self.get_resized_image(self.original, 256, fill_color="#FFFFFF") self.m = self.get_resized_image(self.original, 256, fill_color='#FFFFFF')
self.m_dark = self.get_resized_image(self.original, 256) self.m_dark = self.get_resized_image(self.original, 256)
self.s = self.get_resized_image(self.original, 128, fill_color="#FFFFFF") self.s = self.get_resized_image(self.original, 128, fill_color='#FFFFFF')
self.s_dark = self.get_resized_image(self.original, 128) self.s_dark = self.get_resized_image(self.original, 128)
self.xs = self.get_resized_image(self.original, 96, fill_color="#FFFFFF") self.xs = self.get_resized_image(self.original, 96, fill_color='#FFFFFF')
self.xs_dark = self.get_resized_image(self.original, 96) self.xs_dark = self.get_resized_image(self.original, 96)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
class Meta:
verbose_name = _('imágenes de producto')
verbose_name_plural = _('imágenes de producto')
class ProductBatch(TimestampedModel): class ProductBatch(TimestampedModel):
code = models.CharField( code = models.CharField(max_length=32, unique=True, db_index=True, verbose_name=_('código'))
max_length=32, unique=True, db_index=True, verbose_name=_("código") product = models.ForeignKey('shop.Product', on_delete=models.PROTECT, verbose_name=_('producto'))
) quantity = models.DecimalField(max_digits=13, decimal_places=4, default=Decimal('1'), verbose_name=_('cantidad'))
product = models.ForeignKey(
"shop.Product", on_delete=models.PROTECT, verbose_name=_("producto")
)
quantity = models.DecimalField(
max_digits=13,
decimal_places=4,
default=Decimal("1"),
verbose_name=_("cantidad"),
)
expiration_date = models.DateField( expiration_date = models.DateField(
blank=True, null=True, verbose_name=_("fecha de caducidad / consumo preferente") blank=True, null=True, verbose_name=_('fecha de caducidad / consumo preferente')
) )
provider = models.ForeignKey( provider = models.ForeignKey(
"shop.Provider", 'shop.Provider', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('proveedor')
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("proveedor"),
) )
def __str__(self): def __str__(self):
return f"{self.code} - {self.product.name} - {self.quantity}" return f'{self.code} - {self.product.name} - {self.quantity}'
class Meta: class Meta:
verbose_name = _("remesa de producto") verbose_name = _('remesa de producto')
verbose_name_plural = _("remesas de productos") verbose_name_plural = _('remesas de productos')
class Tax(TimestampedModel): class Tax(TimestampedModel):
code = models.CharField( code = models.CharField(max_length=8, blank=False, unique=True, verbose_name=_('código de impuesto'))
max_length=8, blank=False, unique=True, verbose_name=_("código de impuesto") value = models.PositiveIntegerField(null=False, blank=False, verbose_name=_('valor entero (porcentaje)'))
)
value = models.PositiveIntegerField(
null=False, blank=False, verbose_name=_("valor entero (porcentaje)")
)
def __str__(self): def __str__(self):
return f"{self.code} - {self.value}%" return f'{self.code} - {self.value}%'
class Meta: class Meta:
verbose_name = _("impuesto") verbose_name = _('impuesto')
verbose_name_plural = _("impuestos") verbose_name_plural = _('impuestos')
ordering = ("id",) ordering = ('id',)
class Provider(TimestampedModel): class Provider(TimestampedModel):
vat_id = models.CharField( vat_id = models.CharField(max_length=32, blank=False, unique=True, verbose_name=_('documento de identidad'))
max_length=32, name = models.CharField(max_length=64, blank=False, verbose_name=_('nombre'))
blank=False,
unique=True,
verbose_name=_("documento de identidad"),
)
name = models.CharField(max_length=64, blank=False, verbose_name=_("nombre"))
email = models.EmailField(blank=False, verbose_name=_("e-mail de contacto")) email = models.EmailField(blank=False, verbose_name=_('e-mail de contacto'))
phone = models.CharField( phone = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=_('Teléfono de contacto'))
max_length=16,
blank=True,
null=True,
default="",
verbose_name=_("Teléfono de contacto"),
)
address = models.CharField(max_length=255, blank=False, verbose_name=_("dirección")) address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección'))
city = models.CharField(max_length=64, blank=False, verbose_name=_("ciudad")) city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad'))
state = models.CharField(max_length=64, blank=False, verbose_name=_("región")) state = models.CharField(max_length=64, blank=False, verbose_name=_('región'))
country = models.CharField(max_length=64, blank=False, verbose_name=_("país")) country = models.CharField(max_length=64, blank=False, verbose_name=_('país'))
zip = models.CharField(max_length=32, blank=False, verbose_name=_("código postal")) zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal'))
def __str__(self): def __str__(self):
return f"{self.vat_id} - {self.name}" return f'{self.vat_id} - {self.name}'
class Meta: class Meta:
verbose_name = _("proveedor") verbose_name = _('proveedor')
verbose_name_plural = _("proveedores") verbose_name_plural = _('proveedores')
class Brand(TimestampedModel): class Brand(TimestampedModel):
name = models.CharField( name = models.CharField(max_length=100, unique=True, blank=False, null=False, verbose_name=_('nombre'))
max_length=100, unique=True, blank=False, null=False, verbose_name=_("nombre")
)
class Meta: class Meta:
verbose_name = _("marca") verbose_name = _('marca')
verbose_name_plural = _("marcas") verbose_name_plural = _('marcas')
class OrderLine(TimestampedModel): class OrderLine(TimestampedModel):
order = models.ForeignKey( order = models.ForeignKey('shop.Order', on_delete=models.CASCADE, related_name='lines', verbose_name=_('pedido'))
"shop.Order",
on_delete=models.CASCADE,
related_name="lines",
verbose_name=_("pedido"),
)
product = models.ForeignKey( product = models.ForeignKey(
"shop.Product", 'shop.Product', on_delete=models.CASCADE, null=False, blank=False, verbose_name=_('producto')
on_delete=models.CASCADE,
null=False,
blank=False,
verbose_name=_("producto"),
) )
quantity = models.DecimalField( quantity = models.DecimalField(
max_digits=13, max_digits=13, decimal_places=4, default=Decimal('1'), null=False, blank=False, verbose_name=_('cantidad')
decimal_places=4,
default=Decimal("1"),
null=False,
blank=False,
verbose_name=_("cantidad"),
) )
# Precio del producto en el momento en el que se crea el pedido # Precio del producto en el momento en el que se crea el pedido
price = models.DecimalField( price = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('precio'))
max_digits=13, decimal_places=4, verbose_name=_("precio")
)
# base_total = quantity * price # base_total = quantity * price
base_total = models.DecimalField( base_total = models.DecimalField(
max_digits=13, max_digits=13,
decimal_places=4, decimal_places=4,
default=Decimal("0"), default=Decimal('0'),
null=False, null=False,
blank=False, blank=False,
verbose_name=_("total sin impuestos"), verbose_name=_('total sin impuestos'),
) )
# Valor entero del impuesto en el momento en el que se crea el pedido # Valor entero del impuesto en el momento en el que se crea el pedido
tax_value = models.PositiveIntegerField( tax_value = models.PositiveIntegerField(null=False, blank=False, verbose_name=_('valor de impuestos'))
null=False, blank=False, verbose_name=_("valor de impuestos")
)
# taxes = price * quantity * (tax_value / 100) # taxes = price * quantity * (tax_value / 100)
taxes = models.DecimalField( taxes = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('impuestos'))
max_digits=13, decimal_places=4, verbose_name=_("impuestos")
)
# total = base_total + taxes # total = base_total + taxes
total = models.DecimalField( total = models.DecimalField(max_digits=13, decimal_places=4, verbose_name=_('total'))
max_digits=13, decimal_places=4, verbose_name=_("total")
)
def __str__(self): def __str__(self):
return f"{self.product.name} - {self.quantity} - {self.price}" return f'{self.product.name} - {self.quantity} - {self.price}'
class Meta: class Meta:
verbose_name = _("línea de pedido") verbose_name = _('línea de pedido')
verbose_name_plural = _("líneas de pedido") verbose_name_plural = _('líneas de pedido')
def create_order_code(*args, **kwargs): def create_order_code(*args, **kwargs):
return ( return timezone.now().isoformat().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:-5]
timezone.now()
.isoformat()
.replace("-", "")
.replace("T", "")
.replace(":", "")
.replace(".", "")[:-5]
)
class Order(TimestampedModel): class Order(TimestampedModel):
class Statuses(models.TextChoices): class Statuses(models.TextChoices):
STATUS_PENDING = "PEN", _("pendiente de pago") STATUS_PENDING = 'PEN', _('pendiente de pago')
STATUS_PAID = "PAI", _("pagado") STATUS_PAID = 'PAI', _('pagado')
STATUS_RETURNED = "RTN", _("devuelto") STATUS_RETURNED = 'RTN', _('devuelto')
STATUS_ERROR = "ERR", _("Error") STATUS_ERROR = 'ERR', _('Error')
class ShippingStatuses(models.TextChoices): class ShippingStatuses(models.TextChoices):
STATUS_NOT_READY = "NOT", _("no preparado") STATUS_NOT_READY = 'NOT', _('no preparado')
STATUS_READY = "RDY", _("preparado") STATUS_READY = 'RDY', _('preparado')
STATUS_TO_BE_SENT = "TBS", _("listo para ser enviado") STATUS_TO_BE_SENT = 'TBS', _('listo para ser enviado')
STATUS_SENT = "SNT", _("enviado") STATUS_SENT = 'SNT', _('enviado')
STATUS_DELIVERED = "DLV", _("entregado") STATUS_DELIVERED = 'DLV', _('entregado')
STATUS_FINISHED = "FIN", _("finalizado") STATUS_FINISHED = 'FIN', _('finalizado')
STATUS_CANCELED = "CAN", _("cancelado") STATUS_CANCELED = 'CAN', _('cancelado')
STATUS_REQUESTED_RETURN = "RQT", _("devolución solicitada") STATUS_REQUESTED_RETURN = 'RQT', _('devolución solicitada')
STATUS_RETURNED = "RTN", _("devuelto") STATUS_RETURNED = 'RTN', _('devuelto')
uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True) uuid = models.UUIDField(default=uuid4, verbose_name=_('UUID'), db_index=True)
code = models.CharField(default=create_order_code, max_length=20, unique=True) code = models.CharField(default=create_order_code, max_length=20, unique=True)
status = models.CharField( status = models.CharField(max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_('estado'))
max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("estado")
)
shipping_status = models.CharField( shipping_status = models.CharField(
max_length=3, max_length=3, default=ShippingStatuses.STATUS_NOT_READY, verbose_name=_('estado de envío')
default=ShippingStatuses.STATUS_NOT_READY,
verbose_name=_("estado de envío"),
) )
base_total = models.DecimalField( base_total = models.DecimalField(
default=Decimal("0"), default=Decimal('0'), max_digits=13, decimal_places=2, verbose_name=_('base imponible')
max_digits=13,
decimal_places=2,
verbose_name=_("base imponible"),
) )
total = models.DecimalField( total = models.DecimalField(default=Decimal('0'), max_digits=13, decimal_places=2, verbose_name=_('total'))
default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("total")
)
user = models.ForeignKey( user = models.ForeignKey(User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_('cliente'))
User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("cliente") email = models.EmailField(blank=True, verbose_name=_('e-mail'))
)
email = models.EmailField(blank=True, verbose_name=_("e-mail"))
# Datos de facturación # Datos de facturación
billing_address = models.CharField( billing_address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección de facturación'))
max_length=255, blank=False, verbose_name=_("dirección de facturación") billing_city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad de facturación'))
) billing_state = models.CharField(max_length=64, blank=False, verbose_name=_('región de facturación'))
billing_city = models.CharField( billing_country = models.CharField(max_length=64, blank=False, verbose_name=_('país de facturación'))
max_length=64, blank=False, verbose_name=_("ciudad de facturación") billing_zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal de facturación'))
)
billing_state = models.CharField(
max_length=64, blank=False, verbose_name=_("región de facturación")
)
billing_country = models.CharField(
max_length=64, blank=False, verbose_name=_("país de facturación")
)
billing_zip = models.CharField(
max_length=32, blank=False, verbose_name=_("código postal de facturación")
)
# Datos de envío # Datos de envío
shipping_address = models.CharField( shipping_address = models.CharField(max_length=255, blank=False, verbose_name=_('dirección'))
max_length=255, blank=False, verbose_name=_("dirección") shipping_city = models.CharField(max_length=64, blank=False, verbose_name=_('ciudad'))
) shipping_state = models.CharField(max_length=64, blank=False, verbose_name=_('región'))
shipping_city = models.CharField( shipping_country = models.CharField(max_length=64, blank=False, verbose_name=_('país'))
max_length=64, blank=False, verbose_name=_("ciudad") shipping_zip = models.CharField(max_length=32, blank=False, verbose_name=_('código postal'))
) contact_phone = models.CharField(max_length=32, blank=True, verbose_name=_('teléfono de contacto'))
shipping_state = models.CharField(
max_length=64, blank=False, verbose_name=_("región")
)
shipping_country = models.CharField(
max_length=64, blank=False, verbose_name=_("país")
)
shipping_zip = models.CharField(
max_length=32, blank=False, verbose_name=_("código postal")
)
contact_phone = models.CharField(
max_length=32, blank=True, verbose_name=_("teléfono de contacto")
)
amount_paid = models.DecimalField( amount_paid = models.DecimalField(
default=Decimal("0.00"), default=Decimal('0.00'), max_digits=11, decimal_places=2, verbose_name=_('cantidad pagada')
max_digits=11,
decimal_places=2,
verbose_name=_("cantidad pagada"),
) )
shipping_method = models.ForeignKey( shipping_method = models.ForeignKey(
"shop.ShippingMethod", 'shop.ShippingMethod', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('método de envío')
blank=True,
null=True,
on_delete=models.SET_NULL,
verbose_name=_("método de envío"),
) )
from_cart = models.ForeignKey( from_cart = models.ForeignKey(
"shop.Cart", 'shop.Cart', on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('carrito de origen de pedido')
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("carrito de origen de pedido"),
) )
def calculate_total_from_lines(self): def calculate_total_from_lines(self):
self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get( self.base_total = self.lines.aggregate(base_total=models.Sum('base_total')).get('base_total')
"base_total" self.total = self.lines.aggregate(base_total=models.Sum('total')).get('base_total')
)
self.total = self.lines.aggregate(base_total=models.Sum("total")).get(
"base_total"
)
self.save() self.save()
class Meta: class Meta:
verbose_name = _("pedido") verbose_name = _('pedido')
verbose_name_plural = _("pedidos") verbose_name_plural = _('pedidos')
ordering = ("creation_date",) ordering = ('creation_date',)
class Cart(models.Model): class Cart(models.Model):
uuid = models.UUIDField(default=uuid4, primary_key=True, verbose_name=_("uuid")) uuid = models.UUIDField(default=uuid4, primary_key=True, verbose_name=_('uuid'))
user = models.ForeignKey( user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('usuario'), blank=True, null=True)
User, creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('fecha de creación'))
on_delete=models.CASCADE,
verbose_name=_("usuario"),
blank=True,
null=True,
)
creation_date = models.DateTimeField(
auto_now_add=True, verbose_name=_("fecha de creación")
)
class Meta: class Meta:
verbose_name = _("carrito") verbose_name = _('carrito')
verbose_name_plural = _("carritos") verbose_name_plural = _('carritos')
class CartItem(models.Model): class CartItem(models.Model):
cart = models.ForeignKey( cart = models.ForeignKey('shop.Cart', on_delete=models.CASCADE, related_name='items', verbose_name=_('carrito'))
"shop.Cart", product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'))
on_delete=models.CASCADE, quantity = models.PositiveIntegerField(default=1, verbose_name=_('cantidad'))
related_name="items",
verbose_name=_("carrito"),
)
product = models.ForeignKey(
"shop.Product", on_delete=models.CASCADE, verbose_name=_("producto")
)
quantity = models.PositiveIntegerField(default=1, verbose_name=_("cantidad"))
class Meta: class Meta:
verbose_name = _("línea de carrito") verbose_name = _('línea de carrito')
verbose_name_plural = _("líneas de carrito") verbose_name_plural = _('líneas de carrito')
class CustomerAddress(models.Model): class CustomerAddress(models.Model):
class Types(models.TextChoices): class Types(models.TextChoices):
BILLING = "BILL", _("facturación") BILLING = 'BILL', _('facturación')
SHIPPING = "SHIP", _("envío") SHIPPING = 'SHIP', _('envío')
user = models.ForeignKey( user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_('usuario'))
User, on_delete=models.CASCADE, null=True, verbose_name=_("usuario")
)
vat_id = models.CharField( vat_id = models.CharField(
max_length=16, max_length=16, blank=True, null=True, default='', verbose_name=_('número de identificación fiscal')
blank=True,
null=True,
default="",
verbose_name=_("número de identificación fiscal"),
)
full_name = models.CharField(
max_length=128, blank=False, null=False, verbose_name=_("dirección")
)
address = models.CharField(
max_length=128, blank=False, null=False, verbose_name=_("dirección")
)
address_town = models.CharField(
max_length=64, blank=False, null=False, verbose_name=_("localidad")
)
address_zip = models.CharField(
max_length=16, blank=False, null=False, verbose_name=_("código postal")
)
address_state = models.CharField(
max_length=64, blank=False, null=False, verbose_name=_("provincia")
) )
full_name = models.CharField(max_length=128, blank=False, null=False, verbose_name=_('dirección'))
address = models.CharField(max_length=128, blank=False, null=False, verbose_name=_('dirección'))
address_town = models.CharField(max_length=64, blank=False, null=False, verbose_name=_('localidad'))
address_zip = models.CharField(max_length=16, blank=False, null=False, verbose_name=_('código postal'))
address_state = models.CharField(max_length=64, blank=False, null=False, verbose_name=_('provincia'))
address_country = models.CharField( address_country = models.CharField(
max_length=64, max_length=64, default=settings.SHOP_COUNTRY, blank=False, null=False, verbose_name=_('país')
default=settings.SHOP_COUNTRY,
blank=False,
null=False,
verbose_name=_("país"),
)
address_phone = models.CharField(
max_length=16, blank=True, null=True, default="", verbose_name=_("teléfono")
) )
address_phone = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=_('teléfono'))
address_type = models.CharField( address_type = models.CharField(
max_length=4, max_length=4, choices=Types.choices, default=Types.SHIPPING, verbose_name=_('tipo de dirección')
choices=Types.choices,
default=Types.SHIPPING,
verbose_name=_("tipo de dirección"),
) )
email = models.EmailField(blank=False, null=False, verbose_name=_("e-mail")) email = models.EmailField(blank=False, null=False, verbose_name=_('e-mail'))
default = models.BooleanField(default=True, verbose_name=_("por defecto")) default = models.BooleanField(default=True, verbose_name=_('por defecto'))
hidden = models.BooleanField(default=False, db_index=True, verbose_name=_("oculto")) hidden = models.BooleanField(default=False, db_index=True, verbose_name=_('oculto'))
def __str__(self): def __str__(self):
return f"{self.full_name} - {self.address}, {self.address_zip}, {self.address_state} - {self.address_country}" return f'{self.full_name} - {self.address}, {self.address_zip}, {self.address_state} - {self.address_country}'
class Meta: class Meta:
verbose_name = _("dirección de cliente") verbose_name = _('dirección de cliente')
verbose_name_plural = _("direcciones de cliente") verbose_name_plural = _('direcciones de cliente')
class ShippingMethod(models.Model): class ShippingMethod(models.Model):
name = models.CharField(max_length=64, verbose_name=_("nombre")) name = models.CharField(max_length=64, verbose_name=_('nombre'))
description = models.CharField( description = models.CharField(max_length=256, default='', blank=True, verbose_name=_('description'))
max_length=256, default="", blank=True, verbose_name=_("description") url = models.URLField(default='', blank=True, verbose_name=_('url'))
) shipping_product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto de envío'))
url = models.URLField(default="", blank=True, verbose_name=_("url")) enabled = models.BooleanField(default=True, verbose_name=_('habilitado'))
shipping_product = models.ForeignKey(
"shop.Product", on_delete=models.CASCADE, verbose_name=_("producto de envío")
)
enabled = models.BooleanField(default=True, verbose_name=_("habilitado"))
class Meta: class Meta:
verbose_name = _("método de envío") verbose_name = _('método de envío')
verbose_name_plural = _("métodos de envío") verbose_name_plural = _('métodos de envío')
class WishlistedProduct(TimestampedModel): class WishlistedProduct(TimestampedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("usuario")) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('usuario'))
product = models.ForeignKey( product = models.ForeignKey('shop.Product', on_delete=models.CASCADE, verbose_name=_('producto'))
"shop.Product", on_delete=models.CASCADE, verbose_name=_("producto")
)
class Meta: class Meta:
verbose_name = _("productos deseados") verbose_name = _('productos deseados')
verbose_name_plural = _("productos deseados") verbose_name_plural = _('productos deseados')
unique_together = ( unique_together = ('user', 'product')
"user",
"product",
)
class Payment(models.Model): class Payment(models.Model):
class MethodChoices(models.TextChoices): class MethodChoices(models.TextChoices):
REDSYS = "REDSYS" REDSYS = 'REDSYS'
hash = models.UUIDField(verbose_name=_("Hash"), default=uuid4, primary_key=True) hash = models.UUIDField(verbose_name=_('Hash'), default=uuid4, primary_key=True)
amount = models.DecimalField( amount = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'), verbose_name=_('Cantidad'))
max_digits=8,
decimal_places=2,
default=Decimal("0"),
verbose_name=_("Cantidad"),
)
order = models.ForeignKey( order = models.ForeignKey(
"shop.Order", 'shop.Order', on_delete=models.PROTECT, verbose_name=_('pedido'), related_name='payments'
on_delete=models.PROTECT,
verbose_name=_("pedido"),
related_name="payments",
)
user = models.ForeignKey(
User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_("cliente")
) )
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_('cliente'))
creation_date = models.DateTimeField( creation_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Fecha de creación'))
auto_now_add=True, verbose_name=_("Fecha de creación")
)
metadata = models.JSONField(default=dict, verbose_name=_("Metadata")) metadata = models.JSONField(default=dict, verbose_name=_('Metadata'))
method = models.CharField( method = models.CharField(
max_length=6, max_length=6, choices=MethodChoices.choices, default=MethodChoices.REDSYS, verbose_name=_('método de pago')
choices=MethodChoices.choices,
default=MethodChoices.REDSYS,
verbose_name=_("método de pago"),
) )
class Meta: class Meta:
verbose_name = _("Pago") verbose_name = _('Pago')
verbose_name_plural = _("Pagos") verbose_name_plural = _('Pagos')
class ShopSettings(SingletonModel): class ShopSettings(SingletonModel):
debug = models.BooleanField(default=True, verbose_name=_("Modo depuración")) debug = models.BooleanField(default=True, verbose_name=_('Modo depuración'))
merchant_code = models.CharField( merchant_code = models.CharField(
max_length=9, max_length=9, blank=False, null=False, verbose_name=_('Identificación de comercio')
blank=False,
null=False,
verbose_name=_("Identificación de comercio"),
) )
currency_code = models.CharField( currency_code = models.CharField(
max_length=4, max_length=4, default='978', choices=CURRENCY_CODES, verbose_name=_('Código de moneda')
default="978",
choices=CURRENCY_CODES,
verbose_name=_("Código de moneda"),
) # 978 == EURO ) # 978 == EURO
terminal = models.CharField(max_length=8, verbose_name=_("Terminal")) terminal = models.CharField(max_length=8, verbose_name=_('Terminal'))
shared_secret = models.CharField(max_length=100, verbose_name=_("Clave de Redsys")) shared_secret = models.CharField(max_length=100, verbose_name=_('Clave de Redsys'))
tpv_domain = models.URLField( tpv_domain = models.URLField(max_length=128, blank=True, null=True, verbose_name=_('dominio del tpv'))
max_length=128, blank=True, null=True, verbose_name=_("dominio del tpv")
)
class Meta: class Meta:
verbose_name = _("ajustes de la tienda") verbose_name = _('ajustes de la tienda')
verbose_name_plural = _("ajustes de la tienda") verbose_name_plural = _('ajustes de la tienda')
+49 -93
View File
@@ -13,13 +13,11 @@ from shop.utils import compute_signature, decode_b64_dict
class RedsysClient: class RedsysClient:
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago" DEBUG_ENVIRONMENT_URL = 'https://sis-t.redsys.es:25443/sis/realizarPago'
PROD_ENVIRONMENT_URL = "https://sis.redsys.es/sis/realizarPago" PROD_ENVIRONMENT_URL = 'https://sis.redsys.es/sis/realizarPago'
REST_DEBUG_ENVIRONMENT_URL = ( REST_DEBUG_ENVIRONMENT_URL = 'https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST'
"https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST" REST_PROD_ENVIRONMENT_URL = 'https://sis.redsys.es/sis/rest/trataPeticionREST'
)
REST_PROD_ENVIRONMENT_URL = "https://sis.redsys.es/sis/rest/trataPeticionREST"
def __init__(self): def __init__(self):
self.settings = ShopSettings.load() self.settings = ShopSettings.load()
@@ -46,19 +44,19 @@ class RedsysClient:
return self.settings.currency_code return self.settings.currency_code
def get_merchant_url_ok_for_order(self, order: Order) -> str: def get_merchant_url_ok_for_order(self, order: Order) -> str:
path = reverse("web:order", kwargs={"uuid": order.uuid}) path = reverse('web:order', kwargs={'uuid': order.uuid})
return f"{self.settings.tpv_domain}{path}" return f'{self.settings.tpv_domain}{path}'
def to_integer(self, value: Decimal): def to_integer(self, value: Decimal):
return int(value * 100) return int(value * 100)
def get_merchant_url_ko_for_order(self, order: Order) -> str: def get_merchant_url_ko_for_order(self, order: Order) -> str:
path = reverse("web:order", kwargs={"uuid": order.uuid}) path = reverse('web:order', kwargs={'uuid': order.uuid})
return f"{self.settings.tpv_domain}{path}" return f'{self.settings.tpv_domain}{path}'
def get_webhook_url_for_order(self, order: Order) -> str: def get_webhook_url_for_order(self, order: Order) -> str:
path = reverse("shop:webhook", kwargs={"uuid": order.uuid}) path = reverse('shop:webhook', kwargs={'uuid': order.uuid})
return f"{self.settings.tpv_domain}{path}" return f'{self.settings.tpv_domain}{path}'
def get_shared_secret(self) -> str: def get_shared_secret(self) -> str:
return self.settings.shared_secret return self.settings.shared_secret
@@ -68,131 +66,89 @@ class RedsysClient:
return compute_signature(str(hash), payload, key).decode() return compute_signature(str(hash), payload, key).decode()
def _get_merchant_parameters_for_order( def _get_merchant_parameters_for_order(
self, self, order: Order, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict: ) -> dict:
merchant_code = self.get_merchant_code() merchant_code = self.get_merchant_code()
return { return {
"DS_MERCHANT_AMOUNT": str(self.to_integer(order.total)), 'DS_MERCHANT_AMOUNT': str(self.to_integer(order.total)),
"DS_MERCHANT_CURRENCY": self.get_currency_code(), 'DS_MERCHANT_CURRENCY': self.get_currency_code(),
"DS_MERCHANT_MERCHANTCODE": merchant_code, 'DS_MERCHANT_MERCHANTCODE': merchant_code,
# Webhook # Webhook
"DS_MERCHANT_MERCHANTURL": self.get_webhook_url_for_order(order), 'DS_MERCHANT_MERCHANTURL': self.get_webhook_url_for_order(order),
"DS_MERCHANT_ORDER": order.code, 'DS_MERCHANT_ORDER': order.code,
"DS_MERCHANT_TERMINAL": self.get_terminal(), 'DS_MERCHANT_TERMINAL': self.get_terminal(),
"DS_MERCHANT_TRANSACTIONTYPE": order_type, 'DS_MERCHANT_TRANSACTIONTYPE': order_type,
# Página informativa al usuario - Pago erróneo # Página informativa al usuario - Pago erróneo
"DS_MERCHANT_URLKO": self.get_merchant_url_ko_for_order(order), 'DS_MERCHANT_URLKO': self.get_merchant_url_ko_for_order(order),
# Página informativa al usuario - Pago confirmado # Página informativa al usuario - Pago confirmado
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_order(order), 'DS_MERCHANT_URLOK': self.get_merchant_url_ok_for_order(order),
} }
def _get_encoded_merchant_parameters_for_order( def _get_encoded_merchant_parameters_for_order(
self, self, order: Order, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> str: ) -> str:
body = self._get_merchant_parameters_for_order(order, order_type) body = self._get_merchant_parameters_for_order(order, order_type)
return self._encode_body(body) return self._encode_body(body)
def get_body_for_order( def get_body_for_order(self, order: Order, order_type: int = TransactionTypes.AUTHORIZATION) -> dict:
self, merchant_parameters = self._get_encoded_merchant_parameters_for_order(order, order_type)
order: Order,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
merchant_parameters = self._get_encoded_merchant_parameters_for_order(
order, order_type
)
signature = self.get_signature(order.code, merchant_parameters) signature = self.get_signature(order.code, merchant_parameters)
return { return {
"Ds_MerchantParameters": merchant_parameters, 'Ds_MerchantParameters': merchant_parameters,
"Ds_SignatureVersion": "HMAC_SHA256_V1", 'Ds_SignatureVersion': 'HMAC_SHA256_V1',
"Ds_Signature": signature, 'Ds_Signature': signature,
} }
def _encode_body(self, value): def _encode_body(self, value):
stringified_body = json.dumps(value) stringified_body = json.dumps(value)
return base64.b64encode(stringified_body.encode()).decode("utf-8") return base64.b64encode(stringified_body.encode()).decode('utf-8')
def _get_rest_merchant_parameters_for_order( def _get_rest_merchant_parameters_for_order(
self, self, order: Order, pan: str, expiry_date: str, cvv2: str, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict: ) -> dict:
merchant_code = self.get_merchant_code() merchant_code = self.get_merchant_code()
return { return {
"DS_MERCHANT_AMOUNT": str(self.to_integer(order.total)), 'DS_MERCHANT_AMOUNT': str(self.to_integer(order.total)),
"DS_MERCHANT_CURRENCY": self.get_currency_code(), 'DS_MERCHANT_CURRENCY': self.get_currency_code(),
"DS_MERCHANT_CVV2": cvv2, 'DS_MERCHANT_CVV2': cvv2,
"DS_MERCHANT_EXPIRYDATE": expiry_date, 'DS_MERCHANT_EXPIRYDATE': expiry_date,
"DS_MERCHANT_MERCHANTCODE": merchant_code, 'DS_MERCHANT_MERCHANTCODE': merchant_code,
"DS_MERCHANT_ORDER": order.uuid.hex, 'DS_MERCHANT_ORDER': order.uuid.hex,
"DS_MERCHANT_PAN": pan, 'DS_MERCHANT_PAN': pan,
"DS_MERCHANT_TERMINAL": "1", 'DS_MERCHANT_TERMINAL': '1',
"DS_MERCHANT_TRANSACTIONTYPE": order_type, 'DS_MERCHANT_TRANSACTIONTYPE': order_type,
} }
def _get_rest_encoded_merchant_parameters_for_order( def _get_rest_encoded_merchant_parameters_for_order(
self, self, order: Order, pan: str, expiry_date: str, cvv2: str, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> str: ) -> str:
body = self._get_rest_merchant_parameters_for_order( body = self._get_rest_merchant_parameters_for_order(
order, order, pan=pan, expiry_date=expiry_date, cvv2=cvv2, order_type=order_type
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
order_type=order_type,
) )
return self._encode_body(body) return self._encode_body(body)
def _get_rest_body_for_order( def _get_rest_body_for_order(
self, self, order: Order, pan: str, expiry_date: str, cvv2: str, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
order_type: int = TransactionTypes.AUTHORIZATION,
) -> dict: ) -> dict:
merchant_parameters = self._get_rest_encoded_merchant_parameters_for_order( merchant_parameters = self._get_rest_encoded_merchant_parameters_for_order(
order=order, order=order, pan=pan, expiry_date=expiry_date, cvv2=cvv2, order_type=order_type
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
order_type=order_type,
) )
signature = self.get_signature(order.code, merchant_parameters) signature = self.get_signature(order.code, merchant_parameters)
return { return {
"Ds_MerchantParameters": merchant_parameters, 'Ds_MerchantParameters': merchant_parameters,
"Ds_SignatureVersion": "HMAC_SHA256_V1", 'Ds_SignatureVersion': 'HMAC_SHA256_V1',
"Ds_Signature": signature, 'Ds_Signature': signature,
} }
def make_request_for_order( def make_request_for_order(
self, self, order: Order, pan: str, expiry_date: str, cvv2: str, order_type: int = TransactionTypes.AUTHORIZATION
order: Order,
pan: str,
expiry_date: str,
cvv2: str,
order_type: int = TransactionTypes.AUTHORIZATION,
): ):
body = self._get_rest_body_for_order( body = self._get_rest_body_for_order(
order=order, order=order, pan=pan, expiry_date=expiry_date, cvv2=cvv2, order_type=order_type
pan=pan,
expiry_date=expiry_date,
cvv2=cvv2,
order_type=order_type,
) )
url = self.get_rest_target_url() url = self.get_rest_target_url()
response = requests.post(url, body) response = requests.post(url, body)
@@ -201,14 +157,14 @@ class RedsysClient:
def pay_order_rest(self, order: Order, pan: str, expiry_date: str, cvv2: str): def pay_order_rest(self, order: Order, pan: str, expiry_date: str, cvv2: str):
response = self.make_request_for_order(order, pan, expiry_date, cvv2) response = self.make_request_for_order(order, pan, expiry_date, cvv2)
data = response.json() data = response.json()
error_code: str = data.get("errorCode", "") error_code: str = data.get('errorCode', '')
if error_code: if error_code:
error_code.replace("SIS0", "0") error_code.replace('SIS0', '0')
raise RedsysPaymentException(error_code) raise RedsysPaymentException(error_code)
merchant_parameters = data.get("Ds_MerchantParameters") merchant_parameters = data.get('Ds_MerchantParameters')
parameters = decode_b64_dict(merchant_parameters) parameters = decode_b64_dict(merchant_parameters)
return parameters return parameters
+242 -246
View File
@@ -20,253 +20,249 @@ class TransactionTypes:
CURRENCY_CODES = { CURRENCY_CODES = {
"8": "LEK ALL", '8': 'LEK ALL',
"12": "ALGERIAN DINAR DZD", '12': 'ALGERIAN DINAR DZD',
"24": "ANGOLA KWANZA AOK", '24': 'ANGOLA KWANZA AOK',
"30": "PROBANDO DESA MON", '30': 'PROBANDO DESA MON',
"31": "AZERBAIJANIAN MANAT AZM", '31': 'AZERBAIJANIAN MANAT AZM',
"32": "ARGENTINE AUSTRAL ARP", '32': 'ARGENTINE AUSTRAL ARP',
"36": "AUSTRALIAN DOLLAR AUD", '36': 'AUSTRALIAN DOLLAR AUD',
"44": "BAHAMIAN DOLLAR BSD", '44': 'BAHAMIAN DOLLAR BSD',
"48": "BAHRAINI DINAR BHD", '48': 'BAHRAINI DINAR BHD',
"50": "TAKA BDT", '50': 'TAKA BDT',
"51": "ARMENIAN DRAM AMD", '51': 'ARMENIAN DRAM AMD',
"52": "BARBADOS DOLLAR BBD", '52': 'BARBADOS DOLLAR BBD',
"60": "BERMUDAN DOLLAR BMD", '60': 'BERMUDAN DOLLAR BMD',
"64": "NGULTRUM BTN", '64': 'NGULTRUM BTN',
"68": "BOLIVIAN PESO BOP", '68': 'BOLIVIAN PESO BOP',
"70": "DINAR BAD", '70': 'DINAR BAD',
"72": "PULA BWP", '72': 'PULA BWP',
"76": "CRUZEIRO BRC", '76': 'CRUZEIRO BRC',
"84": "BELIZE DOLLAR 084", '84': 'BELIZE DOLLAR 084',
"90": "SOLOMON ISLANDS DOLL SBD", '90': 'SOLOMON ISLANDS DOLL SBD',
"96": "BRUNEI DOLLAR BND", '96': 'BRUNEI DOLLAR BND',
"100": "LEV BGL", '100': 'LEV BGL',
"104": "KYAT BUK", '104': 'KYAT BUK',
"108": "BURUNDI FRANC BIF", '108': 'BURUNDI FRANC BIF',
"112": "BELARUSSIAN RUBLE BYB", '112': 'BELARUSSIAN RUBLE BYB',
"116": "RIEL KHR", '116': 'RIEL KHR',
"124": "CANADIAN DOLLAR CAD", '124': 'CANADIAN DOLLAR CAD',
"132": "CAPE VERDE ESCUDO CVE", '132': 'CAPE VERDE ESCUDO CVE',
"136": "CAYMAN ISLANDS DOLLA KYD", '136': 'CAYMAN ISLANDS DOLLA KYD',
"144": "SRI LANKA RUPEE LKR", '144': 'SRI LANKA RUPEE LKR',
"152": "CHILEAN PESO CLP", '152': 'CHILEAN PESO CLP',
"156": "YUAN RENMINBI CNY", '156': 'YUAN RENMINBI CNY',
"157": "CHINESE RENMIMBI CNH", '157': 'CHINESE RENMIMBI CNH',
"158": "CHINESE RENMINBI CNX", '158': 'CHINESE RENMINBI CNX',
"170": "COLOMBIAN PESO COP", '170': 'COLOMBIAN PESO COP',
"174": "COMOROS FRANC KMF", '174': 'COMOROS FRANC KMF',
"180": "ZAIRE ZRZ", '180': 'ZAIRE ZRZ',
"188": "COSTA RICA COLON CRC", '188': 'COSTA RICA COLON CRC',
"191": "CROATIAN KUNA HRK", '191': 'CROATIAN KUNA HRK',
"192": "CUBAN PESO CUP", '192': 'CUBAN PESO CUP',
"196": "CYPRUS POUND CYP", '196': 'CYPRUS POUND CYP',
"200": "KORUNA CSK", '200': 'KORUNA CSK',
"203": "CZECH KORUNA CZK", '203': 'CZECH KORUNA CZK',
"208": "DANISH KRONE DKK", '208': 'DANISH KRONE DKK',
"214": "DOMINICAN PESO DOP", '214': 'DOMINICAN PESO DOP',
"218": "SUCRE ECS", '218': 'SUCRE ECS',
"222": "EL SALVADOR COLON SVC", '222': 'EL SALVADOR COLON SVC',
"226": "EKWELE GQE", '226': 'EKWELE GQE',
"230": "ETHIOPIAN BIRR ETB", '230': 'ETHIOPIAN BIRR ETB',
"232": "ERITREAN NAKTAN ERN", '232': 'ERITREAN NAKTAN ERN',
"233": "ESTONIAN KROON EEK", '233': 'ESTONIAN KROON EEK',
"238": "FALKLAND ISLANDS FKP", '238': 'FALKLAND ISLANDS FKP',
"242": "FIJI DOLLAR FJD", '242': 'FIJI DOLLAR FJD',
"262": "DJIBOUTI FRANC DJF", '262': 'DJIBOUTI FRANC DJF',
"268": "GEORGIAN LARI GEL", '268': 'GEORGIAN LARI GEL',
"270": "DALASI GMD", '270': 'DALASI GMD',
"278": "MARK DER DDR DDM", '278': 'MARK DER DDR DDM',
"288": "GHANA CEDI GHC", '288': 'GHANA CEDI GHC',
"292": "GIBRALTAR POUND GIP", '292': 'GIBRALTAR POUND GIP',
"320": "QUETZAL GTQ", '320': 'QUETZAL GTQ',
"324": "SYLI GNS", '324': 'SYLI GNS',
"328": "GUYANA DOLLAR GYD", '328': 'GUYANA DOLLAR GYD',
"332": "GOURDE HTG", '332': 'GOURDE HTG',
"340": "LEMPIRA HNL", '340': 'LEMPIRA HNL',
"344": "HONG KONG DOLLAR HKD", '344': 'HONG KONG DOLLAR HKD',
"348": "FORINT HUF", '348': 'FORINT HUF',
"352": "ICELAND KRONA ISK", '352': 'ICELAND KRONA ISK',
"356": "INDIAN RUPEE INR", '356': 'INDIAN RUPEE INR',
"360": "RUPIAH IDR", '360': 'RUPIAH IDR',
"364": "IRANIAL RIAL IRR", '364': 'IRANIAL RIAL IRR',
"365": "IRANIAN AIRLINE RATE IRA", '365': 'IRANIAN AIRLINE RATE IRA',
"368": "IRAQI DINAR IQD", '368': 'IRAQI DINAR IQD',
"376": "ISRAEL SHEKEL ILS", '376': 'ISRAEL SHEKEL ILS',
"388": "JAMAICAN DOLLAR JMD", '388': 'JAMAICAN DOLLAR JMD',
"392": "YEN JPY", '392': 'YEN JPY',
"398": "TENGE KZT", '398': 'TENGE KZT',
"400": "JORDANIAN DINAR JOD", '400': 'JORDANIAN DINAR JOD',
"404": "KENYAN SHILLING KES", '404': 'KENYAN SHILLING KES',
"408": "NORTH KOREAN WON KPW", '408': 'NORTH KOREAN WON KPW',
"410": "KOREAN WON KRW", '410': 'KOREAN WON KRW',
"414": "KUWAITI DINAR KWD", '414': 'KUWAITI DINAR KWD',
"417": "KYRGYZSTAN SON KGS", '417': 'KYRGYZSTAN SON KGS',
"418": "KIP LAK", '418': 'KIP LAK',
"422": "LEBANESE POUND LBP", '422': 'LEBANESE POUND LBP',
"426": "LESOTHO LOTI LSM", '426': 'LESOTHO LOTI LSM',
"428": "LATVIAN LAT LVL", '428': 'LATVIAN LAT LVL',
"430": "LIBERIAN DOLLAR LRD", '430': 'LIBERIAN DOLLAR LRD',
"434": "LIBYAN DINAR LYD", '434': 'LIBYAN DINAR LYD',
"440": "LITHUANIAN LITAS LTL", '440': 'LITHUANIAN LITAS LTL',
"446": "PATACA MOP", '446': 'PATACA MOP',
"450": "MALAGASY FRANC MGF", '450': 'MALAGASY FRANC MGF',
"454": "MALAWI KWACHA MWK", '454': 'MALAWI KWACHA MWK',
"458": "MALASYAN RINGGIT MYR", '458': 'MALASYAN RINGGIT MYR',
"462": "MALDIVE RUPEE MVR", '462': 'MALDIVE RUPEE MVR',
"466": "MALI MLF", '466': 'MALI MLF',
"470": "MALTESE LIRA MTL", '470': 'MALTESE LIRA MTL',
"478": "OUGUIYA MRO", '478': 'OUGUIYA MRO',
"480": "MAURITIUS RUPEE MUR", '480': 'MAURITIUS RUPEE MUR',
"484": "MEXICAN PESO MXP", '484': 'MEXICAN PESO MXP',
"496": "TUGRIK MNT", '496': 'TUGRIK MNT',
"498": "MOLDOVIAN LEU MDL", '498': 'MOLDOVIAN LEU MDL',
"504": "MORROCAN DIRHAM MAD", '504': 'MORROCAN DIRHAM MAD',
"508": "METICAL MZM", '508': 'METICAL MZM',
"512": "RIAL OMANI OMR", '512': 'RIAL OMANI OMR',
"516": "NAMIBIAN DOLLAR NAD", '516': 'NAMIBIAN DOLLAR NAD',
"524": "NEPALESE RUPEE NPR", '524': 'NEPALESE RUPEE NPR',
"532": "NETHERLANDS ANTILLIA ANG", '532': 'NETHERLANDS ANTILLIA ANG',
"533": "ARUBA AWG", '533': 'ARUBA AWG',
"536": "YUGOSLAVIAN NEW DIAN NTZ", '536': 'YUGOSLAVIAN NEW DIAN NTZ',
"548": "VANUATU VATU VUV", '548': 'VANUATU VATU VUV',
"554": "NEW ZEALAND DOLLAR NZD", '554': 'NEW ZEALAND DOLLAR NZD',
"556": "NAIRA 566", '556': 'NAIRA 566',
"558": "CORDOBA NIC", '558': 'CORDOBA NIC',
"566": "NAIRA NGN", '566': 'NAIRA NGN',
"578": "NORWEGIAN KRONE NOK", '578': 'NORWEGIAN KRONE NOK',
"582": "PACIFIC ISLAND PCI", '582': 'PACIFIC ISLAND PCI',
"586": "PAKISTAN RUPEE PKR", '586': 'PAKISTAN RUPEE PKR',
"590": "BALBOA PAB", '590': 'BALBOA PAB',
"598": "KINA PGK", '598': 'KINA PGK',
"600": "GUARANI PYG", '600': 'GUARANI PYG',
"604": "PERU INTI PEI", '604': 'PERU INTI PEI',
"608": "PHILIPPINE PESO PHP", '608': 'PHILIPPINE PESO PHP',
"616": "ZLOTY PLZ", '616': 'ZLOTY PLZ',
"624": "GUINEA", '624': 'GUINEA',
"626": "TIMOR ESCUDO TPE", '626': 'TIMOR ESCUDO TPE',
"634": "QATARI RIAL QAR", '634': 'QATARI RIAL QAR',
"642": "LEU ROL", '642': 'LEU ROL',
"643": "RUSSIAN ROUBLE RUB", '643': 'RUSSIAN ROUBLE RUB',
"646": "RWANDA FRANC RWF", '646': 'RWANDA FRANC RWF',
"654": "ST.HELENA POUND SHP", '654': 'ST.HELENA POUND SHP',
"678": "DOBRA STD", '678': 'DOBRA STD',
"682": "SAUDI RIYAL SAR", '682': 'SAUDI RIYAL SAR',
"690": "SEYCHELLES RUPEE SCR", '690': 'SEYCHELLES RUPEE SCR',
"694": "LEONE SLL", '694': 'LEONE SLL',
"702": "SINGAPORE DOLLAR SGD", '702': 'SINGAPORE DOLLAR SGD',
"703": "SLOVAK KORUNA SKK", '703': 'SLOVAK KORUNA SKK',
"704": "DONG VND", '704': 'DONG VND',
"705": "SLOVENIAN TOLAR SIT", '705': 'SLOVENIAN TOLAR SIT',
"706": "SOMALI SHILLING SOS", '706': 'SOMALI SHILLING SOS',
"710": "RAND ZAR", '710': 'RAND ZAR',
"716": "ZIMBABWE DOLLAR ZWD", '716': 'ZIMBABWE DOLLAR ZWD',
"720": "YEMENI DINAR YDD", '720': 'YEMENI DINAR YDD',
"728": "SOUTH SUDANESE POUND SSP", '728': 'SOUTH SUDANESE POUND SSP',
"736": "SUDANESE POUND SDP", '736': 'SUDANESE POUND SDP',
"737": "SUDAN AIRLINES SDA", '737': 'SUDAN AIRLINES SDA',
"740": "SURINAM GUILDER SRG", '740': 'SURINAM GUILDER SRG',
"748": "LILANGENI SZL", '748': 'LILANGENI SZL',
"752": "SWEDISH KRONA SEK", '752': 'SWEDISH KRONA SEK',
"756": "SWISS FRANC CHF", '756': 'SWISS FRANC CHF',
"760": "SYRIAN POUND SYP", '760': 'SYRIAN POUND SYP',
"762": "TAJIK RUBLE TJR", '762': 'TAJIK RUBLE TJR',
"764": "BAHT THB", '764': 'BAHT THB',
"776": "PA'ANGA TOP", '776': "PA'ANGA TOP",
"780": "TRINIDAD Y TOBAGO DO TTD", '780': 'TRINIDAD Y TOBAGO DO TTD',
"784": "UAE DIRHAM AED", '784': 'UAE DIRHAM AED',
"788": "TUNISIAN DINAR TND", '788': 'TUNISIAN DINAR TND',
"792": "TURKISH LIRA TRL", '792': 'TURKISH LIRA TRL',
"793": "PSEUDO TURKISH LIRA PTL", '793': 'PSEUDO TURKISH LIRA PTL',
"795": "MANAT TMM", '795': 'MANAT TMM',
"800": "UGANDA SHILLING UGS", '800': 'UGANDA SHILLING UGS',
"804": "KARBOVANET UAK", '804': 'KARBOVANET UAK',
"807": "MACEDONIAN DENAR MKD", '807': 'MACEDONIAN DENAR MKD',
"810": "RUSSIAN ROUBLE RUR", '810': 'RUSSIAN ROUBLE RUR',
"818": "EGYPTIAN POUND EGP", '818': 'EGYPTIAN POUND EGP',
"826": "POUND STERLING GBP", '826': 'POUND STERLING GBP',
"834": "TANZANIAN SHILLING TZS", '834': 'TANZANIAN SHILLING TZS',
"840": "DOLAR U.S.A. USD", '840': 'DOLAR U.S.A. USD',
"858": "URUGUAYAN PESO UYP", '858': 'URUGUAYAN PESO UYP',
"860": "UZBEKISTAN SUM UZS", '860': 'UZBEKISTAN SUM UZS',
"862": "BOLIVAR VEB", '862': 'BOLIVAR VEB',
"882": "TALA WST", '882': 'TALA WST',
"886": "YEMINI RIAL YER", '886': 'YEMINI RIAL YER',
"890": "NEW YUGOSLAVIAN DOLL YUD", '890': 'NEW YUGOSLAVIAN DOLL YUD',
"891": "NEW DINAR YUG", '891': 'NEW DINAR YUG',
"894": "KWACHA ZMK", '894': 'KWACHA ZMK',
"901": "NEW TAIWAN DOLLAR TWD", '901': 'NEW TAIWAN DOLLAR TWD',
"934": "NEW MANAT TMT", '934': 'NEW MANAT TMT',
"936": "GHANA CEDI GHS", '936': 'GHANA CEDI GHS',
"941": "DINAR SERBIO RSD", '941': 'DINAR SERBIO RSD',
"943": "MOZAMBIQUE METICAL MZN", '943': 'MOZAMBIQUE METICAL MZN',
"944": "AZERBAIJANIAN MANAT AZN", '944': 'AZERBAIJANIAN MANAT AZN',
"946": "NEW LEU RON", '946': 'NEW LEU RON',
"949": "TURKISH LIRA TRY", '949': 'TURKISH LIRA TRY',
"950": "CFA FRANC BEAC XAF", '950': 'CFA FRANC BEAC XAF',
"951": "EAST CARIBBEAN DOLLA XCD", '951': 'EAST CARIBBEAN DOLLA XCD',
"952": "CFA FRANC BCEAO XOF", '952': 'CFA FRANC BCEAO XOF',
"953": "CFP FRANC XPF", '953': 'CFP FRANC XPF',
"954": "E.C.U. EUROPEAN CUR XEU", '954': 'E.C.U. EUROPEAN CUR XEU',
"967": "KWACHA ZMW", '967': 'KWACHA ZMW',
"968": "SURINAME DOLLAR SRD", '968': 'SURINAME DOLLAR SRD',
"969": "ARIARY MGA", '969': 'ARIARY MGA',
"971": "AFGHANISTAN AFGHANI AFN", '971': 'AFGHANISTAN AFGHANI AFN',
"972": "TAJIKISTAN SOMONI TJS", '972': 'TAJIKISTAN SOMONI TJS',
"973": "KWANZA ANGOLA AOA", '973': 'KWANZA ANGOLA AOA',
"974": "BELARUSSIAN RUBLE BYR", '974': 'BELARUSSIAN RUBLE BYR',
"975": "NEW LEV BGN", '975': 'NEW LEV BGN',
"976": "FRANCO DEL CONGO CDF", '976': 'FRANCO DEL CONGO CDF',
"977": "BOSNIAN MARKA BAM", '977': 'BOSNIAN MARKA BAM',
"978": "EURO EUR", '978': 'EURO EUR',
"980": "HRYVNIA UAH", '980': 'HRYVNIA UAH',
"981": "GEORGIAN LARI GEL", '981': 'GEORGIAN LARI GEL',
"985": "NEW POLISH ZLOTY PLN", '985': 'NEW POLISH ZLOTY PLN',
"986": "BRAZILIAN REAL BRL", '986': 'BRAZILIAN REAL BRL',
"991": "RAND FINANCIER ZAL", '991': 'RAND FINANCIER ZAL',
} }
ERROR_CODES = { ERROR_CODES = {
"10000": _("Error no tipificado"), '10000': _('Error no tipificado'),
"0900": _("Transacción autorizada para devoluciones y confirmaciones"), '0900': _('Transacción autorizada para devoluciones y confirmaciones'),
"0400": _("Transacción autorizada para anulaciones"), '0400': _('Transacción autorizada para anulaciones'),
"0101": _("Tarjeta caducada"), '0101': _('Tarjeta caducada'),
"0102": _("Tarjeta en excepción transitoria o bajo sospecha de fraude"), '0102': _('Tarjeta en excepción transitoria o bajo sospecha de fraude'),
"0106": _("Intentos de PIN excedidos"), '0106': _('Intentos de PIN excedidos'),
"0125": _("Tarjeta no efectiva"), '0125': _('Tarjeta no efectiva'),
"0129": _("Código de seguridad (CVV2/CVC2) incorrecto"), '0129': _('Código de seguridad (CVV2/CVC2) incorrecto'),
"172": _("Denegada, no repetir."), '172': _('Denegada, no repetir.'),
"173": _("Denegada, no repetir sin actualizar datos de tarjeta."), '173': _('Denegada, no repetir sin actualizar datos de tarjeta.'),
"174": _("Denegada, no repetir antes de 72 horas."), '174': _('Denegada, no repetir antes de 72 horas.'),
"0180": _("Tarjeta ajena al servicio"), '0180': _('Tarjeta ajena al servicio'),
"0184": _("Error en la autenticación del titular"), '0184': _('Error en la autenticación del titular'),
"0190": _("Denegación del emisor sin especificar motivo"), '0190': _('Denegación del emisor sin especificar motivo'),
"0191": _("Fecha de caducidad errónea"), '0191': _('Fecha de caducidad errónea'),
"0195": _("Requiere autenticación SCA"), '0195': _('Requiere autenticación SCA'),
"0202": _( '0202': _('Tarjeta en excepción transitoria o bajo sospecha de fraude con retirada de tarjeta'),
"Tarjeta en excepción transitoria o bajo sospecha de fraude con retirada de tarjeta" '0904': _('Comercio no registrado en FUC'),
), '0909': _('Error de sistema'),
"0904": _("Comercio no registrado en FUC"), '0913': _('Pedido repetido'),
"0909": _("Error de sistema"), '0944': _('Sesión Incorrecta'),
"0913": _("Pedido repetido"), '0950': _('Operación de devolución no permitida'),
"0944": _("Sesión Incorrecta"), '9912': _('Emisor no disponible'),
"0950": _("Operación de devolución no permitida"), '0912': _('Emisor no disponible'),
"9912": _("Emisor no disponible"), '9064': _('Número de posiciones de la tarjeta incorrecto'),
"0912": _("Emisor no disponible"), '9078': _('Tipo de operación no permitida para esa tarjeta'),
"9064": _("Número de posiciones de la tarjeta incorrecto"), '9093': _('Tarjeta no existente'),
"9078": _("Tipo de operación no permitida para esa tarjeta"), '9094': _('Rechazo servidores internacionales'),
"9093": _("Tarjeta no existente"), '9104': _('Comercio con titular seguro y titular sin clave de compra segura'),
"9094": _("Rechazo servidores internacionales"), '9218': _('El comercio no permite op. seguras por entrada /operaciones'),
"9104": _("Comercio con titular seguro y titular sin clave de compra segura"), '9253': _('Tarjeta no cumple el check-digit'),
"9218": _("El comercio no permite op. seguras por entrada /operaciones"), '9256': _('El comercio no puede realizar preautorizaciones'),
"9253": _("Tarjeta no cumple el check-digit"), '9257': _('Esta tarjeta no permite operativa de preautorizaciones'),
"9256": _("El comercio no puede realizar preautorizaciones"), '9261': _('Operación detenida por superar el control de restricciones en la entrada al SIS'),
"9257": _("Esta tarjeta no permite operativa de preautorizaciones"), '9915': _('A petición del usuario se ha cancelado el pago'),
"9261": _( '9997': _('Se está procesando otra transacción en SIS con la misma tarjeta'),
"Operación detenida por superar el control de restricciones en la entrada al SIS" '9998': _('Operación en proceso de solicitud de datos de tarjeta'),
), '9999': _('Operación que ha sido redirigida al emisor a autenticar'),
"9915": _("A petición del usuario se ha cancelado el pago"),
"9997": _("Se está procesando otra transacción en SIS con la misma tarjeta"),
"9998": _("Operación en proceso de solicitud de datos de tarjeta"),
"9999": _("Operación que ha sido redirigida al emisor a autenticar"),
} }
+4 -23
View File
@@ -7,28 +7,9 @@ from shop.models import Product, ProductPrice, Tax
class CreateProductsMixin: class CreateProductsMixin:
def create_product( def create_product(
self, self, sku='000001', name='Producto 1', description='Descripción', price=Decimal('10.00'), is_shipping=False
sku="000001",
name="Producto 1",
description="Descripción",
price=Decimal("10.00"),
is_shipping=False,
) -> Product: ) -> Product:
tax, created = Tax.objects.get_or_create( tax, created = Tax.objects.get_or_create(code='IVA', value=21)
code="IVA", product = Product.objects.create(sku=sku, name=name, description=description, is_shipping_method=is_shipping)
value=21, ProductPrice.objects.create(price=price, product=product, date=now(), tax=tax, current=True)
)
product = Product.objects.create(
sku=sku,
name=name,
description=description,
is_shipping_method=is_shipping,
)
ProductPrice.objects.create(
price=price,
product=product,
date=now(),
tax=tax,
current=True,
)
return product return product
+3 -3
View File
@@ -13,11 +13,11 @@ class TestProductImages(TestCase, CreateProductsMixin):
self.product = self.create_product() self.product = self.create_product()
def create_image_file(self, size=(768, 768)): def create_image_file(self, size=(768, 768)):
image = Image.new("RGB", size, "#ACACAC") image = Image.new('RGB', size, '#ACACAC')
buffer = BytesIO() buffer = BytesIO()
image.save(fp=buffer, format="WEBP") image.save(fp=buffer, format='WEBP')
file = ContentFile(buffer.getvalue(), name=f"{self.product.slug}.webp") file = ContentFile(buffer.getvalue(), name=f'{self.product.slug}.webp')
return file return file
def test_product_image_creation(self): def test_product_image_creation(self):
+84 -116
View File
@@ -22,58 +22,51 @@ User = get_user_model()
def redsys_response_ok(*args, **kwargs): def redsys_response_ok(*args, **kwargs):
response = { response = {
"Ds_SignatureVersion": "HMAC_SHA256_V1", 'Ds_SignatureVersion': 'HMAC_SHA256_V1',
"Ds_MerchantParameters": "eyJEc19BbW91bnQiOiIxNDUiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTQ0NjA2ODU4MSIsIkRzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiMSIsIkRzX1Jlc3BvbnNlIjoiMDAwMCIsIkRzX0F1dGhvcmlzYXRpb25Db2RlIjoiNTAxNjAyIiwiRHNfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX1NlY3VyZVBheW1lbnQiOiIwIiwiRHNfTGFuZ3VhZ2UiOiIxIiwiRHNfQ2FyZE51bWJlciI6IjQ1NDg4MSoqKioqKioqMDQiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSJ9", 'Ds_MerchantParameters': 'eyJEc19BbW91bnQiOiIxNDUiLCJEc19DdXJyZW5jeSI6Ijk3OCIsIkRzX09yZGVyIjoiMTQ0NjA2ODU4MSIsIkRzX01lcmNoYW50Q29kZSI6Ijk5OTAwODg4MSIsIkRzX1Rlcm1pbmFsIjoiMSIsIkRzX1Jlc3BvbnNlIjoiMDAwMCIsIkRzX0F1dGhvcmlzYXRpb25Db2RlIjoiNTAxNjAyIiwiRHNfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX1NlY3VyZVBheW1lbnQiOiIwIiwiRHNfTGFuZ3VhZ2UiOiIxIiwiRHNfQ2FyZE51bWJlciI6IjQ1NDg4MSoqKioqKioqMDQiLCJEc19NZXJjaGFudERhdGEiOiIiLCJEc19DYXJkX0NvdW50cnkiOiI3MjQiLCJEc19DYXJkX0JyYW5kIjoiMSJ9',
"Ds_Signature": "QVxoXwwp919v7XYjyBjhr1VXozESRosHPb3PDW-rcME=", 'Ds_Signature': 'QVxoXwwp919v7XYjyBjhr1VXozESRosHPb3PDW-rcME=',
} }
return Response(response, status_code=200) return Response(response, status_code=200)
def redsys_response_error(*args, **kwargs): def redsys_response_error(*args, **kwargs):
response = {"errorCode": "SIS00001"} response = {'errorCode': 'SIS00001'}
return Response(response, status_code=200) return Response(response, status_code=200)
class TestRedsysTPV(APITestCase, CreateProductsMixin): class TestRedsysTPV(APITestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.tax = Tax.objects.create( self.tax = Tax.objects.create(code='IVA', value=21)
code="IVA",
value=21,
)
self.customer = get_user_model().objects.create_user( self.customer = get_user_model().objects.create_user(
username="11111111H", username='11111111H', first_name='Darth', last_name='Maul', email='darth@maul.com', password='dathomir'
first_name="Darth",
last_name="Maul",
email="darth@maul.com",
password="dathomir",
) )
self.customer_shipping_address = CustomerAddress.objects.create( self.customer_shipping_address = CustomerAddress.objects.create(
user=self.customer, user=self.customer,
address="Dathomir", address='Dathomir',
address_town="Dathomir", address_town='Dathomir',
address_zip="00001", address_zip='00001',
address_state="Dathomir", address_state='Dathomir',
address_phone="900000000", address_phone='900000000',
address_type=CustomerAddress.Types.SHIPPING, address_type=CustomerAddress.Types.SHIPPING,
) )
self.customer_billing_address = CustomerAddress.objects.create( self.customer_billing_address = CustomerAddress.objects.create(
user=self.customer, user=self.customer,
address="Dathomir", address='Dathomir',
address_town="Dathomir", address_town='Dathomir',
address_zip="00001", address_zip='00001',
address_state="Dathomir", address_state='Dathomir',
address_phone="900000000", address_phone='900000000',
address_type=CustomerAddress.Types.BILLING, address_type=CustomerAddress.Types.BILLING,
) )
self.product = self.create_product() self.product = self.create_product()
self.order = self.create_order() self.order = self.create_order()
self.order.calculate_total_from_lines() self.order.calculate_total_from_lines()
self.settings = ShopSettings.load() self.settings = ShopSettings.load()
self.settings.merchant_code = "999008881" self.settings.merchant_code = '999008881'
self.settings.shared_secret = "sq7HjrUOBfKmC576ILgskD5srU870gJ7" # Debug secret self.settings.shared_secret = 'sq7HjrUOBfKmC576ILgskD5srU870gJ7' # Debug secret
self.settings.save() self.settings.save()
def create_order(self): def create_order(self):
@@ -86,66 +79,53 @@ class TestRedsysTPV(APITestCase, CreateProductsMixin):
billing_country=self.customer_billing_address.address_country, billing_country=self.customer_billing_address.address_country,
) )
l1 = create_order_line_for_product( create_order_line_for_product(self.product, quantity=Decimal('1.0'), order=order)
self.product,
quantity=Decimal("1.0"),
order=order,
)
return order return order
def test_redsys_client(self): def test_redsys_client(self):
amount_to_pay = Decimal("12.10") amount_to_pay = Decimal('12.10')
client = RedsysClient() client = RedsysClient()
merchant_parameters = client._get_merchant_parameters_for_order( merchant_parameters = client._get_merchant_parameters_for_order(self.order)
self.order,
)
assert merchant_parameters.get("DS_MERCHANT_ORDER") == self.order.code assert merchant_parameters.get('DS_MERCHANT_ORDER') == self.order.code
assert merchant_parameters.get("DS_MERCHANT_AMOUNT") == str( assert merchant_parameters.get('DS_MERCHANT_AMOUNT') == str(int(amount_to_pay * 100))
int(amount_to_pay * 100) assert merchant_parameters.get('DS_MERCHANT_TERMINAL') == self.settings.terminal
) assert merchant_parameters.get('DS_MERCHANT_MERCHANTCODE') == self.settings.merchant_code
assert merchant_parameters.get("DS_MERCHANT_TERMINAL") == self.settings.terminal
assert (
merchant_parameters.get("DS_MERCHANT_MERCHANTCODE")
== self.settings.merchant_code
)
client.get_body_for_order(self.order) client.get_body_for_order(self.order)
def test_redsys_webhook(self): def test_redsys_webhook(self):
redsys_response_data = { redsys_response_data = {
"Ds_MerchantCode": "999008881", 'Ds_MerchantCode': '999008881',
"Ds_Terminal": "001", 'Ds_Terminal': '001',
"Ds_Order": self.order.code, 'Ds_Order': self.order.code,
"Ds_Amount": str(self.order.total * 100), 'Ds_Amount': str(self.order.total * 100),
"Ds_Currency": "978", 'Ds_Currency': '978',
"Ds_Date": "01/01/2024", 'Ds_Date': '01/01/2024',
"Ds_Hour": "00:00", 'Ds_Hour': '00:00',
"Ds_SecurePayment": "1", 'Ds_SecurePayment': '1',
"Ds_Card_Number": "454881******1156", 'Ds_Card_Number': '454881******1156',
"Ds_Card_Country": "724", 'Ds_Card_Country': '724',
"Ds_Response": "0000", 'Ds_Response': '0000',
"Ds_MerchantData": "", 'Ds_MerchantData': '',
"Ds_TransactionType": "0", 'Ds_TransactionType': '0',
"Ds_ConsumerLanguage": "1", 'Ds_ConsumerLanguage': '1',
"Ds_AuthorisationCode": "182670", 'Ds_AuthorisationCode': '182670',
"Ds_Card_Brand": "1", 'Ds_Card_Brand': '1',
"Ds_ProcessedPayMethod": "80", 'Ds_ProcessedPayMethod': '80',
"Ds_ECI": "05", 'Ds_ECI': '05',
"Ds_Response_Description": "OPERACION AUTORIZADA", 'Ds_Response_Description': 'OPERACION AUTORIZADA',
} }
redsys_response_data_str = json.dumps(redsys_response_data) redsys_response_data_str = json.dumps(redsys_response_data)
b64_merchant_params = base64.b64encode( b64_merchant_params = base64.b64encode(redsys_response_data_str.encode()).decode()
redsys_response_data_str.encode()
).decode()
response = self.client.post( response = self.client.post(
reverse("shop:webhook", kwargs={"uuid": self.order.uuid}), reverse('shop:webhook', kwargs={'uuid': self.order.uuid}),
data={ data={
"Ds_MerchantParameters": b64_merchant_params, 'Ds_MerchantParameters': b64_merchant_params,
"Ds_Signature": self.settings.shared_secret, 'Ds_Signature': self.settings.shared_secret,
"Ds_SignatureVersion": "HMAC_SHA256_V1", 'Ds_SignatureVersion': 'HMAC_SHA256_V1',
}, },
) )
self.order.refresh_from_db() self.order.refresh_from_db()
@@ -154,38 +134,36 @@ class TestRedsysTPV(APITestCase, CreateProductsMixin):
def test_redsys_webhook_payment_error(self): def test_redsys_webhook_payment_error(self):
redsys_response_data = { redsys_response_data = {
"Ds_MerchantCode": "999008881", 'Ds_MerchantCode': '999008881',
"Ds_Terminal": "001", 'Ds_Terminal': '001',
"Ds_Order": self.order.code, 'Ds_Order': self.order.code,
"Ds_Amount": "1000", 'Ds_Amount': '1000',
"Ds_Currency": "978", 'Ds_Currency': '978',
"Ds_Date": "01/01/2024", 'Ds_Date': '01/01/2024',
"Ds_Hour": "00:00", 'Ds_Hour': '00:00',
"Ds_SecurePayment": "1", 'Ds_SecurePayment': '1',
"Ds_Card_Number": "454881******1156", 'Ds_Card_Number': '454881******1156',
"Ds_Card_Country": "724", 'Ds_Card_Country': '724',
"Ds_Response": "0184", 'Ds_Response': '0184',
"Ds_MerchantData": "", 'Ds_MerchantData': '',
"Ds_TransactionType": "0", 'Ds_TransactionType': '0',
"Ds_ConsumerLanguage": "1", 'Ds_ConsumerLanguage': '1',
"Ds_AuthorisationCode": "182670", 'Ds_AuthorisationCode': '182670',
"Ds_Card_Brand": "1", 'Ds_Card_Brand': '1',
"Ds_ProcessedPayMethod": "80", 'Ds_ProcessedPayMethod': '80',
"Ds_ECI": "05", 'Ds_ECI': '05',
"Ds_Response_Description": "ERROR", 'Ds_Response_Description': 'ERROR',
} }
redsys_response_data_str = json.dumps(redsys_response_data) redsys_response_data_str = json.dumps(redsys_response_data)
b64_merchant_params = base64.b64encode( b64_merchant_params = base64.b64encode(redsys_response_data_str.encode()).decode()
redsys_response_data_str.encode()
).decode()
response = self.client.post( response = self.client.post(
reverse("shop:webhook", kwargs={"uuid": self.order.uuid}), reverse('shop:webhook', kwargs={'uuid': self.order.uuid}),
data={ data={
"Ds_MerchantParameters": b64_merchant_params, 'Ds_MerchantParameters': b64_merchant_params,
"Ds_Signature": self.settings.shared_secret, 'Ds_Signature': self.settings.shared_secret,
"Ds_SignatureVersion": "HMAC_SHA256_V1", 'Ds_SignatureVersion': 'HMAC_SHA256_V1',
}, },
) )
@@ -196,37 +174,27 @@ class TestRedsysTPV(APITestCase, CreateProductsMixin):
def test_expiry_date(self): def test_expiry_date(self):
now = timezone.now() now = timezone.now()
previous_year = str(now.year - 1).rjust(2, "0") previous_year = str(now.year - 1).rjust(2, '0')
current_month = str(now.month).rjust(2, "0") current_month = str(now.month).rjust(2, '0')
current_year = str(now.year)[-2:] current_year = str(now.year)[-2:]
assert not validate_expiry_date("042024") assert not validate_expiry_date('042024')
assert not validate_expiry_date(f"{previous_year}{current_month}") assert not validate_expiry_date(f'{previous_year}{current_month}')
assert validate_expiry_date(f"{current_year}{current_month}") assert validate_expiry_date(f'{current_year}{current_month}')
@patch("requests.post", redsys_response_ok) @patch('requests.post', redsys_response_ok)
def test_redsys_rest_client(self): def test_redsys_rest_client(self):
client = RedsysClient() client = RedsysClient()
params = client.pay_order_rest( params = client.pay_order_rest(self.order, pan='4548810000000003', expiry_date='122049', cvv2='285')
self.order,
pan="4548810000000003",
expiry_date="122049",
cvv2="285",
)
assert params.get("Ds_Order") == "1446068581" assert params.get('Ds_Order') == '1446068581'
assert params.get("Ds_MerchantCode") == self.settings.merchant_code assert params.get('Ds_MerchantCode') == self.settings.merchant_code
@patch("requests.post", redsys_response_error) @patch('requests.post', redsys_response_error)
def test_redsys_rest_client_error(self): def test_redsys_rest_client_error(self):
client = RedsysClient() client = RedsysClient()
with pytest.raises(RedsysPaymentException): with pytest.raises(RedsysPaymentException):
client.pay_order_rest( client.pay_order_rest(self.order, pan='4548810000000003', expiry_date='122049', cvv2='285')
self.order,
pan="4548810000000003",
expiry_date="122049",
cvv2="285",
)
+2 -4
View File
@@ -2,9 +2,7 @@ from django.urls import path
from shop.views import webhook from shop.views import webhook
app_name = "shop" app_name = 'shop'
urlpatterns = [ urlpatterns = [path('order/<str:uuid>/webhook/', webhook, name='webhook')]
path("order/<str:uuid>/webhook/", webhook, name="webhook"),
]
+33 -52
View File
@@ -12,17 +12,7 @@ from django.utils import timezone
from django.utils.text import gettext_lazy as _ from django.utils.text import gettext_lazy as _
from shop.exceptions import RedsysPaymentException, RedsysValidationException from shop.exceptions import RedsysPaymentException, RedsysValidationException
from shop.models import ( from shop.models import Cart, CartItem, Order, OrderLine, Payment, Product, ProductBatch, ProductPrice, ShippingMethod
Cart,
CartItem,
Order,
OrderLine,
Payment,
Product,
ProductBatch,
ProductPrice,
ShippingMethod,
)
from shop.settings import ERROR_CODES from shop.settings import ERROR_CODES
from shop.signals import clear_cart from shop.signals import clear_cart
@@ -32,7 +22,7 @@ User = get_user_model()
def create_order_line_for_product(product: Product, quantity: Decimal, order: Order): def create_order_line_for_product(product: Product, quantity: Decimal, order: Order):
price = product.prices.last() price = product.prices.last()
base_total = round(price.price * quantity, 2) base_total = round(price.price * quantity, 2)
tax_value = price.tax.value / Decimal("100") tax_value = price.tax.value / Decimal('100')
taxes = round(base_total * tax_value, 2) taxes = round(base_total * tax_value, 2)
return OrderLine.objects.create( return OrderLine.objects.create(
@@ -54,11 +44,11 @@ def create_order(
billing_state: str, billing_state: str,
billing_country: str, billing_country: str,
billing_zip: str, billing_zip: str,
shipping_address: str = "", shipping_address: str = '',
shipping_city: str = "", shipping_city: str = '',
shipping_state: str = "", shipping_state: str = '',
shipping_country: str = "", shipping_country: str = '',
shipping_zip: str = "", shipping_zip: str = '',
) -> Order: ) -> Order:
order = Order.objects.create( order = Order.objects.create(
user=customer, user=customer,
@@ -87,29 +77,29 @@ def delete_product_batch(batch: ProductBatch):
def create_order_from_cart( def create_order_from_cart(
cart: Cart, cart: Cart,
shipping_method: ShippingMethod, shipping_method: ShippingMethod,
billing_address_full_name="", billing_address_full_name='',
billing_address_address="", billing_address_address='',
billing_address_town="", billing_address_town='',
billing_address_state="", billing_address_state='',
billing_address_country="", billing_address_country='',
billing_address_zip="", billing_address_zip='',
shipping_address_full_name="", shipping_address_full_name='',
shipping_address_address="", shipping_address_address='',
shipping_address_town="", shipping_address_town='',
shipping_address_state="", shipping_address_state='',
shipping_address_country="", shipping_address_country='',
shipping_address_zip="", shipping_address_zip='',
shipping_address_phone="", shipping_address_phone='',
email="", email='',
): ):
order = Order.objects.create( order = Order.objects.create(
billing_address=f"{billing_address_full_name} {billing_address_address}", billing_address=f'{billing_address_full_name} {billing_address_address}',
billing_city=billing_address_town, billing_city=billing_address_town,
billing_state=billing_address_state, billing_state=billing_address_state,
billing_country=billing_address_country, billing_country=billing_address_country,
billing_zip=billing_address_zip, billing_zip=billing_address_zip,
contact_phone=shipping_address_phone, contact_phone=shipping_address_phone,
shipping_address=f"{shipping_address_address} {shipping_address_full_name}", shipping_address=f'{shipping_address_address} {shipping_address_full_name}',
shipping_city=shipping_address_town, shipping_city=shipping_address_town,
shipping_state=shipping_address_state, shipping_state=shipping_address_state,
shipping_country=shipping_address_country, shipping_country=shipping_address_country,
@@ -174,9 +164,7 @@ def compute_signature(salt, payload, key):
""" """
b64_key = base64.b64decode(key) b64_key = base64.b64decode(key)
des3 = pyDes.triple_des( des3 = pyDes.triple_des(b64_key, mode=pyDes.CBC, IV='\0' * 8, pad='\0', padmode=pyDes.PAD_NORMAL)
b64_key, mode=pyDes.CBC, IV="\0" * 8, pad="\0", padmode=pyDes.PAD_NORMAL
)
pepper = des3.encrypt(str(salt)) pepper = des3.encrypt(str(salt))
payload_hash = hmac.new(pepper, payload.encode(), hashlib.sha256).digest() payload_hash = hmac.new(pepper, payload.encode(), hashlib.sha256).digest()
@@ -211,27 +199,25 @@ def validate_payment_for_order(request, order: Order) -> Decimal:
""" """
data = request.POST data = request.POST
merchant_parameters = data.get("Ds_MerchantParameters") merchant_parameters = data.get('Ds_MerchantParameters')
if not merchant_parameters: if not merchant_parameters:
raise RedsysValidationException( raise RedsysValidationException(_('No se ha recibido ningún valor para Ds_MerchantParameters'))
_("No se ha recibido ningún valor para Ds_MerchantParameters")
)
merchant_params = decode_b64_string(merchant_parameters) merchant_params = decode_b64_string(merchant_parameters)
result = json.loads(merchant_params) result = json.loads(merchant_params)
order_code = result.get("Ds_Order") order_code = result.get('Ds_Order')
assert order_code == order.code assert order_code == order.code
status_code = result.get("Ds_Response") status_code = result.get('Ds_Response')
if int(status_code) > 100: if int(status_code) > 100:
reason = ERROR_CODES.get(status_code, _("Error no tipificado")) reason = ERROR_CODES.get(status_code, _('Error no tipificado'))
raise RedsysPaymentException(_(f"No se ha realizado el pago. Motivo: {reason}")) raise RedsysPaymentException(_(f'No se ha realizado el pago. Motivo: {reason}'))
amount = Decimal(result.get("Ds_Amount")) / 100 amount = Decimal(result.get('Ds_Amount')) / 100
return amount return amount
@@ -268,9 +254,7 @@ def validate_expiry_date(expiry_date: str):
def update_order_payment_status(order: Order): def update_order_payment_status(order: Order):
if order.amount_paid >= order.total: if order.amount_paid >= order.total:
order.status = Order.Statuses.STATUS_PAID order.status = Order.Statuses.STATUS_PAID
elif order.status == Order.Statuses.STATUS_PAID and order.amount_paid == Decimal( elif order.status == Order.Statuses.STATUS_PAID and order.amount_paid == Decimal('0.00'):
"0.00"
):
order.status = Order.Statuses.STATUS_RETURNED order.status = Order.Statuses.STATUS_RETURNED
else: else:
order.status = Order.Statuses.STATUS_PENDING order.status = Order.Statuses.STATUS_PENDING
@@ -286,10 +270,7 @@ def delete_cart_items_from_order(order):
def add_payment_to_order(order: Order, amount): def add_payment_to_order(order: Order, amount):
with transaction.atomic(): with transaction.atomic():
payment = Payment.objects.create( payment = Payment.objects.create(
amount=amount, amount=amount, order=order, user=order.user, method=Payment.MethodChoices.REDSYS
order=order,
user=order.user,
method=Payment.MethodChoices.REDSYS,
) )
order.amount_paid += payment.amount order.amount_paid += payment.amount
+1 -1
View File
@@ -15,7 +15,7 @@ def webhook(request, uuid):
add_payment_to_order(order, amount_paid) add_payment_to_order(order, amount_paid)
return HttpResponse(status=200) return HttpResponse(status=200)
except Exception as e: except Exception:
order.status = Order.Statuses.STATUS_ERROR order.status = Order.Statuses.STATUS_ERROR
order.save() order.save()
+2 -2
View File
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class UsersConfig(AppConfig): class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = 'django.db.models.BigAutoField'
name = "users" name = 'users'
+4 -4
View File
@@ -8,9 +8,9 @@ User = get_user_model()
class ChangePasswordForm(StylingMixin, PasswordChangeForm): class ChangePasswordForm(StylingMixin, PasswordChangeForm):
styled_fields = ("old_password", "new_password1", "new_password2") styled_fields = ('old_password', 'new_password1', 'new_password2')
placeholder_for_field = { placeholder_for_field = {
"old_password": _("********"), 'old_password': _('********'),
"new_password1": _("********"), 'new_password1': _('********'),
"new_password2": _("********"), 'new_password2': _('********'),
} }
+4 -12
View File
@@ -8,15 +8,11 @@ User = get_user_model()
class UserInfoForm(StylingMixin, forms.ModelForm): class UserInfoForm(StylingMixin, forms.ModelForm):
styled_fields = ("email", "first_name", "last_name") styled_fields = ('email', 'first_name', 'last_name')
placeholder_for_field = { placeholder_for_field = {'email': _('pablo@shoppy.com'), 'first_name': _('Pablo'), 'last_name': _('Moreno')}
"email": _("pablo@shoppy.com"),
"first_name": _("Pablo"),
"last_name": _("Moreno"),
}
def save(self, commit=True): def save(self, commit=True):
self.instance.username = self.cleaned_data.get("email") self.instance.username = self.cleaned_data.get('email')
return super().save(commit) return super().save(commit)
def is_valid(self): def is_valid(self):
@@ -24,8 +20,4 @@ class UserInfoForm(StylingMixin, forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ( fields = ('email', 'first_name', 'last_name')
"email",
"first_name",
"last_name",
)
+6 -18
View File
@@ -7,28 +7,16 @@ from web.mixins import StylingMixin
class LoginForm(StylingMixin, AuthenticationForm): class LoginForm(StylingMixin, AuthenticationForm):
styled_fields = [] styled_fields = []
placeholder_for_field = {"username": _("pablo@shoppy.com"), "password": "********"} placeholder_for_field = {'username': _('pablo@shoppy.com'), 'password': '********'}
username = UsernameField( username = UsernameField(widget=forms.EmailInput(attrs={'autofocus': True, 'class': 'input-text'}))
widget=forms.EmailInput(
attrs={
"autofocus": True,
"class": "input-text",
}
)
)
password = forms.CharField( password = forms.CharField(
label=_("Contraseña"), label=_('Contraseña'),
strip=False, strip=False,
widget=forms.PasswordInput( widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'class': 'input-text'}),
attrs={
"autocomplete": "current-password",
"class": "input-text",
}
),
) )
remember_me = forms.BooleanField( remember_me = forms.BooleanField(
label=_("Mantener sesión iniciada"), label=_('Mantener sesión iniciada'),
required=False, required=False,
widget=forms.CheckboxInput(attrs={"class": "input-checkbox"}), widget=forms.CheckboxInput(attrs={'class': 'input-checkbox'}),
) )
+12 -20
View File
@@ -9,13 +9,13 @@ from web.mixins import StylingMixin
class RegisterForm(StylingMixin, BaseUserCreationForm, SetPasswordMixin): class RegisterForm(StylingMixin, BaseUserCreationForm, SetPasswordMixin):
password1, password2 = SetPasswordMixin.create_password_fields() password1, password2 = SetPasswordMixin.create_password_fields()
styled_fields = ["email", "first_name", "last_name", "password1", "password2"] styled_fields = ['email', 'first_name', 'last_name', 'password1', 'password2']
placeholder_for_field = { placeholder_for_field = {
"email": _("pablo@shoppy.com"), 'email': _('pablo@shoppy.com'),
"first_name": _("Pablo"), 'first_name': _('Pablo'),
"last_name": _("Moreno"), 'last_name': _('Moreno'),
"password1": "********", 'password1': '********',
"password2": "********", 'password2': '********',
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -24,22 +24,14 @@ class RegisterForm(StylingMixin, BaseUserCreationForm, SetPasswordMixin):
self.fields[field_name].required = True self.fields[field_name].required = True
self.fields[field_name].widget.attrs.update( self.fields[field_name].widget.attrs.update(
{ {
"class": "my-2 bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg " 'class': 'my-2 bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg '
"focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 " 'focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 '
"dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 " 'dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 '
"dark:focus:border-primary-500" 'dark:focus:border-primary-500'
} }
) )
class Meta: class Meta:
model = User model = User
fields = ( fields = ('email', 'first_name', 'last_name')
"email", field_classes = {'email': EmailField, 'first_name': forms.CharField, 'last_name': forms.CharField}
"first_name",
"last_name",
)
field_classes = {
"email": EmailField,
"first_name": forms.CharField,
"last_name": forms.CharField,
}
+8 -24
View File
@@ -9,42 +9,26 @@ from web.mixins import StylingMixin
class PasswordResetForm(StylingMixin, BasePasswordResetForm): class PasswordResetForm(StylingMixin, BasePasswordResetForm):
styled_fields = [ styled_fields = ['email']
"email", placeholder_for_field = {'email': _('pablo@shoppy.com')}
]
placeholder_for_field = {
"email": _("pablo@shoppy.com"),
}
class SetPasswordForm(StylingMixin, BaseSetPasswordForm): class SetPasswordForm(StylingMixin, BaseSetPasswordForm):
styled_fields = [ styled_fields = ['new_password1', 'new_password2']
"new_password1", placeholder_for_field = {'new_password1': '********', 'new_password2': '********'}
"new_password2",
]
placeholder_for_field = {
"new_password1": "********",
"new_password2": "********",
}
token = forms.CharField( token = forms.CharField(required=True, widget=forms.HiddenInput(), validators=[])
required=True,
widget=forms.HiddenInput(),
validators=[],
)
def validate_token(self): def validate_token(self):
is_valid = default_token_generator.check_token( is_valid = default_token_generator.check_token(self.user, self.data.get('token'))
self.user, self.data.get("token")
)
if not is_valid: if not is_valid:
raise ValidationError(_("El token no es válido o ha expirado")) raise ValidationError(_('El token no es válido o ha expirado'))
def is_valid(self): def is_valid(self):
try: try:
self.validate_token() self.validate_token()
except ValidationError as e: except ValidationError as e:
self.add_error("token", str(e)) self.add_error('token', str(e))
return False return False
return super().is_valid() return super().is_valid()
+4 -7
View File
@@ -7,25 +7,22 @@ User = get_user_model()
class TestUsers(TestCase): class TestUsers(TestCase):
def test_create_user(self): def test_create_user(self):
User.objects.create_user({"username": "user 1", "password": "password1"}) User.objects.create_user({'username': 'user 1', 'password': 'password1'})
User.objects.all().count() == 1 User.objects.all().count() == 1
def test_new_user_invalid_username(self): def test_new_user_invalid_username(self):
"""Test creating user with no username raises error""" """Test creating user with no username raises error"""
with pytest.raises(ValueError): with pytest.raises(ValueError):
User.objects.create_user(None, "test123") User.objects.create_user(None, 'test123')
def test_create_new_superuser(self): def test_create_new_superuser(self):
"""Test creating a new superuser""" """Test creating a new superuser"""
# Creation with standard method # Creation with standard method
user = User.objects.create_superuser( user = User.objects.create_superuser('testsuperuser@adminemail.com', 'testadmin123')
"testsuperuser@adminemail.com", "testadmin123"
)
assert user.is_superuser assert user.is_superuser
assert user.is_staff assert user.is_staff
def test_create_group(self): def test_create_group(self):
Group.objects.create(name="Group1") Group.objects.create(name='Group1')
assert Group.objects.count() == 1 assert Group.objects.count() == 1
+1 -1
View File
@@ -1,4 +1,4 @@
app_name = "users" app_name = 'users'
urlpatterns = [] urlpatterns = []
+2 -2
View File
@@ -2,8 +2,8 @@ from django.apps import AppConfig
class WebConfig(AppConfig): class WebConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = 'django.db.models.BigAutoField'
name = "web" name = 'web'
def ready(self): def ready(self):
from .signals import rebuild_theme from .signals import rebuild_theme
+4 -6
View File
@@ -4,12 +4,10 @@ from web.models import WebSettings
def web_settings(request): def web_settings(request):
settings = WebSettings.load() settings = WebSettings.load()
navbar_categories = ProductCategory.objects.filter( navbar_categories = ProductCategory.objects.filter(hidden=False, show_in_navbar=True)
hidden=False, show_in_navbar=True
)
return { return {
"web_title": settings.web_title, 'web_title': settings.web_title,
"logo": settings.logo_32.url if settings.logo_32 else "", 'logo': settings.logo_32.url if settings.logo_32 else '',
"navbar_categories": navbar_categories, 'navbar_categories': navbar_categories,
} }
+1 -9
View File
@@ -42,12 +42,4 @@ class CreateOrderForm(forms.Form):
class CustomerAddressForm(forms.ModelForm): class CustomerAddressForm(forms.ModelForm):
class Meta: class Meta:
model = CustomerAddress model = CustomerAddress
fields = ( fields = ('vat_id', 'full_name', 'address', 'address_town', 'address_zip', 'address_state', 'address_phone')
"vat_id",
"full_name",
"address",
"address_town",
"address_zip",
"address_state",
"address_phone",
)
@@ -26,7 +26,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
web_settings = WebSettings.load() web_settings = WebSettings.load()
path = os.path.join(settings.BASE_DIR, "web", "static", "css", "main.css") path = os.path.join(settings.BASE_DIR, 'web', 'static', 'css', 'main.css')
colors = [] colors = []
for color_name, data in web_settings.theme_colors.items(): for color_name, data in web_settings.theme_colors.items():
@@ -34,23 +34,9 @@ class Command(BaseCommand):
color = Color(color_name, index, value) color = Color(color_name, index, value)
colors.append(color) colors.append(color)
result = str( result = str(render_to_string('theme/main.css.template', context={'colors': colors}))
render_to_string( with open(path, 'w') as f:
"theme/main.css.template",
context={"colors": colors},
)
)
with open(path, "w") as f:
f.write(result) f.write(result)
subprocess.run( subprocess.run(['tailwindcss', '-i', 'web/static/css/main.css', '-o', 'web/static/css/styles.css', '--minify'])
[ call_command('collectstatic', '--no-input')
"tailwindcss",
"-i",
"web/static/css/main.css",
"-o",
"web/static/css/styles.css",
"--minify",
]
)
call_command("collectstatic", "--no-input")
+40 -119
View File
@@ -4,171 +4,92 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="WebSettings", name='WebSettings',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('web_title', models.CharField(max_length=32, verbose_name='título de la web')),
( (
"id", 'web_description',
models.BigAutoField( models.CharField(blank=True, default='', max_length=128, verbose_name='descripción de la web'),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
), ),
('logo', models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo de la web')),
( (
"web_title", 'business_name',
models.CharField(max_length=32, verbose_name="título de la web"),
),
(
"web_description",
models.CharField( models.CharField(
blank=True, blank=True,
default="", help_text='nombre fiscal del comercio',
max_length=128,
verbose_name="descripción de la web",
),
),
(
"logo",
models.ImageField(
blank=True,
null=True,
upload_to="assets",
verbose_name="logo de la web",
),
),
(
"business_name",
models.CharField(
blank=True,
help_text="nombre fiscal del comercio",
max_length=128, max_length=128,
null=True, null=True,
verbose_name="nombre de negocio", verbose_name='nombre de negocio',
), ),
), ),
( (
"business_vat_id", 'business_vat_id',
models.CharField( models.CharField(blank=True, max_length=16, null=True, verbose_name='CIF del negocio'),
blank=True,
max_length=16,
null=True,
verbose_name="CIF del negocio",
),
), ),
( (
"business_brand", 'business_brand',
models.CharField( models.CharField(
blank=True, blank=True,
help_text="marca o nombre comercial que se va a mostrar en la web", help_text='marca o nombre comercial que se va a mostrar en la web',
max_length=64, max_length=64,
null=True, null=True,
verbose_name="marca de negocio", verbose_name='marca de negocio',
), ),
), ),
( (
"business_address", 'business_address',
models.CharField(blank=True, max_length=64, null=True, verbose_name='dirección de negocio'),
),
(
'business_state',
models.CharField( models.CharField(
blank=True, blank=True, max_length=32, null=True, verbose_name='unidad territorial del negocio'
max_length=64,
null=True,
verbose_name="dirección de negocio",
), ),
), ),
( (
"business_state", 'business_zip',
models.CharField(blank=True, max_length=16, null=True, verbose_name='código postal del negocio'),
),
(
'business_phone',
models.CharField( models.CharField(
blank=True, blank=True, max_length=16, null=True, verbose_name='número de teléfono de contacto'
max_length=32,
null=True,
verbose_name="unidad territorial del negocio",
), ),
), ),
( (
"business_zip", 'business_email',
models.CharField( models.EmailField(blank=True, max_length=80, null=True, verbose_name='e-mail de contacto'),
blank=True,
max_length=16,
null=True,
verbose_name="código postal del negocio",
),
), ),
( (
"business_phone", 'business_email_2',
models.CharField(
blank=True,
max_length=16,
null=True,
verbose_name="número de teléfono de contacto",
),
),
(
"business_email",
models.EmailField( models.EmailField(
blank=True, blank=True, max_length=80, null=True, verbose_name='e-mail secundario de contacto'
max_length=80,
null=True,
verbose_name="e-mail de contacto",
), ),
), ),
( (
"business_email_2", 'bg_color',
models.EmailField( models.CharField(blank=True, default='', max_length=9, null=True, verbose_name='color de fondo'),
blank=True,
max_length=80,
null=True,
verbose_name="e-mail secundario de contacto",
),
), ),
( (
"bg_color", 'theme_color',
models.CharField( models.CharField(blank=True, default='', max_length=9, null=True, verbose_name='color de tema'),
blank=True,
default="",
max_length=9,
null=True,
verbose_name="color de fondo",
),
), ),
( (
"theme_color", 'logo_240',
models.CharField( models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo 240x240'),
blank=True,
default="",
max_length=9,
null=True,
verbose_name="color de tema",
),
), ),
( (
"logo_240", 'logo_128',
models.ImageField( models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo 128x128'),
blank=True,
null=True,
upload_to="assets",
verbose_name="logo 240x240",
),
),
(
"logo_128",
models.ImageField(
blank=True,
null=True,
upload_to="assets",
verbose_name="logo 128x128",
),
), ),
], ],
options={ options={'verbose_name': 'ajustes de la web', 'verbose_name_plural': 'ajustes de la web'},
"verbose_name": "ajustes de la web", )
"verbose_name_plural": "ajustes de la web",
},
),
] ]
@@ -4,21 +4,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [('web', '0001_initial')]
dependencies = [
("web", "0001_initial"),
]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name='websettings', name='logo_240'),
model_name="websettings",
name="logo_240",
),
migrations.AddField( migrations.AddField(
model_name="websettings", model_name='websettings',
name="logo_256", name='logo_256',
field=models.ImageField( field=models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo 256x256'),
blank=True, null=True, upload_to="assets", verbose_name="logo 256x256"
),
), ),
] ]
@@ -4,17 +4,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [('web', '0002_remove_websettings_logo_240_websettings_logo_256')]
dependencies = [
("web", "0002_remove_websettings_logo_240_websettings_logo_256"),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name="websettings", model_name='websettings',
name="theme_colors", name='theme_colors',
field=models.JSONField( field=models.JSONField(default=dict, verbose_name='colores del tema', blank=True),
default=dict, verbose_name="colores del tema", blank=True )
),
),
] ]
@@ -4,24 +4,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [('web', '0003_websettings_theme_colors')]
dependencies = [
("web", "0003_websettings_theme_colors"),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name="websettings", model_name='websettings',
name="logo_32", name='logo_32',
field=models.ImageField( field=models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo 32x32'),
blank=True, null=True, upload_to="assets", verbose_name="logo 32x32"
),
), ),
migrations.AddField( migrations.AddField(
model_name="websettings", model_name='websettings',
name="logo_64", name='logo_64',
field=models.ImageField( field=models.ImageField(blank=True, null=True, upload_to='assets', verbose_name='logo 64x64'),
blank=True, null=True, upload_to="assets", verbose_name="logo 64x64"
),
), ),
] ]
+4 -10
View File
@@ -22,7 +22,7 @@ class PaginatedQuerysetMixin:
def get_paginated_queryset(self, qs): def get_paginated_queryset(self, qs):
paginator = Paginator(qs, self.objects_per_page) paginator = Paginator(qs, self.objects_per_page)
page = self.request.GET.get("page", 1) page = self.request.GET.get('page', 1)
qs = paginator.get_page(page) qs = paginator.get_page(page)
return qs return qs
@@ -30,9 +30,7 @@ class PaginatedQuerysetMixin:
class StylingMixin: class StylingMixin:
placeholder_for_field = {} placeholder_for_field = {}
styled_fields = [] styled_fields = []
classes = { classes = {'input-text'}
"input-text",
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -42,11 +40,7 @@ class StylingMixin:
for field_name in self.styled_fields: for field_name in self.styled_fields:
field = self.fields[field_name] field = self.fields[field_name]
field.required = True field.required = True
field.widget.attrs.update({"class": " ".join(self.classes)}) field.widget.attrs.update({'class': ' '.join(self.classes)})
if self.placeholder_for_field.get(field_name): if self.placeholder_for_field.get(field_name):
field.widget.attrs.update( field.widget.attrs.update({'placeholder': self.placeholder_for_field.get(field_name)})
{
"placeholder": self.placeholder_for_field.get(field_name),
}
)
+25 -69
View File
@@ -9,101 +9,57 @@ from config.models import SingletonModel
class WebSettings(SingletonModel): class WebSettings(SingletonModel):
web_title = models.CharField( web_title = models.CharField(max_length=32, blank=False, verbose_name=_('título de la web'))
max_length=32, blank=False, verbose_name=_("título de la web") web_description = models.CharField(max_length=128, blank=True, default='', verbose_name=_('descripción de la web'))
) logo = models.ImageField(blank=True, null=True, upload_to='assets', verbose_name=_('logo de la web'))
web_description = models.CharField(
max_length=128, blank=True, default="", verbose_name=_("descripción de la web")
)
logo = models.ImageField(
blank=True, null=True, upload_to="assets", verbose_name=_("logo de la web")
)
business_name = models.CharField( business_name = models.CharField(
max_length=128, max_length=128,
blank=True, blank=True,
null=True, null=True,
verbose_name=_("nombre de negocio"), verbose_name=_('nombre de negocio'),
help_text=_("nombre fiscal del comercio"), help_text=_('nombre fiscal del comercio'),
)
business_vat_id = models.CharField(
max_length=16, blank=True, null=True, verbose_name=_("CIF del negocio")
) )
business_vat_id = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('CIF del negocio'))
business_brand = models.CharField( business_brand = models.CharField(
max_length=64, max_length=64,
blank=True, blank=True,
null=True, null=True,
verbose_name=_("marca de negocio"), verbose_name=_('marca de negocio'),
help_text=_("marca o nombre comercial que se va a mostrar en la web"), help_text=_('marca o nombre comercial que se va a mostrar en la web'),
) )
business_address = models.CharField( business_address = models.CharField(max_length=64, blank=True, null=True, verbose_name=_('dirección de negocio'))
max_length=64, blank=True, null=True, verbose_name=_("dirección de negocio")
)
business_state = models.CharField( business_state = models.CharField(
max_length=32, max_length=32, blank=True, null=True, verbose_name=_('unidad territorial del negocio')
blank=True,
null=True,
verbose_name=_("unidad territorial del negocio"),
)
business_zip = models.CharField(
max_length=16,
blank=True,
null=True,
verbose_name=_("código postal del negocio"),
) )
business_zip = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('código postal del negocio'))
business_phone = models.CharField( business_phone = models.CharField(
max_length=16, max_length=16, blank=True, null=True, verbose_name=_('número de teléfono de contacto')
blank=True,
null=True,
verbose_name=_("número de teléfono de contacto"),
)
business_email = models.EmailField(
max_length=80, blank=True, null=True, verbose_name=_("e-mail de contacto")
) )
business_email = models.EmailField(max_length=80, blank=True, null=True, verbose_name=_('e-mail de contacto'))
business_email_2 = models.EmailField( business_email_2 = models.EmailField(
max_length=80, max_length=80, blank=True, null=True, verbose_name=_('e-mail secundario de contacto')
blank=True,
null=True,
verbose_name=_("e-mail secundario de contacto"),
) )
bg_color = models.CharField( bg_color = models.CharField(max_length=9, default='', blank=True, null=True, verbose_name=_('color de fondo'))
max_length=9, theme_color = models.CharField(max_length=9, default='', blank=True, null=True, verbose_name=_('color de tema'))
default="",
blank=True,
null=True,
verbose_name=_("color de fondo"),
)
theme_color = models.CharField(
max_length=9, default="", blank=True, null=True, verbose_name=_("color de tema")
)
logo_256 = models.ImageField( logo_256 = models.ImageField(upload_to='assets', blank=True, null=True, verbose_name=_('logo 256x256'))
upload_to="assets", blank=True, null=True, verbose_name=_("logo 256x256") logo_128 = models.ImageField(upload_to='assets', blank=True, null=True, verbose_name=_('logo 128x128'))
) logo_64 = models.ImageField(upload_to='assets', blank=True, null=True, verbose_name=_('logo 64x64'))
logo_128 = models.ImageField( logo_32 = models.ImageField(upload_to='assets', blank=True, null=True, verbose_name=_('logo 32x32'))
upload_to="assets", blank=True, null=True, verbose_name=_("logo 128x128") theme_colors = models.JSONField(default=dict, verbose_name=_('colores del tema'), blank=True)
)
logo_64 = models.ImageField(
upload_to="assets", blank=True, null=True, verbose_name=_("logo 64x64")
)
logo_32 = models.ImageField(
upload_to="assets", blank=True, null=True, verbose_name=_("logo 32x32")
)
theme_colors = models.JSONField(
default=dict, verbose_name=_("colores del tema"), blank=True
)
def get_resized_logo(self, size): def get_resized_logo(self, size):
width, height = size width, height = size
image = Image.open(self.logo) image = Image.open(self.logo)
image = image.resize(size, Image.Resampling.LANCZOS) image = image.resize(size, Image.Resampling.LANCZOS)
buffer = BytesIO() buffer = BytesIO()
image.save(fp=buffer, format="PNG") image.save(fp=buffer, format='PNG')
return ContentFile(buffer.getvalue(), name=f"logo_{width}.png") return ContentFile(buffer.getvalue(), name=f'logo_{width}.png')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.logo: if self.logo:
@@ -115,5 +71,5 @@ class WebSettings(SingletonModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _("ajustes de la web") verbose_name = _('ajustes de la web')
verbose_name_plural = _("ajustes de la web") verbose_name_plural = _('ajustes de la web')
+1 -1
View File
@@ -1 +1 @@
ANONYMOUS_CART_ID_COOKIE_NAME = "caid" ANONYMOUS_CART_ID_COOKIE_NAME = 'caid'
+5 -5
View File
@@ -6,13 +6,13 @@ from shop.signals import clear_cart, send_order_email
from web.models import WebSettings from web.models import WebSettings
@receiver(clear_cart, dispatch_uid="clear_cart") @receiver(clear_cart, dispatch_uid='clear_cart')
def create_payment_from_redsys(sender, **kwargs): def clear_cart(sender, **kwargs):
cart = Cart.objects.get(hash=kwargs.get("cart_id")) cart = Cart.objects.get(hash=kwargs.get('cart_id'))
CartItem.objects.filter(cart=cart).delete() CartItem.objects.filter(cart=cart).delete()
@receiver(send_order_email, dispatch_uid="send_order_email") @receiver(send_order_email, dispatch_uid='send_order_email')
def create_payment_from_redsys(sender, **kwargs): def create_payment_from_redsys(sender, **kwargs):
pass pass
@@ -20,6 +20,6 @@ def create_payment_from_redsys(sender, **kwargs):
@receiver(post_save, sender=WebSettings) @receiver(post_save, sender=WebSettings)
def rebuild_theme(sender, instance: WebSettings, **kwargs): def rebuild_theme(sender, instance: WebSettings, **kwargs):
try: try:
call_command("build_tailwind_theme") call_command('build_tailwind_theme')
except Exception: except Exception:
pass pass
+20 -47
View File
@@ -10,22 +10,20 @@ from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
class TestCart(TestCase, CreateProductsMixin): class TestCart(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.product = self.create_product() self.product = self.create_product()
self.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand") self.user = User.objects.create_user('vader', 'darth@vader.com', 'ihatesand')
def test_anonymous_cart(self): def test_anonymous_cart(self):
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
response = self.client.get(reverse("web:cart_dropdown")) response = self.client.get(reverse('web:cart_dropdown'))
assert response.status_code == 200 assert response.status_code == 200
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
assert cart is not None assert cart is not None
assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str( assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str(cart.uuid)
cart.uuid
)
def test_logged_user_without_cart(self): def test_logged_user_without_cart(self):
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:cart_dropdown")) response = self.client.get(reverse('web:cart_dropdown'))
assert response.status_code == 200 assert response.status_code == 200
assert Cart.objects.count() == 1 assert Cart.objects.count() == 1
assert not response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME) assert not response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME)
@@ -33,7 +31,7 @@ class TestCart(TestCase, CreateProductsMixin):
def test_logged_user_with_cart(self): def test_logged_user_with_cart(self):
Cart.objects.create(user=self.user) Cart.objects.create(user=self.user)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:cart_dropdown")) response = self.client.get(reverse('web:cart_dropdown'))
assert response.status_code == 200 assert response.status_code == 200
assert not response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME) assert not response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME)
@@ -41,17 +39,12 @@ class TestCart(TestCase, CreateProductsMixin):
quantity = 2 quantity = 2
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
assert cart is not None assert cart is not None
assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str( assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str(cart.uuid)
cart.uuid
)
cart_item = CartItem.objects.filter(cart=cart).first() cart_item = CartItem.objects.filter(cart=cart).first()
assert cart_item is not None assert cart_item is not None
@@ -63,10 +56,7 @@ class TestCart(TestCase, CreateProductsMixin):
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
@@ -83,10 +73,7 @@ class TestCart(TestCase, CreateProductsMixin):
Cart.objects.create(user=self.user) Cart.objects.create(user=self.user)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
@@ -103,16 +90,10 @@ class TestCart(TestCase, CreateProductsMixin):
Cart.objects.create(user=self.user) Cart.objects.create(user=self.user)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
@@ -129,17 +110,12 @@ class TestCart(TestCase, CreateProductsMixin):
quantity = 2 quantity = 2
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
assert cart is not None assert cart is not None
assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str( assert response.cookies.get(ANONYMOUS_CART_ID_COOKIE_NAME).value == str(cart.uuid)
cart.uuid
)
cart_item = CartItem.objects.filter(cart=cart).first() cart_item = CartItem.objects.filter(cart=cart).first()
assert cart_item is not None assert cart_item is not None
@@ -147,8 +123,8 @@ class TestCart(TestCase, CreateProductsMixin):
assert cart_item.quantity == quantity assert cart_item.quantity == quantity
response = self.client.post( response = self.client.post(
reverse("web:delete_cart_item", kwargs={"pk": cart_item.pk}), reverse('web:delete_cart_item', kwargs={'pk': cart_item.pk}),
{"product": self.product.pk, "quantity": quantity}, {'product': self.product.pk, 'quantity': quantity},
) )
assert response.status_code == 204 assert response.status_code == 204
assert CartItem.objects.filter(cart=cart).count() == 0 assert CartItem.objects.filter(cart=cart).count() == 0
@@ -158,10 +134,7 @@ class TestCart(TestCase, CreateProductsMixin):
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:add_cart_item'), {'product': self.product.pk, 'quantity': quantity})
reverse("web:add_cart_item"),
{"product": self.product.pk, "quantity": quantity},
)
assert response.status_code == 201 assert response.status_code == 201
cart = Cart.objects.all().first() cart = Cart.objects.all().first()
@@ -174,15 +147,15 @@ class TestCart(TestCase, CreateProductsMixin):
assert cart_item.quantity == quantity assert cart_item.quantity == quantity
response = self.client.post( response = self.client.post(
reverse("web:delete_cart_item", kwargs={"pk": cart_item.pk}), reverse('web:delete_cart_item', kwargs={'pk': cart_item.pk}),
{"product": self.product.pk, "quantity": quantity}, {'product': self.product.pk, 'quantity': quantity},
) )
assert response.status_code == 204 assert response.status_code == 204
assert CartItem.objects.filter(cart=cart).count() == 0 assert CartItem.objects.filter(cart=cart).count() == 0
def test_anonymous_cart_detail_component(self): def test_anonymous_cart_detail_component(self):
assert Cart.objects.count() == 0 assert Cart.objects.count() == 0
response = self.client.get(reverse("web:cart")) response = self.client.get(reverse('web:cart'))
assert response.status_code == 200 assert response.status_code == 200
assert Cart.objects.count() == 1 assert Cart.objects.count() == 1
@@ -191,5 +164,5 @@ class TestCart(TestCase, CreateProductsMixin):
cart = Cart.objects.create(user=self.user) cart = Cart.objects.create(user=self.user)
CartItem.objects.create(cart=cart, product=self.product, quantity=1) CartItem.objects.create(cart=cart, product=self.product, quantity=1)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:cart")) response = self.client.get(reverse('web:cart'))
assert response.status_code == 200 assert response.status_code == 200
+2 -6
View File
@@ -10,10 +10,6 @@ class TestCategoryView(TestCase, CreateProductsMixin):
self.product = self.create_product() self.product = self.create_product()
def test_category_view(self): def test_category_view(self):
category = ProductCategory.objects.create( category = ProductCategory.objects.create(name='category 1', show_in_navbar=True, hidden=False)
name="category 1", show_in_navbar=True, hidden=False response = self.client.get(reverse('web:category_view', kwargs={'slug': category.slug}))
)
response = self.client.get(
reverse("web:category_view", kwargs={"slug": category.slug})
)
assert response.status_code == 200 assert response.status_code == 200
+6 -10
View File
@@ -7,22 +7,18 @@ from shop.tests.mixins import CreateProductsMixin
class TestChangeUserPassword(TestCase, CreateProductsMixin): class TestChangeUserPassword(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.password = "ihatesand" self.password = 'ihatesand'
self.user = User.objects.create_user("vader", "darth@vader.com", self.password) self.user = User.objects.create_user('vader', 'darth@vader.com', self.password)
def test_update_user_info(self): def test_update_user_info(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:change_password_component")) response = self.client.get(reverse('web:change_password_component'))
assert response.status_code == 200 assert response.status_code == 200
new_password = "ifnhatesandbro" new_password = 'ifnhatesandbro'
data = { data = {'old_password': self.password, 'new_password1': new_password, 'new_password2': new_password}
"old_password": self.password,
"new_password1": new_password,
"new_password2": new_password,
}
response = self.client.post(reverse("web:change_password_component"), data) response = self.client.post(reverse('web:change_password_component'), data)
assert response.status_code == 200 assert response.status_code == 200
self.user.refresh_from_db() self.user.refresh_from_db()
+1 -1
View File
@@ -9,5 +9,5 @@ class TestWebComponents(TestCase, CreateProductsMixin):
self.product = self.create_product() self.product = self.create_product()
def test_list_products_component(self): def test_list_products_component(self):
response = self.client.get(reverse("web:list_products")) response = self.client.get(reverse('web:list_products'))
assert response.status_code == 200 assert response.status_code == 200
+21 -24
View File
@@ -8,43 +8,40 @@ from shop.tests.mixins import CreateProductsMixin
class TestCustomerAddresses(TestCase, CreateProductsMixin): class TestCustomerAddresses(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand") self.user = User.objects.create_user('vader', 'darth@vader.com', 'ihatesand')
self.customer_address = CustomerAddress.objects.create( self.customer_address = CustomerAddress.objects.create(
user=self.user, user=self.user,
full_name="Darth Vader", full_name='Darth Vader',
address_type=CustomerAddress.Types.SHIPPING, address_type=CustomerAddress.Types.SHIPPING,
address="Vader Fortress", address='Vader Fortress',
address_town="Mustafar", address_town='Mustafar',
address_country="Mustafar", address_country='Mustafar',
address_state="Mustafar", address_state='Mustafar',
address_phone="612345678", address_phone='612345678',
address_zip="00001", address_zip='00001',
) )
def test_update_customer_address(self): def test_update_customer_address(self):
self.client.force_login(self.user) self.client.force_login(self.user)
data = { data = {
"full_name": "Anakin Skywalker", 'full_name': 'Anakin Skywalker',
"address": "Mos Eisley", 'address': 'Mos Eisley',
"address_town": "Mos Eisley", 'address_town': 'Mos Eisley',
"address_country": "Tatooine", 'address_country': 'Tatooine',
"address_state": "Tatooine", 'address_state': 'Tatooine',
"address_phone": "612345678", 'address_phone': '612345678',
"address_zip": "00001", 'address_zip': '00001',
} }
response = self.client.post( response = self.client.post(
reverse( reverse('web:update_customer_address', kwargs={'pk': self.customer_address.pk}), data
"web:update_customer_address", kwargs={"pk": self.customer_address.pk}
),
data,
) )
assert response.status_code == 200 assert response.status_code == 200
self.customer_address.refresh_from_db() self.customer_address.refresh_from_db()
assert self.customer_address.full_name == data.get("full_name") assert self.customer_address.full_name == data.get('full_name')
assert self.customer_address.address == data.get("address") assert self.customer_address.address == data.get('address')
assert self.customer_address.address_town == data.get("address_town") assert self.customer_address.address_town == data.get('address_town')
assert self.customer_address.address_state == data.get("address_state") assert self.customer_address.address_state == data.get('address_state')
assert self.customer_address.address_phone == data.get("address_phone") assert self.customer_address.address_phone == data.get('address_phone')
+2 -5
View File
@@ -9,14 +9,11 @@ class TestIndex(TestCase, CreateProductsMixin):
self.product = self.create_product() self.product = self.create_product()
def test_index_view(self): def test_index_view(self):
response = self.client.get(reverse("web:index")) response = self.client.get(reverse('web:index'))
assert response.status_code == 200 assert response.status_code == 200
def test_product_detail_view(self): def test_product_detail_view(self):
response = self.client.get( response = self.client.get(
reverse( reverse('web:product_detail', kwargs={'pk': self.product.pk, 'slug': self.product.slug})
"web:product_detail",
kwargs={"pk": self.product.pk, "slug": self.product.slug},
)
) )
assert response.status_code == 200 assert response.status_code == 200
+28 -79
View File
@@ -7,142 +7,91 @@ User = get_user_model()
class TestLogin(TestCase): class TestLogin(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.password = "theonering" self.password = 'theonering'
self.user = User.objects.create( self.user = User.objects.create(username='sauron', email='sauron@mordor.middleearth')
username="sauron",
email="sauron@mordor.middleearth",
)
self.user.set_password(self.password) self.user.set_password(self.password)
self.user.save() self.user.save()
def test_login(self): def test_login(self):
response = self.client.post( response = self.client.post(reverse('web:login'), {'username': self.user.username, 'password': self.password})
reverse("web:login"),
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
def test_login_get(self): def test_login_get(self):
response = self.client.get(reverse("web:login")) response = self.client.get(reverse('web:login'))
assert response.status_code == 200 assert response.status_code == 200
def test_login_already_logged_in(self): def test_login_already_logged_in(self):
response = self.client.post( response = self.client.post(reverse('web:login'), {'username': self.user.username, 'password': self.password})
reverse("web:login"),
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
response = self.client.get(reverse("web:login")) response = self.client.get(reverse('web:login'))
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
def test_login_failed(self): def test_login_failed(self):
response = self.client.post( response = self.client.post(
reverse("web:login"), reverse('web:login'), {'username': self.user.username, 'password': 'wrongpassword'}
{
"username": self.user.username,
"password": "wrongpassword",
},
) )
assert response.status_code == 400 assert response.status_code == 400
def test_login_then_logout(self): def test_login_then_logout(self):
response = self.client.post( response = self.client.post(reverse('web:login'), {'username': self.user.username, 'password': self.password})
reverse("web:login"),
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
response = self.client.post(reverse("web:logout")) response = self.client.post(reverse('web:logout'))
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
def test_logout_already_logged_out(self): def test_logout_already_logged_out(self):
response = self.client.post(reverse("web:logout")) response = self.client.post(reverse('web:logout'))
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
def test_login_then_get_logout(self): def test_login_then_get_logout(self):
response = self.client.post( response = self.client.post(reverse('web:login'), {'username': self.user.username, 'password': self.password})
reverse("web:login"),
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
response = self.client.get(reverse("web:logout")) response = self.client.get(reverse('web:logout'))
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
def test_login_then_get_my_account(self): def test_login_then_get_my_account(self):
response = self.client.post( response = self.client.post(reverse('web:login'), {'username': self.user.username, 'password': self.password})
reverse("web:login"),
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:index") assert response.url == reverse('web:index')
response = self.client.get(reverse("web:my_account")) response = self.client.get(reverse('web:my_account'))
assert response.status_code == 200 assert response.status_code == 200
def test_login_redirection_to_my_account(self): def test_login_redirection_to_my_account(self):
url = f'{reverse("web:login")}?next={reverse("web:my_account")}' url = f'{reverse("web:login")}?next={reverse("web:my_account")}'
response = self.client.post( response = self.client.post(url, {'username': self.user.username, 'password': self.password})
url,
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert ( assert response.url == f'{reverse("web:index")}?next={reverse("web:my_account")}'
response.url == f'{reverse("web:index")}?next={reverse("web:my_account")}'
)
response = self.client.get(response.url) response = self.client.get(response.url)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:my_account") assert response.url == reverse('web:my_account')
def test_login_redirection_to_my_orders(self): def test_login_redirection_to_my_orders(self):
url = f'{reverse("web:login")}?next={reverse("web:orders")}' url = f'{reverse("web:login")}?next={reverse("web:orders")}'
response = self.client.post( response = self.client.post(url, {'username': self.user.username, 'password': self.password})
url,
{
"username": self.user.username,
"password": self.password,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == f'{reverse("web:index")}?next={reverse("web:orders")}' assert response.url == f'{reverse("web:index")}?next={reverse("web:orders")}'
response = self.client.get(response.url) response = self.client.get(response.url)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:orders") assert response.url == reverse('web:orders')
+3 -6
View File
@@ -12,14 +12,11 @@ class TestManifest(TestCase):
def setUp(self): def setUp(self):
self.settings = WebSettings.load() self.settings = WebSettings.load()
with open( with open(os.path.join(settings.BASE_DIR, 'web', 'tests', 'images', 'trolley.png'), 'rb') as f:
os.path.join(settings.BASE_DIR, "web", "tests", "images", "trolley.png"), file = SimpleUploadedFile('logo.png', f.read())
"rb",
) as f:
file = SimpleUploadedFile("logo.png", f.read())
self.settings.logo = file self.settings.logo = file
self.settings.save() self.settings.save()
def test_manifest_view(self): def test_manifest_view(self):
response = self.client.get(reverse("web:manifest_json")) response = self.client.get(reverse('web:manifest_json'))
assert response.status_code == 200 assert response.status_code == 200
+53 -68
View File
@@ -4,91 +4,76 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from shop.models import ( from shop.models import Cart, CartItem, CustomerAddress, Order, ShippingMethod, ShopSettings
Cart,
CartItem,
CustomerAddress,
Order,
ShippingMethod,
ShopSettings,
)
from shop.tests.mixins import CreateProductsMixin from shop.tests.mixins import CreateProductsMixin
class TestOrders(TestCase, CreateProductsMixin): class TestOrders(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.product = self.create_product() self.product = self.create_product()
self.user = User.objects.create_user( self.user = User.objects.create_user('anakin', 'anakin@skywalker.com', 'ihatesand')
"anakin", "anakin@skywalker.com", "ihatesand"
)
self.cart_for_user = self.create_cart(user=self.user) self.cart_for_user = self.create_cart(user=self.user)
self.anonymous_cart = self.create_cart() self.anonymous_cart = self.create_cart()
self.address = CustomerAddress.objects.create( self.address = CustomerAddress.objects.create(
vat_id="11111111H", vat_id='11111111H',
full_name="Vader", full_name='Vader',
address="Tatooine", address='Tatooine',
address_town="Mos Eisley", address_town='Mos Eisley',
address_zip="00001", address_zip='00001',
address_state="Tatooine", address_state='Tatooine',
address_country="Mos Eisley", address_country='Mos Eisley',
address_phone="00000000", address_phone='00000000',
address_type=CustomerAddress.Types.BILLING, address_type=CustomerAddress.Types.BILLING,
email=self.user.email, email=self.user.email,
default=True, default=True,
) )
self.shipping_method = self.create_shipping_method() self.shipping_method = self.create_shipping_method()
self.settings = ShopSettings.load() self.settings = ShopSettings.load()
self.settings.shared_secret = "sq7HjrUOBfKmC576ILgskD5srU870gJ7" # Debug secret self.settings.shared_secret = 'sq7HjrUOBfKmC576ILgskD5srU870gJ7' # Debug secret
self.settings.save() self.settings.save()
def create_cart(self, user=None): def create_cart(self, user=None):
cart = Cart.objects.create( cart = Cart.objects.create(user=user)
user=user, CartItem.objects.create(cart=cart, product=self.product, quantity=1)
)
CartItem.objects.create(
cart=cart,
product=self.product,
quantity=1,
)
return cart return cart
def create_shipping_method(self): def create_shipping_method(self):
product = self.create_product(sku="SM1", name="SM1", price=Decimal("1.00")) product = self.create_product(sku='SM1', name='SM1', price=Decimal('1.00'))
method = ShippingMethod.objects.create(name="SM 1", shipping_product=product) method = ShippingMethod.objects.create(name='SM 1', shipping_product=product)
return method return method
def test_logged_user_orders_page(self): def test_logged_user_orders_page(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:orders")) response = self.client.get(reverse('web:orders'))
assert response.status_code == 200 assert response.status_code == 200
def test_logged_user_create_get_order_page(self): def test_logged_user_create_get_order_page(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:cart_detail")) response = self.client.get(reverse('web:cart_detail'))
assert response.status_code == 200 assert response.status_code == 200
# Crear pedido # Crear pedido
data = { data = {
"email": self.user.email, 'email': self.user.email,
"shipping_address_full_name": self.address.full_name, 'shipping_address_full_name': self.address.full_name,
"shipping_address": self.address.address, 'shipping_address': self.address.address,
"shipping_address_town": self.address.address_town, 'shipping_address_town': self.address.address_town,
"shipping_address_zip": self.address.address_zip, 'shipping_address_zip': self.address.address_zip,
"shipping_address_state": self.address.address_state, 'shipping_address_state': self.address.address_state,
"shipping_address_country": self.address.address_country, 'shipping_address_country': self.address.address_country,
"shipping_address_phone": self.address.address_phone, 'shipping_address_phone': self.address.address_phone,
"same_as_shipping": True, 'same_as_shipping': True,
"billing_address_full_name": self.address.full_name, 'billing_address_full_name': self.address.full_name,
"billing_address": self.address.address, 'billing_address': self.address.address,
"billing_address_town": self.address.address_town, 'billing_address_town': self.address.address_town,
"billing_address_zip": self.address.address_zip, 'billing_address_zip': self.address.address_zip,
"billing_address_state": self.address.address_state, 'billing_address_state': self.address.address_state,
"billing_address_country": self.address.address_country, 'billing_address_country': self.address.address_country,
"billing_address_phone": self.address.address_phone, 'billing_address_phone': self.address.address_phone,
"shipping_method": self.shipping_method.pk, 'shipping_method': self.shipping_method.pk,
} }
response = self.client.post(reverse("web:cart_detail"), data) response = self.client.post(reverse('web:cart_detail'), data)
assert response.status_code == 302 assert response.status_code == 302
order = Order.objects.filter(user=self.user).first() order = Order.objects.filter(user=self.user).first()
@@ -99,30 +84,30 @@ class TestOrders(TestCase, CreateProductsMixin):
def test_logged_user_create_get_order_page_invalid_data(self): def test_logged_user_create_get_order_page_invalid_data(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:cart_detail")) response = self.client.get(reverse('web:cart_detail'))
assert response.status_code == 200 assert response.status_code == 200
# Sin e-mail # Sin e-mail
data = { data = {
"shipping_address_full_name": self.address.full_name, 'shipping_address_full_name': self.address.full_name,
"shipping_address": self.address.address, 'shipping_address': self.address.address,
"shipping_address_town": self.address.address_town, 'shipping_address_town': self.address.address_town,
"shipping_address_zip": self.address.address_zip, 'shipping_address_zip': self.address.address_zip,
"shipping_address_state": self.address.address_state, 'shipping_address_state': self.address.address_state,
"shipping_address_country": self.address.address_country, 'shipping_address_country': self.address.address_country,
"shipping_address_phone": self.address.address_phone, 'shipping_address_phone': self.address.address_phone,
"same_as_shipping": True, 'same_as_shipping': True,
"billing_address_full_name": self.address.full_name, 'billing_address_full_name': self.address.full_name,
"billing_address": self.address.address, 'billing_address': self.address.address,
"billing_address_town": self.address.address_town, 'billing_address_town': self.address.address_town,
"billing_address_zip": self.address.address_zip, 'billing_address_zip': self.address.address_zip,
"billing_address_state": self.address.address_state, 'billing_address_state': self.address.address_state,
"billing_address_country": self.address.address_country, 'billing_address_country': self.address.address_country,
"billing_address_phone": self.address.address_phone, 'billing_address_phone': self.address.address_phone,
"shipping_method": self.shipping_method.pk, 'shipping_method': self.shipping_method.pk,
} }
response = self.client.post(reverse("web:cart_detail"), data) response = self.client.post(reverse('web:cart_detail'), data)
assert response.status_code == 200 assert response.status_code == 200
assert not Order.objects.filter(user=self.user).exists() assert not Order.objects.filter(user=self.user).exists()
+42 -60
View File
@@ -7,124 +7,106 @@ User = get_user_model()
class TestRegister(TransactionTestCase): class TestRegister(TransactionTestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.password = "theonering" self.password = 'theonering'
self.email = "sauron@mordor.middleearth" self.email = 'sauron@mordor.middleearth'
self.first_name = "Sauron" self.first_name = 'Sauron'
self.last_name = "The Lord of the Rings" self.last_name = 'The Lord of the Rings'
def test_get_register_page(self): def test_get_register_page(self):
response = self.client.get(reverse("web:register")) response = self.client.get(reverse('web:register'))
assert response.status_code == 200 assert response.status_code == 200
def test_create_user_account(self): def test_create_user_account(self):
response = self.client.post( response = self.client.post(
reverse("web:register"), reverse('web:register'),
{ {
"email": self.email, 'email': self.email,
"password1": self.password, 'password1': self.password,
"password2": self.password, 'password2': self.password,
"first_name": self.first_name, 'first_name': self.first_name,
"last_name": self.last_name, 'last_name': self.last_name,
}, },
) )
user = User.objects.filter( user = User.objects.filter(
username=self.email, username=self.email, email=self.email, first_name=self.first_name, last_name=self.last_name
email=self.email,
first_name=self.first_name,
last_name=self.last_name,
).first() ).first()
assert user is not None assert user is not None
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:login") assert response.url == reverse('web:login')
def test_create_user_account_distinct_password(self): def test_create_user_account_distinct_password(self):
response = self.client.post( response = self.client.post(
reverse("web:register"), reverse('web:register'),
{ {
"email": self.email, 'email': self.email,
"password1": self.password, 'password1': self.password,
"password2": "otherpassword", 'password2': 'otherpassword',
"first_name": self.first_name, 'first_name': self.first_name,
"last_name": self.last_name, 'last_name': self.last_name,
}, },
) )
assert not User.objects.filter( assert not User.objects.filter(
username=self.email, username=self.email, email=self.email, first_name=self.first_name, last_name=self.last_name
email=self.email,
first_name=self.first_name,
last_name=self.last_name,
).exists() ).exists()
assert response.status_code == 400 assert response.status_code == 400
def test_create_user_account_no_first_name(self): def test_create_user_account_no_first_name(self):
response = self.client.post( response = self.client.post(
reverse("web:register"), reverse('web:register'),
{ {
"email": self.email, 'email': self.email,
"password1": self.password, 'password1': self.password,
"password2": "otherpassword", 'password2': 'otherpassword',
"first_name": "", 'first_name': '',
"last_name": self.last_name, 'last_name': self.last_name,
}, },
) )
assert not User.objects.filter( assert not User.objects.filter(
username=self.email, username=self.email, email=self.email, first_name='', last_name=self.last_name
email=self.email,
first_name="",
last_name=self.last_name,
).exists() ).exists()
assert response.status_code == 400 assert response.status_code == 400
def test_create_user_account_no_last_name(self): def test_create_user_account_no_last_name(self):
response = self.client.post( response = self.client.post(
reverse("web:register"), reverse('web:register'),
{ {
"email": self.email, 'email': self.email,
"password1": self.password, 'password1': self.password,
"password2": self.password, 'password2': self.password,
"first_name": self.first_name, 'first_name': self.first_name,
"last_name": "", 'last_name': '',
}, },
) )
assert not User.objects.filter( assert not User.objects.filter(
username=self.email, username=self.email, email=self.email, first_name=self.first_name, last_name=''
email=self.email,
first_name=self.first_name,
last_name="",
).exists() ).exists()
assert response.status_code == 400 assert response.status_code == 400
def test_create_user_account_already_existing(self): def test_create_user_account_already_existing(self):
assert User.objects.all().count() == 0 assert User.objects.all().count() == 0
User.objects.create_user( User.objects.create_user(email=self.email, username=self.email)
email=self.email,
username=self.email,
)
response = self.client.post( response = self.client.post(
reverse("web:register"), reverse('web:register'),
{ {
"email": self.email, 'email': self.email,
"password1": self.password, 'password1': self.password,
"password2": self.password, 'password2': self.password,
"first_name": self.first_name, 'first_name': self.first_name,
"last_name": self.last_name, 'last_name': self.last_name,
}, },
) )
assert not User.objects.filter( assert not User.objects.filter(
username=self.email, username=self.email, email=self.email, first_name=self.first_name, last_name=''
email=self.email,
first_name=self.first_name,
last_name="",
).exists() ).exists()
assert response.status_code == 400 assert response.status_code == 400
+22 -57
View File
@@ -10,53 +10,36 @@ User = get_user_model()
class TestResetPassword(TransactionTestCase): class TestResetPassword(TransactionTestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.password = "theonering" self.password = 'theonering'
self.email = "sauron@mordor.middleearth" self.email = 'sauron@mordor.middleearth'
self.first_name = "Sauron" self.first_name = 'Sauron'
self.last_name = "The Lord of the Rings" self.last_name = 'The Lord of the Rings'
self.user = User.objects.create_user( self.user = User.objects.create_user(
username=self.email, username=self.email, password=self.password, first_name=self.first_name, last_name=self.last_name
password=self.password,
first_name=self.first_name,
last_name=self.last_name,
) )
def test_get_reset_password_page(self): def test_get_reset_password_page(self):
response = self.client.get(reverse("web:reset_password")) response = self.client.get(reverse('web:reset_password'))
assert response.status_code == 200 assert response.status_code == 200
def test_reset_password(self): def test_reset_password(self):
response = self.client.post( response = self.client.post(reverse('web:reset_password'), {'email': self.email})
reverse("web:reset_password"),
{
"email": self.email,
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:reset_password_email_sent") assert response.url == reverse('web:reset_password_email_sent')
def test_reset_password_not_existing_user(self): def test_reset_password_not_existing_user(self):
response = self.client.post( response = self.client.post(reverse('web:reset_password'), {'email': 'vanpiro@noesiten.com'})
reverse("web:reset_password"),
{
"email": "vanpiro@noesiten.com",
},
)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:reset_password_email_sent") assert response.url == reverse('web:reset_password_email_sent')
def test_reset_password_confirm_get(self): def test_reset_password_confirm_get(self):
token = default_token_generator.make_token(self.user) token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk)) uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
response = self.client.get( response = self.client.get(reverse('web:reset_password_confirm', kwargs={'uidb64': uidb64, 'token': token}))
reverse(
"web:reset_password_confirm", kwargs={"uidb64": uidb64, "token": token}
),
)
assert response.status_code == 200 assert response.status_code == 200
@@ -65,20 +48,14 @@ class TestResetPassword(TransactionTestCase):
form_token = default_token_generator.make_token(self.user) form_token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk)) uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
new_password = "89733wy29" new_password = '89733wy29'
response = self.client.post( response = self.client.post(
reverse( reverse('web:reset_password_confirm', kwargs={'uidb64': uidb64, 'token': token}),
"web:reset_password_confirm", kwargs={"uidb64": uidb64, "token": token} {'token': form_token, 'new_password1': new_password, 'new_password2': new_password},
),
{
"token": form_token,
"new_password1": new_password,
"new_password2": new_password,
},
) )
assert response.status_code == 302 assert response.status_code == 302
assert response.url == reverse("web:login") assert response.url == reverse('web:login')
self.user.refresh_from_db() self.user.refresh_from_db()
assert self.user.check_password(new_password) assert self.user.check_password(new_password)
@@ -88,35 +65,23 @@ class TestResetPassword(TransactionTestCase):
form_token = default_token_generator.make_token(self.user) form_token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk)) uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
new_password = "89733wy29" new_password = '89733wy29'
response = self.client.post( response = self.client.post(
reverse( reverse('web:reset_password_confirm', kwargs={'uidb64': uidb64, 'token': token}),
"web:reset_password_confirm", kwargs={"uidb64": uidb64, "token": token} {'token': form_token, 'new_password1': new_password, 'new_password2': 'otherpassw13'},
),
{
"token": form_token,
"new_password1": new_password,
"new_password2": "otherpassw13",
},
) )
assert response.status_code == 200 assert response.status_code == 200
def test_reset_password_confirm_invalid_token(self): def test_reset_password_confirm_invalid_token(self):
token = default_token_generator.make_token(self.user) token = default_token_generator.make_token(self.user)
form_token = "invalid token" form_token = 'invalid token'
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk)) uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
new_password = "89733wy29" new_password = '89733wy29'
response = self.client.post( response = self.client.post(
reverse( reverse('web:reset_password_confirm', kwargs={'uidb64': uidb64, 'token': token}),
"web:reset_password_confirm", kwargs={"uidb64": uidb64, "token": token} {'token': form_token, 'new_password1': new_password, 'new_password2': new_password},
),
{
"token": form_token,
"new_password1": new_password,
"new_password2": new_password,
},
) )
assert response.status_code == 200 assert response.status_code == 200
+7 -11
View File
@@ -7,25 +7,21 @@ from shop.tests.mixins import CreateProductsMixin
class TestUserInfo(TestCase, CreateProductsMixin): class TestUserInfo(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand") self.user = User.objects.create_user('vader', 'darth@vader.com', 'ihatesand')
def test_update_user_info(self): def test_update_user_info(self):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:user_info_component")) response = self.client.get(reverse('web:user_info_component'))
assert response.status_code == 200 assert response.status_code == 200
data = { data = {'first_name': 'Anakin', 'last_name': 'Skywalker', 'email': 'anakin@skywalker.com'}
"first_name": "Anakin",
"last_name": "Skywalker",
"email": "anakin@skywalker.com",
}
response = self.client.post(reverse("web:user_info_component"), data) response = self.client.post(reverse('web:user_info_component'), data)
assert response.status_code == 200 assert response.status_code == 200
self.user.refresh_from_db() self.user.refresh_from_db()
assert self.user.first_name == data.get("first_name") assert self.user.first_name == data.get('first_name')
assert self.user.last_name == data.get("last_name") assert self.user.last_name == data.get('last_name')
assert self.user.email == data.get("email") assert self.user.email == data.get('email')
+13 -38
View File
@@ -9,77 +9,52 @@ from shop.tests.mixins import CreateProductsMixin
class TestWishlist(TestCase, CreateProductsMixin): class TestWishlist(TestCase, CreateProductsMixin):
def setUp(self): def setUp(self):
self.product = self.create_product() self.product = self.create_product()
self.user = User.objects.create_user("vader", "darth@vader.com", "ihatesand") self.user = User.objects.create_user('vader', 'darth@vader.com', 'ihatesand')
def test_logged_user_wishlist_button_for_non_wishlisted_product(self): def test_logged_user_wishlist_button_for_non_wishlisted_product(self):
assert WishlistedProduct.objects.count() == 0 assert WishlistedProduct.objects.count() == 0
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get( response = self.client.get(reverse('web:wishlist_button', kwargs={'pk': self.product.pk}))
reverse("web:wishlist_button", kwargs={"pk": self.product.pk})
)
assert response.status_code == 200 assert response.status_code == 200
def test_logged_user_wishlist_button_for_wishlisted_product(self): def test_logged_user_wishlist_button_for_wishlisted_product(self):
WishlistedProduct.objects.create( WishlistedProduct.objects.create(user=self.user, product=self.product)
user=self.user,
product=self.product,
)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get( response = self.client.get(reverse('web:wishlist_button', kwargs={'pk': self.product.pk}))
reverse("web:wishlist_button", kwargs={"pk": self.product.pk})
)
assert response.status_code == 200 assert response.status_code == 200
def test_logged_user_create_wishlisted_product(self): def test_logged_user_create_wishlisted_product(self):
assert WishlistedProduct.objects.count() == 0 assert WishlistedProduct.objects.count() == 0
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:add_to_wishlist'), {'product': self.product.pk})
reverse("web:add_to_wishlist"),
{
"product": self.product.pk,
},
)
assert response.status_code == 201 assert response.status_code == 201
assert WishlistedProduct.objects.filter( assert WishlistedProduct.objects.filter(user=self.user, product=self.product).exists()
user=self.user, product=self.product
).exists()
def test_not_logged_user_create_wishlisted_product(self): def test_not_logged_user_create_wishlisted_product(self):
assert WishlistedProduct.objects.count() == 0 assert WishlistedProduct.objects.count() == 0
response = self.client.post( response = self.client.post(reverse('web:add_to_wishlist'), {'product': self.product.pk})
reverse("web:add_to_wishlist"),
{
"product": self.product.pk,
},
)
assert response.status_code == 400 assert response.status_code == 400
def test_logged_user_delete_wishlisted_product(self): def test_logged_user_delete_wishlisted_product(self):
wishlisted = WishlistedProduct.objects.create( wishlisted = WishlistedProduct.objects.create(user=self.user, product=self.product)
user=self.user, product=self.product
)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(reverse('web:delete_from_wishlist', kwargs={'pk': wishlisted.pk}))
reverse("web:delete_from_wishlist", kwargs={"pk": wishlisted.pk})
)
assert response.status_code == 204 assert response.status_code == 204
assert not WishlistedProduct.objects.filter( assert not WishlistedProduct.objects.filter(user=self.user, product=self.product).exists()
user=self.user, product=self.product
).exists()
def test_logged_user_wishlist(self): def test_logged_user_wishlist(self):
WishlistedProduct.objects.create(user=self.user, product=self.product) WishlistedProduct.objects.create(user=self.user, product=self.product)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:wishlist")) response = self.client.get(reverse('web:wishlist'))
assert response.status_code == 200 assert response.status_code == 200
def test_not_logged_user_wishlist(self): def test_not_logged_user_wishlist(self):
WishlistedProduct.objects.create(user=self.user, product=self.product) WishlistedProduct.objects.create(user=self.user, product=self.product)
response = self.client.get(reverse("web:wishlist")) response = self.client.get(reverse('web:wishlist'))
assert response.status_code == 302 assert response.status_code == 302
def test_list_wishlisted_products(self): def test_list_wishlisted_products(self):
WishlistedProduct.objects.create(user=self.user, product=self.product) WishlistedProduct.objects.create(user=self.user, product=self.product)
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("web:list_wishlisted_products")) response = self.client.get(reverse('web:list_wishlisted_products'))
assert response.status_code == 200 assert response.status_code == 200
+33 -67
View File
@@ -38,86 +38,52 @@ from web.views.web import (
wishlist, wishlist,
) )
app_name = "web" app_name = 'web'
i18n_resolvers = i18n_patterns( i18n_resolvers = i18n_patterns(
path("", index, name="index"), path('', index, name='index'),
path( path(_('wishlist'), login_required(wishlist, login_url=_('/login/')), name='wishlist'),
_("wishlist"), login_required(wishlist, login_url=_("/login/")), name="wishlist" path(_('categories/<str:slug>/'), category_view, name='category_view'),
), path(_('products/<int:pk>/<str:slug>/'), product_detail, name='product_detail'),
path(_("categories/<str:slug>/"), category_view, name="category_view"), path(_('cart/'), cart_detail, name='cart_detail'),
path(_("products/<int:pk>/<str:slug>/"), product_detail, name="product_detail"), path(_('orders/'), login_required(orders, login_url=_('/login/')), name='orders'),
path(_("cart/"), cart_detail, name="cart_detail"), path(_('order/<str:uuid>/'), order_detail, name='order'),
path(_("orders/"), login_required(orders, login_url=_("/login/")), name="orders"),
path(_("order/<str:uuid>/"), order_detail, name="order"),
# users # users
path(_("login/"), login, name="login"), path(_('login/'), login, name='login'),
path(_("logout/"), logout, name="logout"), path(_('logout/'), logout, name='logout'),
path(_("register/"), register, name="register"), path(_('register/'), register, name='register'),
path(_("reset-password/"), reset_password, name="reset_password"), path(_('reset-password/'), reset_password, name='reset_password'),
path( path(_('reset-password/<uidb64>/<token>/'), reset_password_confirm, name='reset_password_confirm'),
_("reset-password/<uidb64>/<token>/"), path(_('reset-password/email-sent/'), reset_password_email_sent, name='reset_password_email_sent'),
reset_password_confirm, path(_('my-account/'), login_required(my_account, login_url=_('/login/')), name='my_account'),
name="reset_password_confirm",
),
path(
_("reset-password/email-sent/"),
reset_password_email_sent,
name="reset_password_email_sent",
),
path(
_("my-account/"),
login_required(my_account, login_url=_("/login/")),
name="my_account",
),
) )
urlpatterns = [ urlpatterns = [
# components # components
path("web/components/products/", list_products, name="list_products"), path('web/components/products/', list_products, name='list_products'),
path('web/components/wishlist/', list_wishlisted_products, name='list_wishlisted_products'),
path('web/components/cart-dropdown/', cart_dropdown, name='cart_dropdown'),
path('web/components/cart/', cart, name='cart'),
path('web/components/wishlist-button/<int:pk>/', wishlist_button, name='wishlist_button'),
path( path(
"web/components/wishlist/", 'web/components/user-info/',
list_wishlisted_products, login_required(user_info_component, login_url=_('/login/')),
name="list_wishlisted_products", name='user_info_component',
),
path("web/components/cart-dropdown/", cart_dropdown, name="cart_dropdown"),
path("web/components/cart/", cart, name="cart"),
path(
"web/components/wishlist-button/<int:pk>/",
wishlist_button,
name="wishlist_button",
), ),
path( path(
"web/components/user-info/", 'web/components/change-password/',
login_required(user_info_component, login_url=_("/login/")), login_required(change_password_component, login_url=_('/login/')),
name="user_info_component", name='change_password_component',
),
path(
"web/components/change-password/",
login_required(change_password_component, login_url=_("/login/")),
name="change_password_component",
), ),
# api # api
path("web/add-cart-item/", add_cart_item, name="add_cart_item"), path('web/add-cart-item/', add_cart_item, name='add_cart_item'),
path("web/delete-cart-item/<int:pk>/", delete_cart_item, name="delete_cart_item"), path('web/delete-cart-item/<int:pk>/', delete_cart_item, name='delete_cart_item'),
path("web/add-to-wishlist/", add_to_wishlist, name="add_to_wishlist"), path('web/add-to-wishlist/', add_to_wishlist, name='add_to_wishlist'),
path( path('web/delete-from-wishlist/<int:pk>/', delete_from_wishlist, name='delete_from_wishlist'),
"web/delete-from-wishlist/<int:pk>/", path('web/customer-address/<int:pk>/', update_customer_address, name='update_customer_address'),
delete_from_wishlist, path('web/customer-addresses/', list_customer_addresses_component, name='list_customer_addresses_component'),
name="delete_from_wishlist",
),
path(
"web/customer-address/<int:pk>/",
update_customer_address,
name="update_customer_address",
),
path(
"web/customer-addresses/",
list_customer_addresses_component,
name="list_customer_addresses_component",
),
# manifest # manifest
path("manifest.json", manifest, name="manifest_json"), path('manifest.json', manifest, name='manifest_json'),
] ]
for resolver in i18n_resolvers: for resolver in i18n_resolvers:
+47 -81
View File
@@ -10,14 +10,7 @@ from django.views.generic import TemplateView
from config.mixins import HTMXFormComponent from config.mixins import HTMXFormComponent
from shop.filters import ProductFilter from shop.filters import ProductFilter
from shop.models import ( from shop.models import CartItem, CustomerAddress, Product, ProductPrice, ShippingMethod, WishlistedProduct
CartItem,
CustomerAddress,
Product,
ProductPrice,
ShippingMethod,
WishlistedProduct,
)
from users.forms.change_password import ChangePasswordForm from users.forms.change_password import ChangePasswordForm
from users.forms.info import UserInfoForm from users.forms.info import UserInfoForm
from web.forms import CustomerAddressForm from web.forms import CustomerAddressForm
@@ -27,10 +20,9 @@ from web.utils import get_or_create_cart
class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin): class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
template_name = "web/list_products.html" template_name = 'web/list_products.html'
queryset = Product.objects.filter(hidden=False).prefetch_related( queryset = Product.objects.filter(hidden=False).prefetch_related(
"images", 'images', Prefetch('prices', queryset=ProductPrice.objects.filter(current=True), to_attr='current_prices')
Prefetch("prices", queryset=ProductPrice.objects.filter(current=True), to_attr="current_prices")
) )
filter_class = ProductFilter filter_class = ProductFilter
@@ -39,27 +31,21 @@ class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
page = self.get_paginated_queryset(qs) page = self.get_paginated_queryset(qs)
return { return {
"current_page": page.number, 'current_page': page.number,
"num_pages": page.paginator.num_pages, 'num_pages': page.paginator.num_pages,
"page": page, 'page': page,
"has_next_page": page.has_next(), 'has_next_page': page.has_next(),
"has_previous_page": page.has_previous(), 'has_previous_page': page.has_previous(),
} }
class ListWishlistedProducts( class ListWishlistedProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin template_name = 'web/list_products.html'
): queryset = Product.objects.filter(hidden=False).prefetch_related('images')
template_name = "web/list_products.html"
queryset = Product.objects.filter(hidden=False).prefetch_related(
"images",
)
filter_class = ProductFilter filter_class = ProductFilter
def get_queryset(self): def get_queryset(self):
product_ids = WishlistedProduct.objects.filter( product_ids = WishlistedProduct.objects.filter(user=self.request.user).values_list('product', flat=True)
user=self.request.user
).values_list("product", flat=True)
return super().get_queryset().filter(pk__in=product_ids).distinct() return super().get_queryset().filter(pk__in=product_ids).distinct()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -67,27 +53,21 @@ class ListWishlistedProducts(
page = self.get_paginated_queryset(qs) page = self.get_paginated_queryset(qs)
return { return {
"current_page": page.number, 'current_page': page.number,
"num_pages": page.paginator.num_pages, 'num_pages': page.paginator.num_pages,
"page": page, 'page': page,
"has_next_page": page.has_next(), 'has_next_page': page.has_next(),
"has_previous_page": page.has_previous(), 'has_previous_page': page.has_previous(),
} }
def cart_dropdown(request, *args, **kwargs): def cart_dropdown(request, *args, **kwargs):
cart, created = get_or_create_cart(request) cart, created = get_or_create_cart(request)
response = render( response = render(request, 'components/cart/cart_navbar.html', context={'cart': cart})
request,
"components/cart/cart_navbar.html",
context={
"cart": cart,
},
)
if not request.user.is_authenticated: if not request.user.is_authenticated:
response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite="strict") response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite='strict')
return response return response
@@ -95,15 +75,13 @@ def cart(request, *args, **kwargs):
cart, created = get_or_create_cart(request) cart, created = get_or_create_cart(request)
items = CartItem.objects.filter(cart=cart) items = CartItem.objects.filter(cart=cart)
base_total = Decimal("0.00") base_total = Decimal('0.00')
tax_total = Decimal("0.00") tax_total = Decimal('0.00')
for item in items: for item in items:
price = ProductPrice.objects.filter(product=item.product, current=True).first() price = ProductPrice.objects.filter(product=item.product, current=True).first()
base_total += price.price * item.quantity base_total += price.price * item.quantity
tax_total += round( tax_total += round(price.price * Decimal(price.tax.value / 100) * item.quantity, 2)
price.price * Decimal(price.tax.value / 100) * item.quantity, 2
)
total = base_total + tax_total total = base_total + tax_total
@@ -112,8 +90,7 @@ def cart(request, *args, **kwargs):
user=request.user, address_type=CustomerAddress.Types.BILLING, default=True user=request.user, address_type=CustomerAddress.Types.BILLING, default=True
).first() ).first()
shipping_address = CustomerAddress.objects.filter( shipping_address = CustomerAddress.objects.filter(
user=request.user, user=request.user, address_type=CustomerAddress.Types.SHIPPING
address_type=CustomerAddress.Types.SHIPPING,
).first() ).first()
else: else:
billing_address = None billing_address = None
@@ -123,62 +100,53 @@ def cart(request, *args, **kwargs):
response = render( response = render(
request, request,
"components/cart/cart.html", 'components/cart/cart.html',
context={ context={
"cart": cart, 'cart': cart,
"items": items, 'items': items,
"total": total, 'total': total,
"base_total": base_total, 'base_total': base_total,
"tax_total": tax_total, 'tax_total': tax_total,
"billing_address": billing_address, 'billing_address': billing_address,
"shipping_address": shipping_address, 'shipping_address': shipping_address,
"shipping_methods": shipping_methods, 'shipping_methods': shipping_methods,
}, },
) )
if not request.user.is_authenticated: if not request.user.is_authenticated:
response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite="strict") response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite='strict')
return response return response
class WishlistButton(TemplateView): class WishlistButton(TemplateView):
template_name = "components/products/wishlist_button.html" template_name = 'components/products/wishlist_button.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
product = get_object_or_404(Product, pk=self.kwargs.get("pk")) product = get_object_or_404(Product, pk=self.kwargs.get('pk'))
is_wishlisted = WishlistedProduct.objects.filter( is_wishlisted = WishlistedProduct.objects.filter(user=self.request.user, product=product).exists()
user=self.request.user, product=product
).exists()
return { return {'is_wishlisted': is_wishlisted, 'product': product}
"is_wishlisted": is_wishlisted,
"product": product,
}
class UserInfoFormComponentView(HTMXFormComponent): class UserInfoFormComponentView(HTMXFormComponent):
form_class = UserInfoForm form_class = UserInfoForm
hx_trigger = "updated_user_info" hx_trigger = 'updated_user_info'
save_button_text = _("Actualizar datos") save_button_text = _('Actualizar datos')
def get_object(self) -> models.Model: def get_object(self) -> models.Model:
return self.request.user return self.request.user
def get_view_name(self): def get_view_name(self):
return reverse("web:user_info_component") return reverse('web:user_info_component')
def get_initial_values(self, instance): def get_initial_values(self, instance):
return { return {'email': instance.email, 'first_name': instance.first_name, 'last_name': instance.last_name}
"email": instance.email,
"first_name": instance.first_name,
"last_name": instance.last_name,
}
class ChangePasswordFormComponentView(HTMXFormComponent): class ChangePasswordFormComponentView(HTMXFormComponent):
form_class = ChangePasswordForm form_class = ChangePasswordForm
hx_trigger = "changed_password" hx_trigger = 'changed_password'
save_button_text = _("Cambiar contraseña") save_button_text = _('Cambiar contraseña')
def get_form(self, initial=True): def get_form(self, initial=True):
if initial: if initial:
@@ -190,16 +158,14 @@ class ChangePasswordFormComponentView(HTMXFormComponent):
return self.request.user return self.request.user
def get_view_name(self): def get_view_name(self):
return reverse("web:change_password_component") return reverse('web:change_password_component')
class ListCustomerAddressComponent(TemplateView): class ListCustomerAddressComponent(TemplateView):
template_name = "components/users/retrieve_update_address.html" template_name = 'components/users/retrieve_update_address.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return { return {'customer_addresses': CustomerAddress.objects.filter(user=self.request.user)}
"customer_addresses": CustomerAddress.objects.filter(user=self.request.user)
}
def update_customer_address(request, pk): def update_customer_address(request, pk):
@@ -208,7 +174,7 @@ def update_customer_address(request, pk):
if form.is_valid(): if form.is_valid():
form.save() form.save()
return HttpResponse(status=200, headers={"HX-Trigger": "updated_addresses"}) return HttpResponse(status=200, headers={'HX-Trigger': 'updated_addresses'})
return HttpResponse(form.errors, status=400) return HttpResponse(form.errors, status=400)
+48 -85
View File
@@ -6,11 +6,7 @@ from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.shortcuts import redirect, render, reverse from django.shortcuts import redirect, render, reverse
from django.utils.http import ( from django.utils.http import url_has_allowed_host_and_scheme, urlencode, urlsafe_base64_decode
url_has_allowed_host_and_scheme,
urlencode,
urlsafe_base64_decode,
)
from django.utils.text import gettext_lazy as _ from django.utils.text import gettext_lazy as _
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
@@ -22,22 +18,22 @@ User = get_user_model()
class RedirectionMixin: class RedirectionMixin:
redirect_to = "" redirect_to = ''
def get_redirection(self): def get_redirection(self):
return reverse(self.redirect_to) return reverse(self.redirect_to)
class LoginView(View, RedirectionMixin): class LoginView(View, RedirectionMixin):
template_name = "users/login.html" template_name = 'users/login.html'
redirect_to = "web:index" redirect_to = 'web:index'
login_form_class = LoginForm login_form_class = LoginForm
def get_redirection(self): def get_redirection(self):
next_page = self.request.GET.get("next", "") next_page = self.request.GET.get('next', '')
if next_page: if next_page:
return f"{reverse(self.redirect_to)}?next={next_page}" return f'{reverse(self.redirect_to)}?next={next_page}'
return reverse(self.redirect_to) return reverse(self.redirect_to)
@@ -54,11 +50,7 @@ class LoginView(View, RedirectionMixin):
return render( return render(
request, request,
self.template_name, self.template_name,
{ {'form': login_form, 'title': web_settings.web_title, 'description': _('inicio de sesión')},
"form": login_form,
"title": web_settings.web_title,
"description": _("inicio de sesión"),
},
) )
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -67,35 +59,25 @@ class LoginView(View, RedirectionMixin):
if login_form.is_valid(): if login_form.is_valid():
login_user(request, login_form.get_user()) login_user(request, login_form.get_user())
if not login_form.cleaned_data.get("remember_me"): if not login_form.cleaned_data.get('remember_me'):
request.session.set_expiry(0) request.session.set_expiry(0)
return redirect(self.get_redirection()) return redirect(self.get_redirection())
return render( return render(
request, request, self.template_name, {'form': login_form, 'errors': login_form.errors.get('__all__')}, status=400
self.template_name,
{
"form": login_form,
"errors": login_form.errors.get("__all__"),
},
status=400,
) )
class RegisterView(TemplateView, RedirectionMixin): class RegisterView(TemplateView, RedirectionMixin):
template_name = "users/register.html" template_name = 'users/register.html'
redirect_to = "web:login" redirect_to = 'web:login'
form_class = RegisterForm form_class = RegisterForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
return { return {'form': self.form_class(), 'title': settings.web_title, 'description': _('crear cuenta')}
"form": self.form_class(),
"title": settings.web_title,
"description": _("crear cuenta"),
}
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
@@ -107,34 +89,27 @@ class RegisterView(TemplateView, RedirectionMixin):
user = form.save() user = form.save()
user.username = user.email user.username = user.email
user.first_name = form.cleaned_data.get("first_name") user.first_name = form.cleaned_data.get('first_name')
user.last_name = form.cleaned_data.get("last_name") user.last_name = form.cleaned_data.get('last_name')
user.save() user.save()
login(request, user) login(request, user)
return redirect(self.get_redirection()) return redirect(self.get_redirection())
else: else:
errors = form.errors errors = form.errors
except IntegrityError as e: except IntegrityError:
errors = [ errors = [_('Ya existe un usuario con ese correo electrónico')]
_("Ya existe un usuario con ese correo electrónico"),
]
return render( return render(
request, request,
self.template_name, self.template_name,
{ {'form': form, 'title': settings.web_title, 'description': _('Crear cuenta'), 'errors': errors},
"form": form,
"title": settings.web_title,
"description": _("Crear cuenta"),
"errors": errors,
},
status=400, status=400,
) )
class LogoutView(View, RedirectionMixin): class LogoutView(View, RedirectionMixin):
redirect_to = "web:index" redirect_to = 'web:index'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
logout_user(request) logout_user(request)
@@ -146,31 +121,28 @@ class LogoutView(View, RedirectionMixin):
class ResetPasswordView(TemplateView): class ResetPasswordView(TemplateView):
template_name = "users/reset_password.html" template_name = 'users/reset_password.html'
form_class = PasswordResetForm form_class = PasswordResetForm
redirect_to = "web:reset_password_email_sent" redirect_to = 'web:reset_password_email_sent'
html_reset_password_template_name = "mails/reset_password.html" html_reset_password_template_name = 'mails/reset_password.html'
subject_template_name = "mails/subject_reset_password.txt" subject_template_name = 'mails/subject_reset_password.txt'
def get_email_options(self): def get_email_options(self):
return { return {
"extra_email_context": {}, 'extra_email_context': {},
"email_template_name": self.html_reset_password_template_name, 'email_template_name': self.html_reset_password_template_name,
"html_email_template_name": self.html_reset_password_template_name, 'html_email_template_name': self.html_reset_password_template_name,
"subject_template_name": self.subject_template_name, 'subject_template_name': self.subject_template_name,
} }
def get_base_context(self): def get_base_context(self):
settings = WebSettings.load() settings = WebSettings.load()
return { return {'title': settings.web_title, 'description': _('Restablecer contraseña')}
"title": settings.web_title,
"description": _("Restablecer contraseña"),
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = self.get_base_context() context = self.get_base_context()
context.update({"form": self.form_class()}) context.update({'form': self.form_class()})
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -178,9 +150,9 @@ class ResetPasswordView(TemplateView):
if form.is_valid(): if form.is_valid():
opts = { opts = {
"use_https": request.is_secure(), 'use_https': request.is_secure(),
"from_email": getattr(settings, "DEFAULT_FROM_EMAIL"), 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
"request": request, 'request': request,
} }
opts.update(self.get_email_options()) opts.update(self.get_email_options())
@@ -188,41 +160,32 @@ class ResetPasswordView(TemplateView):
return redirect(self.redirect_to) return redirect(self.redirect_to)
context = self.get_base_context() context = self.get_base_context()
context.update({"form": form}) context.update({'form': form})
return render(request, self.template_name, context) return render(request, self.template_name, context)
class ResetPasswordEmailSent(TemplateView): class ResetPasswordEmailSent(TemplateView):
template_name = "users/reset_password_email_sent.html" template_name = 'users/reset_password_email_sent.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
return { return {'title': settings.web_title, 'description': settings.web_description}
"title": settings.web_title,
"description": settings.web_description,
}
class ResetPasswordConfirmView(TemplateView): class ResetPasswordConfirmView(TemplateView):
template_name = "users/reset_password_confirm.html" template_name = 'users/reset_password_confirm.html'
form_class = SetPasswordForm form_class = SetPasswordForm
redirect_to = "web:login" redirect_to = 'web:login'
token_generator = default_token_generator token_generator = default_token_generator
def get_user(self): def get_user(self):
try: try:
uidb64 = self.kwargs.get("uidb64") uidb64 = self.kwargs.get('uidb64')
uid = urlsafe_base64_decode(uidb64).decode() uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid) user = User.objects.get(pk=uid)
except ( except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
TypeError,
ValueError,
OverflowError,
User.DoesNotExist,
ValidationError,
):
user = None user = None
return user return user
@@ -235,15 +198,15 @@ class ResetPasswordConfirmView(TemplateView):
new_token = self.generate_new_token_for_user(user) new_token = self.generate_new_token_for_user(user)
return { return {
"title": WebSettings.load().web_title, 'title': WebSettings.load().web_title,
"description": _("Establecer nueva contraseña"), 'description': _('Establecer nueva contraseña'),
"form": self.form_class(user, {"token": new_token}), 'form': self.form_class(user, {'token': new_token}),
"valid_token": valid_token, 'valid_token': valid_token,
"new_token": new_token, 'new_token': new_token,
} }
def check_token(self, user): def check_token(self, user):
token = self.kwargs.get("token") token = self.kwargs.get('token')
valid_token = self.token_generator.check_token(user, token) valid_token = self.token_generator.check_token(user, token)
return valid_token return valid_token
@@ -258,15 +221,15 @@ class ResetPasswordConfirmView(TemplateView):
class MyAccountView(TemplateView): class MyAccountView(TemplateView):
template_name = "users/user_details.html" template_name = 'users/user_details.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
addresses = CustomerAddress.objects.filter(user=self.request.user) addresses = CustomerAddress.objects.filter(user=self.request.user)
web_settings = WebSettings.load() web_settings = WebSettings.load()
return { return {
"title": f"{web_settings.web_title} - Mi cuenta", 'title': f'{web_settings.web_title} - Mi cuenta',
"description": f"{web_settings.web_title} - Mi cuenta", 'description': f'{web_settings.web_title} - Mi cuenta',
"customer_addresses": addresses, 'customer_addresses': addresses,
} }
+98 -162
View File
@@ -7,15 +7,7 @@ from django.utils.text import gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import CreateView, TemplateView from django.views.generic import CreateView, TemplateView
from shop.models import ( from shop.models import CartItem, Order, OrderLine, Product, ProductCategory, ShippingMethod, WishlistedProduct
CartItem,
Order,
OrderLine,
Product,
ProductCategory,
ShippingMethod,
WishlistedProduct,
)
from shop.redsys import RedsysClient from shop.redsys import RedsysClient
from shop.utils import create_order_from_cart from shop.utils import create_order_from_cart
from web.forms import CreateOrderForm from web.forms import CreateOrderForm
@@ -26,67 +18,64 @@ from web.utils import get_or_create_cart
class IndexView(TemplateView): class IndexView(TemplateView):
template_name = "web/index.html" template_name = 'web/index.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
return { return {'title': settings.web_title, 'description': settings.web_description}
"title": settings.web_title,
"description": settings.web_description,
}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if self.request.GET.get("next"): if self.request.GET.get('next'):
return redirect(self.request.GET.get("next")) return redirect(self.request.GET.get('next'))
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class CategoryView(TemplateView): class CategoryView(TemplateView):
template_name = "web/index.html" template_name = 'web/index.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
slug = kwargs.get("slug") slug = kwargs.get('slug')
category = ProductCategory.objects.get(slug=slug) category = ProductCategory.objects.get(slug=slug)
return { return {
"title": _(f"{settings.web_title} - {category.name}"), 'title': _(f'{settings.web_title} - {category.name}'),
"description": settings.web_description, 'description': settings.web_description,
"category": category, 'category': category,
"filter_by_category": True, 'filter_by_category': True,
} }
class ProductDetail(TemplateView): class ProductDetail(TemplateView):
template_name = "web/product_detail.html" template_name = 'web/product_detail.html'
def get_context_data(self, pk, slug, **kwargs): def get_context_data(self, pk, slug, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
product = get_object_or_404(Product, pk=pk) product = get_object_or_404(Product, pk=pk)
return { return {
"product": product, 'product': product,
"title": f"{settings.web_title} - {product.name}", 'title': f'{settings.web_title} - {product.name}',
"description": product.description, 'description': product.description,
"image": product.images.first(), 'image': product.images.first(),
} }
class CartDetail(TemplateView, CreateView): class CartDetail(TemplateView, CreateView):
template_name = "web/cart_detail.html" template_name = 'web/cart_detail.html'
form_class = CreateOrderForm form_class = CreateOrderForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
web_settings = WebSettings.load() web_settings = WebSettings.load()
cart, created = get_or_create_cart(self.request) cart, created = get_or_create_cart(self.request)
cart_items = CartItem.objects.filter(cart=cart).prefetch_related("product") cart_items = CartItem.objects.filter(cart=cart).prefetch_related('product')
return { return {
"title": _(f"{web_settings.web_title} - Carrito"), 'title': _(f'{web_settings.web_title} - Carrito'),
"cart": cart, 'cart': cart,
"items": cart_items, 'items': cart_items,
"description": _("resumen del carrito"), 'description': _('resumen del carrito'),
} }
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -94,29 +83,29 @@ class CartDetail(TemplateView, CreateView):
if not form.is_valid(): if not form.is_valid():
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context.update({"errors": form.errors}) context.update({'errors': form.errors})
return self.render_to_response(context) return self.render_to_response(context)
shipping_method_id = form.cleaned_data.get("shipping_method") shipping_method_id = form.cleaned_data.get('shipping_method')
cart, created = get_or_create_cart(request) cart, created = get_or_create_cart(request)
billing_address_full_name = form.cleaned_data.get("billing_address_full_name") billing_address_full_name = form.cleaned_data.get('billing_address_full_name')
billing_address_address = form.cleaned_data.get("billing_address") billing_address_address = form.cleaned_data.get('billing_address')
billing_address_town = form.cleaned_data.get("billing_address_town") billing_address_town = form.cleaned_data.get('billing_address_town')
billing_address_state = form.cleaned_data.get("billing_address_state") billing_address_state = form.cleaned_data.get('billing_address_state')
billing_address_country = form.cleaned_data.get("billing_address_country") billing_address_country = form.cleaned_data.get('billing_address_country')
billing_address_zip = form.cleaned_data.get("billing_address_zip") billing_address_zip = form.cleaned_data.get('billing_address_zip')
shipping_address_full_name = form.cleaned_data.get("shipping_address_full_name") shipping_address_full_name = form.cleaned_data.get('shipping_address_full_name')
shipping_address_address = form.cleaned_data.get("shipping_address") shipping_address_address = form.cleaned_data.get('shipping_address')
shipping_address_town = form.cleaned_data.get("shipping_address_town") shipping_address_town = form.cleaned_data.get('shipping_address_town')
shipping_address_state = form.cleaned_data.get("shipping_address_state") shipping_address_state = form.cleaned_data.get('shipping_address_state')
shipping_address_country = form.cleaned_data.get("shipping_address_country") shipping_address_country = form.cleaned_data.get('shipping_address_country')
shipping_address_zip = form.cleaned_data.get("shipping_address_zip") shipping_address_zip = form.cleaned_data.get('shipping_address_zip')
shipping_address_phone = form.cleaned_data.get("shipping_address_phone") shipping_address_phone = form.cleaned_data.get('shipping_address_phone')
email = form.cleaned_data.get("email") email = form.cleaned_data.get('email')
shipping_method = get_object_or_404(ShippingMethod, pk=shipping_method_id) shipping_method = get_object_or_404(ShippingMethod, pk=shipping_method_id)
@@ -139,72 +128,62 @@ class CartDetail(TemplateView, CreateView):
email=email, email=email,
) )
return redirect(reverse("web:order", kwargs={"uuid": order.uuid})) return redirect(reverse('web:order', kwargs={'uuid': order.uuid}))
class OrderDetail(TemplateView): class OrderDetail(TemplateView):
template_name = "web/order.html" template_name = 'web/order.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
order = get_object_or_404(Order, uuid=kwargs.get("uuid")) order = get_object_or_404(Order, uuid=kwargs.get('uuid'))
redsys_client = RedsysClient() redsys_client = RedsysClient()
parameters = redsys_client.get_body_for_order(order) parameters = redsys_client.get_body_for_order(order)
items = OrderLine.objects.select_related("product").filter( items = OrderLine.objects.select_related('product').filter(order=order, product__is_shipping_method=False)
shipping_cost = OrderLine.objects.select_related('product').filter(
order=order, product__is_shipping_method=True
).aggregate(amount=Sum('total')).get('amount') or Decimal('0')
base_total = OrderLine.objects.select_related('product').filter(
order=order, product__is_shipping_method=False order=order, product__is_shipping_method=False
) ).aggregate(amount=Sum('base_total')).get('amount') or Decimal('0')
shipping_cost = OrderLine.objects.select_related("product").filter( tax_total = OrderLine.objects.select_related('product').filter(
order=order, order=order, product__is_shipping_method=False
product__is_shipping_method=True, ).aggregate(amount=Sum('taxes')).get('amount') or Decimal('0')
).aggregate(amount=Sum("total")).get("amount") or Decimal("0")
base_total = OrderLine.objects.select_related("product").filter(
order=order,
product__is_shipping_method=False,
).aggregate(amount=Sum("base_total")).get("amount") or Decimal("0")
tax_total = OrderLine.objects.select_related("product").filter(
order=order,
product__is_shipping_method=False,
).aggregate(amount=Sum("taxes")).get("amount") or Decimal("0")
settings = WebSettings.load() settings = WebSettings.load()
return { return {
"title": _(f"{settings.web_title} - Comprar"), 'title': _(f'{settings.web_title} - Comprar'),
"description": _(f"{settings.web_title} - Comprar"), 'description': _(f'{settings.web_title} - Comprar'),
"order": order, 'order': order,
"items": items, 'items': items,
"signature_version": parameters.get("Ds_SignatureVersion"), 'signature_version': parameters.get('Ds_SignatureVersion'),
"merchant_parameters": parameters.get("Ds_MerchantParameters"), 'merchant_parameters': parameters.get('Ds_MerchantParameters'),
"signature": parameters.get("Ds_Signature"), 'signature': parameters.get('Ds_Signature'),
"redsys_target_url": redsys_client.get_target_url(), 'redsys_target_url': redsys_client.get_target_url(),
"total": round(order.total, 2), 'total': round(order.total, 2),
"base_total": round(base_total, 2), 'base_total': round(base_total, 2),
"tax_total": round(tax_total, 2), 'tax_total': round(tax_total, 2),
"shipping_cost": round(shipping_cost, 2), 'shipping_cost': round(shipping_cost, 2),
} }
class WishlistView(TemplateView): class WishlistView(TemplateView):
template_name = "web/wishlist.html" template_name = 'web/wishlist.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
return { return {'title': _(f'{settings.web_title} - Lista de deseados'), 'description': settings.web_description}
"title": _(f"{settings.web_title} - Lista de deseados"),
"description": settings.web_description,
}
class OrdersView(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin): class OrdersView(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
template_name = "web/orders.html" template_name = 'web/orders.html'
def get_queryset(self): def get_queryset(self):
return Order.objects.filter(user=self.request.user).exclude( return Order.objects.filter(user=self.request.user).exclude(status=Order.Statuses.STATUS_PENDING)
status=Order.Statuses.STATUS_PENDING
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
@@ -212,129 +191,86 @@ class OrdersView(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
page = self.get_paginated_queryset(qs) page = self.get_paginated_queryset(qs)
return { return {
"title": _(f"{settings.web_title} - Mis pedidos"), 'title': _(f'{settings.web_title} - Mis pedidos'),
"page": page, 'page': page,
"has_next_page": page.has_next(), 'has_next_page': page.has_next(),
"has_previous_page": page.has_previous(), 'has_previous_page': page.has_previous(),
} }
@require_http_methods( @require_http_methods(['POST'])
[
"POST",
]
)
def add_cart_item(request, *args, **kwargs): def add_cart_item(request, *args, **kwargs):
product = get_object_or_404(Product, pk=request.POST.get("product")) product = get_object_or_404(Product, pk=request.POST.get('product'))
cart, created = get_or_create_cart(request) cart, created = get_or_create_cart(request)
response = HttpResponse(status=201, headers={"HX-Trigger": "updated-cart"}) response = HttpResponse(status=201, headers={'HX-Trigger': 'updated-cart'})
if created and request.user.is_anonymous: if created and request.user.is_anonymous:
response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite="strict") response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid, samesite='strict')
# Comprobamos si existe una línea de carrito para ese carrito de ese producto # Comprobamos si existe una línea de carrito para ese carrito de ese producto
existing_cart_item = CartItem.objects.filter(cart=cart, product=product).first() existing_cart_item = CartItem.objects.filter(cart=cart, product=product).first()
# Si existe, simplemente le sumamos la cantidad a la línea ya existente # Si existe, simplemente le sumamos la cantidad a la línea ya existente
if existing_cart_item is not None: if existing_cart_item is not None:
existing_cart_item.quantity += int(request.POST.get("quantity")) existing_cart_item.quantity += int(request.POST.get('quantity'))
existing_cart_item.save() existing_cart_item.save()
# Si no, lo creamos # Si no, lo creamos
else: else:
CartItem.objects.create( CartItem.objects.create(cart=cart, quantity=request.POST.get('quantity'), product=product)
cart=cart,
quantity=request.POST.get("quantity"),
product=product,
)
return response return response
@require_http_methods( @require_http_methods(['POST'])
[
"POST",
]
)
def delete_cart_item(request, pk, *args, **kwargs): def delete_cart_item(request, pk, *args, **kwargs):
cart, created = get_or_create_cart(request) cart, created = get_or_create_cart(request)
cart_item = get_object_or_404(CartItem, pk=pk, cart=cart) cart_item = get_object_or_404(CartItem, pk=pk, cart=cart)
cart_item.delete() cart_item.delete()
return HttpResponse(status=204, headers={"HX-Trigger": "updated-cart"}) return HttpResponse(status=204, headers={'HX-Trigger': 'updated-cart'})
@require_http_methods( @require_http_methods(['POST'])
[
"POST",
]
)
def add_to_wishlist(request, *args, **kwargs): def add_to_wishlist(request, *args, **kwargs):
product = get_object_or_404(Product, pk=request.POST.get("product")) product = get_object_or_404(Product, pk=request.POST.get('product'))
response = HttpResponse(status=201, headers={"HX-Trigger": "updated-wishlist"}) response = HttpResponse(status=201, headers={'HX-Trigger': 'updated-wishlist'})
if request.user.is_anonymous: if request.user.is_anonymous:
return HttpResponse(status=400) return HttpResponse(status=400)
WishlistedProduct.objects.get_or_create( WishlistedProduct.objects.get_or_create(user=request.user, product=product)
user=request.user,
product=product,
)
return response return response
@require_http_methods( @require_http_methods(['POST'])
[
"POST",
]
)
def delete_from_wishlist(request, pk, *args, **kwargs): def delete_from_wishlist(request, pk, *args, **kwargs):
wishlisted_item = get_object_or_404( wishlisted_item = get_object_or_404(WishlistedProduct, product_id=pk, user=request.user)
WishlistedProduct, product_id=pk, user=request.user
)
wishlisted_item.delete() wishlisted_item.delete()
return HttpResponse(status=204, headers={"HX-Trigger": "updated-wishlist"}) return HttpResponse(status=204, headers={'HX-Trigger': 'updated-wishlist'})
def manifest(request, *args, **kwargs): def manifest(request, *args, **kwargs):
settings = WebSettings.load() settings = WebSettings.load()
data = { data = {
"name": settings.web_title, 'name': settings.web_title,
"short_name": settings.web_title, 'short_name': settings.web_title,
"start_url": "/", 'start_url': '/',
"background_color": settings.bg_color, 'background_color': settings.bg_color,
"theme_color": settings.theme_color, 'theme_color': settings.theme_color,
"icons": [], 'icons': [],
"display": "standalone", 'display': 'standalone',
"orientation": "portrait", 'orientation': 'portrait',
} }
if settings.logo: if settings.logo:
data["icons"].append( data['icons'].append({'src': settings.logo.url, 'sizes': '512x512', 'type': 'image/png'})
{
"src": settings.logo.url,
"sizes": "512x512",
"type": "image/png",
}
)
if settings.logo_256: if settings.logo_256:
data["icons"].append( data['icons'].append({'src': settings.logo_256.url, 'sizes': '256x256', 'type': 'image/png'})
{
"src": settings.logo_256.url,
"sizes": "256x256",
"type": "image/png",
}
)
if settings.logo_128: if settings.logo_128:
data["icons"].append( data['icons'].append({'src': settings.logo_128.url, 'sizes': '128x128', 'type': 'image/png'})
{
"src": settings.logo_128.url,
"sizes": "128x128",
"type": "image/png",
}
)
return JsonResponse(data) return JsonResponse(data)