diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..6c0337a8f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{py,pyi}] +indent_size = 4 diff --git a/.github/workflows/canopeum_backend.yml b/.github/workflows/canopeum_backend.yml new file mode 100644 index 000000000..5e5f2ac55 --- /dev/null +++ b/.github/workflows/canopeum_backend.yml @@ -0,0 +1,46 @@ +name: canopeum_backend + +on: + push: + pull_request: + branches: + - main + +jobs: + mypy: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: "**/requirements*.txt" + - run: pip install -r requirements-dev.txt + working-directory: canopeum_backend + - run: mypy . --python-version=3.12 + working-directory: canopeum_backend + + pyright: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: "**/requirements*.txt" + - run: pip install -r requirements-dev.txt + working-directory: canopeum_backend + - uses: jakebailey/pyright-action@v2 + with: + python-version: "3.12" + working-directory: canopeum_backend diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..f00360d49 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "default": true, + "MD013": false, // Use line-wrap for Markdown + "MD029": false // Allow explicit ordered list number for lists split by block codes +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..f08968679 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# You can run this locally with `pre-commit run [--all]` +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 # must match canopeum_backend/requirements-dev.txt + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + +ci: + autoupdate_schedule: quarterly diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..e726d9428 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,47 @@ +// Keep in alphabetical order +{ + "recommendations": [ + "bierner.github-markdown-preview", + "charliermarsh.ruff", + "davidanson.vscode-markdownlint", + "eamodio.gitlens", + "github.vscode-github-actions", + "ms-python.mypy-type-checker", + "ms-python.python", + "ms-python.vscode-pylance", + // "ms-vscode.powershell", + "pkief.material-icon-theme", + // "redhat.vscode-xml", + "redhat.vscode-yaml", + "tamasfe.even-better-toml", + ], + "unwantedRecommendations": [ + // Must disable in this workspace // + // https://github.com/microsoft/vscode/issues/40239 // + // + // even-better-toml has format on save + "bungcip.better-toml", + // VSCode has implemented an optimized version + "coenraads.bracket-pair-colorizer", + "coenraads.bracket-pair-colorizer-2", + "shardulm94.trailing-spaces", + // Don't use two mypy extensions simultaneously + "matangover.mypy", + // Obsoleted by Pylance + "ms-pyright.pyright", + // + // Don't recommend to autoinstall // + // + // Use Ruff instead + "ms-python.black-formatter", + "ms-python.flake8", + "ms-python.isort", + "ms-python.pylint", + // We use autopep8 + "ms-python.black-formatter", + // Not configurable per workspace, tends to conflict with other linters + "sonarsource.sonarlint-vscode", + // Prefer using VSCode itself as a text editor + "vscodevim.vim", + ], +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..1dc28d978 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,118 @@ +{ + "editor.rulers": [ + 80, + 120, + ], + "[git-commit]": { + "editor.rulers": [ + 72, + ], + }, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.eol": "\n", + "editor.comments.insertSpace": true, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + // Let dedicated linter (Ruff) organize imports + "source.organizeImports": "never", + }, + "files.associations": { + ".flake8": "properties", + "*.qrc": "xml", + "*.ui": "xml", + ".markdownlint.json": "jsonc", + }, + "search.exclude": { + "**/*.code-search": true, + "*.lock": true, + }, + // Set the default formatter to help avoid Prettier + "[json][jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features", + }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.tabSize": 4, + "editor.rulers": [ + 72, // PEP8-17 docstrings + // 79, // PEP8-17 default max + // 88, // Black/Ruff default + // 99, // PEP8-17 acceptable max + 120, // Our hard rule + ], + }, + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.args": [ + // https://github.com/microsoft/vscode-mypy/issues/37#issuecomment-1602702174 + "--config-file=${workspaceFolder}/canopeum_backend/pyproject.toml", + ], + "python.terminal.activateEnvironment": true, + // python.analysis is Pylance (pyright) configurations + "python.analysis.fixAll": [ + "source.convertImportFormat" + // Explicitly omiting "source.unusedImports", can be annoying when commenting code for debugging + ], + "python.analysis.importFormat": "relative", + "python.analysis.diagnosticMode": "workspace", + "ruff.importStrategy": "fromEnvironment", + // Use the Ruff extension instead + "isort.check": false, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.trimWhitespaceAroundPipe": true, + "powershell.codeFormatting.useConstantStrings": true, + "powershell.codeFormatting.useCorrectCasing": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + "powershell.integratedConsole.showOnStartup": false, + "terminal.integrated.defaultProfile.windows": "PowerShell", + "terminal.integrated.defaultProfile.linux": "pwsh", + "terminal.integrated.defaultProfile.osx": "pwsh", + "xml.codeLens.enabled": true, + "xml.format.spaceBeforeEmptyCloseTag": false, + "xml.format.preserveSpace": [ + // Default + "xsl:text", + "xsl:comment", + "xsl:processing-instruction", + "literallayout", + "programlisting", + "screen", + "synopsis", + "pre", + "xd:pre", + // Custom + "string" + ], + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "evenBetterToml.formatter.alignComments": false, + "evenBetterToml.formatter.alignEntries": false, + "evenBetterToml.formatter.allowedBlankLines": 1, + "evenBetterToml.formatter.arrayAutoCollapse": true, + "evenBetterToml.formatter.arrayAutoExpand": true, + "evenBetterToml.formatter.arrayTrailingComma": true, + "evenBetterToml.formatter.columnWidth": 80, + "evenBetterToml.formatter.compactArrays": true, + "evenBetterToml.formatter.compactEntries": false, + "evenBetterToml.formatter.compactInlineTables": false, + "evenBetterToml.formatter.indentEntries": false, + "evenBetterToml.formatter.indentTables": false, + "evenBetterToml.formatter.inlineTableExpand": false, + "evenBetterToml.formatter.reorderArrays": true, + "evenBetterToml.formatter.trailingNewline": true, + // We like keeping TOML keys in a certain non-alphabetical order that feels more natural + "evenBetterToml.formatter.reorderKeys": false, +} diff --git a/README.md b/README.md index 8d8c7caa1..82e8ba190 100644 --- a/README.md +++ b/README.md @@ -11,38 +11,66 @@ Follow these instructions to get the project up and running on your local machin ### Prerequisites For frontend + - Node.js - npm (Node Package Manager) - Mockoon For backend -- Python (3.x recommended) +- Python 3.12 - Docker - ### Installation 1. Clone the repository: -```bash +```shell git clone git@github.com:BesLogic/releaf-canopeum.git cd releaf-canopeum ``` -2. Set up Django backend and Database: (Skip this section for Frontend only) +2. Set up a Python 3.12 virtual environment -```bash -docker compose up +```shell +cd canopeum_backend +python3.12 -m venv .venv +``` + +or on Windows if "python3.12" is not a recognized command: + +```powershell cd canopeum_backend -python -m venv .venv +py -3.12 -m venv .venv +``` + +Then activate the environemnt (you need to do this everytime if your editor isn't configured to do so): + +```shell source .venv/scripts/activate -python -m pip install -r requirements.txt +``` + +and on Windows: + +```powershell +.venv/scripts/activate +``` + +In VSCode (Windows): +`CTRL+Shift+P` (Open Command Palette) > `Python: Select Interpreter` +![VSCode_select_venv](/docs/VSCode_select_venv.png) + +3. Set up Django backend and Database: (Skip this section for Frontend only) + +```shell +docker compose up +cd canopeum_backend +python -m pip install -r requirements-dev.txt python manage.py migrate python manage.py runserver ``` -3. Set up React frontend: +4. Set up React frontend: -```bash +```shell cd canopeum_frontend npm install npm run dev @@ -50,7 +78,7 @@ npm run dev Run mock data (For Frontend only) -```bash +```shell # In another CLI npm install -g @mockoon/cli cd releaf-canopeum/canopeum_frontend @@ -59,7 +87,7 @@ mockoon-cli start --data canopeum-mockoon.json ### Folder Architecture -```bash +```ini project_name/ │ ├── backend/ # Django backend diff --git a/canopeum_backend/.gitignore b/canopeum_backend/.gitignore index 1cf01105b..27ca1add6 100644 --- a/canopeum_backend/.gitignore +++ b/canopeum_backend/.gitignore @@ -6,10 +6,10 @@ __pycache__ db.sqlite3 media -# Backup files # -*.bak +# Backup files # +*.bak -# If you are using PyCharm # +# If you are using PyCharm # # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml @@ -45,94 +45,94 @@ out/ # JIRA plugin atlassian-ide-plugin.xml -# Python # -*.py[cod] -*$py.class - -# Distribution / packaging -.Python build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ *.whl -*.egg-info/ -.installed.cfg -*.egg -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -.pytest_cache/ -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery -celerybeat-schedule.* - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -# Sublime Text # -*.tmlanguage.cache -*.tmPreferences.cache -*.stTheme.cache -*.sublime-workspace -*.sublime-project - -# sftp configuration file -sftp-config.json - -# Package control specific files Package -Control.last-run -Control.ca-list -Control.ca-bundle -Control.system-ca-bundle -GitHub.sublime-settings - -# Visual Studio Code # -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -.history \ No newline at end of file +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history diff --git a/canopeum_backend/canopeum_backend/asgi.py b/canopeum_backend/canopeum_backend/asgi.py index 7a0439bb2..5c2c7e69a 100644 --- a/canopeum_backend/canopeum_backend/asgi.py +++ b/canopeum_backend/canopeum_backend/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'canopeum_backend.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "canopeum_backend.settings") application = get_asgi_application() diff --git a/canopeum_backend/canopeum_backend/bdd.sql b/canopeum_backend/canopeum_backend/bdd.sql index 11ef32d12..cdb83478d 100644 --- a/canopeum_backend/canopeum_backend/bdd.sql +++ b/canopeum_backend/canopeum_backend/bdd.sql @@ -158,4 +158,3 @@ CREATE TABLE `BatchTreeType` ( `tree_type_id` integer REFERENCES `TreeType` (`id`), `quantity` integer ); - diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index bdeb917c4..458bd85d7 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -1,12 +1,14 @@ -from django.db import models from django.conf import settings +from django.db import models + class Announcement(models.Model): body = models.TextField(blank=True, null=True) link = models.TextField(blank=True, null=True) + class Batch(models.Model): - site = models.ForeignKey('Site', models.DO_NOTHING, blank=True, null=True) + site = models.ForeignKey("Site", models.DO_NOTHING, blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True) name = models.TextField(blank=True, null=True) sponsor = models.TextField(blank=True, null=True) @@ -15,24 +17,29 @@ class Batch(models.Model): total_number_seed = models.IntegerField(blank=True, null=True) total_propagation = models.IntegerField(blank=True, null=True) + class Batchfertilizer(models.Model): batch = models.ForeignKey(Batch, models.DO_NOTHING, blank=True, null=True) - fertilizer_type = models.ForeignKey('Fertilizertype', models.DO_NOTHING, blank=True, null=True) + fertilizer_type = models.ForeignKey("Fertilizertype", models.DO_NOTHING, blank=True, null=True) + class Batchmulchlayer(models.Model): batch = models.ForeignKey(Batch, models.DO_NOTHING, blank=True, null=True) - mulch_layer_type = models.ForeignKey('Mulchlayertype', models.DO_NOTHING, blank=True, null=True) + mulch_layer_type = models.ForeignKey("Mulchlayertype", models.DO_NOTHING, blank=True, null=True) + class Batchtreetype(models.Model): batch = models.ForeignKey(Batch, models.DO_NOTHING, blank=True, null=True) - tree_type = models.ForeignKey('Treetype', models.DO_NOTHING, blank=True, null=True) + tree_type = models.ForeignKey("Treetype", models.DO_NOTHING, blank=True, null=True) quantity = models.IntegerField(blank=True, null=True) + class Comment(models.Model): body = models.TextField(blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True) auth_user = models.ForeignKey(settings.AUTH_USER_MODEL, models.DO_NOTHING, blank=True, null=True) - post = models.ForeignKey('Post', models.DO_NOTHING, blank=True, null=True) + post = models.ForeignKey("Post", models.DO_NOTHING, blank=True, null=True) + class Contact(models.Model): address = models.TextField(blank=True, null=True) @@ -43,6 +50,7 @@ class Contact(models.Model): instagram_link = models.URLField(blank=True, null=True) linkedin_link = models.URLField(blank=True, null=True) + class Coordinate(models.Model): dms_latitude = models.TextField(blank=True, null=True) dms_longitude = models.TextField(blank=True, null=True) @@ -50,37 +58,45 @@ class Coordinate(models.Model): dd_longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) address = models.TextField(blank=True, null=True) + class Fertilizertype(models.Model): - name = models.ForeignKey('FertilizertypeInternationalization', models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey("FertilizertypeInternationalization", models.DO_NOTHING, blank=True, null=True) + class FertilizertypeInternationalization(models.Model): - en = models.TextField(db_column='EN', blank=True, null=True) - fr = models.TextField(db_column='FR', blank=True, null=True) + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + class Image(models.Model): path = models.TextField(blank=True, null=True) + class Mulchlayertype(models.Model): - name = models.ForeignKey('MulchlayertypeInternationalization', models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey("MulchlayertypeInternationalization", models.DO_NOTHING, blank=True, null=True) + class MulchlayertypeInternationalization(models.Model): - en = models.TextField(db_column='EN', blank=True, null=True) - fr = models.TextField(db_column='FR', blank=True, null=True) + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + class Post(models.Model): - site = models.ForeignKey('Site', models.DO_NOTHING, blank=True, null=True) + site = models.ForeignKey("Site", models.DO_NOTHING, blank=True, null=True) body = models.TextField(blank=True, null=True) like_count = models.IntegerField(blank=True, null=True) share_count = models.IntegerField(blank=True, null=True) created_at = models.DateTimeField(blank=True, null=True) + class Postimage(models.Model): image = models.ForeignKey(Image, models.DO_NOTHING, blank=True, null=True) post = models.ForeignKey(Post, models.DO_NOTHING, blank=True, null=True) + class Site(models.Model): name = models.TextField(blank=True, null=True) - site_type = models.ForeignKey('Sitetype', models.DO_NOTHING, blank=True, null=True) + site_type = models.ForeignKey("Sitetype", models.DO_NOTHING, blank=True, null=True) image = models.ForeignKey(Image, models.DO_NOTHING, blank=True, null=True) coordinate = models.ForeignKey(Coordinate, models.DO_NOTHING, blank=True, null=True) description = models.TextField(blank=True, null=True) @@ -91,38 +107,47 @@ class Site(models.Model): contact = models.ForeignKey(Contact, models.DO_NOTHING, blank=True, null=True) announcement = models.ForeignKey(Announcement, models.DO_NOTHING, blank=True, null=True) + class Siteadmin(models.Model): auth_user = models.ForeignKey(settings.AUTH_USER_MODEL, models.DO_NOTHING, blank=True, null=True) site = models.ForeignKey(Site, models.DO_NOTHING, blank=True, null=True) + class Siteimage(models.Model): image = models.ForeignKey(Image, models.DO_NOTHING, blank=True, null=True) site = models.ForeignKey(Site, models.DO_NOTHING, blank=True, null=True) + class Sitetreespecies(models.Model): site = models.ForeignKey(Site, models.DO_NOTHING, blank=True, null=True) - tree_type = models.ForeignKey('Treetype', models.DO_NOTHING, blank=True, null=True) + tree_type = models.ForeignKey("Treetype", models.DO_NOTHING, blank=True, null=True) quantity = models.IntegerField(blank=True, null=True) + class Sitetype(models.Model): - name = models.ForeignKey('SitetypeInternationalization', models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey("SitetypeInternationalization", models.DO_NOTHING, blank=True, null=True) + class SitetypeInternationalization(models.Model): - en = models.TextField(db_column='EN', blank=True, null=True) - fr = models.TextField(db_column='FR', blank=True, null=True) + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + class TreespeciestypeInternationalization(models.Model): - en = models.TextField(db_column='EN', blank=True, null=True) - fr = models.TextField(db_column='FR', blank=True, null=True) + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + class Treetype(models.Model): name = models.ForeignKey(TreespeciestypeInternationalization, models.DO_NOTHING, blank=True, null=True) + class Widget(models.Model): site = models.ForeignKey(Site, models.DO_NOTHING, blank=True, null=True) title = models.TextField(blank=True, null=True) body = models.TextField(blank=True, null=True) + class Like(models.Model): auth_user = models.ForeignKey(settings.AUTH_USER_MODEL, models.DO_NOTHING, blank=True, null=True) post = models.ForeignKey(Post, models.DO_NOTHING, blank=True, null=True) diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index 22cc46ea5..f2cdb068e 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -1,43 +1,52 @@ -from rest_framework import serializers from django.contrib.auth.models import User -from .models import Site, Post, Batch, Announcement, Like, Comment +from rest_framework import serializers + +from .models import Announcement, Batch, Comment, Like, Post, Site + class AuthUserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('id', 'username', 'email', 'password') + fields = ("id", "username", "email", "password") + class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = '__all__' + fields = "__all__" + class SiteSerializer(serializers.ModelSerializer): class Meta: model = Site - fields = '__all__' + fields = "__all__" + class PostSerializer(serializers.ModelSerializer): class Meta: model = Post - fields = '__all__' + fields = "__all__" + class BatchSerializer(serializers.ModelSerializer): class Meta: model = Batch - fields = '__all__' + fields = "__all__" + class AnnouncementSerializer(serializers.ModelSerializer): class Meta: model = Announcement - fields = '__all__' + fields = "__all__" + class LikeSerializer(serializers.ModelSerializer): class Meta: model = Like - fields = '__all__' + fields = "__all__" + class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment - fields = '__all__' + fields = "__all__" diff --git a/canopeum_backend/canopeum_backend/settings.py b/canopeum_backend/canopeum_backend/settings.py index 9c49e6095..090191a33 100644 --- a/canopeum_backend/canopeum_backend/settings.py +++ b/canopeum_backend/canopeum_backend/settings.py @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-xy@=#v*#0yj@^gsl*0f+ci9+)8@v-x#7+npdvh50fn7^s9ow8g' +SECRET_KEY = "django-insecure-xy@=#v*#0yj@^gsl*0f+ci9+)8@v-x#7+npdvh50fn7^s9ow8g" # noqa: S105 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,100 +31,99 @@ # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'rest_framework.authtoken', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'corsheaders', - 'drf_spectacular', - 'drf_spectacular_sidecar', - 'canopeum_backend', + "django.contrib.admin", + "django.contrib.auth", + "rest_framework.authtoken", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "drf_spectacular", + "drf_spectacular_sidecar", + "canopeum_backend", ] 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', - 'corsheaders.middleware.CorsMiddleware', + "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", + "corsheaders.middleware.CorsMiddleware", ] CORS_ALLOWED_ORIGINS = [ - 'http://localhost:5173', - 'http://localhost:3000', + "http://localhost:5173", + "http://localhost:3000", ] -ROOT_URLCONF = 'canopeum_backend.urls' +ROOT_URLCONF = "canopeum_backend.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', + "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 = 'canopeum_backend.wsgi.application' +WSGI_APPLICATION = "canopeum_backend.wsgi.application" REST_FRAMEWORK = { - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', - 'DEFAULT_RENDERER_CLASSES': [ - 'rest_framework.renderers.JSONRenderer', + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", ], - 'DEFAULT_PARSER_CLASSES': [ - 'rest_framework.parsers.JSONParser', - ] } SPECTACULAR_SETTINGS = { - 'SWAGGER_UI_DIST': 'SIDECAR', - 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', - 'REDOC_DIST': 'SIDECAR', - 'TITLE': 'Canopeum API', - 'DESCRIPTION': 'API for the Canopeum project', - 'VERSION': '0.0.1', - 'BASE_URL': 'http://localhost:3000', + "SWAGGER_UI_DIST": "SIDECAR", + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_DIST": "SIDECAR", + "TITLE": "Canopeum API", + "DESCRIPTION": "API for the Canopeum project", + "VERSION": "0.0.1", + "BASE_URL": "http://localhost:3000", "SWAGGER_UI_SETTINGS": { "deepLinking": True, "persistAuthorization": True, "displayOperationId": True, }, # Split components into request and response parts where appropriate - 'COMPONENT_SPLIT_REQUEST': False, + "COMPONENT_SPLIT_REQUEST": False, # Aid client generator targets that have trouble with read-only properties. - 'COMPONENT_NO_READ_ONLY_REQUIRED': False, + "COMPONENT_NO_READ_ONLY_REQUIRED": False, # Create separate components for PATCH endpoints (without required list) - 'COMPONENT_SPLIT_PATCH': True, + "COMPONENT_SPLIT_PATCH": True, } - # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'canopeum_db', - 'USER': 'root', - 'PASSWORD': 'canopeum', - 'HOST': 'localhost', - 'PORT': '3306', - } + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": "canopeum_db", + "USER": "root", + "PASSWORD": "canopeum", + "HOST": "localhost", + "PORT": "3306", + }, } @@ -133,16 +132,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -150,9 +149,9 @@ # Internationalization # https://docs.djangoproject.com/en/5.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -162,9 +161,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/canopeum_backend/canopeum_backend/urls.py b/canopeum_backend/canopeum_backend/urls.py index 5026742fe..af3e8fe5a 100644 --- a/canopeum_backend/canopeum_backend/urls.py +++ b/canopeum_backend/canopeum_backend/urls.py @@ -1,46 +1,38 @@ from django.contrib import admin from django.urls import path -from . import views from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView -urlpatterns = [ - path('admin/', admin.site.urls), +from . import views +urlpatterns = [ + path("admin/", admin.site.urls), # Auth - path('auth/login/', views.LoginAPIView.as_view(), name='login'), - path('auth/logout/', views.LogoutAPIView.as_view(), name='logout'), - path('auth/register/', views.RegisterAPIView.as_view(), name='register'), - + path("auth/login/", views.LoginAPIView.as_view(), name="login"), + path("auth/logout/", views.LogoutAPIView.as_view(), name="logout"), + path("auth/register/", views.RegisterAPIView.as_view(), name="register"), # User - path('users/', views.UserListAPIView.as_view(), name='user-list'), - path('users//', views.UserDetailAPIView.as_view(), name='user-detail'), - + path("users/", views.UserListAPIView.as_view(), name="user-list"), + path("users//", views.UserDetailAPIView.as_view(), name="user-detail"), # Announcement - path('announcements/', views.AnnouncementListAPIView.as_view(), name='announcement-list'), - path('announcements//', views.AnnouncementDetailAPIView.as_view(), name='announcement-detail'), - + path("announcements/", views.AnnouncementListAPIView.as_view(), name="announcement-list"), + path("announcements//", views.AnnouncementDetailAPIView.as_view(), name="announcement-detail"), # Site - path('sites/', views.SiteListAPIView.as_view(), name='site-list'), - path('sites//', views.SiteDetailAPIView.as_view(), name='site-detail'), - + path("sites/", views.SiteListAPIView.as_view(), name="site-list"), + path("sites//", views.SiteDetailAPIView.as_view(), name="site-detail"), # Post - path('posts/', views.PostListAPIView.as_view(), name='post-list'), - path('posts//', views.PostDetailAPIView.as_view(), name='post-detail'), - + path("posts/", views.PostListAPIView.as_view(), name="post-list"), + path("posts//", views.PostDetailAPIView.as_view(), name="post-detail"), # Batch - path('batches/', views.BatchListAPIView.as_view(), name='batch-list'), - path('batches//', views.BatchDetailAPIView.as_view(), name='batch-detail'), - + path("batches/", views.BatchListAPIView.as_view(), name="batch-list"), + path("batches//", views.BatchDetailAPIView.as_view(), name="batch-detail"), # Like - path('likes/', views.LikeListAPIView.as_view(), name='like-list'), - path('likes//', views.LikeDetailAPIView.as_view(), name='like-detail'), - + path("likes/", views.LikeListAPIView.as_view(), name="like-list"), + path("likes//", views.LikeDetailAPIView.as_view(), name="like-detail"), # Comment - path('comments/', views.CommentListAPIView.as_view(), name='comment-list'), - path('comments//', views.CommentDetailAPIView.as_view(), name='comment-detail'), - + path("comments/", views.CommentListAPIView.as_view(), name="comment-list"), + path("comments//", views.CommentDetailAPIView.as_view(), name="comment-detail"), # SWAGGER - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), - path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), ] diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index 14baa0c4c..842dd950c 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -1,31 +1,44 @@ -from rest_framework import status -from rest_framework.views import APIView -from rest_framework.response import Response -from .models import Site, Post, Batch, Announcement, Like, Comment -from .serializers import AuthUserSerializer, UserSerializer, SiteSerializer, PostSerializer, BatchSerializer, AnnouncementSerializer, LikeSerializer, CommentSerializer +from typing import ClassVar + +from django.contrib.auth import authenticate from django.contrib.auth.models import User +from drf_spectacular.utils import extend_schema +from rest_framework import status from rest_framework.authtoken.models import Token from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema -from django.contrib.auth import authenticate +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import Announcement, Batch, Comment, Like, Post, Site +from .serializers import ( + AnnouncementSerializer, + AuthUserSerializer, + BatchSerializer, + CommentSerializer, + LikeSerializer, + PostSerializer, + SiteSerializer, + UserSerializer, +) + class LoginAPIView(APIView): - permission_classes = [AllowAny] + permission_classes: ClassVar[list[type[AllowAny]]] = [AllowAny] # @extend_schema(request=AuthUserSerializer, responses=UserSerializer) def post(self, request): - username = request.data.get('username') - password = request.data.get('password') + username = request.data.get("username") + password = request.data.get("password") user = authenticate(username=username, password=password) if user is not None: token, _ = Token.objects.get_or_create(user=user) - return Response({'token': token.key}, status=status.HTTP_200_OK) - else: - return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED) + return Response({"token": token.key}, status=status.HTTP_200_OK) + return Response({"error": "Invalid credentials"}, status=status.HTTP_401_UNAUTHORIZED) + class RegisterAPIView(APIView): - permission_classes = [AllowAny] + permission_classes: ClassVar[list[type[AllowAny]]] = [AllowAny] @extend_schema(request=UserSerializer, responses=AuthUserSerializer) def post(self, request): @@ -33,9 +46,9 @@ def post(self, request): if serializer.is_valid(): user = serializer.save() token, _ = Token.objects.get_or_create(user=user) - return Response({'token': token.key}, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"token": token.key}, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class LogoutAPIView(APIView): @extend_schema(responses=status.HTTP_200_OK) @@ -43,13 +56,14 @@ def post(self, request): request.user.auth_token.delete() return Response(status=status.HTTP_200_OK) + class UserListAPIView(APIView): @extend_schema(responses=UserSerializer(many=True), operation_id="users_all") def get(self, request): users = User.objects.all() serializer = UserSerializer(users, many=True) return Response(serializer.data) - + @extend_schema(request=UserSerializer, responses=UserSerializer) def post(self, request): serializer = UserSerializer(data=request.data) @@ -58,6 +72,7 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class UserDetailAPIView(APIView): @extend_schema(request=UserSerializer, responses=UserSerializer) def get(self, request, pk): @@ -91,13 +106,14 @@ def delete(self, request, pk): user.delete() return Response(status=status.HTTP_204_NO_CONTENT) + class AnnouncementListAPIView(APIView): @extend_schema(responses=AnnouncementSerializer(many=True), operation_id="announcements_all") def get(self, request): announcements = Announcement.objects.all() serializer = AnnouncementSerializer(announcements, many=True) return Response(serializer.data) - + @extend_schema(request=AnnouncementSerializer, responses=AnnouncementSerializer) def post(self, request): serializer = AnnouncementSerializer(data=request.data) @@ -105,7 +121,8 @@ def post(self, request): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + + class AnnouncementDetailAPIView(APIView): @extend_schema(request=AnnouncementSerializer, responses=AnnouncementSerializer) def get(self, request, pk): @@ -139,13 +156,14 @@ def delete(self, request, pk): announcement.delete() return Response(status=status.HTTP_204_NO_CONTENT) + class SiteListAPIView(APIView): @extend_schema(responses=SiteSerializer(many=True), operation_id="sites_all") def get(self, request): sites = Site.objects.all() serializer = SiteSerializer(sites, many=True) return Response(serializer.data) - + @extend_schema(request=SiteSerializer, responses=SiteSerializer) def post(self, request): serializer = SiteSerializer(data=request.data) @@ -153,7 +171,8 @@ def post(self, request): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + + class SiteDetailAPIView(APIView): @extend_schema(request=SiteSerializer, responses=SiteSerializer) def get(self, request, pk): @@ -187,6 +206,7 @@ def delete(self, request, pk): site.delete() return Response(status=status.HTTP_204_NO_CONTENT) + class PostListAPIView(APIView): @extend_schema(responses=PostSerializer(many=True), operation_id="posts_all") def get(self, request): @@ -202,6 +222,7 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class PostDetailAPIView(APIView): @extend_schema(request=PostSerializer, responses=PostSerializer) def get(self, request, pk): @@ -212,7 +233,7 @@ def get(self, request, pk): serializer = PostSerializer(post) return Response(serializer.data) - + @extend_schema(request=PostSerializer, responses=PostSerializer) def put(self, request, pk): try: @@ -234,7 +255,8 @@ def delete(self, request, pk): post.delete() return Response(status=status.HTTP_204_NO_CONTENT) - + + class BatchListAPIView(APIView): @extend_schema(responses=BatchSerializer(many=True), operation_id="batches_all") def get(self, request): @@ -250,6 +272,7 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class BatchDetailAPIView(APIView): @extend_schema(request=BatchSerializer, responses=BatchSerializer) def get(self, request, pk): @@ -260,7 +283,7 @@ def get(self, request, pk): serializer = BatchSerializer(batch) return Response(serializer.data) - + @extend_schema(request=BatchSerializer, responses=BatchSerializer) def put(self, request, pk): try: @@ -282,7 +305,8 @@ def delete(self, request, pk): batch.delete() return Response(status=status.HTTP_204_NO_CONTENT) - + + class LikeListAPIView(APIView): @extend_schema(responses=LikeSerializer(many=True), operation_id="likes_all") def get(self, request): @@ -298,6 +322,7 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + class LikeDetailAPIView(APIView): @extend_schema(request=LikeSerializer, responses=LikeSerializer) def get(self, request, pk): @@ -308,7 +333,7 @@ def get(self, request, pk): serializer = LikeSerializer(like) return Response(serializer.data) - + @extend_schema(request=LikeSerializer, responses=LikeSerializer) def put(self, request, pk): try: @@ -330,7 +355,8 @@ def delete(self, request, pk): like.delete() return Response(status=status.HTTP_204_NO_CONTENT) - + + class CommentListAPIView(APIView): @extend_schema(responses=CommentSerializer(many=True), operation_id="comments_all") def get(self, request): @@ -345,7 +371,8 @@ def post(self, request): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + + class CommentDetailAPIView(APIView): @extend_schema(request=CommentSerializer, responses=CommentSerializer) def get(self, request, pk): @@ -378,4 +405,3 @@ def delete(self, request, pk): comment.delete() return Response(status=status.HTTP_204_NO_CONTENT) - \ No newline at end of file diff --git a/canopeum_backend/canopeum_backend/wsgi.py b/canopeum_backend/canopeum_backend/wsgi.py index 0057ac6ec..bef2544b6 100644 --- a/canopeum_backend/canopeum_backend/wsgi.py +++ b/canopeum_backend/canopeum_backend/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'canopeum_backend.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "canopeum_backend.settings") application = get_wsgi_application() diff --git a/canopeum_backend/manage.py b/canopeum_backend/manage.py index ca5840c1d..6a6fdbdd5 100644 --- a/canopeum_backend/manage.py +++ b/canopeum_backend/manage.py @@ -1,22 +1,24 @@ #!/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', 'canopeum_backend.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "canopeum_backend.settings") try: - from django.core.management import execute_from_command_line + # Lazy import only when this file is used as a script + from django.core.management import execute_from_command_line # noqa: PLC0415 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?" + + "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__': +if __name__ == "__main__": main() diff --git a/canopeum_backend/pyproject.toml b/canopeum_backend/pyproject.toml new file mode 100644 index 000000000..83722d4e0 --- /dev/null +++ b/canopeum_backend/pyproject.toml @@ -0,0 +1,128 @@ +# https://docs.astral.sh/ruff/configuration/ +[tool.ruff] +target-version = "py312" +line-length = 120 +preview = true +# Auto-generated +exclude = ["canopeum_backend/migrations/*"] + +[tool.ruff.lint] +select = ["ALL"] +# https://docs.astral.sh/ruff/rules/ +ignore = [ + ### + # Not needed or wanted + ### + "D1", # pydocstyle Missing doctring + "D401", # pydocstyle: non-imperative-mood + "EM", # flake8-errmsg + "EXE", # flake8-executable + # This is often something we can't control: https://github.com/astral-sh/ruff/issues/9497 + # Also false-positive with positional-only arguments: https://github.com/astral-sh/ruff/issues/3247 + "FBT003", # flake8-boolean-trap: boolean-positional-value-in-call + "INP", # flake8-no-pep420 + "ISC003", # flake8-implicit-str-concat: explicit-string-concatenation + # Short messages are still considered "long" messages + "TRY003", # tryceratops : raise-vanilla-args + # Don't remove commented code, also too inconsistant + "ERA001", # eradicate: commented-out-code + # contextlib.suppress is roughly 3x slower than try/except + "SIM105", # flake8-simplify: use-contextlib-suppress + # Negative performance impact + "UP038", # non-pep604-isinstance + # Checked by type-checker (pyright) + "ANN", # flake-annotations + "PGH003", # blanket-type-ignore + "TCH", # flake8-type-checking + # Already shown by Pylance, checked by pyright, and can be caused by overloads. + "ARG002", # Unused method argument + # We want D213: multi-line-summary-second-line and D211: no-blank-line-before-class + "D203", # pydocstyle: one-blank-line-before-class + "D212", # pydocstyle: multi-line-summary-first-line + # Allow differentiating between broken (FIXME) and to be done/added/completed (TODO) + "TD001", # flake8-todos: invalid-todo-tag + + ### + # Conflict with formatter + ### + "COM812", # missing-trailing-comma + "ISC001", # single-line-implicit-string-concatenation + + ### + # These should be warnings (https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774) + ### + "FIX", # flake8-fixme + # Not all TODOs are worth an issue, this would be better as a warning + "TD003", # flake8-todos: missing-todo-link + + # False-positives + "TCH004", # https://github.com/astral-sh/ruff/issues/3821 + + ### + # Specific to this project + ### + "CPY001", # flake8-copyright, using global copyright + "D205", # Not all docstrings have a short description + desrciption + "PERF203", # try-except-in-loop, Python 3.11, introduced "zero cost" exception handling + "PLR6301", # API Views don't use "self" + "TD002", # missing-todo-author, This is a relatively small, low contributors project. Git blame suffice. + + ### FIXME/TODO: I'd normally set them as temporarily warnings, but no warnings in Ruff yet: + ### https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774): + "DJ001", # Avoid using `null=True` on string-based fields + "DJ008", # Model does not define `__str__` method: https://docs.astral.sh/ruff/rules/django-model-without-dunder-str/ +] + +[tool.ruff.format] + +# https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat +[tool.ruff.lint.flake8-implicit-str-concat] +allow-multiline = false + +# https://docs.astral.sh/ruff/settings/#isort +[tool.ruff.lint.isort] +combine-as-imports = true +split-on-trailing-comma = false + +# https://docs.astral.sh/ruff/settings/#mccabe +[tool.ruff.lint.mccabe] +# Arbitrary to 2 bytes, same as SonarLint +max-complexity = 15 + +[tool.ruff.lint.pylint] +# Arbitrary to 1 byte, same as SonarLint +max-args = 7 +# At least same as max-complexity +max-branches = 15 + +# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file +[tool.pyright] +pythonVersion = "3.12" +exclude = [".venv/"] +typeCheckingMode = "standard" + +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +show_column_numbers = true +implicit_reexport = true +python_version = "3.12" +exclude = [".venv/"] + +strict = false +# Implicit return types ! +check_untyped_defs = true +disallow_untyped_calls = false +disallow_untyped_defs = false +disallow_incomplete_defs = false +disable_error_code = [ + "return", # Implicit return types + "var-annotated", # Django models ClassVars not seen as annotated +] +# Note: mypy still has issues with some boolean infered returns like `is_valid_hwnd` +# https://github.com/python/mypy/issues/4409 +# https://github.com/python/mypy/issues/10149 + +[[tool.mypy.overrides]] +# Untyped dependencies +module = ["rest_framework.*"] +ignore_missing_imports = true diff --git a/canopeum_backend/requirements-dev.txt b/canopeum_backend/requirements-dev.txt new file mode 100644 index 000000000..b8768c064 --- /dev/null +++ b/canopeum_backend/requirements-dev.txt @@ -0,0 +1,18 @@ +-r requirements.txt +pre-commit==3.6.2 +ruff==0.3.4 # must match .pre-commit-config.yaml +mypy==1.9.0 +pyright==1.1.355 +# Stubs not yet available for 5.0: https://github.com/typeddjango/django-stubs/issues/2020 +django-stubs>=4.2.7 +# Not necessarily used directly, just taken from requirements.txt +# that are also found in https://github.com/python/typeshed/tree/main/stubs +types-colorama>=0.4.6 +types-docutils>=0.20.0 +types-jsonschema>=4.21.0 +types-pytz>=2024.1 +types-regex>=2023.12.25 +types-requests>=2.31.0 +types-setuptools>=69.2.0 +types-six>=1.16.0 +types-tqdm>=4.66.0 diff --git a/canopeum_frontend/canopeum-mockoon.json b/canopeum_frontend/canopeum-mockoon.json index 5915f5409..9ae00cfa2 100644 --- a/canopeum_frontend/canopeum-mockoon.json +++ b/canopeum_frontend/canopeum-mockoon.json @@ -1482,4 +1482,4 @@ } ], "callbacks": [] -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/pages/Analytics.tsx b/canopeum_frontend/src/pages/Analytics.tsx index c93715318..8985ea1dc 100644 --- a/canopeum_frontend/src/pages/Analytics.tsx +++ b/canopeum_frontend/src/pages/Analytics.tsx @@ -8,4 +8,4 @@ export default function Analytics() { ); -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/pages/Login.tsx b/canopeum_frontend/src/pages/Login.tsx index 8dbca5e35..7cbc24c4b 100644 --- a/canopeum_frontend/src/pages/Login.tsx +++ b/canopeum_frontend/src/pages/Login.tsx @@ -4,4 +4,4 @@ export default function Login() {

