Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Core Integration #543

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c6e17bc
Initial
TreyWW Dec 5, 2024
ec02ccd
Initial Commit
TreyWW Dec 19, 2024
fda48b3
Removed migrations for a fresh start, added extended templates, made …
TreyWW Dec 22, 2024
21b4021
Fixed all modals to work with new system
TreyWW Dec 22, 2024
9c1232b
Moved to core namespace
TreyWW Dec 22, 2024
2d3072c
Added defaults
TreyWW Dec 22, 2024
719e473
Added back myfinances-specific features, and moved some to core
TreyWW Dec 25, 2024
fe3af16
Fix tests + core:dashboard moved to just dashboard
TreyWW Dec 25, 2024
ffeb8f1
Added extended auth
TreyWW Dec 27, 2024
5b20a74
Added custom function into tailwind to fetch content from core folder
TreyWW Dec 27, 2024
c1727ba
Add published version of core
TreyWW Dec 28, 2024
c6c05ef
Merge branch 'main' into core-integration
TreyWW Dec 28, 2024
efba725
Ran poetry lock to get updated hash
TreyWW Dec 28, 2024
36a70da
Fixed tailwind HTML template content finders to work with pip publish…
TreyWW Dec 28, 2024
d0beb39
temp added debug to github action
TreyWW Dec 29, 2024
000cd9b
temp added debug to github action
TreyWW Dec 29, 2024
ed91e32
Updated core to 0.0.4 and fixed settings
TreyWW Dec 30, 2024
527458e
Added venv to gh workflow
TreyWW Dec 30, 2024
65b6a58
Temporarily disable mypy while the version issues are still here
TreyWW Dec 31, 2024
5545d58
Fixed modals
TreyWW Dec 31, 2024
e16ebcd
Removed system_health as defined in core
TreyWW Dec 31, 2024
28637e7
Updated to use core/ templates, attempted to fix migrations (broke th…
TreyWW Jan 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
File renamed without changes.
29 changes: 23 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,33 @@ jobs:
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
- name: Install latest setuptools
run: |
source .venv/bin/activate
pip install setuptools
- name: Install latest Django
- name: Debug Python Environment
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"
run: |
source .venv/bin/activate
pip install Django
python -c "import pkgutil; print([module.name for module in pkgutil.iter_modules()])"
python -c "import core; print(core.__file__)"
python -c "import billing; print(billing.__file__)"
- name: Install dependencies and build frontend
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"
run: |
source .venv/bin/activate
npm ci
npm run tailwind-build
npm run webpack-build
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,4 @@ Pulumi.*.yaml.bak


# Closed Source features
./billing
../core/billing
84 changes: 6 additions & 78 deletions backend/admin.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,5 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from backend.core.api.public import APIAuthToken
from backend.core.models import (
PasswordSecret,
AuditLog,
LoginLog,
Error,
TracebackError,
UserSettings,
Notification,
Organization,
TeamInvitation,
TeamMemberPermission,
User,
FeatureFlags,
VerificationCodes,
QuotaLimit,
QuotaOverrides,
QuotaUsage,
QuotaIncreaseRequest,
EmailSendStatus,
FileStorageFile,
MultiFileUpload,
)
from backend.finance.models import (
Invoice,
InvoiceURL,
Expand All @@ -33,49 +9,25 @@
InvoiceProduct,
Receipt,
ReceiptDownloadToken,
FinanceDefaultValues,
)

from backend.clients.models import Client, DefaultValues

from settings.settings import BILLING_ENABLED

# from django.contrib.auth.models imp/ort User
# admin.register(Invoice)
admin.site.register(
[
UserSettings,
Client,
Invoice,
InvoiceURL,
InvoiceItem,
PasswordSecret,
AuditLog,
LoginLog,
Error,
TracebackError,
Notification,
Organization,
TeamInvitation,
TeamMemberPermission,
InvoiceProduct,
FeatureFlags,
VerificationCodes,
Receipt,
ReceiptDownloadToken,
InvoiceReminder,
APIAuthToken,
InvoiceRecurringProfile,
FileStorageFile,
MultiFileUpload,
DefaultValues,
FinanceDefaultValues,
]
)

if BILLING_ENABLED:
from billing.models import PlanFeature, PlanFeatureGroup, SubscriptionPlan, UserSubscription

admin.site.register([PlanFeature, PlanFeatureGroup, SubscriptionPlan, UserSubscription])


class QuotaLimitAdmin(admin.ModelAdmin):
readonly_fields = ["name", "slug"]
Expand All @@ -96,38 +48,14 @@ def get_queryset(self, request):
return super().get_queryset(request).select_related("quota_limit", "user")


class EmailSendStatusAdmin(admin.ModelAdmin):
readonly_fields = ["aws_message_id"]


class InvoiceURLAdmin(admin.ModelAdmin):
readonly_fields = ["expires"]


admin.site.register(QuotaLimit, QuotaLimitAdmin)
admin.site.register(QuotaUsage, QuotaUsageAdmin)
admin.site.register(QuotaOverrides, QuotaOverridesAdmin)
admin.site.register(QuotaIncreaseRequest, QuotaIncreaseRequestAdmin)
admin.site.register(EmailSendStatus, EmailSendStatusAdmin)

# admin.site.unregister(User)
fields = list(UserAdmin.fieldsets) # type: ignore[arg-type]
fields[0] = (
None,
{
"fields": (
"username",
"password",
"logged_in_as_team",
"awaiting_email_verification",
"stripe_customer_id",
"entitlements",
"require_change_password",
)
},
)
UserAdmin.fieldsets = tuple(fields)
admin.site.register(User, UserAdmin)
# admin.site.register(QuotaLimit, QuotaLimitAdmin)
# admin.site.register(QuotaUsage, QuotaUsageAdmin)
# admin.site.register(QuotaOverrides, QuotaOverridesAdmin)
# admin.site.register(QuotaIncreaseRequest, QuotaIncreaseRequestAdmin)

admin.site.site_header = "MyFinances Admin"
admin.site.index_title = "MyFinances"
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from django.shortcuts import render, redirect
from django_ratelimit.core import is_ratelimited

from backend.decorators import web_require_scopes
from core.decorators import web_require_scopes
from backend.models import EmailSendStatus
from backend.core.types.htmx import HtmxHttpRequest
from core.types.htmx import HtmxHttpRequest


@web_require_scopes("emails:read", True, True)
Expand Down
94 changes: 48 additions & 46 deletions backend/core/api/emails/send.py → backend/api/emails/send.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections.abc import Iterator
from string import Template

from core.data.default_email_templates import email_footer
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
Expand All @@ -15,20 +16,20 @@
from django.views.decorators.http import require_POST
from mypy_boto3_sesv2.type_defs import BulkEmailEntryResultTypeDef

from backend.core.data.default_email_templates import email_footer
from backend.decorators import feature_flag_check, web_require_scopes
from backend.decorators import htmx_only
from core.decorators import feature_flag_check, web_require_scopes
from core.decorators import htmx_only
from backend.models import Client
from backend.models import EmailSendStatus
from backend.models import QuotaLimit
from backend.models import QuotaUsage
from backend.core.types.emails import (

# from backend.models import QuotaLimit
# from backend.models import QuotaUsage
from core.types.emails import (
BulkEmailEmailItem,
)
from backend.core.types.requests import WebRequest
from core.types.requests import WebRequest

from settings.helpers import send_email, send_templated_bulk_email, get_var
from backend.core.types.htmx import HtmxHttpRequest
from core.types.htmx import HtmxHttpRequest


@dataclass
Expand Down Expand Up @@ -81,7 +82,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse:

if validated_bulk:
messages.error(request, validated_bulk)
return render(request, "base/toast.html")
return render(request, "core/base/toast.html")

message += email_footer()
message_single_line_html = message.replace("\r\n", "<br>").replace("\n", "<br>")
Expand Down Expand Up @@ -123,7 +124,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse:
}
)
messages.success(request, f"Successfully emailed {len(email_list)} people.")
return render(request, "base/toast.html")
return render(request, "core/base/toast.html")

