add email setting, closes #5. other misc small fixes

This commit is contained in:
badblocks 2025-04-13 21:14:20 -07:00
parent 135bd95a6a
commit 86b061c971
11 changed files with 85 additions and 99 deletions

View file

@ -87,4 +87,4 @@ class CustomUserCreationForm(SignupForm):
class UserSettingsForm(forms.ModelForm): class UserSettingsForm(forms.ModelForm):
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ['show_friend_code_on_link_previews'] fields = ['show_friend_code_on_link_previews', 'enable_email_notifications']

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-13 05:10 # Generated by Django 5.1.2 on 2025-04-14 04:07
import accounts.models import accounts.models
import django.contrib.auth.models import django.contrib.auth.models
@ -33,6 +33,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('show_friend_code_on_link_previews', models.BooleanField(default=False, help_text='This will primarily affect share link previews on X, Discord, etc.', verbose_name='Show Friend Code on Link Previews')), ('show_friend_code_on_link_previews', models.BooleanField(default=False, help_text='This will primarily affect share link previews on X, Discord, etc.', verbose_name='Show Friend Code on Link Previews')),
('enable_email_notifications', models.BooleanField(default=True, help_text='Receive new trade notifications via email.', verbose_name='Enable Email Notifications')),
('reputation_score', models.IntegerField(default=0)), ('reputation_score', models.IntegerField(default=0)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),

View file

