diff --git a/.github/workflows/run_black_linter.yml b/.github/workflows/run_black_linter.yml index 4b92ce19..f9bcd9b5 100644 --- a/.github/workflows/run_black_linter.yml +++ b/.github/workflows/run_black_linter.yml @@ -14,5 +14,5 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.1.6 - uses: psf/black@stable diff --git a/.github/workflows/run_djlint.yml b/.github/workflows/run_djlint.yml index 02b47210..0d7c02a8 100644 --- a/.github/workflows/run_djlint.yml +++ b/.github/workflows/run_djlint.yml @@ -11,24 +11,18 @@ on: jobs: lint: - strategy: - fail-fast: true - matrix: - os: [ "ubuntu-latest" ] - python-version: [ "3.10", "3.11", "3.12" ] - django-version: [ "4", "5" ] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: #---------------------------------------------- # check-out repo and set-up python #---------------------------------------------- - name: Check out repository - uses: actions/checkout@v4 - - name: Set up python ${{ matrix.python-version }} + uses: actions/checkout@v4.1.6 + - name: Set up python id: setup-python - uses: actions/setup-python@v5 + uses: actions/setup-python@v5.1.0 with: - python-version: ${{ matrix.python-version }} + python-version: "3.12" #---------------------------------------------- # ----- install & configure poetry ----- #---------------------------------------------- @@ -42,7 +36,7 @@ jobs: #---------------------------------------------- - name: Load cached venv id: cached-poetry-dependencies - uses: actions/cache@v3 + uses: actions/cache@v4.0.2 with: path: .venv key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} diff --git a/.github/workflows/run_mypy.yml b/.github/workflows/run_mypy.yml new file mode 100644 index 00000000..286b4b68 --- /dev/null +++ b/.github/workflows/run_mypy.yml @@ -0,0 +1,47 @@ +name: Mypy +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + mypy: + name: mypy + runs-on: ubuntu-latest + env: + SECRET_KEY: "some!random!secret!key!use!online!generator!to!get" + URL: "127.0.0.1" + PROXY_IP: "localhost" + BRANCH: "debug" + DEBUG: "true" + DATABASE_TYPE: "sqlite3" + SITE_URL: "http://myfinances.example.com" + SITE_NAME: "myfinances" + steps: + - name: Clone repo + uses: actions/checkout@v4.1.6 + - name: Set up python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.12" + - name: Install and configure Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4.0.2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + - name: Run mypy checks + run: | + source .venv/bin/activate + mypy . diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5448616..489d8118 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: django-version: ["4", "5"] steps: - name: Clone repo - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.6 - name: Set up python uses: actions/setup-python@v5.1.0 with: @@ -24,7 +24,6 @@ jobs: - name: Install and configure Poetry uses: snok/install-poetry@v1 with: - version: 1.8.0 virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true @@ -79,4 +78,3 @@ jobs: source .venv/bin/activate python3 manage.py collectstatic --no-input python3 manage.py test --parallel - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffbc447c..27138d6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,10 @@ repos: entry: black --check --diff hooks: - id: black + name: Black Formatter + files: . + language: python + types: [ python ] - repo: local hooks: - id: pre-commit-django-migrations @@ -11,28 +15,54 @@ repos: entry: python manage.py makemigrations --dry-run --check --no-input language: system types: [ python ] + files: ./backend/models.py pass_filenames: false - repo: local hooks: - id: pre-commit-djlint name: Djlint (html files) - entry: djlint . --profile django --check + entry: djlint ./frontend/templates/ --profile django --reformat language: system - types: [ python ] + files: ./frontend/templates/ + types: [ html ] pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace + types: [ text ] + language: python - id: end-of-file-fixer - - id: debug-statements - - id: name-tests-test - - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 - hooks: - - id: setup-cfg-fmt - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 - hooks: - - id: pyupgrade - args: [ --py39-plus ] + types: [ python ] + language: python + - id: detect-aws-credentials + - id: detect-private-key + exclude: "docs/getting-started/settings/AWS/static.md" + - id: check-added-large-files + - id: check-ast + types: [ python3 ] + language: python + files: ".*\\.py$" # may not be needed due to type of python3 + - id: check-json + types: [ json ] + files: ".*\\.json$" + exclude: "backend/management/commands/contributors.json" + # - id: debug-statements # (unknown use) + # - id: name-tests-test # (blocking) + # - repo: https://github.com/asottile/setup-cfg-fmt + # rev: v2.5.0 + # hooks: + # - id: setup-cfg-fmt + # - repo: https://github.com/asottile/pyupgrade + # rev: v3.15.2 + # hooks: + # - id: pyupgrade # (blocking) + # args: [ --py39-plus ] + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.7.0 + # hooks: + # - id: mypy # (blocking) + # args: + # - --explicit-package-bases + # - --no-incremental + # additional_dependencies: ['mypy >=1.7.0, <1.8.0'] diff --git a/README.md b/README.md index b37b331d..4631c97e 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ To contribute code: 3. Create a new branch for your changes. 4. Make your changes, following the coding style guidelines. 5. Test your changes thoroughly - - `python manage.py test` - - run the app (`python manage.py runserver`) + - `python3 manage.py test` + - run the app (`python3 manage.py runserver`) - view any changed pages in browser (`http://127.0.0.1:8000`) and make sure the changes work as expected 6. Submit a pull request to the main repository's `main` branch. @@ -112,6 +112,12 @@ pip install black black ./ ``` +For static type checking we are using [mypy](https://mypy-lang.org/). Code tests will be run before PRs can be merged, they will fail if types in you code will be incorrect. You can run check with this command: + +``` +mypy . +``` + ### Version Control We use Git. Make sure your commits are clear, concise, and well-documented. Follow conventional commit message style. @@ -448,6 +454,30 @@ Thank you for your contributions!
๐Ÿž + + + + +
+ + Marvin Lopez + +
+
+ ๐Ÿ–ฅ๐ŸŽจ + + + + + +
+ + Artem Kolpakov + +
+
+ ๐Ÿž + @@ -456,8 +486,8 @@ manually edit with the details below. ### How to edit the contributors table? -- Updated any info using `python manage.py contriubtors` -- Run `python manage.py contributors sync` +- Updated any info using `python3 manage.py contriubtors` +- Run `python3 manage.py contributors sync` - Make a PR to request these changes in - Done :) diff --git a/assets/scripts/htmx.js b/assets/scripts/htmx.js index e0f3750f..d9a408e7 100644 --- a/assets/scripts/htmx.js +++ b/assets/scripts/htmx.js @@ -33,8 +33,9 @@ function htmx_resend(event) { } // https://htmx.org/docs/#config -htmx.config.globalViewTransitions = true -htmx.config.useTemplateFragments = true // for swapping of table items +htmx.config.globalViewTransitions = true; +htmx.config.useTemplateFragments = true; // for swapping of table items +htmx.config.scrollIntoViewOnBoost = false; // to stop hx-boost scrolling down automatically window.addEventListener("DOMContentLoaded", (event) => { diff --git a/backend/api/base/notifications.py b/backend/api/base/notifications.py index 5fcb1f2a..40d02e43 100644 --- a/backend/api/base/notifications.py +++ b/backend/api/base/notifications.py @@ -1,3 +1,4 @@ +from django.contrib import messages from django.http import HttpResponse from django.shortcuts import render @@ -7,25 +8,34 @@ def get_notification_html(request: HtmxHttpRequest): user_notifications = Notification.objects.filter(user=request.user).order_by("-date") - above_5 = False + count = user_notifications.count() - if user_notifications.count() > 5: + if count > 5: user_notifications = user_notifications[:5] - above_5 = True return render( request, "base/topbar/_notification_dropdown_items.html", - {"notifications": user_notifications, "notifications_above_max": above_5}, + {"notifications": user_notifications, "notif_count": count}, ) +def get_notification_count_html(request: HtmxHttpRequest): + user_notifications = Notification.objects.filter(user=request.user).count() + return HttpResponse(f"{user_notifications}") + + def delete_notification(request: HtmxHttpRequest, id: int): notif = Notification.objects.filter(id=id, user=request.user).first() if notif is None or notif.user != request.user: + if request.htmx: + messages.error(request, "Notification not found") + return render(request, "base/toasts.html") return HttpResponse(status=404, content="Notification not found") notif.delete() - return HttpResponse(status=200) + response = HttpResponse(status=200) + response["HX-Trigger"] = "refresh_notification_count" + return response diff --git a/backend/api/base/urls.py b/backend/api/base/urls.py index 3ccb31e9..af987d81 100644 --- a/backend/api/base/urls.py +++ b/backend/api/base/urls.py @@ -17,6 +17,7 @@ notifications.get_notification_html, name="notifications get", ), + path("notifications/get_count", notifications.get_notification_count_html, name="notifications get count"), path( "notifications/delete/", notifications.delete_notification, diff --git a/backend/api/invoices/create/services/add.py b/backend/api/invoices/create/services/add.py index f2f9f78b..3b77951c 100644 --- a/backend/api/invoices/create/services/add.py +++ b/backend/api/invoices/create/services/add.py @@ -9,7 +9,7 @@ @require_http_methods(["POST"]) def add_service(request: HtmxHttpRequest): context: dict = {} - existing_service = request.POST.get("existing_service", "") + existing_service = request.POST.get("existing_service", 0) try: existing_service_obj = InvoiceProduct.objects.get(user=request.user, id=existing_service) @@ -23,10 +23,10 @@ def add_service(request: HtmxHttpRequest): list_of_current_rows = [row for row in zip(list_hours, list_service_name, list_service_description, list_price_per_hour)] if not existing_service: - hours = int(request.POST.get("post_hours", "")) + hours = int(request.POST.get("post_hours", "0")) service_name = request.POST.get("post_service_name") service_description = request.POST.get("post_service_description") - price_per_hour = int(request.POST.get("post_rate", "")) + price_per_hour = int(request.POST.get("post_rate", "0")) if not hours: return JsonResponse( diff --git a/backend/api/invoices/delete.py b/backend/api/invoices/delete.py index 55dca70b..a1f4859a 100644 --- a/backend/api/invoices/delete.py +++ b/backend/api/invoices/delete.py @@ -18,10 +18,12 @@ def delete_invoice(request: HtmxHttpRequest): try: invoice = Invoice.objects.get(id=delete_items.get("invoice", "")) except Invoice.DoesNotExist: - return JsonResponse({"message": "Invoice not found"}, status=404) + messages.error(request, "Invoice Not Found") + return render(request, "base/toasts.html") if not invoice.has_access(request.user): - return JsonResponse({"message": "You do not have permission to delete this invoice"}, status=404) + messages.error(request, "You do not have permission to delete this invoice") + return render(request, "base/toasts.html") QuotaLimit.delete_quota_usage("invoices-count", request.user, invoice.id, invoice.date_created) diff --git a/backend/api/receipts/fetch.py b/backend/api/receipts/fetch.py index 8cd3edd7..94ebdc75 100644 --- a/backend/api/receipts/fetch.py +++ b/backend/api/receipts/fetch.py @@ -29,7 +29,13 @@ def fetch_all_receipts(request: HtmxHttpRequest): results = results.filter(user=request.user) if search_text: - results = results.filter(Q(name__icontains=search_text) | Q(date__icontains=search_text)).order_by("-date") + results = results.filter( + Q(name__icontains=search_text) + | Q(date__icontains=search_text) + | Q(merchant_store__icontains=search_text) + | Q(purchase_category__icontains=search_text) + | Q(id__icontains=search_text) + ).order_by("-date") elif selected_filters: context.update({"selected_filters": [selected_filters]}) results = results.filter(total_price__gte=selected_filters).order_by("-date") diff --git a/backend/context_processors.py b/backend/context_processors.py index 98c497c4..75c6f0ec 100644 --- a/backend/context_processors.py +++ b/backend/context_processors.py @@ -67,7 +67,7 @@ def generate_breadcrumbs(*breadcrumb_list: str) -> list[dict[Any, Any] | None]: """ return [all_items.get(breadcrumb) for breadcrumb in breadcrumb_list] - current_url_name: str | Any = request.resolver_match.url_name # type: ignore[union-attr] + current_url_name: str | Any = request.resolver_match.view_name # type: ignore[union-attr] all_items: dict[str, dict] = { "dashboard": get_item("Dashboard", "dashboard", "house"), diff --git a/backend/management/commands/contributors.json b/backend/management/commands/contributors.json index 1d9be4b8..6dd480c2 100644 --- a/backend/management/commands/contributors.json +++ b/backend/management/commands/contributors.json @@ -1,28 +1,232 @@ [ - {"name": "Trey", "username": "TreyWW", "role": "Project Lead", "tags": ["๐Ÿ‘‘", "๐Ÿ–ฅ"]}, - {"name": "Jacob", "username": "Z3nKrypt", "role": "Documentation", "tags": ["๐Ÿ“–"]}, - {"name": "Tom", "username": "tomkinane", "role": "Design", "tags": ["๐ŸŽจ"]}, - {"name": "SharonAliyas5573", "username": "SharonAliyas5573", "role": "Development", "tags": ["๐Ÿ–ฅ"]}, - {"name": "romana-la", "username": "romana-la", "role": "Documentation", "tags": ["๐Ÿ“–"]}, - {"name": "flyingdev", "username": "flyingdev", "role": "CI", "tags": ["๐Ÿงช"]}, - {"name": "chavi362", "username": "chavi362", "role": "Documentation", "tags": ["๐Ÿ“–"]}, - {"name": "bermr", "username": "bermr", "role": "CI", "tags": ["๐Ÿงช"]}, - {"name": "PhilipZara", "username": "PhilipZara", "role": "Design", "tags": ["๐ŸŽจ"]}, - {"name": "Tianrui-Luo", "username": "Tianrui-Luo", "role": "Development", "tags": ["๐Ÿ–ฅ"]}, - {"name": "HarryHuCodes", "username": "HarryHuCodes", "role": "Development", "tags": ["๐Ÿ–ฅ"]}, - {"name": "Nuova", "username": "Nuovaxu", "role": "Development", "tags": ["๐Ÿ–ฅ"]}, - {"name": "HessTaha", "username": "HessTaha", "role": "CI-CD", "tags": ["๐Ÿณ"]}, - {"name": "wnm210", "username": "wnm210", "role": "Design", "tags": ["๐ŸŽจ"]}, - {"name": "Matt", "username": "matthewjuarez1", "role": "Full Stack", "tags": ["๐Ÿ–ฅ", "๐ŸŽจ"]}, - {"name": "SBMOYO", "username": "SBMOYO", "role": "CI", "tags": ["๐Ÿงช"]}, - {"name": "Kevin Liu", "username": "kliu6151", "role": "Development", "tags": ["๐Ÿ–ฅ"]}, - {"name": "Jehad Altoutou", "username": "HappyLife2", "role": "Design", "tags": ["๐ŸŽจ"]}, - {"name": "Slawek Bierwiaczonek", "username": "Domejko", "role": "CI", "tags": ["๐Ÿงช"]}, - {"name": "Sergey G", "username": "introkun", "role": "Development & CI", "tags": ["โ™ป", "๐Ÿงช", "๐Ÿ–ฅ", "๐ŸŽจ"]}, - {"name": "Samuel P", "username": "spalominor", "role": "Design", "tags": ["๐ŸŽจ"]}, - {"name": "Sabari Ragavendra CK", "username": "CKsabari2001", "role": "Layout", "tags": ["๐ŸŽจ"]}, - {"name": "atulanand25", "username": "atulanand25", "role": "Full Stack", "tags": ["๐ŸŽจ", "๐Ÿ–ฅ", "๐Ÿž"]}, - {"name": "ryansurf", "username": "ryansurf", "role": "Bug Fixes", "tags": ["๐Ÿž"]}, - {"name": "David", "username": "blocage", "role": "Refactoring", "tags": "โ™ป"}, - {"name": "Guillermo", "username": "glizondo", "role": "Bug Fixes", "tags": ["๐Ÿž"]}, -] + { + "name": "Trey", + "username": "TreyWW", + "role": "Project Lead", + "tags": [ + "๐Ÿ‘‘", + "๐Ÿ–ฅ" + ] + }, + { + "name": "Jacob", + "username": "Z3nKrypt", + "role": "Documentation", + "tags": [ + "๐Ÿ“–" + ] + }, + { + "name": "Tom", + "username": "tomkinane", + "role": "Design", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "SharonAliyas5573", + "username": "SharonAliyas5573", + "role": "Development", + "tags": [ + "๐Ÿ–ฅ" + ] + }, + { + "name": "romana-la", + "username": "romana-la", + "role": "Documentation", + "tags": [ + "๐Ÿ“–" + ] + }, + { + "name": "flyingdev", + "username": "flyingdev", + "role": "CI", + "tags": [ + "๐Ÿงช" + ] + }, + { + "name": "chavi362", + "username": "chavi362", + "role": "Documentation", + "tags": [ + "๐Ÿ“–" + ] + }, + { + "name": "bermr", + "username": "bermr", + "role": "CI", + "tags": [ + "๐Ÿงช" + ] + }, + { + "name": "PhilipZara", + "username": "PhilipZara", + "role": "Design", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "Tianrui-Luo", + "username": "Tianrui-Luo", + "role": "Development", + "tags": [ + "๐Ÿ–ฅ" + ] + }, + { + "name": "HarryHuCodes", + "username": "HarryHuCodes", + "role": "Development", + "tags": [ + "๐Ÿ–ฅ" + ] + }, + { + "name": "Nuova", + "username": "Nuovaxu", + "role": "Development", + "tags": [ + "๐Ÿ–ฅ" + ] + }, + { + "name": "HessTaha", + "username": "HessTaha", + "role": "CI-CD", + "tags": [ + "๐Ÿณ" + ] + }, + { + "name": "wnm210", + "username": "wnm210", + "role": "Design", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "Matt", + "username": "matthewjuarez1", + "role": "Full Stack", + "tags": [ + "๐Ÿ–ฅ", + "๐ŸŽจ" + ] + }, + { + "name": "SBMOYO", + "username": "SBMOYO", + "role": "CI", + "tags": [ + "๐Ÿงช" + ] + }, + { + "name": "Kevin Liu", + "username": "kliu6151", + "role": "Development", + "tags": [ + "๐Ÿ–ฅ" + ] + }, + { + "name": "Jehad Altoutou", + "username": "HappyLife2", + "role": "Design", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "Slawek Bierwiaczonek", + "username": "Domejko", + "role": "CI", + "tags": [ + "๐Ÿงช" + ] + }, + { + "name": "Sergey G", + "username": "introkun", + "role": "Development & CI", + "tags": [ + "โ™ป", + "๐Ÿงช", + "๐Ÿ–ฅ", + "๐ŸŽจ" + ] + }, + { + "name": "Samuel P", + "username": "spalominor", + "role": "Design", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "Sabari Ragavendra CK", + "username": "CKsabari2001", + "role": "Layout", + "tags": [ + "๐ŸŽจ" + ] + }, + { + "name": "atulanand25", + "username": "atulanand25", + "role": "Full Stack", + "tags": [ + "๐ŸŽจ", + "๐Ÿ–ฅ", + "๐Ÿž" + ] + }, + { + "name": "ryansurf", + "username": "ryansurf", + "role": "Bug Fixes", + "tags": [ + "๐Ÿž" + ] + }, + { + "name": "David", + "username": "blocage", + "role": "Refactoring", + "tags": "โ™ป" + }, + { + "name": "Guillermo", + "username": "glizondo", + "role": "Bug Fixes", + "tags": [ + "๐Ÿž" + ] + }, + { + "name": "Marvin Lopez", + "username": "marvinl803", + "role": "Full Stack", + "tags": [ + "๐ŸŽจ", + "๐Ÿ–ฅ" + ] + }, + { + "name": "Artem Kolpakov", + "username": "artkolpakov", + "role": "Bug Fixes", + "tags": [ + "๐Ÿž" + ] + } +] \ No newline at end of file diff --git a/backend/migrations/0035_client_contact_method.py b/backend/migrations/0035_client_contact_method.py new file mode 100644 index 00000000..7cda0239 --- /dev/null +++ b/backend/migrations/0035_client_contact_method.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-30 01:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend", "0034_invoice_client_email_quotaincreaserequest_reason_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="client", + name="contact_method", + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/backend/models.py b/backend/models.py index bbea11e4..e7928916 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,9 +1,9 @@ from __future__ import annotations +import typing from datetime import datetime from decimal import Decimal -from typing import Literal -from typing import NoReturn +from typing import Literal, Union from uuid import uuid4 from django.contrib.auth.hashers import check_password @@ -245,6 +245,7 @@ class Client(models.Model): email = models.EmailField(blank=True, null=True) email_verified = models.BooleanField(default=False) company = models.CharField(max_length=100, blank=True, null=True) + contact_method = models.CharField(max_length=100, blank=True, null=True) is_representative = models.BooleanField(default=False) address = models.CharField(max_length=100, blank=True, null=True) @@ -363,7 +364,7 @@ def dynamic_payment_status(self): return self.payment_status @property - def get_to_details(self) -> tuple[str, dict[str, str]] | tuple[str, Client]: + def get_to_details(self) -> tuple[str, dict[str, str | None]] | tuple[str, Client]: """ Returns the client details for the invoice "client" and Client object if client_to @@ -383,13 +384,13 @@ def get_subtotal(self) -> Decimal: subtotal += item.get_total_price() return Decimal(round(subtotal, 2)) - def get_tax(self, amount: Decimal = 0.00) -> Decimal: + def get_tax(self, amount: Decimal = Decimal(0.00)) -> Decimal: amount = amount or self.get_subtotal() if self.vat_number: return Decimal(round(amount * Decimal(0.2), 2)) return Decimal(0) - def get_percentage_amount(self, subtotal: float = 0.00) -> Decimal: + def get_percentage_amount(self, subtotal: Decimal = Decimal(0.00)) -> Decimal: total = subtotal or self.get_subtotal() if self.discount_percentage > 0: @@ -406,7 +407,7 @@ def get_total_price(self) -> Decimal: total -= discount_amount if 0 > total: - total = 0 + total = Decimal(0) else: total -= self.get_tax(total) @@ -654,6 +655,7 @@ def __str__(self): return self.name def get_quota_limit(self, user: User, quota_limit: QuotaLimit | None = None): + user_quota_override: QuotaOverrides | QuotaLimit try: if quota_limit: user_quota_override = quota_limit @@ -674,6 +676,8 @@ def get_period_usage(self, user: User): return "Not available" def strict_goes_above_limit(self, user: User, extra: str | int | None = None, add: int = 0) -> bool: + current: Union[int, None, QuerySet[QuotaUsage], Literal["Not Available"]] + current = self.strict_get_quotas(user, extra) current = current.count() if current != "Not Available" else None return current + add >= self.get_quota_limit(user) if current else False @@ -686,35 +690,39 @@ def strict_get_quotas( :return: QuerySet of quota usages OR "Not Available" if utilisation isn't available (e.g. per invoice you can't get in total) """ current = None - quota_limit = quota_limit.quota_usage if quota_limit else QuotaUsage.objects.filter(user=user, quota_limit=self) + if quota_limit is not None: + quota_lim = quota_limit.quota_usage + else: + quota_lim = QuotaUsage.objects.filter(user=user, quota_limit=self) # type: ignore[assignment] if self.limit_type == "forever": current = self.quota_usage.filter(user=user, quota_limit=self) elif self.limit_type == "per_month": current_month = timezone.now().month current_year = timezone.now().year - current = quota_limit.filter(created_at__year=current_year, created_at__month=current_month) + current = quota_lim.filter(created_at__year=current_year, created_at__month=current_month) elif self.limit_type == "per_day": current_day = timezone.now().day current_month = timezone.now().month current_year = timezone.now().year - current = quota_limit.filter(created_at__year=current_year, created_at__month=current_month, created_at__day=current_day) + current = quota_lim.filter(created_at__year=current_year, created_at__month=current_month, created_at__day=current_day) elif self.limit_type in ["per_client", "per_invoice", "per_team", "per_receipt", "per_quota"] and extra: - current = quota_limit.filter(extra_data=extra) + current = quota_lim.filter(extra_data=extra) else: return "Not Available" return current @classmethod - def delete_quota_usage(cls, quota_limit: str | QuotaLimit, user: User, extra, timestamp=None) -> NoReturn: + @typing.no_type_check + def delete_quota_usage(cls, quota_limit: str | QuotaLimit, user: User, extra, timestamp=None): quota_limit = cls.objects.get(slug=quota_limit) if isinstance(quota_limit, str) else quota_limit all_usages = quota_limit.strict_get_quotas(user, extra) closest_obj = None if all_usages.count() > 1 and timestamp: - earliest: QuotaUsage = all_usages.filter(created_at__gte=timestamp).order_by("created_at").first() - latest: QuotaUsage = all_usages.filter(created_at__lte=timestamp).order_by("created_at").last() + earliest: QuotaUsage | None = all_usages.filter(created_at__gte=timestamp).order_by("created_at").first() + latest: QuotaUsage | None = all_usages.filter(created_at__lte=timestamp).order_by("created_at").last() if earliest and latest: time_until_soonest_obj = abs(earliest.created_at - timestamp) diff --git a/backend/views/core/__init__.py b/backend/views/core/__init__.py index cffae357..5462a300 100644 --- a/backend/views/core/__init__.py +++ b/backend/views/core/__init__.py @@ -3,5 +3,5 @@ from .auth import login from .settings import view as settings_view, teams from .invoices import dashboard as invoices_dashboard, create, view as invoices_view, manage_access -from .clients import dashboard as clients_dashboard, create +from .clients import dashboard as clients_dashboard, create as create_client from .receipts import dashboard as receipts_dashboard diff --git a/backend/views/core/auth/login.py b/backend/views/core/auth/login.py index b1222a51..3c531a86 100644 --- a/backend/views/core/auth/login.py +++ b/backend/views/core/auth/login.py @@ -75,7 +75,7 @@ def login_manual(request: HtmxAnyHttpRequest): # HTMX POST messages.error(request, "Incorrect email or password") return render_toast_message(request) - if user.awaiting_email_verification and ARE_EMAILS_ENABLED: # type: ignore[union-attr] + if user.awaiting_email_verification and ARE_EMAILS_ENABLED: # type: ignore[attr-defined] messages.error(request, "You must verify your email before logging in.") return render_toast_message(request) diff --git a/backend/views/core/auth/verify.py b/backend/views/core/auth/verify.py index 14b9f915..9b1a4207 100644 --- a/backend/views/core/auth/verify.py +++ b/backend/views/core/auth/verify.py @@ -76,10 +76,10 @@ def resend_verification_code(request): subject="Verify your email", message=f""" Hi {user.first_name if user.first_name else "User"}, - + Verification for your email has been requested to link this email to your MyFinances account. If this wasn't you, you can simply ignore this email. - + If it was you, you can complete the verification by clicking the link below. Verify Link: {magic_link_url} """, diff --git a/backend/views/core/clients/create.py b/backend/views/core/clients/create.py index f78c5e96..ffdbef8e 100644 --- a/backend/views/core/clients/create.py +++ b/backend/views/core/clients/create.py @@ -15,6 +15,7 @@ def create_client(request: HtmxHttpRequest): "email": request.POST.get("client_email"), "address": request.POST.get("client_address"), "phone_number": request.POST.get("client_phone"), + "contact_method": request.POST.get("client_contact_method"), "company": request.POST.get("company_name"), "is_representative": (True if request.POST.get("is_representative") == "on" else False), } diff --git a/backend/views/core/invoices/manage_access.py b/backend/views/core/invoices/manage_access.py index 22e20a05..4e480d7e 100644 --- a/backend/views/core/invoices/manage_access.py +++ b/backend/views/core/invoices/manage_access.py @@ -5,6 +5,7 @@ from backend.decorators import quota_usage_check from backend.models import Invoice, InvoiceURL, QuotaUsage, QuotaLimit from backend.types.htmx import HtmxHttpRequest +from backend.utils.quota_limit_ops import quota_usage_check_under def manage_access(request: HtmxHttpRequest, invoice_id): @@ -23,7 +24,6 @@ def manage_access(request: HtmxHttpRequest, invoice_id): ) -@quota_usage_check("invoices-access_codes", 1, api=True, htmx=True) def create_code(request: HtmxHttpRequest, invoice_id): if not request.htmx: return redirect("invoices:dashboard") @@ -36,11 +36,19 @@ def create_code(request: HtmxHttpRequest, invoice_id): except Invoice.DoesNotExist: return HttpResponse("Invoice not found", status=400) + limit = QuotaLimit.objects.get(slug="invoices-access_codes").get_quota_limit(user=request.user) + + current_amount = InvoiceURL.objects.filter(invoice_id=invoice_id).count() + + if current_amount >= limit: + messages.error(request, f"You have reached the quota limit for this service 'access_codes'") + return render(request, "partials/messages_list.html", {"autohide": False}) + code = InvoiceURL.objects.create(invoice=invoice, created_by=request.user) messages.success(request, "Successfully created code") - QuotaUsage.create_str(request.user, "invoices-access_codes", invoice_id) + # QuotaUsage.create_str(request.user, "invoices-access_codes", invoice_id) return render( request, @@ -59,9 +67,10 @@ def delete_code(request: HtmxHttpRequest, code): if not invoice.has_access(request.user): raise Invoice.DoesNotExist except (Invoice.DoesNotExist, InvoiceURL.DoesNotExist): - return redirect("invoices:dashboard") + messages.error(request, "Invalid URL") + return render(request, "base/toasts.html") - QuotaLimit.delete_quota_usage("invoices-access_codes", request.user, invoice.id, code_obj.created_on) + # QuotaLimit.delete_quota_usage("invoices-access_codes", request.user, invoice.id, code_obj.created_on) code_obj.delete() diff --git a/components/+messages_list.html b/components/+messages_list.html index 96766ec2..8285af22 100644 --- a/components/+messages_list.html +++ b/components/+messages_list.html @@ -1,6 +1,5 @@ -
+
{% for message in messages %}
{% if autohide != False %} {% endif %}
diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..ecc69a14 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +docs.myfinances.cloud diff --git a/docs/changelog/index.md b/docs/changelog/index.md index d720235f..4412ad2d 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -3,6 +3,8 @@ !!! danger "Pre-releases" We are currently still in development, and our project should **NOT** be considered usable for production yet. +We may not add a changelog entry for every version, it depends how severe the changes are. You can look at the Git Tags page on github to see all of the changes. + ???+ tip "v0.4.0 (latest)" === "Additions" - ๐Ÿ’ผ Invoice column rename by @Domejko in [(313)](https://github.com/TreyWW/MyFinances/pull/313) diff --git a/docs/contributing/setup.md b/docs/contributing/setup.md index 7dba498a..f82c84b1 100644 --- a/docs/contributing/setup.md +++ b/docs/contributing/setup.md @@ -14,6 +14,7 @@ poetry install --no-root --with dev # installs djlint and black python manage.py test --parallel # runs our django tests djlint ./frontend/templates --reformat # runs our djLint formatter for HTML black ./ # runs our black formatter for python files +mypy . # runs mypy static type check for python files ``` ## Pull in your changes diff --git a/docs/debugging/problem-solving.md b/docs/debugging/problem-solving.md new file mode 100644 index 00000000..5916adc5 --- /dev/null +++ b/docs/debugging/problem-solving.md @@ -0,0 +1,28 @@ +## **Common error messages and their solutions:** + +1. If you receive this error: **ImportError: cannot import name 'login_not_required' from 'login_required**
You may be missing some django middleware. + +To fix this: +```shell +pip install django-login-required-middleware +``` + +2. If you receive this error: **No module named 'forex_python'** + +Do this: +```shell +pip install forex_python +``` + +3. To fix several **boto3** "extension name here" **not found errors** + +Do the following: +```shell +pip install mypy_boto3_iam + +pip install mypy_boto3_scheduler + +pip install mypy_boto3_events + +pip install mypy_boto3_stepfunctions +``` diff --git a/docs/debugging/python/poetry.md b/docs/debugging/python/poetry.md index cd658664..6a97ce2c 100644 --- a/docs/debugging/python/poetry.md +++ b/docs/debugging/python/poetry.md @@ -19,3 +19,7 @@ poetry install ``` And this time they should all be correctly installed! + +# For other poetry issues, or if this doesn't fix the issue try the following: + - Delete all files in your directory associate with poetry. + - Follow these directions to use the installer on [poetry's website]( https://python-poetry.org/docs/#installing-with-the-official-installer). diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 3831f05f..5d5fec21 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -42,6 +42,12 @@ git clone [copied fork url] ./venv/Scripts/activate ``` + OR if you are running in a POSIX system such as: bash/zsh, fish, csh/tcsh, or PowerShell, the command to activate the virtual environment is: + + ```shell + ./venv/bin/activate + ``` + More information [here](https://docs.python.org/3/library/venv.html) 2. Install our dependencies using [python poetry](https://python-poetry.org/docs/#installing-manually) ```shell @@ -49,6 +55,8 @@ git clone [copied fork url] poetry install --no-root --with mypy,django,dev ``` + If the installation of poetry gives error messages check out our [debugging section on poetry](../debugging/python/poetry.md). + 3. Setup a database (we suggest using sqlite so there's no installation!) To do this you can use one of our database guides, we currently only support 3 databases: - [SQlite3 (recommended for dev)](./databases/sqlite.md) @@ -115,3 +123,6 @@ npm run webpack-dev # this only runs it once npm run webpack-watch # this does the same as above, but listens for updates ``` + +### Problem Solving Errors +If you are having trouble with the installation process, we have some possible fixes. Check out our [debugging](../debugging/) section. diff --git a/docs/overrides/partials/integrations/analytics/custom.html b/docs/overrides/partials/integrations/analytics/custom.html index 6d4c1b2f..d8dc5b80 100644 --- a/docs/overrides/partials/integrations/analytics/custom.html +++ b/docs/overrides/partials/integrations/analytics/custom.html @@ -1,4 +1,7 @@ -{% if config.extra.analytics.property and config.extra.analytics.id %} - +{% if config.extra.analytics.property %} + + + {{ config.extra.analytics.property | safe }} +{% else %} + {% endif %} diff --git a/frontend/templates/base/_head.html b/frontend/templates/base/_head.html index d390bf2a..7cb279e1 100644 --- a/frontend/templates/base/_head.html +++ b/frontend/templates/base/_head.html @@ -45,7 +45,7 @@ integrity="sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV" crossorigin="anonymous"> - + {# #} {% else %} {# #} diff --git a/frontend/templates/base/topbar/_notification_bell.html b/frontend/templates/base/topbar/_notification_bell.html deleted file mode 100644 index 4ba9586a..00000000 --- a/frontend/templates/base/topbar/_notification_bell.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - {{ request.user.notification_count }} -
diff --git a/frontend/templates/base/topbar/_notification_count.html b/frontend/templates/base/topbar/_notification_count.html new file mode 100644 index 00000000..5fd63544 --- /dev/null +++ b/frontend/templates/base/topbar/_notification_count.html @@ -0,0 +1,6 @@ +

{{ notif_count | default:request.user.notification_count }}

diff --git a/frontend/templates/base/topbar/_notification_dropdown_items.html b/frontend/templates/base/topbar/_notification_dropdown_items.html index 3383d904..7d9e36d3 100644 --- a/frontend/templates/base/topbar/_notification_dropdown_items.html +++ b/frontend/templates/base/topbar/_notification_dropdown_items.html @@ -41,9 +41,21 @@ {% elif notification.action == "redirect" %}
  • - {{ notification.message }} +
    + {{ notification.message }} + + + + +
  • {% elif notification.action == "modal" %}
  • @@ -63,7 +75,7 @@
  • {% endfor %} -{% if notifications_above_max %} +{% if notif_count > 5 %}
  • You have older notifications. @@ -72,4 +84,6 @@
  • {% endif %} -{% include "base/topbar/_notification_bell.html" %} +{% if not initial_load %} + {% include "base/topbar/_notification_count.html" %} +{% endif %} diff --git a/frontend/templates/base/topbar/_topbar.html b/frontend/templates/base/topbar/_topbar.html index 6dfd21f6..1f48d848 100644 --- a/frontend/templates/base/topbar/_topbar.html +++ b/frontend/templates/base/topbar/_topbar.html @@ -28,7 +28,7 @@
    @@ -60,17 +66,16 @@ {# Profile Picture #} + {# Client Main Contact Method #} +
    +
    +
    + + + +
    +
    +
    diff --git a/frontend/templates/pages/clients/dashboard/dashboard.html b/frontend/templates/pages/clients/dashboard/dashboard.html index 9318cd16..0b3b36ff 100644 --- a/frontend/templates/pages/clients/dashboard/dashboard.html +++ b/frontend/templates/pages/clients/dashboard/dashboard.html @@ -7,7 +7,7 @@ Create Client -
    + {% csrf_token %} {% csrf_token %} -
    STEP 1 - DESTINATIONS
    +
    + STEP 1 - DESTINATIONS * +

    From

    @@ -18,7 +20,9 @@
    -
    STEP 2 - DATES
    +
    + STEP 2 - DATES * +
    @@ -61,10 +65,15 @@
    -
    STEP 3 - SERVICES
    +
    + STEP 3 - SERVICES * +
    {% component_block "pages:invoices:create:services_table" %} {% endcomponent_block %} -
    STEP 4 - BANK DETAILS [OPTIONAL]
    +
    + STEP 4 - BANK DETAILS + [OPTIONAL] +
    @@ -119,7 +128,10 @@
    -
    STEP 5 - CUSTOM DESIGNS [OPTIONAL]
    +
    + STEP 5 - CUSTOM DESIGNS + [OPTIONAL] +
    diff --git a/frontend/templates/pages/receipts/_search.html b/frontend/templates/pages/receipts/_search.html index 50f79e34..ebbe85d3 100644 --- a/frontend/templates/pages/receipts/_search.html +++ b/frontend/templates/pages/receipts/_search.html @@ -1,4 +1,4 @@ - + {% csrf_token %}