From 1506667d288f5aac5f06518d13ece2b821985065 Mon Sep 17 00:00:00 2001 From: Alexander Orvik Date: Tue, 31 Oct 2023 23:19:06 +0100 Subject: [PATCH 1/4] feat(economy): ghost product mutation, tests and other misc. * Remove unused mutation for proudcts and product orders * Upgrade graphene-django-cud to 0.11.1 * Add addict as dev-dependency (when writing schema tests the gql decorator cannot find the user if its not in an object. Meaning it would always give permission errors) * Write basic schema tests for new endpoints * Add `fail_silently` handlingt to stock price calculation util function * Add UserWithPermission factory --- economy/price_strategies.py | 14 +++-- economy/schema.py | 65 +++++++-------------- economy/tests/test_schema.py | 51 +++++++++++++++++ economy/tests/test_utils.py | 17 +++++- poetry.lock | 108 +++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +- users/tests/factories.py | 52 ++++++++++++++++- 7 files changed, 253 insertions(+), 60 deletions(-) create mode 100644 economy/tests/test_schema.py diff --git a/economy/price_strategies.py b/economy/price_strategies.py index a25bf29a..9d04f7b6 100644 --- a/economy/price_strategies.py +++ b/economy/price_strategies.py @@ -7,13 +7,19 @@ from django.conf import settings -def calculate_stock_price_for_product(product_id): +def calculate_stock_price_for_product(product_id: int, fail_silently=True): + """ + Calculates the price of a product provided a specific product id. + """ product = SociProduct.objects.get(id=product_id) if not product.purchase_price: - raise RuntimeError( - "product has no purchase price. Cannot calculate stock price" - ) + if fail_silently: + return product.price + else: + raise RuntimeError( + f"Cannot calculate stock price for product without purchase price: {product}" + ) purchase_window = timezone.now() - settings.STOCK_MODE_PRICE_WINDOW purchases_made_in_window = ProductOrder.objects.filter( diff --git a/economy/schema.py b/economy/schema.py index 3905479a..706a27bc 100644 --- a/economy/schema.py +++ b/economy/schema.py @@ -11,22 +11,13 @@ ValidationError, ) from django.db import transaction -from django.forms import FloatField from graphene import Node from django.db.models import ( Q, Sum, - Count, - F, Avg, - Subquery, - OuterRef, - Value, - Case, - When, - BooleanField, ) -from django.db.models.functions import Coalesce, TruncDay, TruncDate +from django.db.models.functions import Coalesce, TruncDate from graphene_django import DjangoObjectType from django.utils import timezone from graphene_django_cud.mutations import ( @@ -54,6 +45,7 @@ ProductOrder, SociOrderSession, SociOrderSessionOrder, + ProductGhostOrder, ) from economy.price_strategies import calculate_stock_price_for_product from schedules.models import Schedule @@ -606,36 +598,6 @@ def resolve_stock_market_products(self, info, *args, **kwargs): return data -class CreateSociProductMutation(DjangoCreateMutation): - class Meta: - model = SociProduct - - -class PatchSociProductMutation(DjangoPatchMutation): - class Meta: - model = SociProduct - - -class DeleteSociProductMutation(DjangoDeleteMutation): - class Meta: - model = SociProduct - - -class CreateProductOrderMutation(DjangoCreateMutation): - class Meta: - model = ProductOrder - - -class PatchProductOrderMutation(DjangoPatchMutation): - class Meta: - model = ProductOrder - - -class DeleteProductOrderMutation(DjangoDeleteMutation): - class Meta: - model = ProductOrder - - class UndoProductOrderMutation(graphene.Mutation): class Arguments: id = graphene.ID(required=True) @@ -1214,11 +1176,24 @@ def mutate(self, info, amount, deposit_method, description, *args, **kwargs): return CreateDepositMutation(deposit=deposit) -class EconomyMutations(graphene.ObjectType): - create_soci_product = CreateSociProductMutation.Field() - patch_soci_product = PatchSociProductMutation.Field() - delete_soci_product = DeleteSociProductMutation.Field() +class IncrementProductGhostOrderMutation(graphene.Mutation): + class Arguments: + product_id = graphene.ID() + + success = graphene.Boolean() + @gql_has_permissions("economy.add_productghostorder") + def mutate(self, info, product_id, *args, **kwargs): + product_id = disambiguate_id(product_id) + + product = SociProduct.objects.get(id=product_id) + + ProductGhostOrder.objects.create(product=product) + + return IncrementProductGhostOrderMutation(success=True) + + +class EconomyMutations(graphene.ObjectType): place_product_order = PlaceProductOrderMutation.Field() undo_product_order = UndoProductOrderMutation.Field() @@ -1241,3 +1216,5 @@ class EconomyMutations(graphene.ObjectType): ) soci_order_session_next_status = SociOrderSessionNextStatusMutation.Field() invite_users_to_order_session = InviteUsersToSociOrderSessionMutation.Field() + + increment_product_ghost_order = IncrementProductGhostOrderMutation.Field() diff --git a/economy/tests/test_schema.py b/economy/tests/test_schema.py new file mode 100644 index 00000000..79720ffa --- /dev/null +++ b/economy/tests/test_schema.py @@ -0,0 +1,51 @@ +from django.test import TestCase +from addict import Dict +from graphene.test import Client +from ksg_nett.schema import schema +from economy.tests.factories import SociProductFactory +from economy.models import ProductGhostOrder +from users.tests.factories import UserWithPermissionsFactory, UserFactory + + +class TestIncrementProductGhostOrderMutation(TestCase): + def setUp(self) -> None: + self.graphql_client = Client(schema) + self.product = SociProductFactory.create( + name="Smirnoff Ice", price=30, purchase_price=20 + ) + self.user_with_perm = UserWithPermissionsFactory.create( + permissions="economy.add_productghostorder" + ) + self.user_without_perm = UserFactory.create() + + self.mutation = """ + mutation IncrementGhostOrderMutation($productId: ID) { + incrementProductGhostOrder(productId: $productId) { + success + } + } + """ + + def test__correct_input_and_has_permission__creates_new_object(self): + pre_count = ProductGhostOrder.objects.all().count() + self.graphql_client.execute( + self.mutation, + variables={"productId": self.product.id}, + context=Dict(user=self.user_with_perm), + ) + post_count = ProductGhostOrder.objects.all().count() + diff = post_count - pre_count + self.assertEqual(diff, 1) + + def test__correct_input_without_permission__returns_error(self): + pre_count = ProductGhostOrder.objects.all().count() + executed = self.graphql_client.execute( + self.mutation, + variables={"productId": self.product.id}, + context=Dict(user=self.user_without_perm), + ) + result = Dict(executed) + post_count = ProductGhostOrder.objects.all().count() + diff = post_count - pre_count + self.assertEqual(diff, 0) + self.assertIsNotNone(result.data.errors) diff --git a/economy/tests/test_utils.py b/economy/tests/test_utils.py index f7235517..bae5a258 100644 --- a/economy/tests/test_utils.py +++ b/economy/tests/test_utils.py @@ -57,15 +57,28 @@ def test__price_calculation__returns_expected_price(self): calculated_price = calculate_stock_price_for_product(self.tuborg.id) self.assertEqual(expected, calculated_price) - def test__product_has_no_purchase_price__raises_error(self): + def test__product_has_no_purchase_price_no_silent_fail__raises_error(self): no_purchase_price = SociProductFactory.create( price=20, purchase_price=None, name="tuborg 2" ) self.assertRaises( - RuntimeError, calculate_stock_price_for_product, no_purchase_price.id + RuntimeError, + calculate_stock_price_for_product, + no_purchase_price.id, + fail_silently=False, ) + def test__product_has_no_purchase_price_silent_failing__returns_normal_price(self): + no_purchase_price = SociProductFactory.create( + price=20, purchase_price=None, name="tuborg 2" + ) + + result = calculate_stock_price_for_product( + no_purchase_price.id, fail_silently=True + ) + self.assertEqual(result, no_purchase_price.price) + def test__product_orders_outside_price_window__not_included_in_calculation(self): outside_window = ( timezone.now() diff --git a/poetry.lock b/poetry.lock index b47dc8cb..62319479 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,22 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. + +[[package]] +name = "addict" +version = "2.4.0" +description = "Addict is a dictionary whose items can be set using both attribute and item syntax." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc"}, + {file = "addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494"}, +] [[package]] name = "aniso8601" version = "9.0.1" description = "A library for parsing ISO 8601 strings." +category = "main" optional = false python-versions = "*" files = [ @@ -18,6 +31,7 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -32,6 +46,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -43,6 +58,7 @@ files = [ name = "bleach" version = "5.0.1" description = "An easy safelist-based HTML-sanitizing tool." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -62,6 +78,7 @@ dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0 name = "brotli" version = "1.0.9" description = "Python bindings for the Brotli compression library" +category = "main" optional = false python-versions = "*" files = [ @@ -153,6 +170,7 @@ files = [ name = "brotlicffi" version = "1.0.9.2" description = "Python CFFI bindings to the Brotli library" +category = "main" optional = false python-versions = "*" files = [ @@ -195,6 +213,7 @@ cffi = ">=1.0.0" name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -206,6 +225,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -282,6 +302,7 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -293,6 +314,7 @@ files = [ name = "channels" version = "4.0.0" description = "Brings async, event-driven capabilities to Django 3.2 and up." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -312,6 +334,7 @@ tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", " name = "channels-redis" version = "4.1.0" description = "Redis-backed ASGI channel layer implementation" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -333,6 +356,7 @@ tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -417,6 +441,7 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -428,6 +453,7 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -500,6 +526,7 @@ toml = ["tomli"] name = "cryptography" version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -545,6 +572,7 @@ test-randomorder = ["pytest-randomly"] name = "cssselect2" version = "0.7.0" description = "CSS selectors for Python ElementTree" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -564,6 +592,7 @@ test = ["flake8", "isort", "pytest"] name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -575,6 +604,7 @@ files = [ name = "django" version = "4.2.3" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -595,6 +625,7 @@ bcrypt = ["bcrypt"] name = "django-cors-headers" version = "3.14.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -609,6 +640,7 @@ Django = ">=3.2" name = "django-filter" version = "2.4.0" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -623,6 +655,7 @@ Django = ">=2.2" name = "django-storages" version = "1.13.2" description = "Support for many storage backends in Django" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -645,6 +678,7 @@ sftp = ["paramiko (>=1.10.0)"] name = "djangorestframework" version = "3.14.0" description = "Web APIs for Django, made easy." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -660,6 +694,7 @@ pytz = "*" name = "djangorestframework-simplejwt" version = "5.2.2" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -684,6 +719,7 @@ test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", name = "drf-yasg" version = "1.21.7" description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -708,6 +744,7 @@ validation = ["swagger-spec-validator (>=2.1.0)"] name = "et-xmlfile" version = "1.1.0" description = "An implementation of lxml.xmlfile for the standard library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -719,6 +756,7 @@ files = [ name = "factory-boy" version = "3.3.0" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -737,6 +775,7 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] name = "faker" version = "19.2.0" description = "Faker is a Python package that generates fake data for you." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -751,6 +790,7 @@ python-dateutil = ">=2.4" name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -766,6 +806,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "fonttools" version = "4.41.1" description = "Tools to manipulate font files" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -828,6 +869,7 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] name = "graphene" version = "3.2.2" description = "GraphQL Framework for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -848,6 +890,7 @@ test = ["coveralls (>=3.3,<4)", "iso8601 (>=1,<2)", "mock (>=4,<5)", "pytest (>= name = "graphene-django" version = "3.1.3" description = "Graphene Django integration" +category = "main" optional = false python-versions = "*" files = [ @@ -870,13 +913,14 @@ test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", [[package]] name = "graphene-django-cud" -version = "0.11.0" +version = "0.11.1" description = "Create, update and delete mutations for graphene-django" +category = "main" optional = false python-versions = "*" files = [ - {file = "graphene-django-cud-0.11.0.tar.gz", hash = "sha256:b45b7bc152ae170e1fe11509f28e81368340bf9a8fb06c430e86579331371289"}, - {file = "graphene_django_cud-0.11.0-py3-none-any.whl", hash = "sha256:e2d8c213e89259d96a2b1cb927a91d265e94079b54a75a8923284f1b773314c3"}, + {file = "graphene-django-cud-0.11.1.tar.gz", hash = "sha256:54200d20de00a8efcc387b95e8e508c9bcd548e2eb7e3c3f298c16415a65bfdd"}, + {file = "graphene_django_cud-0.11.1-py3-none-any.whl", hash = "sha256:91c8eec4437a387825d706430e8f22c35c355d1ac1df201f6a9256af1705c5c6"}, ] [package.dependencies] @@ -887,6 +931,7 @@ graphene-file-upload = ">=1.2" name = "graphene-file-upload" version = "1.3.0" description = "Lib for adding file upload functionality to GraphQL mutations in Graphene Django and Flask-Graphql" +category = "main" optional = false python-versions = "*" files = [ @@ -907,6 +952,7 @@ tests = ["coverage", "pytest", "pytest-cov", "pytest-django"] name = "graphql-core" version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -918,6 +964,7 @@ files = [ name = "graphql-relay" version = "3.2.0" description = "Relay library for graphql-core" +category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -932,6 +979,7 @@ graphql-core = ">=3.2,<3.3" name = "gunicorn" version = "20.1.0" description = "WSGI HTTP Server for UNIX" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -952,6 +1000,7 @@ tornado = ["tornado (>=0.2)"] name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -973,6 +1022,7 @@ lxml = ["lxml"] name = "icalendar" version = "5.0.7" description = "iCalendar parser/generator" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -988,6 +1038,7 @@ pytz = "*" name = "identify" version = "2.5.26" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1002,6 +1053,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1013,6 +1065,7 @@ files = [ name = "inflection" version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1024,6 +1077,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1035,6 +1089,7 @@ files = [ name = "msgpack" version = "1.0.5" description = "MessagePack serializer" +category = "main" optional = false python-versions = "*" files = [ @@ -1107,6 +1162,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1121,6 +1177,7 @@ setuptools = "*" name = "numpy" version = "1.25.1" description = "Fundamental package for array computing in Python" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1155,6 +1212,7 @@ files = [ name = "openpyxl" version = "3.1.2" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1169,6 +1227,7 @@ et-xmlfile = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1180,6 +1239,7 @@ files = [ name = "pillow" version = "9.5.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1259,6 +1319,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "3.9.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1274,6 +1335,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1289,6 +1351,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1307,6 +1370,7 @@ virtualenv = ">=20.10.0" name = "promise" version = "2.3" description = "Promises/A+ implementation for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -1323,6 +1387,7 @@ test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", name = "psycopg2-binary" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1394,6 +1459,7 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1405,6 +1471,7 @@ files = [ name = "pydash" version = "5.1.2" description = "The kitchen sink of Python utility libraries for doing \"stuff\" in a functional way. Based on the Lo-Dash Javascript library." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1419,6 +1486,7 @@ dev = ["Sphinx", "black", "build", "coverage", "docformatter", "flake8", "flake8 name = "pydyf" version = "0.7.0" description = "A low-level PDF generator." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1434,6 +1502,7 @@ test = ["flake8", "isort", "pillow", "pytest"] name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1451,6 +1520,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyopenssl" version = "23.2.0" description = "Python wrapper module around the OpenSSL library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1469,6 +1539,7 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyphen" version = "0.14.0" description = "Pure Python module to hyphenate text" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1484,6 +1555,7 @@ test = ["flake8", "isort", "pytest"] name = "pypng" version = "0.20220715.0" description = "Pure Python library for saving and loading PNG images" +category = "main" optional = false python-versions = "*" files = [ @@ -1495,6 +1567,7 @@ files = [ name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1515,6 +1588,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1534,6 +1608,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-django" version = "3.10.0" description = "A Django plugin for pytest." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1552,6 +1627,7 @@ testing = ["Django", "django-configurations (>=2.0)", "six"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1566,6 +1642,7 @@ six = ">=1.5" name = "pytz" version = "2023.3" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -1577,6 +1654,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1626,6 +1704,7 @@ files = [ name = "qrcode" version = "7.4.2" description = "QR Code image generator" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1650,6 +1729,7 @@ test = ["coverage", "pytest"] name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1668,6 +1748,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1689,6 +1770,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "scipy" version = "1.10.0" description = "Fundamental algorithms for scientific computing in Python" +category = "main" optional = false python-versions = "<3.12,>=3.8" files = [ @@ -1727,6 +1809,7 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo name = "sentry-sdk" version = "1.14.0" description = "Python client for Sentry (https://sentry.io)" +category = "main" optional = false python-versions = "*" files = [ @@ -1765,6 +1848,7 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1781,6 +1865,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1792,6 +1877,7 @@ files = [ name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1808,6 +1894,7 @@ test = ["pytest", "pytest-cov"] name = "stripe" version = "5.5.0" description = "Python bindings for the Stripe API" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1822,6 +1909,7 @@ requests = {version = ">=2.20", markers = "python_version >= \"3.0\""} name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" +category = "main" optional = false python-versions = "*" files = [ @@ -1833,6 +1921,7 @@ files = [ name = "tinycss2" version = "1.2.1" description = "A tiny CSS parser" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1851,6 +1940,7 @@ test = ["flake8", "isort", "pytest"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1862,6 +1952,7 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1873,6 +1964,7 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -1884,6 +1976,7 @@ files = [ name = "uritemplate" version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1895,6 +1988,7 @@ files = [ name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1912,6 +2006,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "virtualenv" version = "20.24.2" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1932,6 +2027,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "weasyprint" version = "54.3" description = "The Awesome Document Factory" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1957,6 +2053,7 @@ test = ["coverage[toml]", "pytest", "pytest-cov", "pytest-flake8", "pytest-isort name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" +category = "main" optional = false python-versions = "*" files = [ @@ -1968,6 +2065,7 @@ files = [ name = "zopfli" version = "0.2.2" description = "Zopfli module for python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2041,4 +2139,4 @@ test = ["pytest"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "d605feb555733962c43e0542b8c9084cdd2ae21cade4490dfdafe0f9646c7932" +content-hash = "b0ef626078c32defe8f38f46667b057ee055e7f47e92ec94cd91dea92226c453" diff --git a/pyproject.toml b/pyproject.toml index e441d8fc..919a9e3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ psycopg2-binary = "^2.8.6" packaging = "^23.1" weasyprint = "^54" graphene-django = "^3.0.0" -graphene-django-cud = "^0.11.0" +graphene-django-cud = "^0.11.1" django-filter = "^2.4.0" sentry-sdk = "1.14.0" django-storages = "^1.11.1" @@ -36,11 +36,13 @@ cryptography = "^41.0.3" drf-yasg = "^1.21.7" pyopenssl = "^23.2.0" -[tool.poetry.dev-dependencies] + +[tool.poetry.group.dev.dependencies] pytest = "^7.3" pytest-django = "^3.7" pytest-cov = "^2.8" pre-commit = "^2.10.0" +addict = "^2.4.0" [tool.black] line-length = 88 diff --git a/users/tests/factories.py b/users/tests/factories.py index a8e59572..e4eaf1ff 100644 --- a/users/tests/factories.py +++ b/users/tests/factories.py @@ -1,9 +1,10 @@ -import random - -from factory import Faker, SubFactory, sequence, Sequence +from factory import Faker, SubFactory, Sequence, post_generation from factory.django import DjangoModelFactory from factory.django import FileField from users.models import User, UsersHaveMadeOut +from django.contrib.auth.models import Permission +from django.db import models +from typing import Optional class UserFactory(DjangoModelFactory): @@ -30,6 +31,51 @@ class Meta: email = Sequence(lambda n: f"user{n}@example.com") +def _get_permission_from_string(string: str) -> Optional[Permission]: + try: + if "." in string: + app_label, codename = string.split(".") + return Permission.objects.get( + content_type__app_label=app_label, codename=codename + ) + else: + return Permission.objects.get(codename=string) + except models.ObjectDoesNotExist: + return None + + +class UserWithPermissionsFactory(DjangoModelFactory): + class Meta: + model = User + + username = Sequence(lambda n: "permissionsusername%d" % n) + + @post_generation + def permissions(self: User, create, extracted, **kwargs): + if not create: + return + + if not extracted: + return + + if isinstance(extracted, str): + permission = _get_permission_from_string(extracted) + if permission is not None: + self.user_permissions.add(_get_permission_from_string(extracted)) + elif hasattr(extracted, "__iter__"): + for permission_string in extracted: + if not isinstance(permission_string, str): + continue + + permission = _get_permission_from_string(permission_string) + if permission is not None: + self.user_permissions.add(permission) + else: + raise ValueError( + "Invalid variable input as permissions, expected string or iterable" + ) + + class UsersHaveMadeOutFactory(DjangoModelFactory): class Meta: model = UsersHaveMadeOut From 4aad568b384c86463dd59cb54256fd1ba5d82caf Mon Sep 17 00:00:00 2001 From: Alexander Orvik Date: Thu, 2 Nov 2023 18:00:36 +0100 Subject: [PATCH 2/4] fix(admissions): broken location availability tests Due to improper use of timezone aware objects the datetimes passed down to the helper function would default to a +01.22 timezone instead of the +01.00 which is expected from the configuration of this project --- admissions/tests/test_utils.py | 55 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/admissions/tests/test_utils.py b/admissions/tests/test_utils.py index bb1d8f52..2d946045 100644 --- a/admissions/tests/test_utils.py +++ b/admissions/tests/test_utils.py @@ -135,46 +135,41 @@ def test__before_interview_time__returns_no_available_locations(self): def test__start_of_interview_time__returns_3_available_locations(self): now = timezone.datetime.now() + datetime_from = timezone.make_aware( + timezone.datetime(now.year, now.month, now.day, hour=12, minute=00), + timezone=pytz.timezone(settings.TIME_ZONE), + ) + datetime_to = timezone.make_aware( + timezone.datetime(now.year, now.month, now.day, hour=12, minute=45), + timezone=pytz.timezone(settings.TIME_ZONE), + ) + locations = get_available_interview_locations( - datetime_from=timezone.datetime( - now.year, - now.month, - now.day, - hour=12, - minute=00, - tzinfo=pytz.timezone(settings.TIME_ZONE), - ), - datetime_to=timezone.datetime( - now.year, - now.month, - now.day, - hour=12, - minute=45, - tzinfo=pytz.timezone(settings.TIME_ZONE), - ), + datetime_from=datetime_from, datetime_to=datetime_to ) self.assertEqual(locations.count(), 3) def test__remove_1_location_availability__returns_2_available_locations(self): self.bodegaen_day_2.delete() - now = timezone.datetime.now() + timezone.timedelta(days=1) - locations = get_available_interview_locations( - datetime_from=timezone.datetime( - now.year, - now.month, - now.day, - hour=12, - minute=00, - tzinfo=pytz.timezone(settings.TIME_ZONE), + tomorrow = timezone.datetime.now() + timezone.timedelta(days=1) + tomorrow_datetime_from = timezone.make_aware( + timezone.datetime( + tomorrow.year, tomorrow.month, tomorrow.day, hour=12, minute=00 ), - datetime_to=timezone.datetime( - now.year, - now.month, - now.day, + timezone=pytz.timezone(settings.TIME_ZONE), + ) + tomorrow_datetime_to = timezone.make_aware( + timezone.datetime( + tomorrow.year, + tomorrow.month, + tomorrow.day, hour=12, minute=45, - tzinfo=pytz.timezone(settings.TIME_ZONE), ), + timezone=pytz.timezone(settings.TIME_ZONE), + ) + locations = get_available_interview_locations( + datetime_from=tomorrow_datetime_from, datetime_to=tomorrow_datetime_to ) self.assertEqual(locations.count(), 2) From f2e5f612b1832617b0520fd351b78a55f9620c47 Mon Sep 17 00:00:00 2001 From: Alexander Orvik Date: Thu, 2 Nov 2023 18:03:53 +0100 Subject: [PATCH 3/4] chore(economy): fix typo in market mode feature flag --- api/tests/tests.py | 4 ++-- api/views.py | 2 +- ksg_nett/settings.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/tests/tests.py b/api/tests/tests.py index b1d1562d..cce08017 100644 --- a/api/tests/tests.py +++ b/api/tests/tests.py @@ -302,7 +302,7 @@ def setUp(self): self.initial_funds = 1000 self.user_account.add_funds(self.initial_funds) self.flag = FeatureFlagFactory.create( - name=settings.X_APP_AUCTION_MODE, enabled=True + name=settings.X_APP_STOCK_MARKET_MODE, enabled=True ) def test__no_existing_sales__charge_purchase_price(self): @@ -377,7 +377,7 @@ def setUp(self): self.initial_funds = 1000 self.user_account.add_funds(self.initial_funds) self.flag = FeatureFlagFactory.create( - name=settings.X_APP_AUCTION_MODE, enabled=False + name=settings.X_APP_STOCK_MARKET_MODE, enabled=False ) def test__stock_mode_disabled__charge_ordinary_price(self): diff --git a/api/views.py b/api/views.py index f9e770fa..dac156f8 100644 --- a/api/views.py +++ b/api/views.py @@ -253,7 +253,7 @@ def post(self, request, *args, **kwargs): order_size = product_order["order_size"] if ( - check_feature_flag(settings.X_APP_AUCTION_MODE, fail_silently=True) + check_feature_flag(settings.X_APP_STOCK_MARKET_MODE, fail_silently=True) and product.purchase_price ): # Stock mode is enabled and the product has a registered purchase price product_price = calculate_stock_price_for_product(product.id) diff --git a/ksg_nett/settings.py b/ksg_nett/settings.py index 36aaec9c..59556a4a 100644 --- a/ksg_nett/settings.py +++ b/ksg_nett/settings.py @@ -288,7 +288,7 @@ BANK_TRANSFER_DEPOSIT_FEATURE_FLAG = "bank_transfer_deposit" DEPOSIT_TIME_RESTRICTIONS_FEATURE_FLAG = "deposit_time_restrictions" EXTERNAL_CHARGING_FEATURE_FLAG = "external_charging" -X_APP_AUCTION_MODE = "x-app-auction-mode" +X_APP_STOCK_MARKET_MODE = "x-app-stock-market-mode" EXTERNAL_CHARGE_MAX_AMOUNT = os.environ.get("EXTERNAL_CHARGE_MAX_AMOUNT", 300) From 73b597f0e196f182da1ce7155e91ac67686aa62d Mon Sep 17 00:00:00 2001 From: Alexander Orvik Date: Thu, 2 Nov 2023 18:07:30 +0100 Subject: [PATCH 4/4] feat(CI): trigger test pipeline on push --- .github/workflows/test_on_push.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index 7c545392..dbaf0ec4 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -1,9 +1,6 @@ name: Run tests on PRs -on: - push: - branches: - - develop +on: push jobs: test: