diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..37ccaed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,28 @@ +--- +name: 🐛 Bug Report +about: If something is not working as expected 🤔. + +--- + +## Bug Report + +**Problematic behavior** +A clear and concise description of the behavior. + +**Expected behavior/code** +A clear and concise description of what you expected to happen (or code). + +**Steps to Reproduce** +1. Do this... +2. Then this... +3. And then the bug happens! + +**Environment** +- Library version: +- Platform: + +**Possible Solution** + + +**Additional context/Screenshots** +Add any other context about the problem here. If applicable, add screenshots to help explain. diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..51d3170 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,23 @@ +--- +name: ✨ Feature Request +about: I have a suggestion (and may want to build it 💪)! + +--- + +## Feature Request + +**Is your feature request related to a problem or unsupported use case? Please describe.** +A clear and concise description of what the problem is. For example: I need to do some task and I have an issue... + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. Add any considered drawbacks. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Discovery, Documentation, Adoption, Migration Strategy** +If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable). +Maybe a screenshot or design? + +**Do you want to work on it through a Pull Request?** + diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md new file mode 100644 index 0000000..877bdb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Support_question.md @@ -0,0 +1,22 @@ +--- +name: 🤗 Support Question +about: If you have a question 💬, or something was not clear from the docs! + +--- + + + +--- + +Please make sure you have read our [main Readme](https://github.com/suitenumerique/django-lasuite). + +Also make sure it was not already answered in [an open or close issue](https://github.com/suitenumerique/django-lasuite/issues). + +If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌 + +**Topic** +What's the general area of your question: for example, setup, database schema, search functionality,... + +**Question** +Try to be as specific as possible so we can help you as best we can. Please be patient 🙏 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..85cfbe6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Purpose + +Description... + + +## Proposal + +Description... + +- [] item 1... +- [] item 2... diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2a826bb --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,90 @@ +name: Test Workflow + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + + lint-git: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' # Makes sense only for pull requests + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: show + run: git log + - name: Check absence of fixup commits + run: | + ! git log | grep 'fixup!' + - name: Install gitlint + run: pip install --user requests gitlint + - name: Lint commit messages added to main + run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD + + check-changelog: + runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false && + github.event_name == 'pull_request' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check that the CHANGELOG has been modified in the current branch + run: git whatchanged --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG + + lint-changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Check CHANGELOG max line length + run: | + max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L) + if [ $max_line_length -ge 80 ]; then + echo "ERROR: CHANGELOG has lines longer than 80 characters." + exit 1 + fi + + + lint-back: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install development dependencies + run: pip install --user .[dev] + - name: Check code formatting with ruff + run: ~/.local/bin/ruff format . --diff + - name: Lint code with ruff + run: ~/.local/bin/ruff check . + + test-pytest: + runs-on: ubuntu-latest + + env: + DJANGO_SETTINGS_MODULE: test_project.settings + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install development dependencies + run: pip install --user .[dev] + - name: Run tests + run: ~/.local/bin/pytest -vvv tests/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2c0bc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ +staticfiles/ + +# Virtual Environment +venv/ +env/ +ENV/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +coverage.xml +*.cover + +# Editor/IDE specific +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..f7373b6 --- /dev/null +++ b/.gitlint @@ -0,0 +1,78 @@ +# All these sections are optional, edit this file as you like. +[general] +# Ignore certain rules, you can reference them by their id or by their full name +# ignore=title-trailing-punctuation, T3 + +# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this +# verbosity = 2 + +# By default gitlint will ignore merge commits. Set to 'false' to disable. +# ignore-merge-commits=true + +# By default gitlint will ignore fixup commits. Set to 'false' to disable. +# ignore-fixup-commits=true + +# By default gitlint will ignore squash commits. Set to 'false' to disable. +# ignore-squash-commits=true + +# Enable debug mode (prints more output). Disabled by default. +# debug=true + +# Set the extra-path where gitlint will search for user defined rules +# See http://jorisroovers.github.io/gitlint/user_defined_rules for details +extra-path=gitlint/ + +# [title-max-length] +# line-length=80 + +[title-must-not-contain-word] +# Comma-separated list of words that should not occur in the title. Matching is case +# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING" +# will not cause a violation, but "WIP: my title" will. +words=wip + +#[title-match-regex] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit-msg title must be matched to. +# Note that the regex can contradict with other rules if not used correctly +# (e.g. title-must-not-contain-word). +#regex= + +# [B1] +# B1 = body-max-line-length +# line-length=120 +# [body-min-length] +# min-length=5 + +# [body-is-missing] +# Whether to ignore this rule on merge commits (which typically only have a title) +# default = True +# ignore-merge-commits=false + +# [body-changed-file-mention] +# List of files that need to be explicitly mentioned in the body when they are changed +# This is useful for when developers often erroneously edit certain files or git submodules. +# By specifying this rule, developers can only change the file when they explicitly reference +# it in the commit message. +# files=gitlint/rules.py,README.md + +# [author-valid-email] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit author email address should be matched to +# For example, use the following regex if you only want to allow email addresses from foo.com +# regex=[^@]+@foo.com + +[ignore-by-title] +# Allow empty body & wrong title pattern only when bots (pyup/greenkeeper) +# upgrade dependencies +regex=^(⬆️.*|Update (.*) from (.*) to (.*)|(chore|fix)\(package\): update .*)$ +ignore=B6,UC1 + +# [ignore-by-body] +# Ignore certain rules for commits of which the body has a line that matches a regex +# E.g. Match bodies that have a line that that contain "release" +# regex=(.*)release(.*) +# +# Ignore certain rules, you can reference them by their id or by their full name +# Use 'all' to ignore all rules +# ignore=T1,body-min-length diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7c72623 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[unreleased]: https://github.com/suitenumerique/django-lasuite/commits/main/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb3437f --- /dev/null +++ b/Makefile @@ -0,0 +1,91 @@ +# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ +# +# This Makefile is only meant to be used for DEVELOPMENT purpose as we are +# changing the user id that will run in the container. +# +# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER... +# +# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ +# +# Note to developers: +# +# While editing this file, please respect the following statements: +# +# 1. Every variable should be defined in the ad hoc VARIABLES section with a +# relevant subsection +# 2. Every new rule should be defined in the ad hoc RULES section with a +# relevant subsection depending on the targeted service +# 3. Rules should be sorted alphabetically within their section +# 4. When a rule has multiple dependencies, you should: +# - duplicate the rule name to add the help string (if required) +# - write one dependency per line to increase readability and diffs +# 5. .PHONY rule statement should be written after the corresponding rule +# ============================================================================== +# VARIABLES + + + +BOLD := \033[1m +RESET := \033[0m +GREEN := \033[1;32m + +# Use uv for package management +UV = uv + +# ============================================================================== +# RULES + +default: help + +help: ## Display this help message + @echo "$(BOLD)Django LaSuite Makefile" + @echo "Please use 'make $(BOLD)target$(RESET)' where $(BOLD)target$(RESET) is one of:" + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}' +.PHONY: help + +install: ## Install the project + @$(UV) sync +.PHONY: install + +install-dev: ## Install the project with dev dependencies + @$(UV) sync --extra dev +.PHONY: install-dev + +install-build: ## Install the project with build dependencies + @$(UV) sync --extra build + +clean: ## Clean the project folder + @rm -rf build/ + @rm -rf dist/ + @rm -rf *.egg-info + @find . -type d -name __pycache__ -exec rm -rf {} + + @find . -type f -name "*.pyc" -delete +.PHONY: clean + +format: ## Run the formatter + @ruff format +.PHONY: format + +lint: format ## Run the linter + @ruff check . +.PHONY: lint + +test: ## Run the tests + @pytest tests/ -v +.PHONY: test + +build: install-build ## Build the project + @$(UV) build +.PHONY: build + +migrate: ## Run the test project migrations + @cd tests && python -m test_project.manage migrate +.PHONY: migrate + +runserver: ## Run the test project server + @cd tests && python -m test_project.manage runserver +.PHONY: runserver + +shell: ## Run the test project Django shell + @cd tests && python -m test_project.manage shell +.PHONY: shell diff --git a/README.md b/README.md new file mode 100644 index 0000000..49da8e9 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Django La Suite + +A Django library for the common requirement for the "La Suite numérique" projects. + +## Installation + +```bash +pip install django-lasuite +``` + +## Quick Start + +To be documented with the first implementations. + +## Development + +### Requirements + +For this project, we use `uv` Python package manager. +Please follow the installation guidelines on the [uv documentation](https://docs.astral.sh/uv/getting-started/installation/). + +### Setup + +```bash +# Clone the repository +git clone https://github.com/suitenumerique/django-lasuite.git +cd django-lasuite + +# Install development dependencies +make install-dev +``` + +### Testing + +```bash +make test +``` + +### Code Quality + +```bash +make lint +``` + +## License + +MIT License diff --git a/gitlint/gitlint_emoji.py b/gitlint/gitlint_emoji.py new file mode 100644 index 0000000..3ef39f3 --- /dev/null +++ b/gitlint/gitlint_emoji.py @@ -0,0 +1,35 @@ +"""Gitlint extra rule to validate the message title.""" + +import re + +import requests + +from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation + + +class GitmojiTitle(LineRule): + """ + Enforce that each commit title is of the form "() " + where gitmoji is an emoji from the list defined in https://gitmoji.carloscuesta.me and + subject should be all lowercase. + """ + + id = "UC1" + name = "title-should-have-gitmoji-and-scope" + target = CommitMessageTitle + + def validate(self, title, _commit): + """ + Download the list possible gitmojis from the project's GitHub repository and check that + title contains one of them. + """ + gitmojis = requests.get( + "https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json", + timeout=10, + ).json()["gitmojis"] + emojis = [item["emoji"] for item in gitmojis] + pattern = r"^({:s})\(.*\)\s[a-z].*$".format("|".join(emojis)) + if not re.search(pattern, title): + violation_msg = 'Title does not match regex "() "' + return [RuleViolation(self.id, violation_msg, title)] + return None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8be2d47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "django-lasuite" +version = "0.0.1" +description = "Django La Suite - A Django library" +readme = "README.md" +requires-python = ">=3.10" +license = {file = "LICENSE"} +authors = [ + {name = "DINUM", email = "dev@mail.numerique.gouv.fr"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Django", + "Framework :: Django :: 5.0", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "django>=5.0", +] + +[project.urls] +"Homepage" = "https://github.com/suitenumerique/django-lasuite" +"Bug Tracker" = "https://github.com/suitenumerique/django-lasuite/issues" + +[project.optional-dependencies] +build = [ + "setuptools", + "wheel", +] +dev = [ + "pytest", + "ruff", +] + +[tool.hatch.build.targets.sdist] +only-include = ["src"] + +[tool.hatch.build.targets.wheel] +packages = ["src/lasuite"] + +[tool.pytest.ini_options] +python_files = "test_*.py" +testpaths = ["tests"] + +[tool.ruff] +line-length = 120 +target-version = "py310" +lint.select = [ + # pycodestyle + "E", "W", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + # flake8-logging-format + "G", + # flake8-pie + "PIE", + # flake8-comprehensions + "C4", + # flake8-django + "DJ", + # flake8-bandit + "S", + # flake8-builtins + "A", + # flake8-datetimez + "DTZ", + # flake8-gettext + "INT", + # Pylint + "PL", + # flake8-fixme + "FIX", + # flake8-self + "SLF", + # flake8-return + "RET", + # pep8-naming (N) + "N", + # pydocstyle + "D", + # flake8-pytest-style (PT) + "PT", +] +lint.ignore = [ + # incorrect-blank-line-before-class + "D203", + # missing-blank-line-after-summary + "D205", + # multi-line-summary-first-line + "D212", +] +lint.per-file-ignores = {"**/tests/*"= [ + # flake8-bandit + "S", + # flake8-self + "SLF", +]} + + + +[tool.ruff.lint.isort] +known-first-party = ["lasuite"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..abc9f36 --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +"""Setup file for the package.""" + +from setuptools import setup + +# This file is kept for compatibility with older tools +# The build configuration is primarily in pyproject.toml +setup() diff --git a/src/lasuite/__init__.py b/src/lasuite/__init__.py new file mode 100644 index 0000000..31e94e4 --- /dev/null +++ b/src/lasuite/__init__.py @@ -0,0 +1 @@ +"""Django La Suite library.""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..cc2f385 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for the library.""" diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..d9fd781 --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,6 @@ +"""A dummy test to check if the test suite is working.""" + + +def test_success(): + """A dummy test to check if the test suite is working.""" + assert True diff --git a/tests/test_project/__init__.py b/tests/test_project/__init__.py new file mode 100644 index 0000000..4fe57d0 --- /dev/null +++ b/tests/test_project/__init__.py @@ -0,0 +1 @@ +"""Django test project.""" diff --git a/tests/test_project/manage.py b/tests/test_project/manage.py new file mode 100644 index 0000000..ab355ea --- /dev/null +++ b/tests/test_project/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +"""Test project management script.""" + +import os +import sys + +from django.core.management import execute_from_command_line + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + + execute_from_command_line(sys.argv) diff --git a/tests/test_project/settings.py b/tests/test_project/settings.py new file mode 100644 index 0000000..27d32ef --- /dev/null +++ b/tests/test_project/settings.py @@ -0,0 +1,96 @@ +"""Django settings for test project.""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-test-key-for-development-only" # noqa: S105 + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +# 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 = "test_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 = "test_project.wsgi.application" + +# Database +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +# Internationalization +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +STATIC_URL = "/static/" + +# Default primary key field type +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Logging Configuration +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, +} diff --git a/tests/test_project/urls.py b/tests/test_project/urls.py new file mode 100644 index 0000000..d05585c --- /dev/null +++ b/tests/test_project/urls.py @@ -0,0 +1,3 @@ +"""Test project URL configuration.""" + +urlpatterns = [] diff --git a/tests/test_project/wsgi.py b/tests/test_project/wsgi.py new file mode 100644 index 0000000..941e382 --- /dev/null +++ b/tests/test_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for the test 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/3.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + +application = get_wsgi_application()