@ -17,6 +17,11 @@ class CustomUser(AbstractUser):
verbose_name="Show Friend Code on Link Previews", verbose_name="Show Friend Code on Link Previews",
help_text="This will primarily affect share link previews on X, Discord, etc." help_text="This will primarily affect share link previews on X, Discord, etc."
) )
enable_email_notifications = models.BooleanField(
default=True,
verbose_name="Enable Email Notifications",
help_text="Receive new trade notifications via email."
)
reputation_score = models.IntegerField(default=0) reputation_score = models.IntegerField(default=0)
def __str__(self): def __str__(self):

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-13 05:10 # Generated by Django 5.1.2 on 2025-04-14 04:07
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -46,6 +46,10 @@ module.exports = {
'alert-success', 'alert-success',
'alert-warning', 'alert-warning',
'alert-error', 'alert-error',
'btn-info',
'btn-success',
'btn-warning',
'btn-error',
'bg-info', 'bg-info',
'bg-success', 'bg-success',
'bg-warning', 'bg-warning',

View file

@ -1,4 +1,4 @@
<a href="{{ url }}"> <a href="{{ url }}" @click.stop>
<div class="relative block"> <div class="relative block">
{% if not expanded %} {% if not expanded %}
<div class="flex flex-row items-center h-[32px] p-1.5 w-40 text-white shadow-lg" style="{{ style }}"> <div class="flex flex-row items-center h-[32px] p-1.5 w-40 text-white shadow-lg" style="{{ style }}">

View file

@ -24,7 +24,6 @@
<script defer> <script defer>
if (!window.updateGlobalCardFilters) { if (!window.updateGlobalCardFilters) {
window.updateGlobalCardFilters = function() { window.updateGlobalCardFilters = function() {
console.log("updateGlobalCardFilters called.");
const selects = document.querySelectorAll('.card-multiselect'); const selects = document.querySelectorAll('.card-multiselect');
// Rebuild global selections and rarity filtering. // Rebuild global selections and rarity filtering.
@ -46,8 +45,6 @@ if (!window.updateGlobalCardFilters) {
} }
}); });
console.log("Global selected card IDs:", globalSelectedIds, "Current Global rarity:", globalRarity);
selects.forEach(select => { selects.forEach(select => {
if (select.choicesInstance && select.choicesInstance.dropdown.element) { if (select.choicesInstance && select.choicesInstance.dropdown.element) {
// Reset all options to enabled. // Reset all options to enabled.
@ -82,7 +79,6 @@ if (!window.updateGlobalCardFilters) {
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log("DOM fully loaded. Initializing card multiselect for field '{{ field_id }}'");
const selectField = document.getElementById('{{ field_id }}'); const selectField = document.getElementById('{{ field_id }}');
const placeholder = selectField.getAttribute('data-placeholder') || ''; const placeholder = selectField.getAttribute('data-placeholder') || '';
@ -156,7 +152,6 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
console.log("Initialized Choices.js instance for field '{{ field_id }}':", choicesInstance);
// Associate the Choices instance with the select field. // Associate the Choices instance with the select field.
selectField.choicesInstance = choicesInstance; selectField.choicesInstance = choicesInstance;
@ -166,7 +161,6 @@ document.addEventListener('DOMContentLoaded', function() {
window.cardMultiselectInstances.push(selectField); window.cardMultiselectInstances.push(selectField);
selectField.addEventListener('change', function() { selectField.addEventListener('change', function() {
console.log("Select field changed. Current selected values:", selectField.choicesInstance.getValue(true));
if (window.updateGlobalCardFilters) { if (window.updateGlobalCardFilters) {
window.updateGlobalCardFilters(); window.updateGlobalCardFilters();
} }
@ -177,7 +171,6 @@ document.addEventListener('DOMContentLoaded', function() {
choicesContainer.addEventListener('click', function(e) { choicesContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('increment')) { if (e.target.classList.contains('increment')) {
console.log("Increment button clicked.");
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
@ -185,14 +178,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (container) { if (container) {
let quantityBadge = container.querySelector('.card-quantity-badge'); let quantityBadge = container.querySelector('.card-quantity-badge');
let quantity = getOptionQuantity(container); let quantity = getOptionQuantity(container);
console.log("Increment action on card", container.getAttribute('data-card-id'), "original quantity:", quantity, "new quantity:", quantity + 1);
quantity = quantity + 1; quantity = quantity + 1;
quantityBadge.innerText = quantity; quantityBadge.innerText = quantity;
updateOptionQuantity(container, quantity); updateOptionQuantity(container, quantity);
} }
} }
if (e.target.classList.contains('decrement')) { if (e.target.classList.contains('decrement')) {
console.log("Decrement button clicked.");
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
@ -202,20 +193,15 @@ document.addEventListener('DOMContentLoaded', function() {
let quantity = getOptionQuantity(container); let quantity = getOptionQuantity(container);
const cardId = container.getAttribute('data-card-id'); const cardId = container.getAttribute('data-card-id');
if (quantity === 1) { if (quantity === 1) {
console.log("Decrement action: quantity is 1 for card", cardId, "initiating removal.");
const option = selectField.querySelector('option[value="' + cardId + '"]'); const option = selectField.querySelector('option[value="' + cardId + '"]');
if (option) { if (option) {
console.log("Removing card from Choices.js instance. Value removed:", option.value);
choicesInstance.removeActiveItemsByValue(option.value); choicesInstance.removeActiveItemsByValue(option.value);
option.selected = false; option.selected = false;
} else {
console.log("No active item found for card", cardId);
} }
if (window.updateGlobalCardFilters) { if (window.updateGlobalCardFilters) {
window.updateGlobalCardFilters(); window.updateGlobalCardFilters();
} }
} else { } else {
console.log("Decrement action on card", cardId, "reducing quantity from", quantity, "to", quantity - 1);
quantity = quantity - 1; quantity = quantity - 1;
quantityBadge.innerText = quantity; quantityBadge.innerText = quantity;
updateOptionQuantity(container, quantity); updateOptionQuantity(container, quantity);
@ -232,18 +218,16 @@ document.addEventListener('DOMContentLoaded', function() {
function updateOptionQuantity(item, quantity) { function updateOptionQuantity(item, quantity) {
const cardId = item.getAttribute('data-card-id'); const cardId = item.getAttribute('data-card-id');
console.log("Updating option quantity for card", cardId, "to", quantity);
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]'); const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
if (option) { if (option) {
option.setAttribute('data-quantity', quantity); option.setAttribute('data-quantity', quantity);
console.log("Updated data-quantity for card", cardId, "to", quantity);
} }
} }
function getOptionQuantity(item) { function getOptionQuantity(item) {
const cardId = item.getAttribute('data-card-id'); const cardId = item.getAttribute('data-card-id');
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]'); const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
return option ? parseInt(option.getAttribute('data-quantity')) : 1; return option ? parseInt(option.getAttribute('data-quantity')) : "";
} }
if (choicesInstance.getValue(true).length > 0 && window.updateGlobalCardFilters) { if (choicesInstance.getValue(true).length > 0 && window.updateGlobalCardFilters) {

View file

@ -7,7 +7,7 @@
<div class="flex justify-start items-center"> <div class="flex justify-start items-center">
<!-- Left: Initiator's avatar and "Has" --> <!-- Left: Initiator's avatar and "Has" -->
<div class="flex items-center"> <div class="flex items-center">
<div class="avatar me-2"> <div class="avatar me-2 tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ acceptance.trade_offer.initiated_by.user.username }} | {{ acceptance.trade_offer.initiated_by.user.reputation_score }} rep">
<div class="w-10 rounded-full"> <div class="w-10 rounded-full">
{{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }} {{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }}
</div> </div>
@ -44,7 +44,7 @@
Waiting on {{ acceptance.accepted_by.user.username }} to {{ acceptance.next_action_label }}... Waiting on {{ acceptance.accepted_by.user.username }} to {{ acceptance.next_action_label }}...
{% endif %} {% endif %}
</div> </div>
<div class="avatar ms-2"> <div class="avatar ms-2 tooltip tooltip-bottom cursor-default" @click.stop.prevent data-tip="{{ acceptance.accepted_by.user.username}} | {{ acceptance.accepted_by.user.reputation_score }} rep">
<div class="w-10 rounded-full"> <div class="w-10 rounded-full">
{{ acceptance.accepted_by.user.email|gravatar:40 }} {{ acceptance.accepted_by.user.email|gravatar:40 }}
</div> </div>

View file

@ -3,25 +3,27 @@
{% cache 60 trade_offer offer_pk %} {% cache 60 trade_offer offer_pk %}
<div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card"> <div x-data="{ flipped: {{flipped|lower}}, offerExpanded: {{flipped|yesno:'false,true'}}, acceptanceExpanded: {{flipped|lower}} }" x-ref="tradeOffer" class="transition-all duration-500 trade-offer-card">
<div class="flip-container"> <div class="flip-container">
<div{% if not on_detail_page %} @click="window.location.href = '{% url 'trade_offer_detail' pk=offer_pk %}'"{% endif %} class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out{% if not on_detail_page %} cursor-pointer{% endif %}{%if flipped %} rotate-y-180{% endif %}" <div class="flip-inner grid grid-cols-1 grid-rows-1 card bg-base-100 card-border shadow-lg w-90 transform transition-transform duration-500 ease-in-out{%if flipped %} rotate-y-180{% endif %}"
:class="{'rotate-y-180': flipped}"> :class="{'rotate-y-180': flipped}">
<div class="flip-face front rotate-y-0 col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between"> <div class="flip-face front rotate-y-0 col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between">
<div class="flip-face-header self-start"> <div class="flip-face-header self-start">
<a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}>
<div class="relative mt-6 mb-4 mx-2 sm:mx-4"> <div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<span class="text-sm font-semibold text-center">Has</span> <span class="text-sm font-semibold text-center">Has</span>
<span class="text-sm font-semibold text-center">Wants</span> <span class="text-sm font-semibold text-center">Wants</span>
</div> </div>
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"> <div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="avatar tooltip tooltip-top" data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep"> <div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep">
<div class="w-10 rounded-full"> <div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }} {{ initiated_by_email|gravatar:40 }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</a>
</div> </div>
<div class="flip-face-body self-start"> <div class="flip-face-body self-start{% if not on_detail_page %} cursor-pointer{% endif %}"{% if not on_detail_page %} @click.stop.prevent="window.location.href = '{% url 'trade_offer_detail' pk=offer_pk %}'"{% endif %}>
<div x-show="offerExpanded" x-collapse.duration.500ms class="px-2 badges"> <div x-show="offerExpanded" x-collapse.duration.500ms class="px-2 badges">
<div class="flex flex-row justify-around"> <div class="flex flex-row justify-around">
{% if num_cards_available > 0 %} {% if num_cards_available > 0 %}
@ -42,92 +44,81 @@
</div> </div>
</div> </div>
<div class="flip-face-footer self-end"> <div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2"> <a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}>
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}"> <div class="flex justify-between px-2 pb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
stroke="currentColor" class="size-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
<path stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" class="size-5">
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /> <path stroke-linecap="round" stroke-linejoin="round"
</svg> d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</div>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="cursor-pointer text-gray-500"
@click.stop="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' });
offerExpanded = false;
setTimeout(() => {
flipped = true;
setTimeout(() => { acceptanceExpanded = true; }, 500);
}, 500);">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061A1.125 1.125 0 0 1 3 16.811V8.69ZM12.75 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061a1.125 1.125 0 0 1-1.683-.977V8.69Z" />
</svg> </svg>
</div>
<div class="text-xs text-transparent shadow-gray-700 dark:shadow-white" style="text-shadow: 0 0 0 var(--tw-shadow-color);">{{ rarity_icon }}</div>
<div class="cursor-pointer text-gray-500"
@click.stop.prevent="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' });
offerExpanded = false;
setTimeout(() => {
flipped = true;
setTimeout(() => { acceptanceExpanded = true; }, 500);
}, 500);">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061A1.125 1.125 0 0 1 3 16.811V8.69ZM12.75 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061a1.125 1.125 0 0 1-1.683-.977V8.69Z" />
</svg>
</div>
</div> </div>
</div> </a>
</div> </div>
</div> </div>
<div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between rotate-y-180"> <div class="flip-face back col-start-1 row-start-1 grid grid-cols-1 auto-rows-min gap-2 content-between rotate-y-180">
<div class="flip-face-header self-start"> <div class="flip-face-header self-start">
<div class="relative mt-6 mb-4 mx-2 sm:mx-4"> <a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}>
<div class="grid grid-cols-2 items-center"> <div class="relative mt-6 mb-4 mx-2 sm:mx-4">
<span class="text-sm font-semibold text-center">Has</span> <div class="grid grid-cols-2 items-center">
<span class="text-sm font-semibold text-center">Wants</span> <span class="text-sm font-semibold text-center">Has</span>
</div> <span class="text-sm font-semibold text-center">Wants</span>
<div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center"> </div>
<div class="avatar tooltip tooltip-top" data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep"> <div class="absolute inset-x-0 top-1/2 transform -translate-y-1/2 flex justify-center">
<div class="w-10 rounded-full"> <div class="avatar tooltip tooltip-top cursor-default" @click.stop.prevent data-tip="{{ initiated_by_username }} | {{ initiated_reputation }} rep">
{{ initiated_by_email|gravatar:40 }} <div class="w-10 rounded-full">
{{ initiated_by_email|gravatar:40 }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </a>
</div> </div>
<div class="flip-face-body self-start"> <div class="flip-face-body self-start">
<div class="px-2"> <div class="px-2 space-y-3 text-xs">
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3"> <div x-show="acceptanceExpanded" x-collapse.duration.500ms class="flex flex-row flex-wrap justify-start items-baseline w-90 lg:w-181 mx-auto">
{% for acceptance in acceptances %} {% for trade in acceptances %}
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline" <div class="p-2 grid grid-cols-2 justify-items-center items-baseline gap-1">
data-tooltip-html='<div class="flex items-center space-x-2"> <div class="">{% card_badge trade.offered_card %}</div>
<div class="avatar"> <div class="">{% card_badge trade.requested_card %}</div>
<div class="w-10 rounded-full"> <div class="col-span-2 text-center">{{ trade.accepted_by.user.username }} &bull; <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> &bull; {{ trade.get_state_display }}</div>
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
</div>
</div>
<div class="flex flex-col">
<span class="text-sm">Accepted by: {{ acceptance.accepted_by.user.username }}</span>
<span class="text-sm">State: {{ acceptance.state }}</span>
<span class="text-sm">ID: {{ acceptance.hash }}</span>
</div>
</div>'>
<div class="grid grid-cols-2 gap-4 items-center">
<div>
{% card_badge acceptance.requested_card %}
</div>
<div>
{% card_badge acceptance.offered_card %}
</div>
</div> </div>
</a> {% empty %}
{% empty %} <div class="text-center justify-center w-full text-center">No trades yet.</div>
<div class="text-center text-sm mb-2">No trades yet!</div> {% endfor %}
{% endfor %} </div>
</div>
</div> </div>
</div> </div>
<div class="flip-face-footer self-end"> <div class="flip-face-footer self-end">
<div class="flex justify-between px-2 pb-2"> <a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}></a>
<div class="text-gray-500 cursor-pointer" <div class="flex justify-between px-2 pb-2">
@click.stop="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' }); acceptanceExpanded = false; setTimeout(() => { flipped = false; setTimeout(() => { offerExpanded = true; }, 500); }, 500);"> <div class="text-gray-500 cursor-pointer"
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5"> @click.stop.prevent="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' }); acceptanceExpanded = false; setTimeout(() => { flipped = false; setTimeout(() => { offerExpanded = true; }, 500); }, 500);">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061A1.125 1.125 0 0 1 21 8.689v8.122ZM11.25 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061a1.125 1.125 0 0 1 1.683.977v8.122Z" /> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061A1.125 1.125 0 0 1 21 8.689v8.122ZM11.25 16.811c0 .864-.933 1.406-1.683.977l-7.108-4.061a1.125 1.125 0 0 1 0-1.954l7.108-4.061a1.125 1.125 0 0 1 1.683.977v8.122Z" />
</div> </svg>
<div class="text-xs">{{ rarity_icon }}</div>
<div class="px-1 text-center text-gray-500">
<span class="text-sm font-semibold">
({{ acceptances|length }})
</span>
</div> </div>
</div> <div class="text-xs text-transparent shadow-gray-700 dark:shadow-white" style="text-shadow: 0 0 0 var(--tw-shadow-color);">{{ rarity_icon }}</div>
<div class="px-1 text-center text-gray-500">
<span class="text-sm font-semibold">
({{ acceptances|length }})
</span>
</div>
</div>
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2025-04-13 05:10 # Generated by Django 5.1.2 on 2025-04-14 04:07
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View file

@ -139,6 +139,9 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
else: else:
return return
if not recipient_user.enable_email_notifications:
return
is_initiator = instance.trade_offer.initiated_by.user.pk == acting_user.pk is_initiator = instance.trade_offer.initiated_by.user.pk == acting_user.pk
email_context = { email_context = {
@ -159,8 +162,6 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
email_subject += render_to_string("email/trades/trade_update_" + state + "_subject.txt", email_context) email_subject += render_to_string("email/trades/trade_update_" + state + "_subject.txt", email_context)
email_body = render_to_string(email_template, email_context) email_body = render_to_string(email_template, email_context)
print("initiated by: ", instance.trade_offer.initiated_by, ", accepted by: ", instance.accepted_by, ", acting user: ", acting_user, ", recipient user: ", recipient_user, ", state: ", state)
send_mail( send_mail(
email_subject, email_subject,
email_body, email_body,