Skip to content

Commit

Permalink
feat: add user registration kata
Browse files Browse the repository at this point in the history
  • Loading branch information
pmareke committed Jul 11, 2023
1 parent ff059a9 commit bfe112c
Show file tree
Hide file tree
Showing 23 changed files with 996 additions and 88 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ Repository with solutions to several katas:
## Codely
- [X] [Tiered pricing](https://github.com/CodelyTV/refactoring-code-smells/tree/master/exercises/tiered_pricing)

## Codium
- [ ] [User Registration](https://github.com/CodiumTeam/legacy-training-python/tree/master/user-registration-refactoring-kata)

## How to run the tests:

```sh
Expand Down
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-fantasy_battle.*]
ignore_missing_imports = True
[mypy-django.*]
ignore_missing_imports = True
690 changes: 602 additions & 88 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = "^3.10"
pyupgrade = "^3.2.0"
django = "^4.2.3"

[tool.poetry.dev-dependencies]
yapf = "^0.32.0"
Expand Down
2 changes: 2 additions & 0 deletions user-registration-refactoring/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
db.sqlite3
__pycache__
13 changes: 13 additions & 0 deletions user-registration-refactoring/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM python:3.8

MAINTAINER Codium <[email protected]>

WORKDIR /opt/project

COPY requirements.txt /tmp
RUN pip install -r /tmp/requirements.txt

VOLUME ["/opt/project"]
EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
20 changes: 20 additions & 0 deletions user-registration-refactoring/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.PHONY: default docker-tests

default:
@printf "$$HELP"

docker-build:
docker build -t django-docker-bootstrap .

docker-tests:
docker run --rm -v "${PWD}:/opt/project" django-docker-bootstrap python manage.py test test

define HELP
# Docker commands
- make docker-build\tGenerate the docker image with Django installed
- make docker-tests\t\tRun the tests
Please execute "make <command>". Example make help

endef

export HELP
45 changes: 45 additions & 0 deletions user-registration-refactoring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Description

This all about an API that registers the users of a web application.

It is designed to practice how to identify the different responsibilities in the code and decouple them.

## Goal
There are two main objectives:
1. Part 1: decouple the code from the Framework used.
2. Part 2: decouple the code from Database and Libraries.

Start on the [views.py](src/framework/views.py)

## Part 1: decouple the code from the Framework used.
1. Run the tests.
2. Do not write Business logic on the Controllers → Instead move ALL the logic to a Use Case class.
3. Do not use the Inputs of the Framework as arguments of your Use Case class.
4. Create your own exceptions to handle errors.
5. Do not use the Framework response as the response of your Use Case class.

## Part 2: decouple the code from Database and Libraries.
For the second part of the exercise you need to repeat this 4 steps for each coupling:
1. Define an Interface using Dependency Inversion Principle.
2. Evolve your legacy code to match the Interface.
3. Create an adapter that implements the Interface and uses the Library.
4. Remove the coupling with the infrastructure (Database and Libraries) injecting the collaborator.


## Install the dependencies

within docker

make docker-build

## Run the tests

within docker

make docker-tests

## Authors
Luis Rovirosa [@luisrovirosa](https://www.twitter.com/luisrovirosa)

Jordi Anguela [@jordianguela](https://www.twitter.com/jordianguela)
14 changes: 14 additions & 0 deletions user-registration-refactoring/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python
import os
import sys

if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.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)
Empty file.
119 changes: 119 additions & 0 deletions user-registration-refactoring/project/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Django settings for ur project.
Generated by 'django-admin startproject' using Django 2.1.7.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'wdra&6411lo0jxx^-!51wuax9kd&27w1i+y(kg6cw$@_j*(nw4'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS: list[str] = []

# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

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 = 'project.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 = 'project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}


# Password validation
# https://docs.djangoproject.com/en/2.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/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = '/static/'
21 changes: 21 additions & 0 deletions user-registration-refactoring/project/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""ur URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.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.urls import path
from src.framework.views import UserController

urlpatterns = [
path('', UserController.as_view()),
]
16 changes: 16 additions & 0 deletions user-registration-refactoring/project/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for ur 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/2.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')

application = get_wsgi_application()
1 change: 1 addition & 0 deletions user-registration-refactoring/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
django
Empty file.
Empty file.
8 changes: 8 additions & 0 deletions user-registration-refactoring/src/domain/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass
class User:
user_id: int
name: str
email: str
Empty file.
Empty file.
31 changes: 31 additions & 0 deletions user-registration-refactoring/src/framework/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import smtplib
import ssl
from random import randint
from django.http import JsonResponse, HttpResponseBadRequest, HttpRequest, HttpResponse
from django.views import View
from src.domain.user import User
from src.infrastructure.user_framework_repository import UserFrameworkRepository

class UserController(View):
# Create your views here.
def get(self) -> HttpResponse:
return JsonResponse("Hello, world. You're at the polls index.")

def post(self, request: HttpRequest) -> HttpResponse:
if len(request.POST['password']) <= 8:
return HttpResponseBadRequest('Password is not valid')
if "_" not in request.POST['password']:
return HttpResponseBadRequest('Password is not valid')
if UserFrameworkRepository.get_instance().find_by_email(request.POST['email']) is not None:
return HttpResponseBadRequest('The email is already in use')
user = User(randint(1, 999999), request.POST['name'], request.POST['email'])
UserFrameworkRepository.get_instance().save(user)

# Send a confirmation email
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server:
# Uncomment this lines with a valid username and password
server.login("[email protected]", "myPassword")
server.sendmail('[email protected]', request.POST['email'], 'Confirmation link')

return JsonResponse({'name': user.name, 'email': user.email, 'id': user.user_id})
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from src.domain.user import User


class UserFrameworkRepository:
repository: "UserFrameworkRepository" | None = None

def __init__(self) -> None:
self.users: dict[str, User] = {}

def save(self, user: User) -> None:
self.users[user.email] = user

def find_by_email(self, email_address: str) -> User | None:
return self.users.get(email_address)

@staticmethod
def get_instance() -> "UserFrameworkRepository":
if UserFrameworkRepository.repository is None:
UserFrameworkRepository.repository = UserFrameworkRepository()
return UserFrameworkRepository.repository

@staticmethod
def set_instance(the_instance: "UserFrameworkRepository") -> None:
UserFrameworkRepository.repository = the_instance
Loading

0 comments on commit bfe112c

Please sign in to comment.