EMAIL_SENT = send_templated_bulk_email(
email_list=email_list,
Expand All @@ -137,7 +138,7 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse:

if EMAIL_SENT.failed:
messages.error(request, EMAIL_SENT.error)
return render(request, "base/toast.html")
return render(request, "core/base/toast.html")

# todo - fix

Expand Down Expand Up @@ -174,20 +175,20 @@ def _send_bulk_email_view(request: WebRequest) -> HttpResponse:

messages.success(request, f"Successfully emailed {len(email_list)} people.")

try:
quota_limits = QuotaLimit.objects.filter(slug__in=["emails-single-count", "emails-bulk-count"])

QuotaUsage.objects.bulk_create(
[
QuotaUsage(user=request.user, quota_limit=quota_limits.get(slug="emails-single-count"), extra_data=status.id)
for status in SEND_STATUS_OBJECTS
]
+ [QuotaUsage(user=request.user, quota_limit=quota_limits.get(slug="emails-bulk-count"))]
)
except QuotaLimit.DoesNotExist:
...
# try:
# quota_limits = QuotaLimit.objects.filter(slug__in=["emails-single-count", "emails-bulk-count"])
#
# QuotaUsage.objects.bulk_create(
# [
# QuotaUsage(user=request.user, quota_limit=quota_limits.get(slug="emails-single-count"), extra_data=status.id)
# for status in SEND_STATUS_OBJECTS
# ]
# + [QuotaUsage(user=request.user, quota_limit=quota_limits.get(slug="emails-bulk-count"))]
# )
# except QuotaLimit.DoesNotExist:
# ...

return render(request, "base/toast.html")
return render(request, "core/base/toast.html")


def _send_single_email_view(request: WebRequest) -> HttpResponse:
Expand All @@ -204,7 +205,7 @@ def _send_single_email_view(request: WebRequest) -> HttpResponse:

if validated_single:
messages.error(request, validated_single)
return render(request, "base/toast.html")
return render(request, "core/base/toast.html")

message += email_footer()
message_single_line_html = message.replace("\r\n", "<br>").replace("\n", "<br>")
Expand Down Expand Up @@ -246,9 +247,9 @@ def _send_single_email_view(request: WebRequest) -> HttpResponse:

status_object.save()

QuotaUsage.create_str(request.user, "emails-single-count", status_object.id)
# QuotaUsage.create_str(request.user, "emails-single-count", status_object.id)

return render(request, "base/toast.html")
return render(request, "core/base/toast.html")


def validate_bulk_inputs(*, request, emails, clients, message, subject) -> str | None:
Expand Down Expand Up @@ -284,20 +285,20 @@ def validate_bulk_quotas(*, request: HtmxHttpRequest, emails: list) -> str | Non
email_count = len(emails)

slugs = ["emails-bulk-count", "emails-bulk-max_sends"]
quota_limits: QuerySet[QuotaLimit] = QuotaLimit.objects.prefetch_related("quota_overrides", "quota_usage").filter(slug__in=slugs)
# quota_limits: QuerySet[QuotaLimit] = QuotaLimit.objects.prefetch_related("quota_overrides", "quota_usage").filter(slug__in=slugs)

# quota_limits.get().

above_bulk_sends_limit: bool = quota_limits.get(slug="emails-bulk-count").strict_goes_above_limit(request.user)
if above_bulk_sends_limit:
return "You have exceeded the quota limit for bulk email sends per month"
# above_bulk_sends_limit: bool = quota_limits.get(slug="emails-bulk-count").strict_goes_above_limit(request.user)
# if above_bulk_sends_limit:
# return "You have exceeded the quota limit for bulk email sends per month"

max_email_count = quota_limits.get(slug="emails-bulk-max_sends").get_quota_limit(user=request.user)
# max_email_count = quota_limits.get(slug="emails-bulk-max_sends").get_quota_limit(user=request.user)

if email_count > max_email_count:
return "You have exceeded the quota limit for the number of emails allowed per bulk send"
else:
return None
# if email_count > max_email_count:
# return "You have exceeded the quota limit for the number of emails allowed per bulk send"
# else:
# return None


def validate_client_email(email, client) -> str | None:
Expand Down Expand Up @@ -348,27 +349,28 @@ def validate_email_subject(subject: str) -> str | None:
max_count = 64

if len(subject) < min_count:
return "The minimum character count is 16 for a subject"
return f"The minimum character count is {min_count} for a subject"

if len(subject) > max_count:
return "The maximum character count is 64 characters for a subject"

alpha_count = len(re.findall("[a-zA-Z ]", subject))
non_alpha_count = len(subject) - alpha_count
return f"The maximum character count is {max_count} characters for a subject"

if non_alpha_count > 0 and alpha_count / non_alpha_count < 10:
return "The subject should have at least 10 letters per 'symbol'"
# alpha_count = len(re.findall("[a-zA-Z ]", subject))
# non_alpha_count = len(subject) - alpha_count

# if non_alpha_count > 0 and alpha_count / non_alpha_count < 10:
# return "The subject should have at least 10 letters per 'symbol'"
#
return None


def validate_email_content(message: str, request: HtmxHttpRequest) -> str | None:
min_count = 64
max_count = QuotaLimit.objects.get(slug="emails-email_character_count").get_quota_limit(user=request.user)
max_count = 1000
# max_count = QuotaLimit.objects.get(slug="emails-email_character_count").get_quota_limit(user=request.user)

if len(message) < min_count:
return "The minimum character count is 64 for an email"
return f"The minimum character count is {min_count} for an email"

if len(message) > max_count:
return "The maximum character count is 1000 characters for an email"
return f"The maximum character count is {max_count} characters for an email"
return None
Loading
Loading