Login

); -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/pages/Map.tsx b/canopeum_frontend/src/pages/Map.tsx index fd5d73406..4e73d7a71 100644 --- a/canopeum_frontend/src/pages/Map.tsx +++ b/canopeum_frontend/src/pages/Map.tsx @@ -6,4 +6,4 @@ export default function Map() { ); -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/pages/Settings.tsx b/canopeum_frontend/src/pages/Settings.tsx index 5eadab22b..ca655b833 100644 --- a/canopeum_frontend/src/pages/Settings.tsx +++ b/canopeum_frontend/src/pages/Settings.tsx @@ -8,4 +8,4 @@ export default function Settings() { ); -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/pages/Utilities.tsx b/canopeum_frontend/src/pages/Utilities.tsx index 92bbd0374..7cf4c8442 100644 --- a/canopeum_frontend/src/pages/Utilities.tsx +++ b/canopeum_frontend/src/pages/Utilities.tsx @@ -4,7 +4,7 @@ import facebook_logo from '@assets/icons/facebook-regular.svg'; export default function Utilities() { return (
-
+

Utilities

Icons

@@ -85,7 +85,7 @@ export default function Utilities() {
A simple secondary alert—check it out! -
+

Cards

@@ -178,7 +178,7 @@ export default function Utilities() {
- +
@@ -223,4 +223,4 @@ export default function Utilities() {
); -} \ No newline at end of file +} diff --git a/canopeum_frontend/src/services/api-interface.ts b/canopeum_frontend/src/services/api-interface.ts index ffe2a8088..a2cb52232 100644 --- a/canopeum_frontend/src/services/api-interface.ts +++ b/canopeum_frontend/src/services/api-interface.ts @@ -26,4 +26,4 @@ const api = { batchesClient: new BatchesClient(API_URL), }; -export default api; \ No newline at end of file +export default api; diff --git a/canopeum_frontend/src/services/api.ts b/canopeum_frontend/src/services/api.ts index f7928e948..a04618be9 100644 --- a/canopeum_frontend/src/services/api.ts +++ b/canopeum_frontend/src/services/api.ts @@ -2419,4 +2419,4 @@ function throwException(message: string, status: number, response: string, heade throw result; else throw new ApiException(message, status, response, headers, null); -} \ No newline at end of file +} diff --git a/docs/VSCode_select_venv.png b/docs/VSCode_select_venv.png new file mode 100644 index 000000000..a3b02c05e Binary files /dev/null and b/docs/VSCode_select_venv.png differ