Copyright © 2024 Beyond
The job hunter's workbench.
There are five types of branch:
main: Current stable version of the project.release: Next version in development.dev: Workspace for the next version.${name}: Each developer has his/her own branch.${tag}-${description}: For separate feature or bug-fix, e.g.feature--add-cache.
-
Developers should be on his/her own branch for daily development. You should remember to pull from
devregularly. -
Once a change is done, push from
${name}to$devbranch, must be sure that this update is runnable. -
When
devbranch passed test, the leader of the team should push it toreleasebranch. -
Finally, when
releasebranch is ready to go, the leader should then push it tomasterbranch.
All push and pull should follow the convention mentioned in the following section.
To make commit more consistent, you MUST follow the commit convention. There should better be fewer changes in one commit.
'Fewer' doesn't mean fewer lines of code, but fewer functions.
Generally, commit should be lower-case, and no final period punctuation.
To introduce a new feature.
feat: add User model
Not to introduce a new feature, but only made updates to an old one.
update: add extra-check for username
This can also be used when new files are added or removed. The 'add' or 'remove' action must be included.
update: add website favicon
update: remove register.py
It indicates a bug is fixed.
fix: invalid login password ignored
It indicates that changes are made, but no affect to the function.
refactor: divide views.py to multiple files
It indicates this is a trivial change. Such commit should not include any change to the business logic.
trivial: correct spelling error
Environment is based on Python 3.10 using conda.
For remote development, refer to Configure Remote Interpreter in PyCharm.
Some packages may not be easy to install, here are some hints.
conda install -c conda-forge django-cors-headersApplications have their own packages, and common infrastructures are in shared package.
See
userapp for example.
In any application, do not use the default views.py, instead, create a endpoints.py for all endpoints. Also, do use urls.py to wrap all APIs.
All functions in endpoints.py are request handlers, and should be named with _ suffix to avoid naming conflict. (I hate Django.)
You can directly write login in endpoints.py, but for complicated logics, create a services.py and move logics there.
See
user/models.pyfor example.
Model class should be in Camel Case.
Models must have a static create method as constructor to hide concrete construction.
Models must have a Meta class, with at least db_table field set. db_table should be in snake_case.
See
userapp for example.
We have provided some graceful decorators to handle parameter problems. So please follow the best practice.
For more information, see Chapter 5 below.
Make sure to wrap response with our unified classes.
You should return different status code for different errors, see Http Status Code. Use OkResponse, UnautorizedResponse or else to wrap them, and use OkDto, UnauthorizedDto, ErrorDto or else to wrap the payload.
For more information, see Chapter 5 below.
This section lists some of the best practices to follow when you contribute to this project.
Before you continue, make sure you understand Chapter 4.
See
userapp for example.
If there are too many request parameters, or the parameters need some more validation, use DTO.
Make sure your DTO class inherit this base class. You can overwrite these two methods if necessary. With this and @with_dto decorator, you can simply complete parameter validation, and get a well-formed request data.
class BaseRequestDto:
def cleanup(self):
return self
def validate(self) -> bool:
return TrueUse @api_view to specify the request method, these are in endpoints.py.
# e.g.
@api_view(['POST'])
def register_(request):
passDO NOT parse params like primitives! Use decorators provided in shared/decorators.
For complicated parameters:
# e.g.
@with_dto(RegisterDto)
def register_(request, dto: RegisterDto):
passFor flexible complicated parameters:
@with_dict_params()
def update_user_info_(request, params: dict):
passFor simple parameters without type:
Will only check existence, not type. It may be an int or a string, or None. Better use this for optional parameters.
#e.g.
@with_typed_params(["uid", "pid"])
def get_user_info_(request, uid, pid):
passFor simple parameters with type:
Will check type of parameter, and try to make casting, e.g. str to int.
# e.g.
@with_typed_params([("uid", int)])
def refresh_jwt_token_(request, uid: int):
passFor request that need authentication:
If not authorized, will return UnauthorizedResponse with the specified message prematurely.
# e.g.
@with_auth()
def update_user_info_(request, auth: AuthView):
passFor files in request:
This will extract files from request of form-data.
@with_file(["file"])
def update_user_avatar_(request, file):
passFor a single cookie value:
May not be used that often.
@with_cookie(key="refresh_token")
def refresh_jwt_token_(request, cookie: str):
passDecorators can co-exists, so you can use multiple decorators at the same time.
Make sure to log important events in service. Use @with_logger decorator to inject logger to the service.
# e.g.
@api_view(['GET'])
@with_user("Login first")
@with_logger()
def get_logging_status_service(request, user, logger: Logger):
logger.info(f"User {user.id} checked logging status")
return OkResponse(OkDto("Congratulations, you are logged in!"))Five levels for logging: debug, info, warning, error, critical.
Only warning and above will be logged in production.
Do not directly use response provided by Django it self. Use our well-defined response instead.
See shared/dtos/ordinary_response_dto.py for all base response DTOs, and shared/response/json_response.py for response wrappers.
By default, the response DTO only takes a message. If data is available, use data= to specify it. And if data is too complex, define a class for it.
# e.g.
data = GenerateTokenSuccessData(uid, jwt_token, refresh_token.expires)
return OkResponse(OkDto(data=data))
# e.g.
return BadRequestResponse(BadRequestDto("Bad requets", data={
"expect": "uid",
"got": "id"
}))Write comments for each function and class you write.
For services, just description is acceptable, for example:
def register_service(request, dto):
"""
Register a new user.
"""
passFor other functions, specify parameters and return value. It can be generated as you type three quotes. For example:
def generate_jwt_token_pair(uid: int):
"""
Generate JWT token and refresh token. Will save the refresh token
in cache.
:param uid: The user id for which we will generate the token.
:return: A tuple of (jwt_token, refresh_token).
"""
passEnglish comments are preferred. If use Chinese, make sure your file encoding is UTF-8.