Fakultas Ilmu Komputer UI

Skip to content
Snippets Groups Projects
Commit 77f06e5c authored by Ryo's avatar Ryo
Browse files

Initializing repository

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 823 additions and 0 deletions
# Created by https://www.toptal.com/developers/gitignore/api/django
# Edit at https://www.toptal.com/developers/gitignore?templates=django
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
<django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
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
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
# Django stuff:
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# 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
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# VSCode
.vscode/*
.history
# mypy
.mypy_cache/
# End of https://www.toptal.com/developers/gitignore/api/django
\ No newline at end of file
Pipfile 0 → 100644
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "~=3.2.12"
djangorestframework = "3.13.1"
psycopg2-binary = "2.9.3"
injector = "0.19.0"
djangorestframework-dataclasses = "1.1.1"
validate-email = "1.3"
[dev-packages]
[requires]
python_version = "3.9.5"
README.md 0 → 100755
# Lexam - Backend Repository
This repository is used for E-Assessment web based app development for Group Thesis, created by:
- Jonathan - 1806204985
- Kirana Alfatianisa - 1806186843
- Ryo Axtonlie - 1806205571
## Installation Guide (for Development)
### Dependencies:
* Python (3.9.5) - [here](https://www.python.org/downloads/release/python-395/)
* Pipenv - [here](https://pipenv.pypa.io/en/latest/)
* Docker - [here](https://www.docker.com/products/docker-desktop)
* PostgreSQL (Docker Image) - [here](https://hub.docker.com/_/postgres) / PostgreSQL - [here](https://www.postgresql.org/download/)
* PyCharm - [here](https://www.jetbrains.com/pycharm/) (Highly reccomended, use the Professional Edition if you are using WSL)
### Installation Step:
#### Creating Database:
1. Create PSQL Docker Container (Optional)
1. Download docker image postgres from [here](https://hub.docker.com/_/postgres)
1. run container with command:
> docker run --name lexam_postgres -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres
2. Access to PSQL
- Access with CMD:
> psql postgresql://postgres:mypassword@localhost
or if you are using Docker (the step above can be used for Docker based PSQL as well)
> docker exec -it lexam_postgres psql -U postgres
- Access via DataGrip:
- Add Add New Data Source -> PostgreSQL
- Name: lexam_postgres@localhost
- Host: localhost
- Port: 5432
- Authentication: User & Password
- Username: <b>postgres</b>
- Password: <b>password</b>
- Database: postgres
- URL: jdbc:postgresql://localhost:5432/postgres
3. Create Database with name `lexam`
#### Activate Virtual Environment (Pipenv)
1. Create or Activate the Virtual Environment on the main directory by using command below:
> pipenv shell
2. Install the dependencies needed by using the command below:
> pipenv install
#### Integrating with PyCharm
1. Add your current pipenv interpreter by clicking bottom right part of window (usually there is your current Python version or it is asking to select interpreter)
2. Click Add Interpreter option
3. Click WSL from the option (Windows)
4. Set the directory so it become like
1. For Windows:
> \\wsl$\Ubuntu-18.04/home/[username]/.local/share/virtualenvs/lexam_backend-[any_string]/bin/python
2. For Ubuntu:
> home/[username]/.local/share/virtualenvs/lexam_backend-[any_string]/bin/python
5. Click Ok button
6. Click Edit Configuration button located on top right side of the window
7. Press the + button and pick Django Server template, then edit the template so the content will become like this:
- Environment Settings: PYTHONUNBUFFERED=1;DJANGO_SETTINGS_MODULE=lexam_backend.settings
- Python Interpreter: [Interpreter you previously set upping from step 1-4]
- Working Directory: [Directory where this project is placed]
8. You can then run the App by pressing the Play shaped button on PyCharm
9. You can also run the App from command line with command below on the project base directory
> python manage.py runserver
#### Migrations
1. Create a migrations when there is changes on the existing Model:
> python manage.py makemigrations
2. Apply the changes to the database:
> python manage.py migrate
#### Create Superuser
> python manage.py createsuperuser
#### Pattern Used
For the lexam_backend development, we are using the Hexagonal Architecture, you can learn about it on [here](https://blog.octo.com/hexagonal-architecture-three-principles-and-an-implementation-example/)
#### Directory Pattern
```
.
├── .gitignore
├── Pipfile
├── Pipfile.lock
├── README.md
├── lexam_api
│ ├── __init__.py
│ ├── admin.py
│ ├── api
│ │ └── [entity]
│ │ ├── injectors.py
│ │ ├── module.py
│ │ └── view.py
│ ├── apps.py
│ ├── core
│ │ ├── common
│ │ │ ├── enums.py
│ │ │ ├── ...
│ │ │ └── exceptions.py
│ │ └── [entity]
│ │ ├── models.py
│ │ ├── port
│ │ │ ├── database
│ │ │ │ └── [entity]_accessor.py
│ │ │ └── service
│ │ │ └── [entity]_service.py
│ │ └── service.py
│ ├── database
│ │ └── [entity]
│ │ ├── accessor.py
│ │ └── module.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20220328_1728.py
│ │ ├── ...
│ │ └── __init__.py
│ ├── models.py
│ └── urls.py
├── lexam_backend
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
```
#### Guide:
- Main logic of the code that needed to be running using the existing business logic implementation should be placed under the `lexam_api` apps
- Do note that by using hexagonal pattern, the client side and server side don't need to know bussiness logic implementation, so both should be depends on the business core `port`. Even each business core service implementation should only known their own, without knowing what the other service implementation will be looks like
- For each user side and server side, since it depends on `Interface` of the core services `port`, you have to `bind` the `Interface` to the implementation by using the `module.py` file.
- For the user side, please bind the `Interface` of a core services to corresponding implementation on `lexam_api/api/[entity]/module.py` like:
```
class UserViewModule(Module):
def configure(self, binder: Binder):
binder.bind(InterfaceUserService, to=UserService, scope=singleton)
```
- For the server side, please bind the `Interface` of the `accessor` `port` in to its own `accessor` implementation on `lexam_api/database/[entity]/module.py` like:
```
class UserAccessorModule(Module):
def configure(self, binder: Binder):
binder.bind(InterfaceUserAccessor, to=UserAccessor, scope=singleton)
```
- Then, each corresponding API should only inject a service and accessor that they need by adding them on `lexam_api/api/[entity]/injectors.py` so it will be like:
```
injector = Injector([UserViewModule, UserAccessorModule])
```
- Each time you are going to make a new API endpoint, please make it inside `[entity]ViewSet(ViewSet)` class, so it will be able to be grouped under one routing url.
- After creating the `ViewSet`, please register it on `lexam_api/urls.py` by using the `router.register` like `router.register('user', UserViewSet, basename='user')`
- Place anything will be widely used on the service on `common` directory, for example place `RequestInvalidError` on the `lexam_api/core.common.exceptions.py` since it will be widely used to `raise RequestInvalidError` on several services
- Each time you create a new `models.py` with a new class Model inside the `lexam_api/core/[entity]/models.py`, please import it on `lexam_api/models.py` like:
> import lexam_api.core.user.models # noqa
if you don't, it will not be read during the `makemigrations` and `migrate` steps.
- Each core service should serve their only domain, like `User` core to handle everything regarding to `User` while `Group` will handle everything regarding `Group`. If one service need others, please Inject the corresponding `Interface` on the corresponding service initiation
- To makesure that each service only known every class implementation <b>must be depends</b> on `Interface` class of the others that they are going to have dependency. For instance, `API` depends on `ServiceA`, then the `API` should get the `InterfaceA` via `injector`
- In business service implementation, since it won't have the `injector` itself, it will be using help from `@inject` decorator on the class `__init__`, there you can specify the `Interface` class that already `binded` to the `__init__` parameter and put it as the class attribute like:
```
class AService(InterfaceAService):
@inject
def __init__(self, a_accessor: InterfaceAAccessor, b_service: InterfaceBService):
self.a_accessor = a_accessor
self.b_service = b_service
```
- Each time you are making a new business logic on `core` services, please don't forget to make the `port` on the `lexam_api/core/[entity]/port` with directory `database` for creating `Interface[Entity]Accessor` with the functionality will be implemented on the real accessor class and directory `service` for creating `Interface[Entity]Service` with the functionality will be implemented on its own.
@startuml
left to right direction
partition User {
(*) --> "1. Membuka halaman Create User"
--> "2. Mengisi data User yang ingin dibuat"
--> "3. Mengklik tombol Create User"
--> "4. Send Request menuju API"
--> ===WaitAPIResponse===
--> "16. Menampilkan Response"
--> (*)
}
partition API {
===WaitAPIResponse=== --> "5. Menerima Request from User"
if "6. request.valid ?" then
-->[true] if "7. request['email'].valid ?" then
-->[true] ===WaitServiceResponse===
--> "15. Return Response"
--> ===WaitAPIResponse===
else
->[false] "15. Return Response"
endif
else
->[false] "15. Return Response"
endif
}
partition Service {
===WaitServiceResponse=== --> "8. Get User by Username"
--> ===WaitGetUserByUsernameResponseFromDatabase===
if "10. username.exist ?" then
-->[true] "10.a. raise EntityExistedError"
--> ===WaitServiceResponse===
else
-->[false] "11. Get User by Email"
--> ===WaitGetUserByEmailResponseFromDatabase===
--> if " 13. email.exists ?" then
-->[true] "13.a. raise EntityExistedError"
--> ===WaitServiceResponse===
else
-->[false] ===WaitDatabaseResponse===
--> ===WaitServiceResponse===
endif
endif
}
partition Database {
===WaitGetUserByUsernameResponseFromDatabase=== --> "9. Get User by Username"
--> ===WaitGetUserByUsernameResponseFromDatabase===
===WaitGetUserByEmailResponseFromDatabase=== --> "12. Get User by Email"
--> ===WaitGetUserByEmailResponseFromDatabase===
===WaitDatabaseResponse=== --> "14. Create New User"
--> ===WaitDatabaseResponse===
}
@enduml
@startuml
left to right direction
partition User {
(*) --> "1. Membuka List User"
--> "2. Mengklik User yang ingin dilihat"
--> "3. Send Request menuju API"
--> ===WaitAPIResponse===
--> "10. Menampilkan Response"
--> (*)
}
partition UserAPI {
===WaitAPIResponse=== --> "4. Menerima Request from User"
if "5. request.valid ?" then
->[true] ===WaitServiceResponse===
--> "9. Return Response"
--> ===WaitAPIResponse===
else
->[false] "9. Return Response"
endif
}
partition UserService {
===WaitServiceResponse=== --> "6. Get User by Username"
--> ===WaitDatabaseResponse===
if "8. user.exists ?" then
->[true] "8.a. Construct Result"
--> ===WaitServiceResponse===
else
--> "8.b. raise EntityNotFoundError"
--> ===WaitServiceResponse===
endif
}
partition UserDatabase {
===WaitDatabaseResponse=== --> "7. Get User by Username"
--> ===WaitDatabaseResponse===
}
@enduml
@startuml
left to right direction
package API <<rectangle>> {
class UserViewSet
}
package core <<rectangle>> {
interface InterfaceUserService
class UserService
interface InterfaceUserAccessor
}
package database <<rectangle>> {
class UserAccessor
}
API -[hidden]> core
database -[hidden]> core
UserViewSet o-- InterfaceUserService
InterfaceUserService <|-- UserService
InterfaceUserAccessor <|-- UserAccessor
UserService o-- InterfaceUserAccessor
@enduml
\ No newline at end of file
@startuml
left to right direction
entity User {
**id: serial <<primary key>>**
--
username: varchar (255)
password: varchar (128)
email: varchar (254)
first_name: varchar (150)
last_name: varchar (150)
role: varchar (255)
is_staff: boolean
is_active: boolean
is_superuser: boolean
last_login: timestamp
--
created_at: timestamp
updated_at: timestamp
}
@enduml
\ No newline at end of file
@startuml
actor User
participant UserViewSet
participant UserService
participant UserAccessor
User -> UserViewSet: Create User Request
activate UserViewSet
alt Request is valid case
alt Email is valid case
UserViewSet -> UserViewSet: Construct spec
UserViewSet -> UserService: create_new_user(spec)
activate UserService
UserService -> UserAccessor: get_user_by_username(spec.username)
activate UserAccessor
UserAccessor -> UserAccessor: get user by username
UserAccessor --> UserService: return user
deactivate UserAccessor
alt User with username is exists case
UserService --> UserViewSet: raise EntityExistedError
UserViewSet --> User: return Error Response
else User with username is not exists case
UserService -> UserAccessor: get_user_by_email(spec.email)
activate UserAccessor
UserAccessor -> UserAccessor: get user by email
UserAccessor --> UserService: return User
deactivate UserAccessor
alt User with email is exists case
UserService --> UserViewSet: raise EntityExistedError
UserViewSet --> User: return Error Response
else User with email is not exists case
UserService -> UserAccessor: create_new_user(spec)
activate UserAccessor
UserAccessor -> UserAccessor: create new user
UserAccessor --> UserService: return created_user
deactivate UserAccessor
UserService -> UserService: Construct created used result
UserService --> UserViewSet: return Result
UserViewSet --> User: return Response
end
end
deactivate UserService
else Email is not valid case
UserViewSet --> User: return Error Response
end
else Request invalid case
UserViewSet --> User: return Error Response
deactivate UserViewSet
end
@enduml
\ No newline at end of file
@startuml
@startuml
actor User
participant UserViewSet
participant UserService
participant UserAccessor
User -> UserViewSet: Create User Request
activate UserViewSet
alt Request is valid case
UserViewSet -> UserViewSet: Construct spec
UserViewSet -> UserService: get_user_by_username(spec)
activate UserService
UserService -> UserAccessor: get_user_by_username(spec.username)
activate UserAccessor
UserAccessor -> UserAccessor: get user by username
UserAccessor --> UserService: return user
deactivate UserAccessor
alt User with username is exists case
UserService -> UserService: construct Result
UserService --> UserViewSet: return Result
UserViewSet --> User: return Response
else User with username is not exists case
UserService --> UserViewSet: raise EntityNotFoundError
UserViewSet --> User: return Error Response
end
deactivate UserService
else Request invalid case
UserViewSet --> User: return Error Response
end
deactivate UserViewSet
@enduml
@enduml
\ No newline at end of file
@startuml
left to right direction
actor User as User
actor Dosen as Dosen
actor Mahasiswa as Mahasiswa
rectangle user {
usecase "Create User" as CreateUser
usecase "Get User by Username" as GetUserByUsername
}
User <|-- Dosen
User <|-- Mahasiswa
Dosen --> CreateUser
Dosen --> GetUserByUsername
@enduml
\ No newline at end of file
from django.contrib import admin
# Register your models here.
from injector import Injector
from lexam_api.api.user.module import UserViewModule
from lexam_api.database.user.module import UserAccessorModule
injector = Injector([UserViewModule, UserAccessorModule])
from injector import Module, Binder, singleton
from lexam_api.core.user.port.service.user_service import InterfaceUserService
from lexam_api.core.user.service import UserService
class UserViewModule(Module):
def configure(self, binder: Binder):
binder.bind(InterfaceUserService, to=UserService, scope=singleton)
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from rest_framework_dataclasses.serializers import DataclassSerializer
from validate_email import validate_email
from lexam_api.api.user.injectors import injector
from lexam_api.core.common.exceptions import CustomBaseException
from lexam_api.core.user.port.service.user_service import InterfaceUserService, CreateUserSpec, CRUDUserResult, \
GetUserByUsernameSpec
user_service = injector.get(InterfaceUserService)
class CreateUserRequest(DataclassSerializer):
class Meta:
dataclass = CreateUserSpec
class CRUDUserResponse(DataclassSerializer):
class Meta:
dataclass = CRUDUserResult
class GetUserByUsernameRequest(DataclassSerializer):
class Meta:
dataclass = GetUserByUsernameSpec
class UserViewSet(ViewSet):
@action(methods=['post'], detail=False, url_path='create')
def create_user(self, request: Request):
serializer = CreateUserRequest(data=request.data)
serializer.is_valid(raise_exception=True)
spec_data = serializer.data
if not validate_email(spec_data['email']):
return Response(data={'Please enter a valid email'}, status=422)
spec = CreateUserSpec(
username=spec_data['username'],
password=spec_data['password'],
email=spec_data['email'],
first_name=spec_data['first_name'],
last_name=spec_data['last_name'],
role=spec_data['role']
)
try:
result = user_service.create_new_user(spec=spec)
return Response(CRUDUserResponse(result).data)
except CustomBaseException as e:
return Response(data={str(e)}, status=422)
@action(methods=['get'], detail=False, url_path='get-by-username')
def get_user_by_username(self, request: Request):
serializer = GetUserByUsernameRequest(data=request.data)
serializer.is_valid(raise_exception=True)
spec_data = serializer.data
spec = GetUserByUsernameSpec(username=spec_data['username'])
try:
result = user_service.get_user_by_username(spec=spec)
return Response(CRUDUserResponse(result).data)
except CustomBaseException as e:
return Response(data={str(e)}, status=422)
from django.apps import AppConfig
class LexamApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'lexam_api'
from enum import Enum
class BaseEnum(Enum):
@classmethod
def get_enum_value(cls):
return list(map(lambda enum: enum.value, cls))
@classmethod
def choices(cls):
return tuple((enum.name, enum.value) for enum in cls)
class RoleEnum(BaseEnum):
DOSEN = 'DOSEN'
MAHASISWA = 'MAHASISWA'
from typing import Any
class CustomBaseException(Exception):
pass
class RequestInvalidError(CustomBaseException):
error_code = 'REQUEST_INVALID'
class BaseEntityError(CustomBaseException):
def __init__(self, entity_name: str, entity_identifier: str, entity_identifier_value: Any):
self.entity_name = entity_name
self.entity_identifier = entity_identifier
self.entity_identifier_value = entity_identifier_value
class EntityExistedError(BaseEntityError):
error_code = 'ENTITY_EXISTED'
def __str__(self):
return f'{self.entity_name} with {self.entity_identifier}: {self.entity_identifier_value} is exist'
class EntityNotFoundError(BaseEntityError):
error_code = 'ENTITY_NOT_FOUND'
def __str__(self):
return f'{self.entity_name} with {self.entity_identifier}: {self.entity_identifier_value} is not found'
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import UserManager, AbstractUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from lexam_api.core.common.enums import RoleEnum
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(max_length=255, unique=True)
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=150)
last_name = models.CharField(max_length=150)
role = models.CharField(max_length=255, choices=RoleEnum.choices(), default=RoleEnum.MAHASISWA.name)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(blank=True, null=True)
USERNAME_FIELD = 'username'
objects = UserManager()
from abc import ABC, abstractmethod
from typing import Optional
from lexam_api.core.user.models import User
from lexam_api.core.user.port.service.user_service import CreateUserSpec
class InterfaceUserAccessor(ABC):
@abstractmethod
def get_user_by_username(self, username: str) -> Optional[User]:
raise NotImplementedError
@abstractmethod
def get_user_by_email(self, email: str) -> Optional[User]:
raise NotImplementedError
@abstractmethod
def create_new_user(self, spec: CreateUserSpec) -> User:
raise NotImplementedError
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment