Email Notification and Notification Center API
This commit is contained in:
parent
6702c5e590
commit
7013b53e09
13 changed files with 395 additions and 25 deletions
|
@ -1,7 +1,7 @@
|
||||||
from djmoney.contrib.django_rest_framework import MoneyField
|
from djmoney.contrib.django_rest_framework import MoneyField
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.models import Invoice, BillingProject
|
from core.models import Invoice, BillingProject, Notification
|
||||||
from core.component import component
|
from core.component import component
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,3 +63,11 @@ class BillingProjectSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BillingProject
|
model = BillingProject
|
||||||
fields = ['tenant_id', 'email_notification']
|
fields = ['tenant_id', 'email_notification']
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSerializer(serializers.ModelSerializer):
|
||||||
|
project = BillingProjectSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Notification
|
||||||
|
fields = ['id', 'project', 'title', 'short_description', 'content', 'sent_status', 'is_read']
|
||||||
|
|
|
@ -12,6 +12,7 @@ router.register(r'settings', views.DynamicSettingViewSet, basename='settings')
|
||||||
router.register(r'invoice', views.InvoiceViewSet, basename='invoice')
|
router.register(r'invoice', views.InvoiceViewSet, basename='invoice')
|
||||||
router.register(r'admin_overview', views.AdminOverviewViewSet, basename='admin_overview')
|
router.register(r'admin_overview', views.AdminOverviewViewSet, basename='admin_overview')
|
||||||
router.register(r'project_overview', views.ProjectOverviewViewSet, basename='project_overview')
|
router.register(r'project_overview', views.ProjectOverviewViewSet, basename='project_overview')
|
||||||
|
router.register(r'notification', views.NotificationViewSet, basename='notification')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
|
|
56
api/views.py
56
api/views.py
|
@ -8,14 +8,16 @@ from rest_framework import viewsets, serializers
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer
|
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer, NotificationSerializer
|
||||||
from core.component import component, labels
|
from core.component import component, labels
|
||||||
from core.component.component import INVOICE_COMPONENT_MODEL
|
from core.component.component import INVOICE_COMPONENT_MODEL
|
||||||
from core.exception import PriceNotFound
|
from core.exception import PriceNotFound
|
||||||
from core.models import Invoice, BillingProject
|
from core.models import Invoice, BillingProject, Notification
|
||||||
|
from core.notification import send_notification_from_template
|
||||||
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED, \
|
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED, \
|
||||||
INVOICE_TAX
|
INVOICE_TAX, COMPANY_NAME, COMPANY_LOGO, COMPANY_ADDRESS
|
||||||
from core.utils.model_utils import InvoiceComponentMixin
|
from core.utils.model_utils import InvoiceComponentMixin
|
||||||
|
from yuyu import settings
|
||||||
|
|
||||||
|
|
||||||
def get_generic_model_view_set(model):
|
def get_generic_model_view_set(model):
|
||||||
|
@ -175,6 +177,19 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
if invoice.state == Invoice.InvoiceState.UNPAID:
|
if invoice.state == Invoice.InvoiceState.UNPAID:
|
||||||
invoice.finish()
|
invoice.finish()
|
||||||
|
|
||||||
|
send_notification_from_template(
|
||||||
|
project=invoice.project,
|
||||||
|
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} has been Paid',
|
||||||
|
short_description=f'Invoice is paid with total of {invoice.total}',
|
||||||
|
template='invoice.html',
|
||||||
|
context={
|
||||||
|
'invoice': invoice,
|
||||||
|
'company_name': get_dynamic_setting(COMPANY_NAME),
|
||||||
|
'logo': get_dynamic_setting(COMPANY_LOGO),
|
||||||
|
'address': get_dynamic_setting(COMPANY_ADDRESS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
serializer = InvoiceSerializer(invoice)
|
serializer = InvoiceSerializer(invoice)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@ -184,6 +199,19 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
||||||
if invoice.state == Invoice.InvoiceState.FINISHED:
|
if invoice.state == Invoice.InvoiceState.FINISHED:
|
||||||
invoice.rollback_to_unpaid()
|
invoice.rollback_to_unpaid()
|
||||||
|
|
||||||
|
send_notification_from_template(
|
||||||
|
project=invoice.project,
|
||||||
|
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} in Unpaid',
|
||||||
|
short_description=f'Invoice is Unpaid with total of {invoice.total}',
|
||||||
|
template='invoice.html',
|
||||||
|
context={
|
||||||
|
'invoice': invoice,
|
||||||
|
'company_name': get_dynamic_setting(COMPANY_NAME),
|
||||||
|
'logo': get_dynamic_setting(COMPANY_LOGO),
|
||||||
|
'address': get_dynamic_setting(COMPANY_ADDRESS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
serializer = InvoiceSerializer(invoice)
|
serializer = InvoiceSerializer(invoice)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@ -339,3 +367,25 @@ class ProjectOverviewViewSet(viewsets.ViewSet):
|
||||||
data['data'].append(sum_of_price)
|
data['data'].append(sum_of_price)
|
||||||
|
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
tenant_id = self.request.query_params.get('tenant_id', None)
|
||||||
|
if tenant_id is None:
|
||||||
|
return Notification.objects.order_by('-created_at')
|
||||||
|
if tenant_id == 0:
|
||||||
|
return Notification.objects.filter(project=None).order_by('-created_at')
|
||||||
|
|
||||||
|
return Notification.objects.filter(project__tenant_id=tenant_id).order_by('-created_at')
|
||||||
|
|
||||||
|
@action(detail=True, methods=['GET'])
|
||||||
|
def resend(self, request, pk):
|
||||||
|
notification = Notification.objects.filter(id=pk).first()
|
||||||
|
notification.send()
|
||||||
|
|
||||||
|
serializer = NotificationSerializer(notification)
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
|
|
||||||
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, BillingProject, Invoice, InvoiceVolume, \
|
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, BillingProject, Invoice, InvoiceVolume, \
|
||||||
InvoiceFloatingIp, InvoiceInstance, DynamicSetting, InvoiceImage, ImagePrice, SnapshotPrice, RouterPrice, \
|
InvoiceFloatingIp, InvoiceInstance, DynamicSetting, InvoiceImage, ImagePrice, SnapshotPrice, RouterPrice, \
|
||||||
InvoiceSnapshot, InvoiceRouter
|
InvoiceSnapshot, InvoiceRouter, Notification
|
||||||
|
|
||||||
|
|
||||||
@admin.register(DynamicSetting)
|
@admin.register(DynamicSetting)
|
||||||
|
@ -34,6 +34,7 @@ class RouterPriceAdmin(admin.ModelAdmin):
|
||||||
class SnapshotPriceAdmin(admin.ModelAdmin):
|
class SnapshotPriceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('hourly_price', 'monthly_price')
|
list_display = ('hourly_price', 'monthly_price')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ImagePrice)
|
@admin.register(ImagePrice)
|
||||||
class ImagePriceAdmin(admin.ModelAdmin):
|
class ImagePriceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('hourly_price', 'monthly_price')
|
list_display = ('hourly_price', 'monthly_price')
|
||||||
|
@ -73,7 +74,12 @@ class InvoiceRouterAdmin(admin.ModelAdmin):
|
||||||
class InvoiceSnapshotAdmin(admin.ModelAdmin):
|
class InvoiceSnapshotAdmin(admin.ModelAdmin):
|
||||||
list_display = ('snapshot_id', 'name', 'space_allocation_gb')
|
list_display = ('snapshot_id', 'name', 'space_allocation_gb')
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InvoiceImage)
|
@admin.register(InvoiceImage)
|
||||||
class InvoiceImageAdmin(admin.ModelAdmin):
|
class InvoiceImageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('image_id', 'name', 'space_allocation_gb')
|
list_display = ('image_id', 'name', 'space_allocation_gb')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Notification)
|
||||||
|
class InvoiceImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('project', 'title', 'short_description', 'sent_status')
|
||||||
|
|
|
@ -6,6 +6,7 @@ from djmoney.money import Money
|
||||||
|
|
||||||
from core.exception import PriceNotFound
|
from core.exception import PriceNotFound
|
||||||
from core.models import InvoiceComponentMixin, PriceMixin
|
from core.models import InvoiceComponentMixin, PriceMixin
|
||||||
|
from core.notification import send_notification
|
||||||
|
|
||||||
|
|
||||||
class InvoiceHandler(metaclass=abc.ABCMeta):
|
class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
|
@ -25,7 +26,14 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
price = self.get_price(payload)
|
price = self.get_price(payload)
|
||||||
if price is None:
|
if price is None:
|
||||||
raise PriceNotFound()
|
raise PriceNotFound()
|
||||||
except PriceNotFound:
|
except PriceNotFound as e:
|
||||||
|
send_notification(
|
||||||
|
project=None,
|
||||||
|
title=f'{settings.EMAIL_TAG} [Error] Price not found when create invoice',
|
||||||
|
short_description=f'Price not found for {e.identifier} with payload {payload}',
|
||||||
|
content=f'Price not found or {e.identifier} with payload {payload}. Will use fallback price as 0.',
|
||||||
|
)
|
||||||
|
|
||||||
if fallback_price:
|
if fallback_price:
|
||||||
price = PriceMixin()
|
price = PriceMixin()
|
||||||
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
|
@ -74,15 +82,25 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||||
price = self.get_price(self.get_price_dependency_from_instance(instance))
|
price = self.get_price(self.get_price_dependency_from_instance(instance))
|
||||||
if price is None:
|
if price is None:
|
||||||
raise PriceNotFound()
|
raise PriceNotFound()
|
||||||
except PriceNotFound:
|
|
||||||
|
hourly_price = price.hourly_price
|
||||||
|
monthly_price = price.monthly_price
|
||||||
|
except PriceNotFound as e:
|
||||||
|
send_notification(
|
||||||
|
project=None,
|
||||||
|
title=f'{settings.EMAIL_TAG} [Error] Price not found when rolling invoice',
|
||||||
|
short_description=f'Please check your Price configuration. Error on ID {getattr(instance, self.KEY_FIELD)}',
|
||||||
|
content=f'Please check your Price configuration fro {e.identifier}. Error on ID {getattr(instance, self.KEY_FIELD)} for '
|
||||||
|
f'invoice {getattr(instance, "invoice").id}',
|
||||||
|
)
|
||||||
|
|
||||||
if fallback_price:
|
if fallback_price:
|
||||||
price = PriceMixin()
|
hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
||||||
price.monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
|
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
instance.hourly_price = price.hourly_price
|
instance.hourly_price = hourly_price
|
||||||
instance.monthly_price = price.monthly_price
|
instance.monthly_price = monthly_price
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
from oslo_messaging import NotificationResult
|
from oslo_messaging import NotificationResult
|
||||||
|
|
||||||
from core.component import component
|
from core.component import component
|
||||||
|
from core.notification import send_notification
|
||||||
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
|
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
|
||||||
|
from yuyu import settings
|
||||||
|
|
||||||
LOG = logging.getLogger("yuyu_notification")
|
LOG = logging.getLogger("yuyu_notification")
|
||||||
|
|
||||||
|
@ -23,7 +26,14 @@ class EventEndpoint(object):
|
||||||
return NotificationResult.HANDLED
|
return NotificationResult.HANDLED
|
||||||
|
|
||||||
# TODO: Error Handling
|
# TODO: Error Handling
|
||||||
for handler in self.event_handler:
|
try:
|
||||||
handler.handle(event_type, payload)
|
for handler in self.event_handler:
|
||||||
|
handler.handle(event_type, payload)
|
||||||
|
except Exception:
|
||||||
|
send_notification(
|
||||||
|
project=None,
|
||||||
|
title=f'{settings.EMAIL_TAG} [Error] Error when handling OpenStack Notification',
|
||||||
|
short_description=f'There is an error when handling OpenStack Notification',
|
||||||
|
content=f'There is an error when handling OpenStack Notification \n {traceback.format_exc()}',
|
||||||
|
)
|
||||||
return NotificationResult.HANDLED
|
return NotificationResult.HANDLED
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from typing import Mapping, Dict, Iterable
|
from typing import Mapping, Dict, Iterable
|
||||||
|
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
@ -6,7 +7,10 @@ from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Invoice, InvoiceComponentMixin
|
from core.models import Invoice, InvoiceComponentMixin
|
||||||
from core.component import component, labels
|
from core.component import component, labels
|
||||||
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX
|
from core.notification import send_notification_from_template, send_notification
|
||||||
|
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX, COMPANY_LOGO, COMPANY_NAME, \
|
||||||
|
COMPANY_ADDRESS
|
||||||
|
from yuyu import settings
|
||||||
|
|
||||||
LOG = logging.getLogger("yuyu_new_invoice")
|
LOG = logging.getLogger("yuyu_new_invoice")
|
||||||
|
|
||||||
|
@ -16,15 +20,23 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
print("Processing Invoice")
|
print("Processing Invoice")
|
||||||
if not get_dynamic_setting(BILLING_ENABLED):
|
try:
|
||||||
return
|
if not get_dynamic_setting(BILLING_ENABLED):
|
||||||
|
return
|
||||||
|
|
||||||
self.close_date = timezone.now()
|
self.close_date = timezone.now()
|
||||||
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
||||||
|
|
||||||
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
||||||
for active_invoice in active_invoices:
|
for active_invoice in active_invoices:
|
||||||
self.close_active_invoice(active_invoice)
|
self.close_active_invoice(active_invoice)
|
||||||
|
except Exception:
|
||||||
|
send_notification(
|
||||||
|
project=None,
|
||||||
|
title='[Error] Error when processing Invoice',
|
||||||
|
short_description=f'There is an error when Processing Invoice',
|
||||||
|
content=f'There is an error when handling Processing Invoice \n {traceback.format_exc()}',
|
||||||
|
)
|
||||||
print("Processing Done")
|
print("Processing Done")
|
||||||
|
|
||||||
def close_active_invoice(self, active_invoice: Invoice):
|
def close_active_invoice(self, active_invoice: Invoice):
|
||||||
|
@ -55,3 +67,16 @@ class Command(BaseCommand):
|
||||||
handler.roll(active_component, self.close_date, update_payload={
|
handler.roll(active_component, self.close_date, update_payload={
|
||||||
"invoice": new_invoice
|
"invoice": new_invoice
|
||||||
}, fallback_price=True)
|
}, fallback_price=True)
|
||||||
|
|
||||||
|
send_notification_from_template(
|
||||||
|
project=active_invoice.project,
|
||||||
|
title=settings.EMAIL_TAG + f' Your Invoice #{active_invoice.id} is Ready',
|
||||||
|
short_description=f'Invoice is ready with total of {active_invoice.total}',
|
||||||
|
template='invoice.html',
|
||||||
|
context={
|
||||||
|
'invoice': active_invoice,
|
||||||
|
'company_name': get_dynamic_setting(COMPANY_NAME),
|
||||||
|
'logo': get_dynamic_setting(COMPANY_LOGO),
|
||||||
|
'address': get_dynamic_setting(COMPANY_ADDRESS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
31
core/migrations/0009_notification.py
Normal file
31
core/migrations/0009_notification.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 3.2.6 on 2022-07-08 04:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0008_auto_20220621_2253'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notification',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('title', models.CharField(max_length=256)),
|
||||||
|
('short_description', models.CharField(max_length=524)),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('sent_status', models.BooleanField()),
|
||||||
|
('is_read', models.BooleanField()),
|
||||||
|
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.billingproject')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,12 +1,18 @@
|
||||||
|
import re
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.html import strip_tags
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
|
|
||||||
from core.component import labels
|
from core.component import labels
|
||||||
|
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
|
||||||
|
LABEL_VOLUMES
|
||||||
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,6 +109,17 @@ class Invoice(BaseModel, TimestampMixin):
|
||||||
|
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_str(self):
|
||||||
|
if self.state == Invoice.InvoiceState.IN_PROGRESS:
|
||||||
|
return 'In Progress'
|
||||||
|
|
||||||
|
if self.state == Invoice.InvoiceState.UNPAID:
|
||||||
|
return 'Unpaid'
|
||||||
|
|
||||||
|
if self.state == Invoice.InvoiceState.FINISHED:
|
||||||
|
return 'Finished'
|
||||||
|
|
||||||
def close(self, date, tax_percentage):
|
def close(self, date, tax_percentage):
|
||||||
self.state = Invoice.InvoiceState.UNPAID
|
self.state = Invoice.InvoiceState.UNPAID
|
||||||
self.end_date = date
|
self.end_date = date
|
||||||
|
@ -120,6 +137,37 @@ class Invoice(BaseModel, TimestampMixin):
|
||||||
self.finish_date = None
|
self.finish_date = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# Price for individual key
|
||||||
|
@property
|
||||||
|
def instance_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_INSTANCES).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_VOLUMES).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fip_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_FLOATING_IPS).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def router_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_ROUTERS).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def snapshot_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_SNAPSHOTS).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def images_price(self):
|
||||||
|
relation_all_row = getattr(self, LABEL_IMAGES).all()
|
||||||
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
||||||
|
|
||||||
|
|
||||||
# end region
|
# end region
|
||||||
|
|
||||||
|
@ -204,4 +252,38 @@ class InvoiceImage(BaseModel, InvoiceComponentMixin):
|
||||||
def price_charged(self):
|
def price_charged(self):
|
||||||
price_without_allocation = super().price_charged
|
price_without_allocation = super().price_charged
|
||||||
return price_without_allocation * math.ceil(self.space_allocation_gb)
|
return price_without_allocation * math.ceil(self.space_allocation_gb)
|
||||||
|
|
||||||
|
|
||||||
# end region
|
# end region
|
||||||
|
|
||||||
|
class Notification(BaseModel, TimestampMixin):
|
||||||
|
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE, blank=True, null=True)
|
||||||
|
title = models.CharField(max_length=256)
|
||||||
|
short_description = models.CharField(max_length=524)
|
||||||
|
content = models.TextField()
|
||||||
|
sent_status = models.BooleanField()
|
||||||
|
is_read = models.BooleanField()
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
from core.utils.dynamic_setting import get_dynamic_setting, EMAIL_ADMIN
|
||||||
|
|
||||||
|
def textify(html):
|
||||||
|
# Remove html tags and continuous whitespaces
|
||||||
|
text_only = re.sub('[ \t]+', ' ', strip_tags(html))
|
||||||
|
# Strip single spaces in the beginning of each line
|
||||||
|
return text_only.replace('\n ', '\n').strip()
|
||||||
|
|
||||||
|
recipient = [get_dynamic_setting(EMAIL_ADMIN)]
|
||||||
|
if self.project is not None:
|
||||||
|
recipient.append(self.project.email_notification)
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject=self.title,
|
||||||
|
message=textify(self.content),
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipient_list=recipient,
|
||||||
|
html_message=self.content,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sent_status = True
|
||||||
|
self.save()
|
||||||
|
|
24
core/notification.py
Normal file
24
core/notification.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
from core.models import BillingProject, Notification
|
||||||
|
|
||||||
|
|
||||||
|
def send_notification(project, title: str, short_description: str, content: str):
|
||||||
|
notification = Notification(
|
||||||
|
project=project,
|
||||||
|
title=title,
|
||||||
|
short_description=short_description,
|
||||||
|
content=content,
|
||||||
|
sent_status=False,
|
||||||
|
is_read=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
notification.save()
|
||||||
|
notification.send()
|
||||||
|
|
||||||
|
|
||||||
|
def send_notification_from_template(project: BillingProject, title: str, short_description: str, template: str,
|
||||||
|
context):
|
||||||
|
msg_html = render_to_string(template, context=context)
|
||||||
|
|
||||||
|
send_notification(project=project, title=title, short_description=short_description, content=msg_html)
|
105
templates/invoice.html
Normal file
105
templates/invoice.html
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-family: sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;font-size: 10px;-webkit-tap-highlight-color: rgba(0,0,0,0);">
|
||||||
|
<head style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
</head>
|
||||||
|
<body style="margin-top: 20px;background: #eee;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin: 0;font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;font-size: 14px;line-height: 1.42857143;color: #333;background-color: #fff;">
|
||||||
|
<div class="container" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding-right: 15px;padding-left: 15px;margin-right: auto;margin-left: auto;">
|
||||||
|
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
|
||||||
|
<!-- BEGIN INVOICE -->
|
||||||
|
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
|
||||||
|
<div class="grid invoice" style="padding: 30px;position: relative;width: 100%;background: #fff;color: #666666;border-radius: 2px;margin-bottom: 25px;box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1);-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<div class="grid-body" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<div class="invoice-title" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
|
||||||
|
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
|
||||||
|
<img src="{{ logo }}" alt="" height="50" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;border: 0;vertical-align: middle;page-break-inside: avoid;max-width: 100%!important;">
|
||||||
|
<!-- <img src="https://www.seekpng.com/png/detail/246-2468199_logo-placeholder-png-white.png" alt="" height="35"> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
|
||||||
|
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
|
||||||
|
<h2 style="margin-top: 0px;line-height: 0.8em;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;orphans: 3;widows: 3;page-break-after: avoid;font-family: inherit;font-weight: 500;color: inherit;margin-bottom: 10px;font-size: 30px;">invoice<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<span class="small" style="font-weight: 300;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-size: 65%;line-height: 1;color: #777;">order #{{ invoice.id }}</span></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr style="margin-top: 10px;border-color: #ddd;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;height: 0;margin-bottom: 20px;border: 0;border-top: 1px solid #eee;">
|
||||||
|
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
|
||||||
|
<div class="col-xs-6" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 50%;">
|
||||||
|
<address style="max-width: 30%;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-bottom: 20px;font-style: normal;line-height: 1.42857143;">
|
||||||
|
{{ address }}
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6 text-right" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;text-align: right;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 50%;">
|
||||||
|
<address style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-bottom: 20px;font-style: normal;line-height: 1.42857143;">
|
||||||
|
<strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Invoice Month:</strong><br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
{{ invoice.start_date | date:"M Y" }}
|
||||||
|
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Invoice State:</strong><br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
{{ invoice.state_str }}
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
|
||||||
|
<div class="col-md-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;width: 100%;">
|
||||||
|
<h3 style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;orphans: 3;widows: 3;page-break-after: avoid;font-family: inherit;font-weight: 500;line-height: 1.1;color: inherit;margin-top: 20px;margin-bottom: 10px;font-size: 24px;">ORDER SUMMARY</h3>
|
||||||
|
<table class="table table-striped" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;border-spacing: 0;border-collapse: collapse!important;background-color: transparent;width: 100%;max-width: 100%;margin-bottom: 20px;">
|
||||||
|
<thead style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;display: table-header-group;">
|
||||||
|
<tr class="line" style="border-bottom: 1px solid #ccc;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">#</strong></td>
|
||||||
|
<td width="70%" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">COMPONENT</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">TOTAL COST</strong></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">1</td>
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Instance</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.instance_price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">2</td>
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Volume</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.volume_price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">3</td>
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Floating IP</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.fip_price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">4</td>
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Router</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.router_price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="line" style="border-bottom: 1px solid #ccc;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">5</td>
|
||||||
|
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Snapshot</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.snapshot_price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Subtotal</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.subtotal }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Tax</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.tax }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
|
||||||
|
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Total</strong></td>
|
||||||
|
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.total }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- END INVOICE -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -5,3 +5,14 @@ YUYU_NOTIFICATION_TOPICS = ["notifications"]
|
||||||
CURRENCIES = ('IDR', 'USD')
|
CURRENCIES = ('IDR', 'USD')
|
||||||
DEFAULT_CURRENCY = "IDR"
|
DEFAULT_CURRENCY = "IDR"
|
||||||
|
|
||||||
|
# Email Setting
|
||||||
|
|
||||||
|
EMAIL_TAG = '[YUYU]'
|
||||||
|
DEFAULT_FROM_EMAIL = 'no-reply@btech.id'
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
EMAIL_HOST = 'smtp.gmail.com'
|
||||||
|
EMAIL_HOST_USER = '<paste your gmail account here>'
|
||||||
|
EMAIL_HOST_PASSWORD = '<paste Google password or app password here>'
|
||||||
|
EMAIL_PORT = 587
|
||||||
|
EMAIL_USE_TLS = True
|
|
@ -58,8 +58,7 @@ ROOT_URLCONF = 'yuyu.urls'
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [BASE_DIR / 'templates']
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
,
|
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
|
Loading…
Add table
Reference in a new issue