From e2472b085376db67281cec021b0e905e29c77b9d Mon Sep 17 00:00:00 2001 From: Gabriella Date: Wed, 19 Oct 2022 12:41:19 -0300 Subject: [PATCH 1/8] Initial commit with all the API structure --- .flake8 | 5 + .gitignore | 137 ++++++++++++++++ .vscode/launch.json | 19 +++ Makefile | 20 +++ README.md | 91 +---------- app/.env.example | 5 + app/core/__init__.py | 0 app/core/asgi.py | 16 ++ app/core/pytest.ini | 5 + app/core/router.py | 9 ++ app/core/settings.py | 175 +++++++++++++++++++++ app/core/test_pytest.py | 25 +++ app/core/urls.py | 43 +++++ app/core/wsgi.py | 16 ++ app/investments/__init__.py | 0 app/investments/admin.py | 3 + app/investments/apps.py | 6 + app/investments/migrations/0001_initial.py | 30 ++++ app/investments/migrations/__init__.py | 0 app/investments/models.py | 38 +++++ app/investments/serializers.py | 70 +++++++++ app/investments/tests.py | 3 + app/investments/views.py | 79 ++++++++++ app/manage.py | 22 +++ app/persons/__init__.py | 0 app/persons/admin.py | 3 + app/persons/apps.py | 6 + app/persons/migrations/0001_initial.py | 25 +++ app/persons/migrations/__init__.py | 0 app/persons/models.py | 11 ++ app/persons/serializers.py | 8 + app/persons/tests.py | 3 + app/persons/views.py | 29 ++++ docker-compose.yml | 28 ++++ docker/Dockerfile | 12 ++ pyproject.toml | 30 ++++ 36 files changed, 886 insertions(+), 86 deletions(-) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 Makefile create mode 100644 app/.env.example create mode 100644 app/core/__init__.py create mode 100644 app/core/asgi.py create mode 100644 app/core/pytest.ini create mode 100644 app/core/router.py create mode 100644 app/core/settings.py create mode 100644 app/core/test_pytest.py create mode 100644 app/core/urls.py create mode 100644 app/core/wsgi.py create mode 100644 app/investments/__init__.py create mode 100644 app/investments/admin.py create mode 100644 app/investments/apps.py create mode 100644 app/investments/migrations/0001_initial.py create mode 100644 app/investments/migrations/__init__.py create mode 100644 app/investments/models.py create mode 100644 app/investments/serializers.py create mode 100644 app/investments/tests.py create mode 100644 app/investments/views.py create mode 100644 app/manage.py create mode 100644 app/persons/__init__.py create mode 100644 app/persons/admin.py create mode 100644 app/persons/apps.py create mode 100644 app/persons/migrations/0001_initial.py create mode 100644 app/persons/migrations/__init__.py create mode 100644 app/persons/models.py create mode 100644 app/persons/serializers.py create mode 100644 app/persons/tests.py create mode 100644 app/persons/views.py create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..110313d61 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length=120 +ignore=E731 +ignore=C0114 +ignore=c0116 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f62cee117 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pipenv +# According to pypa/pipenv#598, it is +recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Sublime Text +*.sublime-* + +# local env +.local + +# PyCharm +.idea/* + +# MacOS X +.DS_Store + +# SQL +*.sql + +# VSCODE +.vscode/settings.json + +# poetry +poetry.lock \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..312d83407 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}\\app\\manage.py", + "args": [ + "runserver" + ], + "django": true, + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..75c9110eb --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +init: + cp .env.example +build: + docker-compose build --no-cache + docker-compose up -d + docker-compose logs -f +rebuild: + docker-compose down --remove-orphans --volumes + sudo rm -rf data + make build +run: + docker-compose down + docker-compose up -d + docker-compose logs -f +down: + docker-compose down --remove-orphans --volumes +migrate: + docker exec -it servi_app bash -c "python manage.py migrate" +terminal: + docker exec -it servi_app bash \ No newline at end of file diff --git a/README.md b/README.md index ea8115e67..b024af1eb 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,7 @@ -# Back End Test Project +

Coderockr Investments API

