add email setting, closes #5. other misc small fixes
This commit is contained in:
parent
135bd95a6a
commit
86b061c971
11 changed files with 85 additions and 99 deletions
|
|
@ -87,4 +87,4 @@ class CustomUserCreationForm(SignupForm):
|
|||
class UserSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = ['show_friend_code_on_link_previews']
|
||||
fields = ['show_friend_code_on_link_previews', 'enable_email_notifications']
|
||||
|
|
@ -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 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')),
|
||||
('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')),
|
||||
('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)),
|
||||
('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')),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ class CustomUser(AbstractUser):
|
|||
verbose_name="Show Friend Code on Link Previews",
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ module.exports = {
|
|||
'alert-success',
|
||||
'alert-warning',
|
||||
'alert-error',
|
||||
'btn-info',
|
||||
'btn-success',
|
||||
'btn-warning',
|
||||
'btn-error',
|
||||
'bg-info',
|
||||
'bg-success',
|
||||
'bg-warning',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<a href="{{ url }}">
|
||||
<a href="{{ url }}" @click.stop>
|
||||
<div class="relative block">
|
||||
{% if not expanded %}
|
||||
<div class="flex flex-row items-center h-[32px] p-1.5 w-40 text-white shadow-lg" style="{{ style }}">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
<script defer>
|
||||
if (!window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters = function() {
|
||||
console.log("updateGlobalCardFilters called.");
|
||||
const selects = document.querySelectorAll('.card-multiselect');
|
||||
|
||||
// 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 => {
|
||||
if (select.choicesInstance && select.choicesInstance.dropdown.element) {
|
||||
// Reset all options to enabled.
|
||||
|
|
@ -82,7 +79,6 @@ if (!window.updateGlobalCardFilters) {
|
|||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("DOM fully loaded. Initializing card multiselect for field '{{ field_id }}'");
|
||||
const selectField = document.getElementById('{{ field_id }}');
|
||||
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.
|
||||
selectField.choicesInstance = choicesInstance;
|
||||
|
||||
|
|
@ -166,7 +161,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
window.cardMultiselectInstances.push(selectField);
|
||||
|
||||
selectField.addEventListener('change', function() {
|
||||
console.log("Select field changed. Current selected values:", selectField.choicesInstance.getValue(true));
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
|
|
@ -177,7 +171,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
choicesContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('increment')) {
|
||||
console.log("Increment button clicked.");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
|
@ -185,14 +178,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (container) {
|
||||
let quantityBadge = container.querySelector('.card-quantity-badge');
|
||||
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;
|
||||
quantityBadge.innerText = quantity;
|
||||
updateOptionQuantity(container, quantity);
|
||||
}
|
||||
}
|
||||
if (e.target.classList.contains('decrement')) {
|
||||
console.log("Decrement button clicked.");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
|
@ -202,20 +193,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
let quantity = getOptionQuantity(container);
|
||||
const cardId = container.getAttribute('data-card-id');
|
||||
if (quantity === 1) {
|
||||
console.log("Decrement action: quantity is 1 for card", cardId, "initiating removal.");
|
||||
const option = selectField.querySelector('option[value="' + cardId + '"]');
|
||||
if (option) {
|
||||
console.log("Removing card from Choices.js instance. Value removed:", option.value);
|
||||
choicesInstance.removeActiveItemsByValue(option.value);
|
||||
option.selected = false;
|
||||
} else {
|
||||
console.log("No active item found for card", cardId);
|
||||
}
|
||||
if (window.updateGlobalCardFilters) {
|
||||
window.updateGlobalCardFilters();
|
||||
}
|
||||
} else {
|
||||
console.log("Decrement action on card", cardId, "reducing quantity from", quantity, "to", quantity - 1);
|
||||
quantity = quantity - 1;
|
||||
quantityBadge.innerText = quantity;
|
||||
updateOptionQuantity(container, quantity);
|
||||
|
|
@ -232,18 +218,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
function updateOptionQuantity(item, quantity) {
|
||||
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 + '"]');
|
||||
if (option) {
|
||||
option.setAttribute('data-quantity', quantity);
|
||||
console.log("Updated data-quantity for card", cardId, "to", quantity);
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionQuantity(item) {
|
||||
const cardId = item.getAttribute('data-card-id');
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="flex justify-start items-center">
|
||||
<!-- Left: Initiator's avatar and "Has" -->
|
||||
<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">
|
||||
{{ acceptance.trade_offer.initiated_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
Waiting on {{ acceptance.accepted_by.user.username }} to {{ acceptance.next_action_label }}...
|
||||
{% endif %}
|
||||
</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">
|
||||
{{ acceptance.accepted_by.user.email|gravatar:40 }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,25 +3,27 @@
|
|||
{% 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 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}">
|
||||
<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">
|
||||
<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="grid grid-cols-2 items-center">
|
||||
<span class="text-sm font-semibold text-center">Has</span>
|
||||
<span class="text-sm font-semibold text-center">Wants</span>
|
||||
</div>
|
||||
<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">
|
||||
{{ initiated_by_email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</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 class="flex flex-row justify-around">
|
||||
{% if num_cards_available > 0 %}
|
||||
|
|
@ -42,6 +44,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flip-face-footer self-end">
|
||||
<a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}>
|
||||
<div class="flex justify-between px-2 pb-2">
|
||||
<div class="text-gray-500 text-sm tooltip tooltip-right" data-tip="ID: {{ offer_hash }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
|
|
@ -50,9 +53,9 @@
|
|||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs">{{ rarity_icon }}</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="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
@click.stop.prevent="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
offerExpanded = false;
|
||||
setTimeout(() => {
|
||||
flipped = true;
|
||||
|
|
@ -63,71 +66,59 @@
|
|||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</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-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="grid grid-cols-2 items-center">
|
||||
<span class="text-sm font-semibold text-center">Has</span>
|
||||
<span class="text-sm font-semibold text-center">Wants</span>
|
||||
</div>
|
||||
<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">
|
||||
{{ initiated_by_email|gravatar:40 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flip-face-body self-start">
|
||||
<div class="px-2">
|
||||
<div x-show="acceptanceExpanded" x-collapse.duration.500ms class="space-y-3">
|
||||
{% for acceptance in acceptances %}
|
||||
<a href="{% url 'trade_acceptance_update' pk=acceptance.pk %}" class="no-underline"
|
||||
data-tooltip-html='<div class="flex items-center space-x-2">
|
||||
<div class="avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
{{ acceptance.accepted_by.user.email|gravatar:"40" }}
|
||||
<div class="px-2 space-y-3 text-xs">
|
||||
<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 trade in acceptances %}
|
||||
<div class="p-2 grid grid-cols-2 justify-items-center items-baseline gap-1">
|
||||
<div class="">{% card_badge trade.offered_card %}</div>
|
||||
<div class="">{% card_badge trade.requested_card %}</div>
|
||||
<div class="col-span-2 text-center">{{ trade.accepted_by.user.username }} • <a class="link link-hover" href="{% url 'trade_acceptance_update' pk=trade.pk %}">#{{ trade.hash }}</a> • {{ trade.get_state_display }}</div>
|
||||
</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>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="text-center text-sm mb-2">No trades yet!</div>
|
||||
<div class="text-center justify-center w-full text-center">No trades yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flip-face-footer self-end">
|
||||
<a{% if not on_detail_page %} href="{% url 'trade_offer_detail' pk=offer_pk %}"{% endif %}></a>
|
||||
<div class="flex justify-between px-2 pb-2">
|
||||
<div class="text-gray-500 cursor-pointer"
|
||||
@click.stop="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' }); acceptanceExpanded = false; setTimeout(() => { flipped = false; setTimeout(() => { offerExpanded = true; }, 500); }, 500);">
|
||||
@click.stop.prevent="$refs.tradeOffer.scrollIntoView({ behavior: 'auto', block: 'start' }); acceptanceExpanded = false; setTimeout(() => { flipped = false; setTimeout(() => { offerExpanded = 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="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>
|
||||
</div>
|
||||
<div class="text-xs">{{ rarity_icon }}</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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
from django.db import migrations, models
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ def trade_acceptance_email_notification(sender, instance, created, **kwargs):
|
|||
else:
|
||||
return
|
||||
|
||||
if not recipient_user.enable_email_notifications:
|
||||
return
|
||||
|
||||
is_initiator = instance.trade_offer.initiated_by.user.pk == acting_user.pk
|
||||
|
||||
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_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(
|
||||
email_subject,
|
||||
email_body,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue