V1.30 notification

This commit is contained in:
Tibor Bossanyi (Freelancer) 2021-10-01 15:53:20 +02:00
parent 405fefe7b9
commit 696ce5a368
24 changed files with 357 additions and 79 deletions

View File

@ -15,3 +15,4 @@ from .training_plan_day import TrainingPlanDayAdmin
from .controlling import ControllingAdmin from .controlling import ControllingAdmin
from .sport import SportAdmin from .sport import SportAdmin
from .app_text import AppTextAdmin from .app_text import AppTextAdmin
from .notification import NotificationAdmin

View File

@ -0,0 +1,35 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from ..models.notification import Notification
class NotificationAdmin(admin.ModelAdmin):
list_display = ('notification_id', 'internal_name', 'message_title', 'active' )
fields = ('internal_name', 'internal_description', 'message_title', 'message_body', 'image_url', 'get_image_preview', 'schedule_date', 'schedule_hook', 'schedule_sql', 'active' )
list_editable = ('internal_name', 'active')
readonly_fields = ("get_image_preview",)
def get_image_preview(self, obj):
image_url = '/media/' + str(obj.image_url)
if obj.pk:
return format_html('<img src="{url}" title="{url}" width="30%" height="30%"/> ' \
.format(url=image_url))
get_image_preview.short_description = _("Image Preview")
def clone_notification(self, request, queryset):
for notif in queryset:
notif.pk = None
notif.internal_name = notif.internal_name + "_copy"
notif.save()
clone_notification.short_description = "Clone the selected notification"
actions = [clone_notification]
admin.site.register(Notification, NotificationAdmin)
admin.autodiscover()

View File

@ -12,6 +12,9 @@ class TestRouter:
def db_for_write(self, model, **hints): def db_for_write(self, model, **hints):
if model._meta.app_label == 'controlling': if model._meta.app_label == 'controlling':
if model._meta.db_table == 'notification_history':
return 'live'
else:
raise Exception("This table cannot be changed!") raise Exception("This table cannot be changed!")
return 'default' return 'default'

View File

@ -18,3 +18,4 @@ from .training_plan_day import TrainingPlanDay, TrainingPlanDayTranslation
from .controlling import Controlling from .controlling import Controlling
from .sports import Sport, SportTranslation from .sports import Sport, SportTranslation
from .app_text import AppText, AppTextTranslation from .app_text import AppText, AppTextTranslation
from .notification import Notification

View File

@ -0,0 +1,22 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Notification(models.Model):
notification_id = models.AutoField(primary_key=True)
message_title = models.CharField(max_length=50)
message_body = models.TextField(max_length=100, blank=True, null=True)
image_url = models.ImageField(upload_to='images/', help_text='The notification image')
schedule_date = models.DateField(blank=True, null=True)
schedule_hook = models.TextField(max_length=100, blank=True, null=True)
schedule_sql = models.TextField(max_length=1000, blank=True, null=True)
internal_name = models.CharField(max_length=50, blank=True, null=True)
internal_description = models.TextField(max_length=500, blank=True, null=True)
active = models.BooleanField(default=0)
class Meta:
db_table = 'notification'
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
def __str__(self):
return self.internal_name

View File

