Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions backend/apps/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
from backend.apps.account_payment.graphql import Mutation as PaymentMutation
from backend.apps.account_payment.graphql import Query as PaymentQuery
from backend.apps.api.v1.graphql import Query as APIQuery
from backend.apps.user_notifications.graphql import (
DeactivateAllTableUpdateNotification,
DeactivateTableUpdateNotification,
TableUpdateNotification,
)
from backend.apps.user_notifications.graphql import Query as UserNotificationQuery
from backend.custom.graphql_auto import build_schema

schema = build_schema(
applications=["account", "v1"],
extra_queries=[
APIQuery,
PaymentQuery,
],
extra_queries=[APIQuery, PaymentQuery, UserNotificationQuery],
extra_mutations=[
AccountMutation,
PaymentMutation,
TableUpdateNotification,
DeactivateTableUpdateNotification,
DeactivateAllTableUpdateNotification,
],
)
Empty file.
53 changes: 53 additions & 0 deletions backend/apps/user_notifications/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Register your models here.
from django.contrib import admin

from .models import TableUpdateSubscription


class TableUpdateSubscriptionAdmin(admin.ModelAdmin):
# Campos que serão exibidos na lista de objetos
list_display = (
"id",
"table",
"user",
"created_at",
"deactivate_at",
"last_notification",
"updated_at",
"status",
)

# Filtros laterais para facilitar a busca
list_filter = ("status", "table", "user")

# Campos que podem ser pesquisados diretamente
search_fields = ("table__name", "user__username")

# Campos que serão editáveis diretamente na lista (inline)
list_editable = ("status",)

# Exibir um campo de data mais legível na interface
date_hierarchy = "created_at"

# Definindo o formulário de exibição de detalhes
fieldsets = (
(None, {"fields": ("table", "user", "status")}),
(
"Datas",
{
"fields": ("created_at", "deactivate_at", "last_notification", "updated_at"),
"classes": ("collapse",),
},
),
)

# Campos que não serão mostrados no formulário de edição
readonly_fields = ("created_at", "deactivate_at")

# Exibir os campos na ordem desejada no formulário de edição
ordering = ("-created_at",)


# Registrar o modelo e a classe de admin personalizada
admin.site.register(TableUpdateSubscription, TableUpdateSubscriptionAdmin)
7 changes: 7 additions & 0 deletions backend/apps/user_notifications/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig


class UserNotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "backend.apps.user_notifications"
182 changes: 182 additions & 0 deletions backend/apps/user_notifications/graphql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
# ruff: noqa

from uuid import UUID

import graphene
from django.utils import timezone
from graphene import Int, Mutation, String
from graphene_django import DjangoObjectType
from graphql_jwt.decorators import login_required

from backend.apps.account.models import Account
from backend.apps.api.v1.models import Table

from .models import TableUpdateSubscription


class TableUpdateSubscriptionType(DjangoObjectType):
class Meta:
model = TableUpdateSubscription
fields = "__all__"


### Mutations


class CreateTableUpdateSubscription(Mutation):
class Arguments:
table_id = String()
user_id = Int()

table_update_subscription = graphene.Field(TableUpdateSubscriptionType)

@login_required
def mutate(self, info, table_id: str, user_id: int):
# Verifica se já existe uma assinatura ativa para o usuário e a tabela
try:
table = Table.objects.get(id=UUID(table_id))
user = Account.objects.get(id=user_id)

existing_subscription = TableUpdateSubscription.objects.filter(
user_id=user,
table_id=table,
status=True, # Aqui você verifica apenas as assinaturas ativas
).first()

if existing_subscription:
# Se já existir, você pode retornar a assinatura existente ou lançar um erro
raise Exception("Já existe uma assinatura ativa para esse usuário e tabela.")

# Caso contrário, cria uma nova assinatura

subscription = TableUpdateSubscription.objects.create(
table=table,
user=user,
updated_at=table.last_updated_at,
status=True, # A assinatura será criada como ativa
)

return CreateTableUpdateSubscription(table_update_subscription=subscription)

except Table.DoesNotExist:
return Exception("Tabela não encontrada.")
except Account.DoesNotExist:
return Exception("Usuário não encontrado.")


class TableUpdateNotification:
_table_upadate_notification = CreateTableUpdateSubscription.Field()


class DeactivateTableUpdateSubscription(Mutation):
class Arguments:
table_id = String()
user_id = Int()

table_update_subscription = graphene.Field(TableUpdateSubscriptionType)

@login_required
def mutate(self, info, table_id, user_id):
# Verifica se já existe uma assinatura ativa para o usuário e a tabela
try:
table = Table.objects.get(id=UUID(table_id))
user = Account.objects.get(id=user_id)

subscription = TableUpdateSubscription.objects.filter(
table=table,
user=user,
status=True, # Apenas as assinaturas ativas
).first()

if not subscription:
# Se já existir, você pode retornar a assinatura existente ou lançar um erro
raise Exception(
f"Não existe uma assinatura ativa para a tabela {table.name} e o usuário {user.username}."
)

# Atualizando o status para False e registrando a data de desativação
subscription.status = False
subscription.deactivate_at = timezone.now() # Atualizando com a data e hora atual
subscription.save()