+# An API for an application that stores and manages investments, it should have the following features: -You should see this challenge as an opportunity to create an application following modern development best practices (given the stack of your choice), but also feel free to use your own architecture preferences (coding standards, code organization, third-party libraries, etc). It’s perfectly fine to use vanilla code or any framework or libraries. +Inicializando +Você precisa ter o docker e o docker-compose instalado em sua máquina. para isso verifique os links de documentação prorietária: Docker e Docker-compose, nessa ordem. -## Scope - -In this challenge you should build an API for an application that stores and manages investments, it should have the following features: - -1. __Creation__ of an investment with an owner, a creation date and an amount. - 1. The creation date of an investment can be today or a date in the past. - 2. An investment should not be or become negative. -2. __View__ of an investment with its initial amount and expected balance. - 1. Expected balance should be the sum of the invested amount and the [gains][]. - 2. If an investment was already withdrawn then the balance must reflect the gains of that investment -3. __Withdrawal__ of a investment. - 1. The withdraw will always be the sum of the initial amount and its gains, - partial withdrawn is not supported. - 2. Withdrawals can happen in the past or today, but can't happen before the investment creation or the future. - 3. [Taxes][taxes] need to be applied to the withdrawals before showing the final value. -4. __List__ of a person's investments - 1. This list should have pagination. - -__NOTE:__ the implementation of an interface will not be evaluated. - -### Gain Calculation - -The investment will pay 0.52% every month in the same day of the investment creation. - -Given that the gain is paid every month, it should be treated as [compound gain][], which means that every new period (month) the amount gained will become part of the investment balance for the next payment. - -### Taxation - -When money is withdrawn, tax is triggered. Taxes apply only to the profit/gain portion of the money withdrawn. For example, if the initial investment was 1000.00, the current balance is 1200.00, then the taxes will be applied to the 200.00. - -The tax percentage changes according to the age of the investment: -* If it is less than one year old, the percentage will be 22.5% (tax = 45.00). -* If it is between one and two years old, the percentage will be 18.5% (tax = 37.00). -* If older than two years, the percentage will be 15% (tax = 30.00). - -## Requirements -1. Create project using any technology of your preference. It’s perfectly OK to use vanilla code or any framework or libraries; -2. Although you can use as many dependencies as you want, you should manage them wisely; -3. It is not necessary to send the notification emails, however, the code required for that would be welcome; -4. The API must be documented in some way. - -## Deliverables -The project source code and dependencies should be made available in GitHub. Here are the steps you should follow: -1. Fork this repository to your GitHub account (create an account if you don't have one, you will need it working with us). -2. Create a "development" branch and commit the code to it. Do not push the code to the main branch. -3. Include a README file that describes: - - Special build instructions, if any - - List of third-party libraries used and short description of why/how they were used - - A link to the API documentation. -4. Once the work is complete, create a pull request from "development" into "main" and send us the link. -5. Avoid using huge commits hiding your progress. Feel free to work on a branch and use `git rebase` to adjust your commits before submitting the final version. - -## Coding Standards -When working on the project be as clean and consistent as possible. - -## Project Deadline -Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week. - -## Quality Assurance -Use the following checklist to ensure high quality of the project. - -### General -- First of all, the application should run without errors. -- Are all requirements set above met? -- Is coding style consistent? -- The API is well documented? -- The API has unit tests? - -## Submission -1. A link to the Github repository. -2. Briefly describe how you decided on the tools that you used. - -## Have Fun Coding 🤘 -- This challenge description is intentionally vague in some aspects, but if you need assistance feel free to ask for help. -- If any of the seems out of your current level, you may skip it, but remember to tell us about it in the pull request. - -## Credits - -This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md) - -[gains]: #gain-calculation -[taxes]: #taxation -[interest]: #interest-calculation -[compound gain]: https://www.investopedia.com/terms/g/gain.asp +Primeiramente leia a seção de arquivos .env para setar as variáveis de ambiente como senhas de banco de dados. Para instalar todos os pacotes e dependências rode: \ No newline at end of file diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 000000000..a745d6e0c --- /dev/null +++ b/app/.env.example @@ -0,0 +1,5 @@ +POSTGRES_NAME=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/core/asgi.py b/app/core/asgi.py new file mode 100644 index 000000000..76ae1d870 --- /dev/null +++ b/app/core/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for core project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_asgi_application() diff --git a/app/core/pytest.ini b/app/core/pytest.ini new file mode 100644 index 000000000..22a6a45c3 --- /dev/null +++ b/app/core/pytest.ini @@ -0,0 +1,5 @@ +# -- FILE: pytest.ini (or tox.ini) +[pytest] +DJANGO_SETTINGS_MODULE = test.settings +# -- recommended but optional: +python_files = tests.py test_*.py *_tests.py \ No newline at end of file diff --git a/app/core/router.py b/app/core/router.py new file mode 100644 index 000000000..080f03942 --- /dev/null +++ b/app/core/router.py @@ -0,0 +1,9 @@ +from rest_framework import routers +from persons.views import UserViewSet +from investments.views import InvestmentViewSet, WithdrawnViewSet + + +router = routers.DefaultRouter() +router.register('persons', UserViewSet) +router.register('investments', InvestmentViewSet) +router.register('investments', WithdrawnViewSet) \ No newline at end of file diff --git a/app/core/settings.py b/app/core/settings.py new file mode 100644 index 000000000..6fbb5c3e0 --- /dev/null +++ b/app/core/settings.py @@ -0,0 +1,175 @@ +""" +Django settings for core project. + +Generated by 'django-admin startproject' using Django 4.1.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" +import os +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv() + +# .Env Variables +POSTGRES_NAME = os.getenv("POSTGRES_NAME") +POSTGRES_USER = os.getenv("POSTGRES_USER") +POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD") +POSTGRES_HOST = os.getenv("POSTGRES_HOST") +POSTGRES_PORT = os.getenv("POSTGRES_PORT") + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = '587' +EMAIL_HOST_USER = 'investmentscoderockr@gmail.com' +EMAIL_HOST_PASSWORD = 'pvcxleglfznmtgvk' +EMAIL_USE_TLS = True +EMAIL_USE_SSL = False + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-eka=r2^l_-93b0**4_iv2&l*de0r%#am)%7&t9#$1+6)ngvooi' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [ + '127.0.0.1', + 'host.docker.internal', + 'localhost', +] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # installed apps + 'rest_framework', + 'django_filters', + 'drf_yasg', + #myapps + 'investments', + 'persons', +] + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 5 +} + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + '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', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + + 'NAME': POSTGRES_NAME, + 'USER': POSTGRES_USER, + 'PASSWORD': POSTGRES_PASSWORD, + 'HOST': POSTGRES_HOST, + 'PORT': POSTGRES_PORT, + } +} + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ +STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +LOGIN_URL = "/admin/login/" \ No newline at end of file diff --git a/app/core/test_pytest.py b/app/core/test_pytest.py new file mode 100644 index 000000000..b15734486 --- /dev/null +++ b/app/core/test_pytest.py @@ -0,0 +1,25 @@ +import requests +from rest_framework import test + +BASE_URL = 'http://127.0.0.1:8000/api/V1/' + +class TestUsers(test.APITestCase): + BASE_URI = 'users/' + + def test_get_users(self): + users = requests.get(BASE_URL + self.BASE_URI) + + assert users.status_code == 200 + + def test_get_user(self): + user = requests.get(BASE_URL + self.BASE_URI + '1') + + assert user.status_code == 200 + + def test_post_user(self): + data = {"email": "teste2@gmail.com", "name": "teste"} + user = self.client.post(url=BASE_URL + self.BASE_URI, data=data) + + + + assert user.status_code == 201 diff --git a/app/core/urls.py b/app/core/urls.py new file mode 100644 index 000000000..6c5cd5b40 --- /dev/null +++ b/app/core/urls.py @@ -0,0 +1,43 @@ +"""core URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path +from rest_framework import permissions +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from django.urls import include + +from core.router import router + +schema_view = get_schema_view( + openapi.Info( + title="Coderockr Investments", + default_version='v1', + description="Investments Api Documentation - Coderockr", + contact=openapi.Contact(email="gabriellasoares2@gmail.com"), + ), + public=False, + permission_classes=[permissions.AllowAny], +) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('api/V1/', include(router.urls)), +] + diff --git a/app/core/wsgi.py b/app/core/wsgi.py new file mode 100644 index 000000000..63bb09994 --- /dev/null +++ b/app/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/app/investments/__init__.py b/app/investments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/investments/admin.py b/app/investments/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/app/investments/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/investments/apps.py b/app/investments/apps.py new file mode 100644 index 000000000..bec2521a0 --- /dev/null +++ b/app/investments/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InvestmentsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'investments' diff --git a/app/investments/migrations/0001_initial.py b/app/investments/migrations/0001_initial.py new file mode 100644 index 000000000..943e77816 --- /dev/null +++ b/app/investments/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.2 on 2022-10-19 03:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('persons', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Investment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creation_date', models.DateField()), + ('initial_amount', models.FloatField()), + ('withdrawn_date', models.DateField(blank=True, null=True)), + ('active', models.BooleanField(default=True)), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='investments', to='persons.person')), + ], + options={ + 'verbose_name': 'Investment', + }, + ), + ] diff --git a/app/investments/migrations/__init__.py b/app/investments/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/investments/models.py b/app/investments/models.py new file mode 100644 index 000000000..a7a216bee --- /dev/null +++ b/app/investments/models.py @@ -0,0 +1,38 @@ +from django.core.mail import send_mail +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +import pandas as pd +from datetime import datetime + +from persons.models import Person + + +class Investment(models.Model): + owner = models.ForeignKey(Person, related_name='investments', blank=True, null=True,on_delete=models.CASCADE) + creation_date = models.DateField() + initial_amount = models.FloatField() + withdrawn_date = models.DateField(blank=True, null=True) + active = models.BooleanField(default=True) + + def __str__(self): + return f'{self.owner}' + + class Meta: + verbose_name = 'Investment' + + +@receiver(post_save, sender=Investment) +def send_mail_on_create(sender, instance=None, created=False, **kwargs): + if created: + initial_amount = instance.initial_amount + df = pd.DataFrame(data={'date':[datetime.strptime(str(instance.creation_date), '%Y-%m-%d')]}) + day = df.date.dt.day[0] + name = instance.owner.name + email = send_mail( + 'You have a new investment', + f'Hi {name}, \n\nYou have a new investment in your name. The initial amount is ${initial_amount}, this value increases by 0.52% every month on day {day}. \nThank you for the trust, have a great day', + 'investmentscoderockr@outlook.com', + [instance.owner.email], + ) + print(email) diff --git a/app/investments/serializers.py b/app/investments/serializers.py new file mode 100644 index 000000000..ac5ac1649 --- /dev/null +++ b/app/investments/serializers.py @@ -0,0 +1,70 @@ +from datetime import datetime + +import numpy as np +import pandas as pd +from rest_framework import serializers + +from investments.models import Investment + +class InvestmentSerializer(serializers.ModelSerializer): + expected_balance = serializers.SerializerMethodField() + withdrawn_balance = serializers.SerializerMethodField() + + class Meta: + model = Investment + fields = ('id', 'owner', 'creation_date', 'initial_amount', 'expected_balance', 'withdrawn_balance', 'withdrawn_date', 'active') + extra_kwargs = { + 'active': {'read_only': True}, + 'expected_balance': {'read_only': True}, + 'withdrawn_date': {'read_only': True}, + 'owner': {'required': True}, + } + + def calculate_number_of_months(self, investment): + date_now = datetime.today() + creation_date = datetime.strptime(str(investment.creation_date), '%Y-%m-%d') + withdrawn_date = investment.withdrawn_date + if withdrawn_date is None: + data = {'date1': [date_now], 'date2': [creation_date]} + else: + withdrawn_date = datetime.strptime(str(withdrawn_date), '%Y-%m-%d') + data = {'date1': [withdrawn_date], 'date2': [creation_date]} + df = pd.DataFrame(data=data) + df['nb_months'] = ((df.date1 - df.date2)/np.timedelta64(1, 'M')) + df['nb_months'] = df['nb_months'].astype(int) + + return df['nb_months'] + + def get_expected_balance(self, investment): + nb_months = self.calculate_number_of_months(investment) + initial_amount = investment.initial_amount + expected_balance = round(float(initial_amount * (pow((1 + 0.52 / 100), nb_months))), 2) + + return expected_balance + + def get_withdrawn_balance(self, investment): + expected_balance = self.get_expected_balance(investment) + nb_months_series = self.calculate_number_of_months(investment) + initial_amount = investment.initial_amount + profit = expected_balance - initial_amount + nb_months = nb_months_series.item() + if nb_months < 12: + return round(float((profit * 0.775) + initial_amount), 2) + elif nb_months >= 12 and nb_months < 24: + return round(float((profit * 0.815) + initial_amount), 2) + else: + return round(float((profit * 0.85) + initial_amount), 2) + + +class WithdrawnSerializer(InvestmentSerializer): + class Meta: + model = Investment + fields = ('id', 'owner', 'creation_date', 'initial_amount', 'expected_balance', 'withdrawn_balance', 'withdrawn_date', 'active') + extra_kwargs = { + 'active': {'read_only': True}, + 'expected_balance': {'read_only': True}, + 'initial_amount': {'read_only': True}, + 'creation_date': {'read_only': True}, + 'owner': {'read_only': True}, + 'withdrawn_date': {'required': True}, + } diff --git a/app/investments/tests.py b/app/investments/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/app/investments/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/investments/views.py b/app/investments/views.py new file mode 100644 index 000000000..26e5eb2ad --- /dev/null +++ b/app/investments/views.py @@ -0,0 +1,79 @@ +from datetime import datetime + +from django.core.mail import send_mail +from rest_framework import mixins, status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from investments.models import Investment +from investments.serializers import InvestmentSerializer, WithdrawnSerializer + + +class InvestmentViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + queryset = Investment.objects.all() + serializer_class = InvestmentSerializer + + def create(self, request): + serializer = self.get_serializer(data=request.data) + + # valid creation date + creation_date = datetime.strptime(request.data['creation_date'], '%Y-%m-%d') + now = datetime.today() + diff = (now - creation_date).days + if(diff < 0): + return Response('The creation date of an investment can be today or a date in the past.', status=status.HTTP_400_BAD_REQUEST) + + #valid amout + initial_amount = request.data['initial_amount'] + if(initial_amount < 0): + return Response('The initial amount needs to be positive.', status=status.HTTP_400_BAD_REQUEST) + + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + +class WithdrawnViewSet(mixins.UpdateModelMixin,viewsets.GenericViewSet): + queryset = Investment.objects.all() + serializer_class = WithdrawnSerializer + + def send_email_on_withdrawn(self, investment,serializer): + initial_amount = investment.initial_amount + withdrawn_balance = serializer.get_withdrawn_balance(investment) + name = investment.owner.name + print(withdrawn_balance) + email = send_mail( + 'You have a new investment', + f'Hi {name}, \n\nYour investment has been withdrawn. The initial investment amount was {initial_amount} and the amount withdrawn was {withdrawn_balance}. \nThank you for the trust, have a great day', + 'investmentscoderockr@outlook.com', + [investment.owner.email], + ) + print(email) + + @action(detail=True, methods=['put']) + def withdrawn(self, request, pk=None): + investments = self.get_object() + serializer = WithdrawnSerializer(investments, data=request.data) + + withdrawn_date = datetime.strptime(request.data['withdrawn_date'], '%Y-%m-%d') + creation_date = datetime.strptime(str(investments.creation_date), '%Y-%m-%d') + now = datetime.today() + diff_now_withdrawn_date = (now - withdrawn_date).days + diff_creation_date_withdrawn_date = (withdrawn_date - creation_date).days + if(diff_now_withdrawn_date < 0 or diff_creation_date_withdrawn_date < 0): + return Response('Withdrawals can happen in the past or today, but cannot happen before the investment creation or the future.', status=status.HTTP_400_BAD_REQUEST) + # elif investments.active == False: + # return Response('The withdrawal of this investment has already been made.', status=status.HTTP_400_BAD_REQUEST) + serializer.is_valid(raise_exception=True) + investments.active = False + self.send_email_on_withdrawn(investments, serializer) + serializer.save() + return Response(serializer.data) + + + \ No newline at end of file diff --git a/app/manage.py b/app/manage.py new file mode 100644 index 000000000..f2a662cfd --- /dev/null +++ b/app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/app/persons/__init__.py b/app/persons/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/persons/admin.py b/app/persons/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/app/persons/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/persons/apps.py b/app/persons/apps.py new file mode 100644 index 000000000..825dc19ee --- /dev/null +++ b/app/persons/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'persons' diff --git a/app/persons/migrations/0001_initial.py b/app/persons/migrations/0001_initial.py new file mode 100644 index 000000000..7ed274f1c --- /dev/null +++ b/app/persons/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.2 on 2022-10-19 03:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=256)), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email Adress')), + ], + options={ + 'verbose_name': 'User', + }, + ), + ] diff --git a/app/persons/migrations/__init__.py b/app/persons/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/persons/models.py b/app/persons/models.py new file mode 100644 index 000000000..dd0cfd7cb --- /dev/null +++ b/app/persons/models.py @@ -0,0 +1,11 @@ +from django.db import models + +class Person(models.Model): + name = models.CharField(max_length=256, blank=True) + email = models.EmailField('Email Adress', unique=True) + + def __str__(self): + return f'{self.name}' + + class Meta: + verbose_name = 'User' diff --git a/app/persons/serializers.py b/app/persons/serializers.py new file mode 100644 index 000000000..8ba25f6f6 --- /dev/null +++ b/app/persons/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from persons.models import Person + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = Person + fields = ('id', 'email', 'name') diff --git a/app/persons/tests.py b/app/persons/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/app/persons/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/persons/views.py b/app/persons/views.py new file mode 100644 index 000000000..11545a4bf --- /dev/null +++ b/app/persons/views.py @@ -0,0 +1,29 @@ +from rest_framework import mixins, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from persons.models import Person +from persons.serializers import UserSerializer +from investments.serializers import InvestmentSerializer + + +class UserViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + queryset = Person.objects.all() + serializer_class = UserSerializer + + @action(detail=True, methods=['get']) + def investments(self, request, pk=None): + person = self.get_object() + page = self.paginate_queryset(person.investments.all()) + if page is not None: + serializer = InvestmentSerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(person.investments.all(), many=True) + return Response(serializer.data) + # return Response(serializer.data) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..324ea07ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.7' + +services: + db: + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + image: postgres + restart: always + ports: + - '5432:5432' + volumes: + - /var/lib/postgresql + admin: + image: fenglc/pgadmin4 + ports: + - '5050:5050' + environment: + - DEFAULT_USER=admin + application: + build: + context: . + dockerfile: ./docker/Dockerfile + ports: + - '8080:8080' + depends_on: + - 'db' diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..df3ce824b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10 + +WORKDIR /usr/src/app + +COPY pyproject.toml /usr/src/app +RUN pip install poetry +RUN poetry install + + +COPY ./app /usr/src/app + +CMD [ "poetry", "run", "python", "/usr/src/app/manage.py", "runserver", "0.0.0.0:8080"] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..df4d5ec19 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "CodeRockr" +version = "0.1.0" +description = "" +authors = ["gabriella "] + +[tool.poetry.dependencies] +python = "^3.10" +Django = "^4.1" +python-dotenv = "^0.20.0" +psycopg2 = "^2.9.3" +djangorestframework = "^3.13.1" +Pillow = "^9.2.0" +django-filter = "^22.1" +rest_condition = "^1.0.3" +drf-yasg = "^1.21.4" +pytest = "^7.1.3" +pytest-django = "^4.5.2" +numpy = "^1.23.4" +pandas = "^1.5.0" + +[tool.poetry.dev-dependencies] +mypy = "^0.971" +flake8 = "^5.0.4" +pytest = "^7.1.2" +pytest-django = "^4.5.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 69305317dc1d15195e4310c2d79026eb9eff51f4 Mon Sep 17 00:00:00 2001 From: Gabriella Date: Wed, 19 Oct 2022 19:12:17 -0300 Subject: [PATCH 2/8] Atualizando README.md --- Makefile | 3 +- README.md | 67 +++++++++++++++++++++++++++++++--- app/investments/serializers.py | 5 +-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 75c9110eb..b65ef74b6 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,6 @@ run: docker-compose logs -f down: docker-compose down --remove-orphans --volumes -migrate: - docker exec -it servi_app bash -c "python manage.py migrate" + terminal: docker exec -it servi_app bash \ No newline at end of file diff --git a/README.md b/README.md index b024af1eb..8079d47e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,64 @@ -