@ -1,6 +1,6 @@
import os import os
BACKOFFICE_VERSION = "1.29.1" BACKOFFICE_VERSION = "1.30"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -31,6 +31,7 @@ INSTALLED_APPS = [
'adminsortable2', 'adminsortable2',
'inline_actions', 'inline_actions',
'django_cron', 'django_cron',
'firebase-admin'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -159,5 +160,5 @@ LOGGING = {
CRON_CLASSES = [ CRON_CLASSES = [
'controlling.cron.cron.MyCronJob', 'controlling.cron.cron.NotificationJob',
] ]

View File

@ -1,6 +1,7 @@
import os import os
from firebase_admin import initialize_app
BACKOFFICE_VERSION = "1.29.1" BACKOFFICE_VERSION = "1.30"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -32,6 +33,8 @@ INSTALLED_APPS = [
'django_cron', 'django_cron',
] ]
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@ -143,7 +146,7 @@ LOGGING = {
'loggers': { 'loggers': {
'mylogger': { 'mylogger': {
'handlers': ['console'], 'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
'propagate': True, 'propagate': True,
}, },
}, },
@ -156,5 +159,7 @@ LOGGING = {
CRON_CLASSES = [ CRON_CLASSES = [
'controlling.cron.cron.MyCronJob', 'controlling.cron.cron.NotificationJob',
] ]

View File

@ -1,6 +1,6 @@
import os import os
BACKOFFICE_VERSION = "1.29.1" BACKOFFICE_VERSION = "1.30"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -31,6 +31,7 @@ INSTALLED_APPS = [
'adminsortable2', 'adminsortable2',
'inline_actions', 'inline_actions',
'django_cron', 'django_cron',
'firebase-admin',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -174,5 +175,5 @@ CACHES = {
CRON_CLASSES = [ CRON_CLASSES = [
'aitrainer_backoffice.controlling.cron.cron.MyCronJob', 'aitrainer_backoffice.controlling.cron.cron.NotificationJob',
] ]

View File

@ -24,7 +24,7 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path(r'^ckeditor/', include('ckeditor_uploader.urls')), path(r'ckeditor', include('ckeditor_uploader.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -10,10 +10,7 @@ from django.urls import path
from ..models.customer import Customer from ..models.customer import Customer
from ..models.customer import Sport from ..models.customer import Sport
from ..mautic import MauticHelper from ..automation.notification import Notification
from ..cron import cron
from ..push_notification import messaging
class SportFilter(SimpleListFilter, ABC): class SportFilter(SimpleListFilter, ABC):
title = "Sport" title = "Sport"
@ -22,7 +19,7 @@ class SportFilter(SimpleListFilter, ABC):
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
data = [] data = []
for s in Sport.objects.filter(): for s in Sport.objects.filter():
data.append([s.sport_id, s.sport_name]) data.append([s.sport_id, s.name])
return data return data
def queryset(self, request, queryset): def queryset(self, request, queryset):
@ -33,6 +30,8 @@ class SportFilter(SimpleListFilter, ABC):
class CustomerAdmin(admin.ModelAdmin): class CustomerAdmin(admin.ModelAdmin):
notif = Notification()
change_list_template = "controlling/mautic.html" change_list_template = "controlling/mautic.html"
list_display = ('customer_id', 'name','firstname', 'email', 'date_add', 'get_sport') list_display = ('customer_id', 'name','firstname', 'email', 'date_add', 'get_sport')
list_filter = ( list_filter = (
@ -41,10 +40,10 @@ class CustomerAdmin(admin.ModelAdmin):
) )
def get_sport(self, obj): def get_sport(self, obj):
return obj.sport.sport_name return obj.sport.name
get_sport.short_description = 'Sport' get_sport.short_description = 'Sport'
get_sport.admin_order_field = 'sport__sport_name' get_sport.admin_order_field = 'sport__name'
# If you would like to add a default range filter # If you would like to add a default range filter
@ -75,7 +74,7 @@ class CustomerAdmin(admin.ModelAdmin):
return my_urls + urls return my_urls + urls
def set_mautic(self, request): def set_mautic(self, request):
messaging.send_to_token() self.notif.run()
return HttpResponseRedirect("../") return HttpResponseRedirect("../")

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from ..models import ExerciseType from aitrainer_backoffice.models.exercise_type import ExerciseType
class FrequentExerciseTypeAdmin(admin.ModelAdmin): class FrequentExerciseTypeAdmin(admin.ModelAdmin):
@ -11,5 +11,5 @@ class FrequentExerciseTypeAdmin(admin.ModelAdmin):
return qs.filter(active=True) return qs.filter(active=True)
admin.site.register(ExerciseType, FrequentExerciseTypeAdmin) #admin.site.register(FrequentExerciseTypeAdmin)
admin.autodiscover() #admin.autodiscover()

View File

@ -0,0 +1,97 @@
import datetime
from firebase_admin import messaging, initialize_app, exceptions
class FCM:
logo_url = 'https://workouttest.com/wp-content/uploads/2020/10/WT_long_logo.png'
# default constructor
def __init__(self):
# To learn more, visit the docs here:
# https://cloud.google.com/docs/authentication/getting-started>
default_app = initialize_app()
def send_to_multiple_token(self, title, body, registration_token, image_url = None):
try:
notification_image_url = image_url
#if image_url == None:
# notification_image_url = self.logo_url
message = messaging.MulticastMessage(
notification=messaging.Notification(
title=title,
body=body,
),
android=messaging.AndroidConfig(
ttl=datetime.timedelta(seconds=3600),
priority='normal',
notification=messaging.AndroidNotification(
image=notification_image_url,
),
),
apns=messaging.APNSConfig(
payload=messaging.APNSPayload(
aps=messaging.Aps(badge=1),
),
fcm_options= messaging.APNSFCMOptions(
image=notification_image_url,
)
),
tokens= registration_token,
)
response = messaging.send_multicast(message)
# Response is a message ID string.
print('Successfully sent message:', response);
except exceptions.FirebaseError as error:
print('Error sending message:', error);
except ValueError as value_error:
print('Error sending message:', value_error);
def send_to_token(self, title, body, image_url = None, registration_token = None):
if registration_token == None:
return "Registration token is null"
try:
#notification_image_url = image_url
#if image_url == None:
notification_image_url = self.logo_url
registration_token = 'cOqNt8rzo074gbIkBSpCgW:APA91bEBuNi3iVzGKb4JhxqN2j80MoJbNptLHk2qsdeKBQz5grpHtrPPXvDqn5BJVVSaj1nwGPwgN7pi6FIApog_TTP3g1yobgmgpPN6udrYgzILlVPMvdGGFDSDh6gKlczhlTL9NEp0'
print(f'image: {notification_image_url}' )
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body,
),
android=messaging.AndroidConfig(
ttl=datetime.timedelta(seconds=3600),
priority='normal',
notification=messaging.AndroidNotification(
image=notification_image_url,
),
),
apns=messaging.APNSConfig(
payload=messaging.APNSPayload(
aps=messaging.Aps(badge=1),
),
fcm_options= messaging.APNSFCMOptions(
image=notification_image_url,
)
),
token= registration_token,
)
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response);
rc = 'OK'
except exceptions.FirebaseError as error:
print('Error sending message:', error);
rc = error
except ValueError as value_error:
print('Error sending message:', value_error);
rc = value_error
return rc

View File

@ -6,7 +6,7 @@ import datetime
from ..models.customer import Customer from ..models.customer import Customer
class MauticHelper: class Mautic:
def syncTrial(self): def syncTrial(self):
tenDays = datetime.datetime - datetime.timedelta(days=10) tenDays = datetime.datetime - datetime.timedelta(days=10)

View File

@ -11,27 +11,31 @@ import argparse
import json import json
import requests import requests
import datetime import datetime
import os
from google.oauth2 import service_account from google.oauth2 import service_account
from firebase_admin import messaging from firebase_admin import messaging, initialize_app
from aitrainer_backoffice.settings.prod import BASE_DIR
PROJECT_ID = 'aitrainer-af0ec' PROJECT_ID = 'aitrainer-af0ec'
BASE_URL = 'https://fcm.googleapis.com' BASE_URL = 'https://fcm.googleapis.com'
FCM_ENDPOINT = 'v1/projects/' + PROJECT_ID + '/messages:send' FCM_ENDPOINT = 'v1/projects/' + PROJECT_ID + '/messages:send'
FCM_URL = BASE_URL + '/' + FCM_ENDPOINT FCM_URL = BASE_URL + '/' + FCM_ENDPOINT
SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'] SCOPES = ['https://www.googleapis.com/auth/firebase.messaging', 'https://www.googleapis.com/auth/cloud-platform', 'email']
ASSET_ROOT = os.path.join(BASE_DIR, "asset")
SERVER_KEY = 'AAAA18iNwog:APA91bFo_kDhK4Nd_zHAtyPj6D5CR_amV0aenNn9VPMMVz6j8mgyTCSr3J3IDD_U9duBe1AN35oAiWKp6LmDdJ5n2UQU6unfyUUmsFOSaDlEQZtYl6ZeIQ5XW52Fmkc29Xh62GZ4E-Ic'
# [START retrieve_access_token] # [START retrieve_access_token]
def _get_access_token(): def _get_access_token():
"""Retrieve a valid access token that can be used to authorize requests. """Retrieve a valid access token that can be used to authorize requests.
:return: Access token. :return: Access token.
""" """
credentials = credentials = service_account.Credentials.from_service_account_file(
"asset/aitrainer-firebase-adminsdk.json", default_app = initialize_app()
scopes=['email'],
) access_token = default_app.credential.get_access_token()
access_token_info = credentials.get_access_token() return access_token.access_token
return access_token_info.access_token #return access_token_info.access_token
# [END retrieve_access_token] # [END retrieve_access_token]
def _send_fcm_message(fcm_message): def _send_fcm_message(fcm_message):
@ -46,13 +50,16 @@ def _send_fcm_message(fcm_message):
} }
# [END use_access_token] # [END use_access_token]
resp = requests.post(FCM_URL, data=json.dumps(fcm_message), headers=headers) resp = requests.post(FCM_URL, data=json.dumps(fcm_message), headers=headers)
return resp
'''
if resp.status_code == 200: if resp.status_code == 200:
print('Message sent to Firebase for delivery, response:') print('Message sent to Firebase for delivery, response:')
print(resp.text) print(resp.text)
else: else:
print('Unable to send message to Firebase') print('Unable to send message to Firebase')
print(resp.text) print(resp.text)
'''
def _build_common_message(): def _build_common_message():
"""Construct common notifiation message. """Construct common notifiation message.
@ -120,6 +127,63 @@ def send_to_token():
print('Successfully sent message:', response) print('Successfully sent message:', response)
# [END send_to_token] # [END send_to_token]
def send_fcm_to_token():
# [START send_to_token]
# This registration token comes from the client FCM SDKs.
'''
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + _get_access_token(),
}
# See documentation on defining a message payload.
message = messaging.Message(
data = _build_override_message(),
token= registration_token,
)
'''
registration_token = ['fFjCZmrHREpRvxZMIKhNSI:APA91bH7cfctHFHbKxtQ5XGRlL26jgLLzo3a1x4hlPfZYi9WxrauMkdIBmqnIQnyD8Jc3xEs0gAsgNYNMLDEgdrHV3bbH4gvFHYUrYzOHZFr-2aVCsYF9otT8_fmAV380egGf5HiCIYd',
'cOqNt8rzo074gbIkBSpCgW:APA91bEBuNi3iVzGKb4JhxqN2j80MoJbNptLHk2qsdeKBQz5grpHtrPPXvDqn5BJVVSaj1nwGPwgN7pi6FIApog_TTP3g1yobgmgpPN6udrYgzILlVPMvdGGFDSDh6gKlczhlTL9NEp0',
'eUrsvYw9ekx0sr_R8pGTD8:APA91bHJKl1D9gLBz7xi0gILb7ng576DDnCvNba7MHqRaFn9MeeCSVLhEU1yC10b1v0KrZ4pVYgUqxkSv2t-Rh0mXtHR7ABGQENuGDfqjokWPNamXhp99Fuq66o3jnlXxSzRKe_aSCtk']
message = messaging.MulticastMessage(
notification=messaging.Notification(
title='$GOOG up 1.43% on the day',
body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
),
android=messaging.AndroidConfig(
ttl=datetime.timedelta(seconds=3600),
priority='normal',
notification=messaging.AndroidNotification(
icon='stock_ticker_update',
color='#f45342'
),
),
apns=messaging.APNSConfig(
payload=messaging.APNSPayload(
aps=messaging.Aps(badge=42),
),
),
tokens= registration_token,
)
print(f'SEND FCM message {message}')
response = messaging.send_multicast(message)
#data = json.dumps(message)
#print(f'SEND FCM message {FCM_URL} - Header {headers} - body: {data}')
#response = requests.post(FCM_URL, headers = headers, data=data)
print(f'RESPONSE: {response}')
#response = _send_fcm_message(message)
# Response is a message ID string.
#print('Successfully sent message:', response)
# [END send_to_token]
def send_to_topic(): def send_to_topic():
# [START send_to_topic] # [START send_to_topic]
# The topic name can be optionally prefixed with "/topics/". # The topic name can be optionally prefixed with "/topics/".
@ -141,6 +205,7 @@ def send_to_topic():
# [END send_to_topic] # [END send_to_topic]
def send_to_condition(): def send_to_condition():
# [START send_to_condition] # [START send_to_condition]
# Define a condition which will send to devices which are subscribed # Define a condition which will send to devices which are subscribed

View File

@ -0,0 +1,44 @@
import datetime
from .notification_hook import NotificationHook
from ..models import notification as notif
from ..models.notification import NotificationHistory
from .fcm import FCM
class Notification:
fcm = FCM()
def run(self):
notification_queryset = notif.Notification.objects.using('live').raw('SELECT * from notification WHERE active = 1')
for notification in notification_queryset:
if notification.schedule_date != None:
pass
elif notification.schedule_hook != None:
hook = NotificationHook()
try:
hook_function = notification.schedule_hook
hook_sql = notification.schedule_sql
if hook_sql == None:
customers = getattr(hook, hook_function)()
else:
customers = getattr(hook, hook_function)(hook_sql)
for customer in customers:
if customer.firebase_reg_token != None:
print(f'-- Notify Customer {customer.customer_id}')
rc= self.fcm.send_to_token(notification.message_title, notification.message_body, notification.image_url, customer.firebase_reg_token)
self.insert_history(notification=notification, customer=customer, rc=rc)
except Exception as ex:
print(f'Notification Hook {notification.schedule_hook} has no callback function: {ex}')
def insert_history(self, notification, customer, rc):
history = NotificationHistory()
history.pk = None
history.notification = notification
history.customer = customer
history.response = rc
history.notification_date = datetime.datetime.now()
history.save()
print(f'-- Notification History "{history}" has been saved')

View File

@ -0,0 +1,16 @@
from ..models.customer import Customer
import datetime
class NotificationHook:
def __init__(self) -> None:
pass
def NotificationSelectAdmins(self):
print(datetime.datetime.now(), " *** START automation NotificationSelectAdmins ")
qs = Customer.objects.raw('SELECT customer_id, firebase_reg_token from customer WHERE admin = 1')
return qs
def NotificationCommonSQL(self, sql):
print(datetime.datetime.now(), " *** START automation NotificationCommonSQL ")
qs = Customer.objects.raw(sql)
return qs

View File

@ -1,19 +1,27 @@
from ..mautic import MauticHelper from ..automation.notification import Notification
from django_cron import CronJobBase, Schedule from django_cron import CronJobBase, Schedule
import datetime import datetime
class MyCronJob(CronJobBase): class MyCronJob(CronJobBase):
print(datetime.datetime.now(), " *** START sync customers ")
RUN_EVERY_MINS = 60
RUN_EVERY_MINS = 60
schedule = Schedule(run_every_mins=RUN_EVERY_MINS) schedule = Schedule(run_every_mins=RUN_EVERY_MINS)
code = 'aitrainer_backoffice.controlling.cron' # a unique code code = 'aitrainer_backoffice.controlling.cron' # a unique code
print(datetime.datetime.now(), " *** END sync customers ")
def do(self): def do(self):
pass print(datetime.datetime.now(), " *** START sync customers ")
#helper = MauticHelper()
#helper.syncTrial()
class NotificationJob(CronJobBase):
notif = Notification()
RUN_EVERY_MINS = 60
schedule = Schedule(run_every_mins=RUN_EVERY_MINS)
code = 'aitrainer_backoffice.controlling.notification' # a unique code
def do(self):
print(datetime.datetime.now(), " *** START notification ")
self.notif.run()
print(datetime.datetime.now(), " *** END notification ")

View File

@ -1 +0,0 @@
from .helper import MauticHelper

View File

@ -1,4 +1,3 @@
from .customer import Customer from .customer import Customer
from .exercises import Exercises from .exercises import Exercises
from .exercise_type import ExerciseType
from .frequent_customers import FrequentCustomers from .frequent_customers import FrequentCustomers

View File

@ -1,15 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from aitrainer_backoffice.models.sports import Sport
class Sport(models.Model):
sport_id = models.AutoField(primary_key = True)
language_code = models.CharField(max_length=2,default="hu")
sport_name = models.CharField(max_length=100)
class Meta:
db_table = 'sport_translation'
class Customer(models.Model): class Customer(models.Model):
customer_id = models.BigAutoField(primary_key=True) customer_id = models.BigAutoField(primary_key=True)
@ -21,6 +12,7 @@ class Customer(models.Model):
fitness_level = models.CharField(max_length=20) fitness_level = models.CharField(max_length=20)
date_add = models.DateField() date_add = models.DateField()
synced_date = models.DateTimeField(blank=True,null=True) synced_date = models.DateTimeField(blank=True,null=True)
firebase_reg_token = models.CharField(max_length=255, blank=True, null=True)
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False

View File

@ -1,28 +0,0 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from ..models.exercises import Exercises
class ExerciseType(models.Model):
exercise_type_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50)
active = models.BooleanField()
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
@property
def exercise_count(self):
count = Exercises.objects.filter(exercise_type_id=self.exercise_type_id).count()
return count
class Meta:
db_table = 'exercise_type'
verbose_name = _("Frequent Exercise")
verbose_name_plural = _("Frequent Exercises")

View File

@ -1,6 +1,4 @@
from django.db import models from django.db import models
#from ..models.exercise_type import ExerciseType
from ..models.customer import Customer from ..models.customer import Customer

View File

@ -0,0 +1,18 @@
from django.db import models
from aitrainer_backoffice.models.notification import Notification
from ..models import Customer
class NotificationHistory(models.Model):
notification_history_id = models.AutoField(primary_key=True)
notification = models.ForeignKey(Notification, on_delete=models.CASCADE)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
response = models.CharField(max_length=255)
notification_date = models.DateField(blank=True, null=True)
class Meta:
db_table = 'notification_history'
app_label = 'controlling'
def __str__(self):
return f'{self.notification};{self.customer};{self.response}'

View File

@ -0,0 +1,2 @@
Set-ExecutionPolicy Unrestricted -Scope Process
d:/projects/aitrainer/src/aitrainer_backoffice/venv/Scripts/Activate.ps1