return CreateTableUpdateSubscription(table_update_subscription=subscription)

except Table.DoesNotExist:
return Exception("Tabela não encontrada.")
except Account.DoesNotExist:
return Exception("Usuário não encontrado.")


class DeactivateTableUpdateNotification:
_deactivate_table_upadate_notification = DeactivateTableUpdateSubscription.Field()


class DeactivateAllTableUpdateSubscription(Mutation):
class Arguments:
user_id = Int()

success = graphene.Boolean()
deactivated_count = graphene.Int()

@login_required
def mutate(self, info, user_id):
# Verifica se já existe uma assinatura ativa para o usuário e a tabela
try:
user = Account.objects.get(id=user_id)

subscriptions_qs = TableUpdateSubscription.objects.filter(
user=user,
status=True, # Apenas as assinaturas ativas
).all()

if not subscriptions_qs:
# Se já existir, você pode retornar a assinatura existente ou lançar um erro
raise Exception(f"Não existe uma assinatura ativa para o usuário {user.username}.")

deactivated_count = subscriptions_qs.update(status=False, deactivate_at=timezone.now())

return DeactivateAllTableUpdateSubscription(
success=True, deactivated_count=deactivated_count
)

except Account.DoesNotExist:
return Exception("Usuário não encontrado.")


class DeactivateAllTableUpdateNotification:
_deactivate_all_table_upadate_notification = DeactivateAllTableUpdateSubscription.Field()


### Querys


class StatusTableUpadateNotificationQueryType(DjangoObjectType):
class Meta:
model = TableUpdateSubscription
fields = ("status",)


class StatusTableUpadateNotificationQuery(graphene.ObjectType):
# Definindo a query para obter o status de uma inscrição
status_table_update_notification = graphene.Field(
StatusTableUpadateNotificationQueryType, table_id=String(), user_id=Int()
)

@login_required
def resolve_status_table_update_notification(
self, info, table_id: str, user_id: int
) -> TableUpdateSubscription | None:
try:
subscription = TableUpdateSubscription.objects.filter(
table=table_id,
user=user_id,
status=True,
).exists()
return TableUpdateSubscription(status=subscription)
except TableUpdateSubscription.DoesNotExist:
return None


class Query(StatusTableUpadateNotificationQuery, graphene.ObjectType):
pass
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-

from collections import defaultdict

from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils import timezone as dj_timezone

from backend.apps.user_notifications.models import Account, TableUpdateSubscription
from backend.custom.environment import get_frontend_url


def check_for_updates(subscription: TableUpdateSubscription) -> TableUpdateSubscription | bool:
table = subscription.table

if subscription.updated_at < table.last_updated_at:
return subscription
return False


def send_update_notification_email(user: Account, subscriptions: list, date_today: dj_timezone):
from_email = settings.EMAIL_HOST_USER
recipient_list = [user.email]

subject = "Atualização disponível para sua tabela de interesse"
message = (
f"Olá {user.username}, \n\nHá atualizações disponíveis para uma das tabelas que você segue."
)

content = render_to_string(
"notification/update_table_notification.html",
{"domain": get_frontend_url(), "subscriptions": subscriptions},
)

msg = EmailMultiAlternatives(subject, message, from_email, recipient_list)
msg.attach_alternative(content, "text/html")
msg.send()

for subscription in subscriptions:
subscription.last_notification = date_today
subscription.updated_at = subscription.table.last_updated_at
subscription.save()


class Command(BaseCommand):
help = "Botão para testar o envio de emails"

def handle(self, *args, **options):
# Pegar
self.check_for_updates_and_send_emails()

self.stdout.write(self.style.SUCCESS('Successfully "check_for_updates_and_send_emails"'))

def check_for_updates_and_send_emails(self):
# Pega todas as inscrições ativas
subscriptions = TableUpdateSubscription.objects.filter(status=True)
# Pega a data atual
date_today = dj_timezone.now()

users_to_notify = defaultdict(list)
# Lista de usuários que precisam receber o email e agrupada eles

for subscription in subscriptions:
if check_for_updates(subscription):
users_to_notify[subscription.user].append(subscription)

self.stdout.write(
self.style.SUCCESS(f"Serão enviados um total de {len(users_to_notify.keys())} emails")
)

# Envia e-mail para cada usuário que precisa ser notificado
for user, subscriptions_for_user in users_to_notify.items():
send_update_notification_email(user, subscriptions_for_user, date_today)
46 changes: 46 additions & 0 deletions backend/apps/user_notifications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# ruff: noqa
# Generated by Django 4.2.10 on 2026-01-14 18:13

import uuid

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("v1", "0055_alter_type_fields_many_tables"),
]

operations = [
migrations.CreateModel(
name="TableUpdateSubscription",
fields=[
("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("deactivate_at", models.DateTimeField(blank=True, null=True)),
("last_notification", models.DateTimeField(blank=True, null=True)),
("updated_at", models.DateTimeField(blank=True, null=True)),
("status", models.BooleanField(default=True)),
(
"table",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="v1.table"),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "Table Update Subscription",
"verbose_name_plural": "Table Update Subscriptions",
},
),
]
Empty file.
Loading
Loading