Coderockr Investments API

-# An API for an application that stores and manages investments, it should have the following features: +# Coderockr Investments API -Inicializando -Você precisa ter o docker e o docker-compose instalado em sua máquina. para isso verifique os links de documentação prorietária: Docker e Docker-compose, nessa ordem. +An API for an application that stores and manages people and their investments. + +## Initializing + +You need to have `docker` and `docker-compose` installed on your machine. for that check the proprietary documentation links: [Docker](https://docs.docker.com/engine/install/) e [Docker-compose](https://docs.docker.com/compose/install/), in that order. + +Then you should copy the data from `.env.example` to `.env`. + +To install all packages and dependencies, run: + +``` +make build +``` + +Or, if you're using windows open the `Makefile` file and run the `build` block, line by line. To know more[leia](makefile). + +Access http://localhost:8080 and you will see the service running. + +## Link to the API documentation + +**There are 2 different documentations** +`Swagger:` + - With Swagger it's possible to test the endpoints directly from this documentation, it makes testing a lot easier. Access the link **http://localhost:8080** + +`Redoc:` + - Redoc is user-friendly and perfect to use on a daily basis and facilitate API absorption. Access the link **http://localhost:8080/redoc** + +## List of third-party libraries used + +#### Docker +Docker makes it easy to run the application without having to put in a lot of effort. With application development using Docker, you don’t need to install a bunch of language environments on your system. You can simply run the application inside docker container with the help of a image. + +#### Python + +Python is an extremely powerful and versatile programming language in terms of the types of applications you can create. + +#### Django and Django-RestFramework + +Django is a open source framework that is compatible with major operating systems and databases. It has a great number of advantages. It's considered a developer-friendly framework easy to pick up. It provides robust security features, helps to avoid the common mistakes of web development including SQL injection, clickjacking, cross-site request forgery and scripting. + +Django REST framework (DRF) is a open source, mature and well supported Python/Django library that aims at building sophisticated web APIs. It is flexible and fully-featured toolkit with modular and customizable architecture that makes possible development of both simple, turn-key API endpoints and complicated REST constructs. + +#### Poetry + +Poetry is a tool for dependency management and packaging in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution. + +#### drf_yasg + +drf_yasg is a API doc generation tool which provides the option to choose between swagger-ui and redoc or both for generating documentation for your APIs + +#### NumPy + +NumPy arrays are faster and more compact than Python lists. An array consumes less memory and is convenient to use. NumPy uses much less memory to store data and it provides a mechanism of specifying the data types. **In this case it was used to calculate the difference of months between two dates** + +#### Pandas + +Pandas is an open-source Python library designed to deal with data analysis and data manipulation. It is built on top of **NumPy** and it has several functions for cleaning, analyzing, and manipulating data, which can help you extract valuable insights about your data set. **In this case it was used to prepare the dates for later calculations.** + +#### postgre + +PostgreSQL comes with many features aimed to help developers build applications, administrators to protect data integrity and build fault-tolerant environments, and help you manage your data no matter how big or small the dataset. In addition to being free and open source, PostgreSQL is highly extensible. -Primeiramente leia a seção de arquivos .env para setar as variáveis de ambiente como senhas de banco de dados. Para instalar todos os pacotes e dependências rode: \ No newline at end of file diff --git a/app/investments/serializers.py b/app/investments/serializers.py index ac5ac1649..8616bf06d 100644 --- a/app/investments/serializers.py +++ b/app/investments/serializers.py @@ -33,7 +33,7 @@ def calculate_number_of_months(self, investment): df['nb_months'] = ((df.date1 - df.date2)/np.timedelta64(1, 'M')) df['nb_months'] = df['nb_months'].astype(int) - return df['nb_months'] + return df['nb_months'].item() def get_expected_balance(self, investment): nb_months = self.calculate_number_of_months(investment) @@ -44,10 +44,9 @@ def get_expected_balance(self, investment): def get_withdrawn_balance(self, investment): expected_balance = self.get_expected_balance(investment) - nb_months_series = self.calculate_number_of_months(investment) + nb_months = self.calculate_number_of_months(investment) initial_amount = investment.initial_amount profit = expected_balance - initial_amount - nb_months = nb_months_series.item() if nb_months < 12: return round(float((profit * 0.775) + initial_amount), 2) elif nb_months >= 12 and nb_months < 24: From a0cf0e2de205be854ac17d28a126291099ef6b47 Mon Sep 17 00:00:00 2001 From: Gabriella Date: Wed, 19 Oct 2022 20:29:00 -0300 Subject: [PATCH 3/8] Atualizando README.md --- README.md | 53 ++++++++++++++++++++++++++++++++++++-------- app/persons/views.py | 7 +++--- pyproject.toml | 2 +- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8079d47e4..25d15d15b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ An API for an application that stores and manages people and their investments. ## Initializing +### With Docker + You need to have `docker` and `docker-compose` installed on your machine. for that check the proprietary documentation links: [Docker](https://docs.docker.com/engine/install/) e [Docker-compose](https://docs.docker.com/compose/install/), in that order. Then you should copy the data from `.env.example` to `.env`. @@ -18,6 +20,40 @@ Or, if you're using windows open the `Makefile` file and run the `build` block, Access http://localhost:8080 and you will see the service running. +### Without Docker + +You need to have `Python 3.9^` installed on your machine. for that check the proprietary download [Link](https://www.python.org/downloads/) + +Then you should copy the data from `.env.example` to `.env`. + +To install `Poetry` run: + +``` +pip install poetry +``` + +To install all packages and dependencies, run: + +``` +poetry install +``` + +To run all migrations: + +``` +poetry run python ./app/manage.py makemigrations +``` +``` +poetry run python ./app/manage.py migrate +``` + +Finally, run the server: + +``` +poetry run python ./app/manage.py runserver +``` + + ## Link to the API documentation **There are 2 different documentations** @@ -29,36 +65,35 @@ Access http://localhost:8080 and you will see the service running. ## List of third-party libraries used -#### Docker +### Docker Docker makes it easy to run the application without having to put in a lot of effort. With application development using Docker, you don’t need to install a bunch of language environments on your system. You can simply run the application inside docker container with the help of a image. -#### Python +### Python Python is an extremely powerful and versatile programming language in terms of the types of applications you can create. -#### Django and Django-RestFramework +### Django and Django-RestFramework Django is a open source framework that is compatible with major operating systems and databases. It has a great number of advantages. It's considered a developer-friendly framework easy to pick up. It provides robust security features, helps to avoid the common mistakes of web development including SQL injection, clickjacking, cross-site request forgery and scripting. Django REST framework (DRF) is a open source, mature and well supported Python/Django library that aims at building sophisticated web APIs. It is flexible and fully-featured toolkit with modular and customizable architecture that makes possible development of both simple, turn-key API endpoints and complicated REST constructs. -#### Poetry +### Poetry Poetry is a tool for dependency management and packaging in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution. -#### drf_yasg +### drf_yasg drf_yasg is a API doc generation tool which provides the option to choose between swagger-ui and redoc or both for generating documentation for your APIs -#### NumPy +### NumPy NumPy arrays are faster and more compact than Python lists. An array consumes less memory and is convenient to use. NumPy uses much less memory to store data and it provides a mechanism of specifying the data types. **In this case it was used to calculate the difference of months between two dates** -#### Pandas +### Pandas Pandas is an open-source Python library designed to deal with data analysis and data manipulation. It is built on top of **NumPy** and it has several functions for cleaning, analyzing, and manipulating data, which can help you extract valuable insights about your data set. **In this case it was used to prepare the dates for later calculations.** -#### postgre +### postgre PostgreSQL comes with many features aimed to help developers build applications, administrators to protect data integrity and build fault-tolerant environments, and help you manage your data no matter how big or small the dataset. In addition to being free and open source, PostgreSQL is highly extensible. - diff --git a/app/persons/views.py b/app/persons/views.py index 11545a4bf..95e29684f 100644 --- a/app/persons/views.py +++ b/app/persons/views.py @@ -1,23 +1,22 @@ +from investments.serializers import InvestmentSerializer from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response from persons.models import Person from persons.serializers import UserSerializer -from investments.serializers import InvestmentSerializer class UserViewSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): queryset = Person.objects.all() serializer_class = UserSerializer @action(detail=True, methods=['get']) - def investments(self, request, pk=None): + def investments(self, request, pk=None, *args, **kwargs): person = self.get_object() page = self.paginate_queryset(person.investments.all()) if page is not None: @@ -26,4 +25,4 @@ def investments(self, request, pk=None): serializer = self.get_serializer(person.investments.all(), many=True) return Response(serializer.data) - # return Response(serializer.data) \ No newline at end of file + # return Response(serializer.data) diff --git a/pyproject.toml b/pyproject.toml index df4d5ec19..f179a32e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,10 @@ Pillow = "^9.2.0" django-filter = "^22.1" rest_condition = "^1.0.3" drf-yasg = "^1.21.4" -pytest = "^7.1.3" pytest-django = "^4.5.2" numpy = "^1.23.4" pandas = "^1.5.0" +ptw = "^1.0.1" [tool.poetry.dev-dependencies] mypy = "^0.971" From df674779012eb49c9683064a2f0a61e1c6cf83b3 Mon Sep 17 00:00:00 2001 From: Gabriella Soares <72384261+gabssoaresr@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:40:12 -0300 Subject: [PATCH 4/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25d15d15b..4aad7dc29 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,6 @@ NumPy arrays are faster and more compact than Python lists. An array consumes le Pandas is an open-source Python library designed to deal with data analysis and data manipulation. It is built on top of **NumPy** and it has several functions for cleaning, analyzing, and manipulating data, which can help you extract valuable insights about your data set. **In this case it was used to prepare the dates for later calculations.** -### postgre +### PostgreSQL PostgreSQL comes with many features aimed to help developers build applications, administrators to protect data integrity and build fault-tolerant environments, and help you manage your data no matter how big or small the dataset. In addition to being free and open source, PostgreSQL is highly extensible. From 9a9d7ddd25b06c1cd87e754217043f82fedc8629 Mon Sep 17 00:00:00 2001 From: Gabriella Soares Date: Sat, 22 Oct 2022 19:36:06 -0300 Subject: [PATCH 5/8] Creating Unit Tests --- .gitignore | 22 ---- README.md | 18 +++- app/core/pytest.ini | 5 - app/core/router.py | 4 +- app/core/test_pytest.py | 25 ----- app/investments/serializers.py | 1 - app/investments/tests.py | 3 - app/investments/tests/__init__.py | 0 app/investments/tests/test_investments_api.py | 102 ++++++++++++++++++ .../tests/test_investments_base.py | 29 +++++ app/investments/views.py | 18 ++-- app/persons/serializers.py | 2 +- app/persons/tests.py | 3 - app/persons/tests/__init__.py | 0 app/persons/tests/test_person_base.py | 11 ++ app/persons/tests/test_person_models.py | 9 ++ app/persons/views.py | 11 +- app/pytest.ini | 11 ++ poetry.toml | 3 + pyproject.toml | 2 +- 20 files changed, 199 insertions(+), 80 deletions(-) delete mode 100644 app/core/pytest.ini delete mode 100644 app/core/test_pytest.py delete mode 100644 app/investments/tests.py create mode 100644 app/investments/tests/__init__.py create mode 100644 app/investments/tests/test_investments_api.py create mode 100644 app/investments/tests/test_investments_base.py delete mode 100644 app/persons/tests.py create mode 100644 app/persons/tests/__init__.py create mode 100644 app/persons/tests/test_person_base.py create mode 100644 app/persons/tests/test_person_models.py create mode 100644 app/pytest.ini create mode 100644 poetry.toml diff --git a/.gitignore b/.gitignore index f62cee117..8aff6f037 100644 --- a/.gitignore +++ b/.gitignore @@ -3,28 +3,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.md b/README.md index 4aad7dc29..c5432524c 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,23 @@ Access http://localhost:8080 and you will see the service running. ### Without Docker -You need to have `Python 3.9^` installed on your machine. for that check the proprietary download [Link](https://www.python.org/downloads/) +You need to have `Python 3.10^` installed on your machine. for that check the proprietary download [Link](https://www.python.org/downloads/) -Then you should copy the data from `.env.example` to `.env`. +You need to have `PostgreSQL` installed on your machine. for that check the proprietary download [Link](https://www.postgresql.org/download/) + +Then you should copy the data from `.env.example` to `.env`, it is necessary to put the **PASSWORD** and the **PORT** chosen in postgreSQL to `.env` file. + +To create the `Venv` file run: + +``` +python -m venv venv +``` + +To activate `VirtualEnv` run: + +``` +./venv/scripts/activate +``` To install `Poetry` run: diff --git a/app/core/pytest.ini b/app/core/pytest.ini deleted file mode 100644 index 22a6a45c3..000000000 --- a/app/core/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -# -- FILE: pytest.ini (or tox.ini) -[pytest] -DJANGO_SETTINGS_MODULE = test.settings -# -- recommended but optional: -python_files = tests.py test_*.py *_tests.py \ No newline at end of file diff --git a/app/core/router.py b/app/core/router.py index 080f03942..4609485d2 100644 --- a/app/core/router.py +++ b/app/core/router.py @@ -1,9 +1,9 @@ from rest_framework import routers -from persons.views import UserViewSet +from persons.views import PersonViewSet from investments.views import InvestmentViewSet, WithdrawnViewSet router = routers.DefaultRouter() -router.register('persons', UserViewSet) +router.register('persons', PersonViewSet) router.register('investments', InvestmentViewSet) router.register('investments', WithdrawnViewSet) \ No newline at end of file diff --git a/app/core/test_pytest.py b/app/core/test_pytest.py deleted file mode 100644 index b15734486..000000000 --- a/app/core/test_pytest.py +++ /dev/null @@ -1,25 +0,0 @@ -import requests -from rest_framework import test - -BASE_URL = 'http://127.0.0.1:8000/api/V1/' - -class TestUsers(test.APITestCase): - BASE_URI = 'users/' - - def test_get_users(self): - users = requests.get(BASE_URL + self.BASE_URI) - - assert users.status_code == 200 - - def test_get_user(self): - user = requests.get(BASE_URL + self.BASE_URI + '1') - - assert user.status_code == 200 - - def test_post_user(self): - data = {"email": "teste2@gmail.com", "name": "teste"} - user = self.client.post(url=BASE_URL + self.BASE_URI, data=data) - - - - assert user.status_code == 201 diff --git a/app/investments/serializers.py b/app/investments/serializers.py index 8616bf06d..11d5b04f4 100644 --- a/app/investments/serializers.py +++ b/app/investments/serializers.py @@ -1,5 +1,4 @@ from datetime import datetime - import numpy as np import pandas as pd from rest_framework import serializers diff --git a/app/investments/tests.py b/app/investments/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/app/investments/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/investments/tests/__init__.py b/app/investments/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/investments/tests/test_investments_api.py b/app/investments/tests/test_investments_api.py new file mode 100644 index 000000000..ccf3dc590 --- /dev/null +++ b/app/investments/tests/test_investments_api.py @@ -0,0 +1,102 @@ +from investments.tests.test_investments_base import InvestmentsTestBase +from datetime import date, timedelta + +class InvestmentsAPIv1Test(InvestmentsTestBase): + BASE_URL = 'http://127.0.0.1:8000/api/V1/investments/' + + def test_investment_list_returns_status_code_200(self): + response = self.client.get(self.BASE_URL) + self.assertEqual(response.status_code, 200) + + def test_person_post_returns_status_code_201(self): + response = self.client.post( + 'http://127.0.0.1:8000/api/V1/persons/', + data={ + 'email': 'example@example.com', + 'name': 'Teste' + }) + return response.data.get('id') + + def test_investment_list_has_pagination(self): + wanted_investments = 100 + self.create_investment_in_batch(qtd=wanted_investments) + response = self.client.get(self.BASE_URL) + assert response.data.get('next') is not None + + def test_investments_post_returns_status_code_201(self): + person = self.test_person_post_returns_status_code_201() + response = self.client.post( + self.BASE_URL, + data={ + 'owner': person, + 'creation_date': '2019-10-22', + 'initial_amount': 10000 + } + ) + self.assertEqual( + response.status_code, + 201 + ) + + return response.data.get('id') + + def test_investment_cannot_be_created_without_an_owner(self): + response = self.client.post(self.BASE_URL) + self.assertEqual( + response.status_code, + 400 + ) + + def test_investments_creation_date_cannot_be_in_the_future(self): + person = self.test_person_post_returns_status_code_201() + today_date = date.today() + future_date = today_date + timedelta(1) + response = self.client.post( + self.BASE_URL, + data={ + 'owner': person, + 'creation_date': future_date, + 'initial_amount': 10000 + } + ) + self.assertEqual( + response.status_code, + 400 + ) + + def test_investment_initial_amount_cannot_be_negative(self): + person = self.test_person_post_returns_status_code_201() + response = self.client.post( + self.BASE_URL, + data={ + 'owner': person, + 'creation_date': '2019-10-22', + 'initial_amount': -2 + } + ) + self.assertEqual( + response.status_code, + 400 + ) + + def test_investment_withdrawal_returns_status_code_200(self): + investment = self.test_investments_post_returns_status_code_201() + print(investment) + response = self.client.put(f'{self.BASE_URL}{investment}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + self.assertEqual( + response.status_code, + 200 + ) + + return investment + + def test_investment_cannot_have_more_than_one_withdrawal(self): + investment = self.test_investment_withdrawal_returns_status_code_200() + response = self.client.put(f'{self.BASE_URL}{investment}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + self.assertEqual( + response.status_code, + 400 + ) + + def test_investments_withdrawal_date_cannot_be_in_the_future_or_before_it_creation_date(self): + \ No newline at end of file diff --git a/app/investments/tests/test_investments_base.py b/app/investments/tests/test_investments_base.py new file mode 100644 index 000000000..6259d69e1 --- /dev/null +++ b/app/investments/tests/test_investments_base.py @@ -0,0 +1,29 @@ +from random import randrange +from rest_framework.test import APITestCase +from investments.models import Investment +from persons.models import Person + +class InvestmentsMixin: + def create_person(self, email, name): + return Person.objects.create(email=email, name=name) + + def create_investment(self, owner, creation_date, initial_amount): + return Investment.objects.create(owner=owner, creation_date=creation_date, initial_amount=initial_amount) + + def create_investment_in_batch(self, qtd=0): + investments = [] + person = self.create_person(email='teste@example.com', name='Teste') + for i in range(qtd): + kwargs = { + 'owner': person, + 'creation_date': '2019-10-22', + 'initial_amount': randrange(10000, 50000) + } + investment = self.create_investment(**kwargs) + investments.append(investment) + return investments + + +class InvestmentsTestBase(APITestCase, InvestmentsMixin): + def setUp(self) -> None: + return super().setUp() diff --git a/app/investments/views.py b/app/investments/views.py index 26e5eb2ad..1b9af5abf 100644 --- a/app/investments/views.py +++ b/app/investments/views.py @@ -14,22 +14,24 @@ class InvestmentViewSet( mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - queryset = Investment.objects.all() + queryset = Investment.objects.all().order_by('-id') serializer_class = InvestmentSerializer def create(self, request): serializer = self.get_serializer(data=request.data) # valid creation date - creation_date = datetime.strptime(request.data['creation_date'], '%Y-%m-%d') + if request.data.get('creation_date') is None: + return Response('The creation date is mandatory.', status=status.HTTP_400_BAD_REQUEST) + creation_date = datetime.strptime(request.data.get('creation_date'), '%Y-%m-%d') now = datetime.today() diff = (now - creation_date).days if(diff < 0): return Response('The creation date of an investment can be today or a date in the past.', status=status.HTTP_400_BAD_REQUEST) #valid amout - initial_amount = request.data['initial_amount'] - if(initial_amount < 0): + initial_amount = request.data.get('initial_amount') + if(int(initial_amount) < 0): return Response('The initial amount needs to be positive.', status=status.HTTP_400_BAD_REQUEST) serializer.is_valid(raise_exception=True) @@ -39,21 +41,19 @@ def create(self, request): class WithdrawnViewSet(mixins.UpdateModelMixin,viewsets.GenericViewSet): - queryset = Investment.objects.all() + queryset = Investment.objects.all().order_by('-id') serializer_class = WithdrawnSerializer def send_email_on_withdrawn(self, investment,serializer): initial_amount = investment.initial_amount withdrawn_balance = serializer.get_withdrawn_balance(investment) name = investment.owner.name - print(withdrawn_balance) email = send_mail( 'You have a new investment', f'Hi {name}, \n\nYour investment has been withdrawn. The initial investment amount was {initial_amount} and the amount withdrawn was {withdrawn_balance}. \nThank you for the trust, have a great day', 'investmentscoderockr@outlook.com', [investment.owner.email], ) - print(email) @action(detail=True, methods=['put']) def withdrawn(self, request, pk=None): @@ -67,8 +67,8 @@ def withdrawn(self, request, pk=None): diff_creation_date_withdrawn_date = (withdrawn_date - creation_date).days if(diff_now_withdrawn_date < 0 or diff_creation_date_withdrawn_date < 0): return Response('Withdrawals can happen in the past or today, but cannot happen before the investment creation or the future.', status=status.HTTP_400_BAD_REQUEST) - # elif investments.active == False: - # return Response('The withdrawal of this investment has already been made.', status=status.HTTP_400_BAD_REQUEST) + elif investments.active == False: + return Response('The withdrawal of this investment has already been made.', status=status.HTTP_400_BAD_REQUEST) serializer.is_valid(raise_exception=True) investments.active = False self.send_email_on_withdrawn(investments, serializer) diff --git a/app/persons/serializers.py b/app/persons/serializers.py index 8ba25f6f6..40e053222 100644 --- a/app/persons/serializers.py +++ b/app/persons/serializers.py @@ -2,7 +2,7 @@ from persons.models import Person -class UserSerializer(serializers.ModelSerializer): +class PersonSerializer(serializers.ModelSerializer): class Meta: model = Person fields = ('id', 'email', 'name') diff --git a/app/persons/tests.py b/app/persons/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/app/persons/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/persons/tests/__init__.py b/app/persons/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/persons/tests/test_person_base.py b/app/persons/tests/test_person_base.py new file mode 100644 index 000000000..435025498 --- /dev/null +++ b/app/persons/tests/test_person_base.py @@ -0,0 +1,11 @@ +from rest_framework.test import APITestCase +from persons.models import Person + +class PersonMixin: + def create_person(self, email, name): + return Person.objects.create(email=email, name=name) + + +class PersonTestBase(APITestCase, PersonMixin): + def setUp(self) -> None: + return super().setUp() diff --git a/app/persons/tests/test_person_models.py b/app/persons/tests/test_person_models.py new file mode 100644 index 000000000..e321e21b2 --- /dev/null +++ b/app/persons/tests/test_person_models.py @@ -0,0 +1,9 @@ +from persons.tests.test_person_base import PersonTestBase + +class PersonModelTest(PersonTestBase): + def setUp(self): + self.person = self.create_person(email='teste@example.com', name='Teste') + return super().setUp() + + def test_the_test(self): + person = self.person \ No newline at end of file diff --git a/app/persons/views.py b/app/persons/views.py index 95e29684f..3e80c22bc 100644 --- a/app/persons/views.py +++ b/app/persons/views.py @@ -4,25 +4,24 @@ from rest_framework.response import Response from persons.models import Person -from persons.serializers import UserSerializer +from persons.serializers import PersonSerializer -class UserViewSet( +class PersonViewSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - queryset = Person.objects.all() - serializer_class = UserSerializer + queryset = Person.objects.all().order_by('-id') + serializer_class = PersonSerializer @action(detail=True, methods=['get']) def investments(self, request, pk=None, *args, **kwargs): person = self.get_object() - page = self.paginate_queryset(person.investments.all()) + page = self.paginate_queryset(person.investments.all().order_by('-id')) if page is not None: serializer = InvestmentSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(person.investments.all(), many=True) return Response(serializer.data) - # return Response(serializer.data) diff --git a/app/pytest.ini b/app/pytest.ini new file mode 100644 index 000000000..dfd92f93b --- /dev/null +++ b/app/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +DJANGO_SETTINGS_MODULE = core.settings +python_files = test.py tests.py test_*.py tests_*.py *_test.py *_tests.py +addopts = + --doctest-modules + --strict-markers + # -rP +markers = + slow: Run tests that are slow + fast: Run fast tests + functional_test: Run tests that are selenium based diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 000000000..53b35d370 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/pyproject.toml b/pyproject.toml index f179a32e0..a799e37ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,10 @@ Pillow = "^9.2.0" django-filter = "^22.1" rest_condition = "^1.0.3" drf-yasg = "^1.21.4" -pytest-django = "^4.5.2" numpy = "^1.23.4" pandas = "^1.5.0" ptw = "^1.0.1" +pytest-django = "^4.5.2" [tool.poetry.dev-dependencies] mypy = "^0.971" From 737792602237d8e730b69734459e484868e1a7c4 Mon Sep 17 00:00:00 2001 From: Gabriella Soares Date: Sun, 23 Oct 2022 16:42:03 -0300 Subject: [PATCH 6/8] adding tests and fixing possible errors --- README.md | 29 +++++-- app/.env.example | 4 +- app/investments/models.py | 1 - app/investments/serializers.py | 2 + app/investments/tests/test_investments_api.py | 76 ++++++++++++++++--- app/investments/views.py | 20 +++-- .../migrations/0002_alter_person_name.py | 18 +++++ app/persons/models.py | 2 +- app/persons/tests/__init__.py | 0 app/persons/tests/test_person_base.py | 11 --- app/persons/tests/test_person_models.py | 9 --- 11 files changed, 127 insertions(+), 45 deletions(-) create mode 100644 app/persons/migrations/0002_alter_person_name.py delete mode 100644 app/persons/tests/__init__.py delete mode 100644 app/persons/tests/test_person_base.py delete mode 100644 app/persons/tests/test_person_models.py diff --git a/README.md b/README.md index c5432524c..107e8a350 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Coderockr Investments API -An API for an application that stores and manages people and their investments. +An API for an application that stores and manages people and their investments, and send emails whenever an investment is created or withdrawal. ## Initializing @@ -8,7 +8,7 @@ An API for an application that stores and manages people and their investments. You need to have `docker` and `docker-compose` installed on your machine. for that check the proprietary documentation links: [Docker](https://docs.docker.com/engine/install/) e [Docker-compose](https://docs.docker.com/compose/install/), in that order. -Then you should copy the data from `.env.example` to `.env`. +Then you should copy the data from `.env.example` to `.env`, you need to choose a **PASSWORD** and a **PORT** in the `.env` file. With docker the **HOST** must be `host.docker.internal` To install all packages and dependencies, run: @@ -26,7 +26,7 @@ You need to have `Python 3.10^` installed on your machine. for that check the pr You need to have `PostgreSQL` installed on your machine. for that check the proprietary download [Link](https://www.postgresql.org/download/) -Then you should copy the data from `.env.example` to `.env`, it is necessary to put the **PASSWORD** and the **PORT** chosen in postgreSQL to `.env` file. +Then you should copy the data from `.env.example` to `.env`, it is necessary to put the **PASSWORD** and the **PORT** chosen in postgreSQL to `.env` file. Without docker the **HOST** must be `localhost` To create the `Venv` file run: @@ -67,15 +67,34 @@ Finally, run the server: poetry run python ./app/manage.py runserver ``` +Access http://localhost:8000 and you will see the service running. + +## Running Unit Tests + +First you need to initialize the app `Without Docker`. + +Then go to the `app` folder: +``` +cd app +``` + +Finally run: +``` +poetry run pytest +``` ## Link to the API documentation **There are 2 different documentations** `Swagger:` - - With Swagger it's possible to test the endpoints directly from this documentation, it makes testing a lot easier. Access the link **http://localhost:8080** + - With Swagger it's possible to test the endpoints directly from this documentation, it makes testing a lot easier. If you're running in **docker**, access the link **http://localhost:8080**. + **Without Docker**, access the link **http://localhost:8000** `Redoc:` - - Redoc is user-friendly and perfect to use on a daily basis and facilitate API absorption. Access the link **http://localhost:8080/redoc** + - Redoc is user-friendly and perfect to use on a daily basis and facilitate API absorption. If you're running in **docker**, access the link **http://localhost:8080/redoc**. + **Without Docker**, access the link **http://localhost:8000/redoc** + + ## List of third-party libraries used diff --git a/app/.env.example b/app/.env.example index a745d6e0c..6ced21934 100644 --- a/app/.env.example +++ b/app/.env.example @@ -1,5 +1,5 @@ POSTGRES_NAME=postgres POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_HOST=localhost +POSTGRES_PASSWORD=coderockr +POSTGRES_HOST=host.docker.internal POSTGRES_PORT=5432 \ No newline at end of file diff --git a/app/investments/models.py b/app/investments/models.py index a7a216bee..be201f21f 100644 --- a/app/investments/models.py +++ b/app/investments/models.py @@ -35,4 +35,3 @@ def send_mail_on_create(sender, instance=None, created=False, **kwargs): 'investmentscoderockr@outlook.com', [instance.owner.email], ) - print(email) diff --git a/app/investments/serializers.py b/app/investments/serializers.py index 11d5b04f4..6b498a5f6 100644 --- a/app/investments/serializers.py +++ b/app/investments/serializers.py @@ -1,10 +1,12 @@ from datetime import datetime + import numpy as np import pandas as pd from rest_framework import serializers from investments.models import Investment + class InvestmentSerializer(serializers.ModelSerializer): expected_balance = serializers.SerializerMethodField() withdrawn_balance = serializers.SerializerMethodField() diff --git a/app/investments/tests/test_investments_api.py b/app/investments/tests/test_investments_api.py index ccf3dc590..6a94dfd72 100644 --- a/app/investments/tests/test_investments_api.py +++ b/app/investments/tests/test_investments_api.py @@ -1,13 +1,10 @@ +from urllib import response from investments.tests.test_investments_base import InvestmentsTestBase -from datetime import date, timedelta +from datetime import date, timedelta, datetime class InvestmentsAPIv1Test(InvestmentsTestBase): BASE_URL = 'http://127.0.0.1:8000/api/V1/investments/' - def test_investment_list_returns_status_code_200(self): - response = self.client.get(self.BASE_URL) - self.assertEqual(response.status_code, 200) - def test_person_post_returns_status_code_201(self): response = self.client.post( 'http://127.0.0.1:8000/api/V1/persons/', @@ -17,14 +14,24 @@ def test_person_post_returns_status_code_201(self): }) return response.data.get('id') + def test_investment_list_returns_status_code_200(self): + response = self.client.get(self.BASE_URL) + self.assertEqual(response.status_code, 200) + def test_investment_list_has_pagination(self): wanted_investments = 100 self.create_investment_in_batch(qtd=wanted_investments) + response = self.client.get(self.BASE_URL) + assert response.data.get('next') is not None + + return {"response": response.data, "wanted_investments": wanted_investments} + def test_investments_post_returns_status_code_201(self): person = self.test_person_post_returns_status_code_201() + response = self.client.post( self.BASE_URL, data={ @@ -33,15 +40,17 @@ def test_investments_post_returns_status_code_201(self): 'initial_amount': 10000 } ) + self.assertEqual( response.status_code, 201 ) - return response.data.get('id') + return response.data def test_investment_cannot_be_created_without_an_owner(self): response = self.client.post(self.BASE_URL) + self.assertEqual( response.status_code, 400 @@ -51,6 +60,7 @@ def test_investments_creation_date_cannot_be_in_the_future(self): person = self.test_person_post_returns_status_code_201() today_date = date.today() future_date = today_date + timedelta(1) + response = self.client.post( self.BASE_URL, data={ @@ -59,6 +69,7 @@ def test_investments_creation_date_cannot_be_in_the_future(self): 'initial_amount': 10000 } ) + self.assertEqual( response.status_code, 400 @@ -81,8 +92,10 @@ def test_investment_initial_amount_cannot_be_negative(self): def test_investment_withdrawal_returns_status_code_200(self): investment = self.test_investments_post_returns_status_code_201() - print(investment) - response = self.client.put(f'{self.BASE_URL}{investment}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + investment_id = investment['id'] + + response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + self.assertEqual( response.status_code, 200 @@ -92,11 +105,54 @@ def test_investment_withdrawal_returns_status_code_200(self): def test_investment_cannot_have_more_than_one_withdrawal(self): investment = self.test_investment_withdrawal_returns_status_code_200() - response = self.client.put(f'{self.BASE_URL}{investment}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + investment_id = investment['id'] + + response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': '2022-10-22'}) + self.assertEqual( response.status_code, 400 ) - def test_investments_withdrawal_date_cannot_be_in_the_future_or_before_it_creation_date(self): + def test_investments_withdrawal_date_cannot_be_before_it_creation_date(self): + investment = self.test_investments_post_returns_status_code_201() + + investment_id = investment['id'] + creation_date = datetime.strptime(str(investment['creation_date']), '%Y-%m-%d') + withdrawn_date = (creation_date - timedelta(1)).date() + + response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': withdrawn_date}) + + self.assertEqual( + response.status_code, + 400 + ) + + def test_investments_withdrawal_date_cannot_be_in_the_future(self): + investment = self.test_investments_post_returns_status_code_201() + + investment_id = investment['id'] + now = datetime.today() + withdrawn_date = (now + timedelta(1)).date() + + response = self.client.put(f'{self.BASE_URL}{investment_id}/withdrawn/', data={'withdrawn_date': withdrawn_date}) + self.assertEqual( + response.status_code, + 400 + ) + + def test_person_investments_list_return_all_investments(self): + investments = self.test_investment_list_has_pagination() + person_id = investments.get('response').get('results')[0].get('owner') + wanted_investments = investments.get('wanted_investments') + + response = self.client.get(f'http://127.0.0.1:8000/api/V1/persons/{person_id}/investments/') + + self.assertEqual( + response.data.get('count'), + wanted_investments + ) + + + \ No newline at end of file diff --git a/app/investments/views.py b/app/investments/views.py index 1b9af5abf..c993aa9f6 100644 --- a/app/investments/views.py +++ b/app/investments/views.py @@ -19,6 +19,12 @@ class InvestmentViewSet( def create(self, request): serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + datetime.strptime(request.data.get('creation_date'), '%Y-%m-%d') + except: + return Response('Please, enter a correct date. Ex: 2022-10-22', status=status.HTTP_400_BAD_REQUEST) # valid creation date if request.data.get('creation_date') is None: @@ -34,7 +40,6 @@ def create(self, request): if(int(initial_amount) < 0): return Response('The initial amount needs to be positive.', status=status.HTTP_400_BAD_REQUEST) - serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) @@ -57,18 +62,21 @@ def send_email_on_withdrawn(self, investment,serializer): @action(detail=True, methods=['put']) def withdrawn(self, request, pk=None): + try: + datetime.strptime(request.data['withdrawn_date'], '%Y-%m-%d') + except: + return Response('Please, enter a correct date. Ex: 2022-10-22', status=status.HTTP_400_BAD_REQUEST) investments = self.get_object() serializer = WithdrawnSerializer(investments, data=request.data) - withdrawn_date = datetime.strptime(request.data['withdrawn_date'], '%Y-%m-%d') creation_date = datetime.strptime(str(investments.creation_date), '%Y-%m-%d') now = datetime.today() diff_now_withdrawn_date = (now - withdrawn_date).days - diff_creation_date_withdrawn_date = (withdrawn_date - creation_date).days - if(diff_now_withdrawn_date < 0 or diff_creation_date_withdrawn_date < 0): - return Response('Withdrawals can happen in the past or today, but cannot happen before the investment creation or the future.', status=status.HTTP_400_BAD_REQUEST) - elif investments.active == False: + diff_creation_date_withdrawn_date = (withdrawn_date - creation_date).days + if investments.active == False: return Response('The withdrawal of this investment has already been made.', status=status.HTTP_400_BAD_REQUEST) + elif(diff_now_withdrawn_date < 0 or diff_creation_date_withdrawn_date < 0): + return Response('Withdrawals can happen in the past or today, but cannot happen before the investment creation or the future.', status=status.HTTP_400_BAD_REQUEST) serializer.is_valid(raise_exception=True) investments.active = False self.send_email_on_withdrawn(investments, serializer) diff --git a/app/persons/migrations/0002_alter_person_name.py b/app/persons/migrations/0002_alter_person_name.py new file mode 100644 index 000000000..a4faa666d --- /dev/null +++ b/app/persons/migrations/0002_alter_person_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.2 on 2022-10-23 18:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('persons', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='person', + name='name', + field=models.CharField(max_length=256), + ), + ] diff --git a/app/persons/models.py b/app/persons/models.py index dd0cfd7cb..6b25b8245 100644 --- a/app/persons/models.py +++ b/app/persons/models.py @@ -1,7 +1,7 @@ from django.db import models class Person(models.Model): - name = models.CharField(max_length=256, blank=True) + name = models.CharField(max_length=256) email = models.EmailField('Email Adress', unique=True) def __str__(self): diff --git a/app/persons/tests/__init__.py b/app/persons/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/persons/tests/test_person_base.py b/app/persons/tests/test_person_base.py deleted file mode 100644 index 435025498..000000000 --- a/app/persons/tests/test_person_base.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework.test import APITestCase -from persons.models import Person - -class PersonMixin: - def create_person(self, email, name): - return Person.objects.create(email=email, name=name) - - -class PersonTestBase(APITestCase, PersonMixin): - def setUp(self) -> None: - return super().setUp() diff --git a/app/persons/tests/test_person_models.py b/app/persons/tests/test_person_models.py deleted file mode 100644 index e321e21b2..000000000 --- a/app/persons/tests/test_person_models.py +++ /dev/null @@ -1,9 +0,0 @@ -from persons.tests.test_person_base import PersonTestBase - -class PersonModelTest(PersonTestBase): - def setUp(self): - self.person = self.create_person(email='teste@example.com', name='Teste') - return super().setUp() - - def test_the_test(self): - person = self.person \ No newline at end of file From 6c7a4bd04e9b981348c14b46c296f40b0c4a4936 Mon Sep 17 00:00:00 2001 From: Gabriella Soares Date: Sun, 23 Oct 2022 19:25:38 -0300 Subject: [PATCH 7/8] removing migrations --- app/investments/migrations/0001_initial.py | 30 ------------------- app/persons/migrations/0001_initial.py | 25 ---------------- .../migrations/0002_alter_person_name.py | 18 ----------- 3 files changed, 73 deletions(-) delete mode 100644 app/investments/migrations/0001_initial.py delete mode 100644 app/persons/migrations/0001_initial.py delete mode 100644 app/persons/migrations/0002_alter_person_name.py diff --git a/app/investments/migrations/0001_initial.py b/app/investments/migrations/0001_initial.py deleted file mode 100644 index 943e77816..000000000 --- a/app/investments/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.1.2 on 2022-10-19 03:50 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('persons', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Investment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateField()), - ('initial_amount', models.FloatField()), - ('withdrawn_date', models.DateField(blank=True, null=True)), - ('active', models.BooleanField(default=True)), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='investments', to='persons.person')), - ], - options={ - 'verbose_name': 'Investment', - }, - ), - ] diff --git a/app/persons/migrations/0001_initial.py b/app/persons/migrations/0001_initial.py deleted file mode 100644 index 7ed274f1c..000000000 --- a/app/persons/migrations/0001_initial.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.2 on 2022-10-19 03:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Person', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, max_length=256)), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email Adress')), - ], - options={ - 'verbose_name': 'User', - }, - ), - ] diff --git a/app/persons/migrations/0002_alter_person_name.py b/app/persons/migrations/0002_alter_person_name.py deleted file mode 100644 index a4faa666d..000000000 --- a/app/persons/migrations/0002_alter_person_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.2 on 2022-10-23 18:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('persons', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='person', - name='name', - field=models.CharField(max_length=256), - ), - ] From 58f8cd0275f30376b537002606e0b7a2e59b9679 Mon Sep 17 00:00:00 2001 From: Gabriella Soares Date: Mon, 24 Oct 2022 16:18:53 -0300 Subject: [PATCH 8/8] .env file fix --- app/.env.example => .env.example | 0 app/core/settings.py | 5 ++--- docker-compose.yml | 8 ++++---- docker/Dockerfile | 2 ++ 4 files changed, 8 insertions(+), 7 deletions(-) rename app/.env.example => .env.example (100%) diff --git a/app/.env.example b/.env.example similarity index 100% rename from app/.env.example rename to .env.example diff --git a/app/core/settings.py b/app/core/settings.py index 6fbb5c3e0..f5cd91b44 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -14,7 +14,7 @@ from dotenv import load_dotenv -load_dotenv() +load_dotenv(dotenv_path=".env") # .Env Variables POSTGRES_NAME = os.getenv("POSTGRES_NAME") @@ -151,7 +151,6 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': POSTGRES_NAME, 'USER': POSTGRES_USER, 'PASSWORD': POSTGRES_PASSWORD, @@ -172,4 +171,4 @@ # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -LOGIN_URL = "/admin/login/" \ No newline at end of file +LOGIN_URL = "/admin/login/" diff --git a/docker-compose.yml b/docker-compose.yml index 324ea07ee..2cd89d963 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,13 @@ version: '3.7' services: db: environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=postgres + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_NAME} image: postgres restart: always ports: - - '5432:5432' + - '${POSTGRES_PORT}:5432' volumes: - /var/lib/postgresql admin: diff --git a/docker/Dockerfile b/docker/Dockerfile index df3ce824b..e91ccc80c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,6 +2,7 @@ FROM python:3.10 WORKDIR /usr/src/app +COPY .env /usr/src/app COPY pyproject.toml /usr/src/app RUN pip install poetry RUN poetry install @@ -9,4 +10,5 @@ RUN poetry install COPY ./app /usr/src/app + CMD [ "poetry", "run", "python", "/usr/src/app/manage.py", "runserver", "0.0.0.0:8080"] \ No newline at end of file