From 15e45c81c52cf765986ae58288a8e45d30844094 Mon Sep 17 00:00:00 2001
From: = <=>
Date: Mon, 16 Sep 2024 23:16:14 +0000
Subject: [PATCH 01/11] fix #159
---
.env.example | 2 +
auctions/consumers.py | 58 +-
auctions/forms.py | 16 +-
auctions/management/commands/endauctions.py | 122 +---
.../webpush_notifications_deduplicate.py | 19 +
...n_message_users_when_lots_sell_and_more.py | 56 ++
...rdata_push_notifications_when_lots_sell.py | 21 +
auctions/models.py | 99 ++-
auctions/routing.py | 13 +-
auctions/static/webpush/new.json | 1 +
auctions/static/webpush/webpush.js | 212 ++++++
.../static/webpush/webpush_serviceworker.js | 20 +
auctions/templates/all_auctions.html | 1 +
.../auctions/dynamic_set_lot_winner.html | 191 ++++++
auctions/templates/base.html | 45 +-
auctions/templates/user_map.html | 6 +
auctions/templates/user_preferences.html | 29 +-
auctions/templates/view_lot_images.html | 76 +-
auctions/templates/view_lot_simple.html | 34 +-
auctions/tests.py | 373 +++++-----
auctions/urls.py | 34 +-
auctions/views.py | 648 ++++++++++++------
crontab | 2 +
fishauctions/asgi.py | 3 +-
fishauctions/settings.py | 7 +
requirements.in | 1 +
requirements.txt | 177 +++--
27 files changed, 1565 insertions(+), 701 deletions(-)
create mode 100644 auctions/management/commands/webpush_notifications_deduplicate.py
create mode 100644 auctions/migrations/0147_auction_message_users_when_lots_sell_and_more.py
create mode 100644 auctions/migrations/0148_userdata_push_notifications_when_lots_sell.py
create mode 100644 auctions/static/webpush/new.json
create mode 100644 auctions/static/webpush/webpush.js
create mode 100644 auctions/static/webpush/webpush_serviceworker.js
create mode 100755 auctions/templates/auctions/dynamic_set_lot_winner.html
diff --git a/.env.example b/.env.example
index 5889c24..fd80edc 100644
--- a/.env.example
+++ b/.env.example
@@ -5,6 +5,8 @@ NGINX_CONF_LOCATION='/config/nginx/site-confs/default.conf' # leave unset for de
SECRET_KEY='secret'
DATABASE_PASSWORD='secret'
DATABASE_ROOT_PASSWORD="secret"
+VAPID_PUBLIC_KEY="public-key"
+VAPID_PRIVATE_KEY="private-key"
POST_OFFICE_EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# if POST_OFFICE_EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend', set the following
EMAIL_USE_TLS='True'
diff --git a/auctions/consumers.py b/auctions/consumers.py
index 169e449..37861f8 100755
--- a/auctions/consumers.py
+++ b/auctions/consumers.py
@@ -17,6 +17,7 @@
Invoice,
Lot,
LotHistory,
+ User,
UserBan,
UserData,
UserInterestCategory,
@@ -191,12 +192,7 @@ def bid_on_lot(lot, user, amount):
auctiontos_winner = AuctionTOS.objects.filter(auction=lot.auction, user=user).first()
if auctiontos_winner:
lot.auctiontos_winner = auctiontos_winner
- invoice, created = Invoice.objects.get_or_create(
- auctiontos_user=lot.auctiontos_winner,
- auction=lot.auction,
- defaults={},
- )
- invoice.recalculate
+ lot.create_update_invoices
lot.winning_price = lot.buy_now_price
lot.buy_now_used = True
# this next line makes the lot end immediately after buy now is used
@@ -526,3 +522,53 @@ def error_message(self, event):
# Receive message from room group
def chat_message(self, event):
self.send(text_data=json.dumps(event))
+
+
+class UserConsumer(WebsocketConsumer):
+ """This is ready to use and corresponding code to connect added (commented out) to base.html
+ You can use userdata.send_websocket_message to message the user, like this:
+ result = {
+ "type": "toast",
+ "message": "Hello world!",
+ }
+ user.userdata.send_websocket_message(result)
+ It would make a good messaging system for some stuff like chat messages,
+ but at this time it does not seem like a good idea
+ """
+
+ def connect(self):
+ try:
+ self.pk = self.scope["url_route"]["kwargs"]["user_pk"]
+ user_for = User.objects.filter(pk=self.pk).first()
+ self.user = self.scope["user"]
+ self.user_notification_channel = f"user_{self.pk}"
+ if not user_for or user_for != self.user:
+ self.close()
+ else:
+ self.accept()
+ # Add to the group after accepting the connection
+ async_to_sync(self.channel_layer.group_add)(self.user_notification_channel, self.channel_name)
+
+ # Send a message after accepting the connection
+ # async_to_sync(self.channel_layer.group_send)(
+ # self.user_notification_channel,
+ # {"type": "toast", "message": 'Welcome!', 'bg': 'success'},
+ # )
+ except Exception as e:
+ print(e)
+ self.close()
+
+ def disconnect(self, close_code):
+ # Leave room group
+ async_to_sync(self.channel_layer.group_discard)(self.user_notification_channel, self.channel_name)
+ print("disconnected")
+
+ # Receive message from WebSocket
+ def receive(self, text_data):
+ text_data_json = json.loads(text_data)
+ print(text_data_json)
+
+ def toast(self, event):
+ message = event["message"]
+ bg = event.get("bg", "info")
+ self.send(text_data=json.dumps({"type": "toast", "message": message, "bg": bg}))
diff --git a/auctions/forms.py b/auctions/forms.py
index 405ad15..ae43e71 100755
--- a/auctions/forms.py
+++ b/auctions/forms.py
@@ -1548,6 +1548,7 @@ class Meta:
"date_online_bidding_starts",
"allow_deleting_bids",
"auto_add_images",
+ "message_users_when_lots_sell",
]
widgets = {
"date_start": DateTimePickerInput(),
@@ -1581,6 +1582,8 @@ def __init__(self, *args, **kwargs):
].help_text = "This should be 1-24 hours before the end of your auction"
self.fields["allow_bidding_on_lots"].help_text = "Leave this checked or people won't be able to bid!"
self.fields["allow_bidding_on_lots"].widget = forms.HiddenInput()
+ self.fields["message_users_when_lots_sell"].widget = forms.HiddenInput()
+ self.fields["advanced_lot_adding"].widget = forms.HiddenInput()
# self.fields['pre_register_lot_entry_fee_discount'].widget=forms.HiddenInput()
self.fields["pre_register_lot_discount_percent"].widget = forms.HiddenInput()
# self.fields['set_lot_winners_url'].widget=forms.HiddenInput()
@@ -1777,17 +1780,21 @@ def __init__(self, *args, **kwargs):
"buy_now",
css_class="col-md-3",
),
- # Div('set_lot_winners_url', css_class='col-md-3',),
Div(
- "promote_this_auction",
+ "message_users_when_lots_sell",
css_class="col-md-3",
),
+ # Div('set_lot_winners_url', css_class='col-md-3',),
PrependedAppendedText(
"tax",
"",
"%",
wrapper_class="col-md-3",
),
+ Div(
+ "promote_this_auction",
+ css_class="col-md-3",
+ ),
css_class="row",
),
Submit("submit", "Save", css_class="create-update-auction btn-success"),
@@ -2473,6 +2480,7 @@ class Meta:
"username_visible",
"share_lot_images",
"auto_add_images",
+ "push_notifications_when_lots_sell",
)
def __init__(self, user, *args, **kwargs):
@@ -2510,6 +2518,10 @@ def __init__(self, user, *args, **kwargs):
# Div('use_list_view',css_class='col-md-4',),
# Div('use_dark_theme',css_class='col-md-4',),
# Div('show_ads',css_class='col-md-3',),
+ Div(
+ "push_notifications_when_lots_sell",
+ css_class="col-md-6",
+ ),
css_class="row",
),
HTML("
Notifications
"),
diff --git a/auctions/management/commands/endauctions.py b/auctions/management/commands/endauctions.py
index e9375b2..af37e07 100644
--- a/auctions/management/commands/endauctions.py
+++ b/auctions/management/commands/endauctions.py
@@ -1,57 +1,21 @@
import datetime
-import channels.layers
-from asgiref.sync import async_to_sync
from django.contrib.sites.models import Site
from django.core.management.base import BaseCommand
-from django.db.models import F
from django.utils import timezone
from easy_thumbnails.files import get_thumbnailer
from post_office import mail
-from auctions.models import AuctionTOS, Invoice, Lot, LotHistory, LotImage
-
-
-def sendWarning(lot_number, result):
- channel_layer = channels.layers.get_channel_layer()
- async_to_sync(channel_layer.group_send)(f"lot_{lot_number}", result)
-
-
-def fix_seller_info(part_of_auction):
- """This function is really not needed - it's been run once to update all existing lots.
- Fill out the user if auctiontos_seller is set, or auctiontos_seller if the user is set."""
- # first, let's fill out the seller info (1 of 2)
- needs_auctiontos_seller_filled_out = part_of_auction.filter(
- auctiontos_seller__isnull=True, auction__auctiontos__user=F("user")
- )
- for lot in needs_auctiontos_seller_filled_out:
- auctionTOS = AuctionTOS.objects.filter(user=lot.user, auction=lot.auction).first()
- lot.auctiontos_seller = auctionTOS
- lot.save() # presave receiver will make an invoice for this lot/seller
-
- # next, let's fill out the user info (2 of 2)
- # user when auctiontos filled out - handy for when users are part of an auction (auctiontos__user__isnull=True), but then later sign up
- # User is set for auctiontos on user sign in event - see models.user_logged_in_callback()
- needs_user_filled_out = part_of_auction.filter(
- auctiontos_seller__isnull=False,
- user__isnull=True,
- auctiontos_seller__user__isnull=False,
- auction__auctiontos=F("auctiontos_seller"),
- )
- for lot in needs_user_filled_out:
- user = lot.auctiontos_seller.user
- print("setting user based on auction tos:")
- print(lot, lot.auctiontos_seller, user)
- lot.user = user
- lot.save()
+from auctions.models import Lot, LotHistory, LotImage
def declare_winners_on_lots(lots):
"""Set the winner and winning price on all lots"""
for lot in lots:
if lot.ended:
- # note - lots that are part of in-person auctions will only get here if a winner and price is set
- # they always have lot.ended = False
+ # note - lots that are part of in-person auctions will not get here
+ # if they are active, they always have lot.ended = False, and if they are sold,
+ # the method that sells them should set active=False, so they won't be filtered here
# But, see https://github.com/iragm/fishauctions/issues/116
try:
lot.active = False
@@ -60,21 +24,18 @@ def declare_winners_on_lots(lots):
# lots will be bought via buy now will get here
# as well as in-person auction lots whose winner has been set manually
# We still need to make those active=False, done above, and then save, below
+ # I don't think the above is true anymore, lots bought with buy now should also be made inactive
pass
else:
info = None
- if (
- lot.high_bidder
- ): # and not lot.sold: # not sure what that was here, we already filter this in the if above
- lot.winner = lot.high_bidder
- lot.winning_price = lot.high_bid
+ if lot.high_bidder:
+ lot.sell_to_online_high_bidder
info = "LOT_END_WINNER"
bidder = lot.high_bidder
high_bidder_pk = lot.high_bidder.pk
high_bidder_name = str(lot.high_bidder_display)
current_high_bid = lot.high_bid
message = f"Won by {lot.high_bidder_display}"
- lot.save()
# at this point, the lot should have a winner filled out if it's sold. If it still doesn't:
if not lot.sold:
high_bidder_pk = None
@@ -92,7 +53,7 @@ def declare_winners_on_lots(lots):
"current_high_bid": current_high_bid,
}
if info:
- sendWarning(lot.lot_number, result)
+ lot.send_websocket_message(result)
LotHistory.objects.create(
lot=lot,
user=bidder,
@@ -100,30 +61,7 @@ def declare_winners_on_lots(lots):
changed_price=True,
current_price=lot.high_bid,
)
- # if this is part of an auction, update invoices
- if lot.sold and lot.auction:
- if lot.auctiontos_winner:
- auctiontos_winner = lot.auctiontos_winner
- else:
- # look for the TOS and create the invoice
- auctiontos_winner = AuctionTOS.objects.filter(auction=lot.auction, user=lot.high_bidder).first()
- if auctiontos_winner:
- lot.auctiontos_winner = auctiontos_winner
- lot.save()
- if lot.auctiontos_winner:
- invoice, created = Invoice.objects.get_or_create(
- auctiontos_user=lot.auctiontos_winner,
- auction=lot.auction,
- defaults={},
- )
- invoice.recalculate
- if lot.auctiontos_seller:
- invoice, created = Invoice.objects.get_or_create(
- auctiontos_user=lot.auctiontos_seller,
- auction=lot.auction,
- defaults={},
- )
- invoice.recalculate
+ lot.create_update_invoices
# logic to email winner and buyer for lots not in an auction
if lot.winner and not lot.auction:
current_site = Site.objects.get_current()
@@ -207,48 +145,12 @@ def declare_winners_on_lots(lots):
"pk": -1,
"username": "System",
}
- sendWarning(lot.lot_number, result)
-
-
-def fix_winner_info(part_of_auction):
- """set winner if auctiontos_winner is set, and auctiontos_winner if winner is set. It's a good idea to run this AFTER declaring winners"""
- # set auctiontos_winner if only winner is set
- needs_auctiontos_winner_filled_out = part_of_auction.filter(
- winner_isnull=False,
- auctiontos_winner__isnull=True,
- auction__auctiontos__user=F("winner"),
- )
- for lot in needs_auctiontos_winner_filled_out:
- auctionTOS = AuctionTOS.objects.filter(user=lot.winner, auction=lot.auction).first()
- lot.auctiontos_winner = auctionTOS
- lot.save()
- invoice, created = Invoice.objects.get_or_create(
- auctiontos_user=lot.auctiontos_winner, auction=lot.auction, defaults={}
- )
-
- # declate winners where there is already an auctiontos_winner (hopefully rare - but see models.user_logged_in_callback()
- needs_winner_filled_out = part_of_auction.filter(
- auctiontos_winner__isnull=False,
- winner__isnull=True,
- auctiontos_winner__user__isnull=False,
- auction__auctiontos=F("auctiontos_winner"),
- )
- for lot in needs_winner_filled_out:
- winner = lot.auctiontos_seller.user
- print("setting winner based on auction tos:")
- print(lot, lot.auctiontos_winner, winner)
- # fixme - this code is not yet fully tested, no saving yet!!
- # lot.winner = winner
- # lot.save()
+ lot.send_websocket_message(result)
class Command(BaseCommand):
help = "Sets the winner, and winning price on all ended lots. Send lot ending soon and lot ended messages to websocket connected users. Sets active to false on lots"
def handle(self, *args, **options):
- lots = Lot.objects.filter(is_deleted=False, banned=False, deactivated=False)
- # part_of_auction = lots.filter(auction__isnull=False)
-
- # fix_seller_info(part_of_auction)
- declare_winners_on_lots(lots.filter(active=True))
- # fix_winner_info(part_of_auction)
+ lots = Lot.objects.filter(active=True, is_deleted=False, banned=False, deactivated=False)
+ declare_winners_on_lots(lots)
diff --git a/auctions/management/commands/webpush_notifications_deduplicate.py b/auctions/management/commands/webpush_notifications_deduplicate.py
new file mode 100644
index 0000000..185b540
--- /dev/null
+++ b/auctions/management/commands/webpush_notifications_deduplicate.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+from django.db.models import Count, Min
+from webpush.models import SubscriptionInfo
+
+
+class Command(BaseCommand):
+ help = "Duplicate subscriptions can be created, this removes them. See https://github.com/safwanrahman/django-webpush/issues/135"
+
+ def handle(self, *args, **options):
+ # Step 1: Annotate each WebpushSubscriptionInfo with the minimum id for each group of auth and p256dh.
+ duplicates = (
+ SubscriptionInfo.objects.values("auth", "p256dh")
+ .annotate(min_id=Min("id"), count_id=Count("id"))
+ .filter(count_id__gt=1)
+ )
+ # Step 2: Collect the IDs of the entries with the lowest id in each group.
+ min_ids_to_delete = [entry["min_id"] for entry in duplicates]
+ # Step 3: Delete these entries.
+ SubscriptionInfo.objects.filter(id__in=min_ids_to_delete).delete()
diff --git a/auctions/migrations/0147_auction_message_users_when_lots_sell_and_more.py b/auctions/migrations/0147_auction_message_users_when_lots_sell_and_more.py
new file mode 100644
index 0000000..7a47616
--- /dev/null
+++ b/auctions/migrations/0147_auction_message_users_when_lots_sell_and_more.py
@@ -0,0 +1,56 @@
+# Generated by Django 5.0.8 on 2024-09-13 20:34
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("auctions", "0146_auction_auto_add_images_userdata_auto_add_images_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="auction",
+ name="message_users_when_lots_sell",
+ field=models.BooleanField(
+ blank=True,
+ default=True,
+ help_text="When you enter a lot number on the set lot winners screen, send a notification to any users watching that lot",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="auction",
+ name="auto_add_images",
+ field=models.BooleanField(
+ blank=True,
+ default=True,
+ help_text="Images taken from older lots with the same name in any auctions created by you or other admins",
+ verbose_name="Automatically add images to lots",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="lot",
+ name="buy_now_price",
+ field=models.PositiveIntegerField(
+ blank=True,
+ default=None,
+ help_text="This lot will be sold with no bidding for this price, if someone is willing to pay this much",
+ null=True,
+ validators=[
+ django.core.validators.MinValueValidator(1),
+ django.core.validators.MaxValueValidator(1000),
+ ],
+ ),
+ ),
+ migrations.AlterField(
+ model_name="userdata",
+ name="auto_add_images",
+ field=models.BooleanField(
+ blank=True,
+ default=True,
+ help_text="If another lot with the same name has been added previously. Images are only added to lots that are part of an auction.",
+ verbose_name="Automatically add images to my lots",
+ ),
+ ),
+ ]
diff --git a/auctions/migrations/0148_userdata_push_notifications_when_lots_sell.py b/auctions/migrations/0148_userdata_push_notifications_when_lots_sell.py
new file mode 100644
index 0000000..5bc5660
--- /dev/null
+++ b/auctions/migrations/0148_userdata_push_notifications_when_lots_sell.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.8 on 2024-09-14 00:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("auctions", "0147_auction_message_users_when_lots_sell_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="userdata",
+ name="push_notifications_when_lots_sell",
+ field=models.BooleanField(
+ blank=True,
+ default=False,
+ help_text="For in-person auctions, get a notification when bidding starts on a lot that you've watched",
+ ),
+ ),
+ ]
diff --git a/auctions/models.py b/auctions/models.py
index 05a0487..e3db2b2 100755
--- a/auctions/models.py
+++ b/auctions/models.py
@@ -4,6 +4,8 @@
from datetime import time
from random import randint
+import channels.layers
+from asgiref.sync import async_to_sync
from autoslug import AutoSlugField
from django.conf import settings
from django.contrib.auth.models import User
@@ -648,6 +650,10 @@ class Auction(models.Model):
auto_add_images.help_text = (
"Images taken from older lots with the same name in any auctions created by you or other admins"
)
+ message_users_when_lots_sell = models.BooleanField(default=True, blank=True)
+ message_users_when_lots_sell.help_text = (
+ "When you enter a lot number on the set lot winners screen, send a notification to any users watching that lot"
+ )
def __str__(self):
result = self.title
@@ -785,7 +791,8 @@ def user_admin_link(self):
@property
def set_lot_winners_link(self):
- return f"{self.get_absolute_url()}lots/set-winners/{self.set_lot_winners_url}"
+ # return f"{self.get_absolute_url()}lots/set-winners/{self.set_lot_winners_url}"
+ return f"{self.get_absolute_url()}lots/set-winners/"
def permission_check(self, user):
"""See if `user` can make changes to this auction"""
@@ -1925,7 +1932,9 @@ class Lot(models.Model):
blank=True,
null=True,
)
- buy_now_price.help_text = "This lot will be sold instantly for this price if someone is willing to pay this much. Leave blank unless you know exactly what you're doing"
+ buy_now_price.help_text = (
+ "This lot will be sold with no bidding for this price, if someone is willing to pay this much"
+ )
species = models.ForeignKey(Product, null=True, blank=True, on_delete=models.SET_NULL)
species_category = models.ForeignKey(
Category,
@@ -2102,10 +2111,11 @@ def __str__(self):
def add_winner_message(self, user, tos, winning_price):
"""Create a lot history message when a winner is declared (or changed)
It's critical that this function is called every time the winner is changed so that invoices get recalculated"""
+ message = f"{user.username} has set bidder {tos} as the winner of this lot (${winning_price})"
LotHistory.objects.create(
lot=self,
user=user,
- message=f"{user.username} has set bidder {tos} as the winner of this lot (${winning_price}).",
+ message=message,
notification_sent=True,
bid_amount=winning_price,
changed_price=True,
@@ -2113,6 +2123,20 @@ def add_winner_message(self, user, tos, winning_price):
)
invoice, created = Invoice.objects.get_or_create(auctiontos_user=tos, auction=self.auction, defaults={})
invoice.recalculate
+ self.send_websocket_message(
+ {
+ "type": "chat_message",
+ "info": "LOT_END_WINNER",
+ "message": message,
+ "high_bidder_pk": tos.user.pk if tos.user else -1,
+ "high_bidder_name": tos.display_name_for_admins,
+ "current_high_bid": winning_price,
+ }
+ )
+
+ def send_websocket_message(self, message):
+ channel_layer = channels.layers.get_channel_layer()
+ async_to_sync(channel_layer.group_send)(f"lot_{self.pk}", message)
def refund(self, amount, user, message=None):
"""Call this to add a message when refunding a lot"""
@@ -2277,20 +2301,6 @@ def winner_email(self):
return self.winner.email
return ""
- @property
- def table_class(self):
- """We need to color rows red if a winner is set without a price, or a price is set without a winner"""
- if self.auctiontos_winner and not self.winning_price:
- return "table-danger"
- if not self.auctiontos_winner and self.winning_price:
- return "table-danger"
- return ""
-
- # @property
- # def user_as_str(self):
- # """String value of the seller of this lot"""
- # return str(self.user)
-
@property
def seller_as_str(self):
"""String of the seller name or number, for use on lot pages"""
@@ -2369,6 +2379,20 @@ def winner_as_str(self):
return "Anonymous"
return ""
+ @property
+ def sell_to_online_high_bidder(self):
+ if self.high_bidder:
+ self.winner = self.high_bidder
+ self.winning_price = self.high_bid
+ self.active = False
+ tos = AuctionTOS.objects.filter(auction=self.auction, user=self.high_bidder).order_by("-createdon").first()
+ if tos:
+ self.auctiontos_winner = tos
+ self.save()
+ return f"{self.high_bidder_for_admins} is now the winner of lot {self.custom_lot_number} for ${self.winning_price}"
+ else:
+ return "No high bidder"
+
@property
def sold(self):
if self.winner or self.auctiontos_winner:
@@ -2801,11 +2825,10 @@ def auto_image(self):
@property
def thumbnail(self):
- try:
- return LotImage.objects.get(lot_number=self.lot_number, is_primary=True)
- except:
- pass
- return None
+ default = LotImage.objects.filter(lot_number=self.lot_number, is_primary=True).first()
+ if default:
+ return default
+ return self.auto_image
def get_absolute_url(self):
return f"/lots/{self.lot_number}/{self.slug}/"
@@ -2929,6 +2952,28 @@ def video_link(self):
return match.group(1)
return None
+ @property
+ def create_update_invoices(self):
+ """Call whenever ending this lot, or when creating it"""
+ if self.auction and self.winner and not self.auctiontos_winner:
+ tos = AuctionTOS.objects.filter(auction=self.auction, user=self.winner).first()
+ self.auctiontos_winner = tos
+ self.save()
+ if self.auction and self.auctiontos_winner:
+ invoice, created = Invoice.objects.get_or_create(
+ auctiontos_user=self.auctiontos_winner,
+ auction=self.auction,
+ defaults={},
+ )
+ invoice.recalculate
+ if self.auction and self.auctiontos_seller:
+ invoice, created = Invoice.objects.get_or_create(
+ auctiontos_user=self.auctiontos_seller,
+ auction=self.auction,
+ defaults={},
+ )
+ invoice.recalculate
+
class Invoice(models.Model):
"""
@@ -3497,9 +3542,7 @@ class UserData(models.Model):
)
last_ip_address = models.CharField(max_length=100, blank=True, null=True)
email_me_when_people_comment_on_my_lots = models.BooleanField(default=True, blank=True)
- email_me_when_people_comment_on_my_lots.help_text = (
- "Notifications will be sent once a day, only for messages you haven't seen"
- )
+ email_me_when_people_comment_on_my_lots.help_text = "Notifications will be sent once a day, only for messages you haven't seen. If you'd like to get a notification right away, leave a comment here"
email_me_about_new_auctions = models.BooleanField(
default=True, blank=True, verbose_name="Email me about new online auctions"
)
@@ -3573,6 +3616,8 @@ class UserData(models.Model):
share_lot_images.help_text = "Images will be added to other lots without an image that have the same name"
auto_add_images = models.BooleanField("Automatically add images to my lots", default=True, blank=True)
auto_add_images.help_text = "If another lot with the same name has been added previously. Images are only added to lots that are part of an auction."
+ push_notifications_when_lots_sell = models.BooleanField(default=False, blank=True)
+ push_notifications_when_lots_sell.help_text = "For in-person auctions, get a notification when bidding starts on a lot that you've watched"
# breederboard info
rank_unique_species = models.PositiveIntegerField(null=True, blank=True)
@@ -3596,6 +3641,10 @@ class UserData(models.Model):
def __str__(self):
return f"{self.user.username}'s data"
+ def send_websocket_message(self, message):
+ channel_layer = channels.layers.get_channel_layer()
+ async_to_sync(channel_layer.group_send)(f"user_{self.user.pk}", message)
+
@property
def my_lots_qs(self):
"""All lots this user submitted, whether in an auction, or independently"""
diff --git a/auctions/routing.py b/auctions/routing.py
index c53fc6e..31cf00a 100755
--- a/auctions/routing.py
+++ b/auctions/routing.py
@@ -1,10 +1,9 @@
-# chat/routing.py
-from django.urls import re_path
+# # chat/routing.py
+# from django.urls import re_path
-from . import consumers
+# from . import consumers
-print("routing")
-websocket_urlpatterns = [
- re_path(r"ws/lots/(?P\w+)/$", consumers.LotConsumer.as_asgi()),
-]
+# websocket_urlpatterns = [
+# re_path(r"ws/lots/(?P\w+)/$", consumers.LotConsumer.as_asgi()),
+# ]
diff --git a/auctions/static/webpush/new.json b/auctions/static/webpush/new.json
new file mode 100644
index 0000000..7e8c553
--- /dev/null
+++ b/auctions/static/webpush/new.json
@@ -0,0 +1 @@
+{"1": 2}
diff --git a/auctions/static/webpush/webpush.js b/auctions/static/webpush/webpush.js
new file mode 100644
index 0000000..9656021
--- /dev/null
+++ b/auctions/static/webpush/webpush.js
@@ -0,0 +1,212 @@
+// Based On https://github.com/chrisdavidmills/push-api-demo/blob/283df97baf49a9e67705ed08354238b83ba7e9d3/main.js
+
+var isPushEnabled = false,
+ registration,
+ subBtn;
+
+window.addEventListener('load', function() {
+ subBtn = document.getElementById('webpush-subscribe-button');
+
+ subBtn.textContent = gettext('Subscribe to Push Messaging');
+
+ subBtn.addEventListener('click',
+ function() {
+ subBtn.disabled = true;
+ if (isPushEnabled) {
+ return unsubscribe(registration);
+ }
+ return subscribe(registration);
+ }
+ );
+
+ // Do everything if the Browser Supports Service Worker
+ if ('serviceWorker' in navigator) {
+ const serviceWorker = document.querySelector('meta[name="service-worker-js"]').content;
+ navigator.serviceWorker.register(serviceWorker).then(
+ function(reg) {
+ registration = reg;
+ initialiseState(reg);
+ });
+ }
+ // If service worker not supported, show warning to the message box
+ else {
+ showMessage(gettext('Service workers are not supported in your browser.'));
+ }
+
+ // Once the service worker is registered set the initial state
+ function initialiseState(reg) {
+ // Are Notifications supported in the service worker?
+ if (!(reg.showNotification)) {
+ // Show a message and activate the button
+ showMessage(gettext('Showing notifications are not supported in your browser.'));
+ return;
+ }
+
+ // Check the current Notification permission.
+ // If its denied, it's a permanent block until the
+ // user changes the permission
+ if (Notification.permission === 'denied') {
+ // Show a message and activate the button
+ subBtn.disabled = false;
+ showMessage(gettext('Push notifications are blocked by your browser.'));
+ return;
+ }
+
+ // Check if push messaging is supported
+ if (!('PushManager' in window)) {
+ // Show a message and activate the button
+ subBtn.disabled = false;
+ showMessage(gettext('Push notifications are not available in your browser.'));
+ return;
+ }
+
+ // We need to get subscription state for push notifications and send the information to server
+ reg.pushManager.getSubscription().then(
+ function(subscription) {
+ if (subscription){
+ postSubscribeObj('subscribe', subscription,
+ function(response) {
+ // Check the information is saved successfully into server
+ if (response.status === 201) {
+ // Show unsubscribe button instead
+ subBtn.textContent = gettext('Unsubscribe from Push Messaging');
+ subBtn.disabled = false;
+ isPushEnabled = true;
+ showMessage(gettext('Successfully subscribed to push notifications.'));
+ }
+ });
+ }
+ });
+ }
+}
+);
+
+function showMessage(message) {
+ const messageBox = document.getElementById('webpush-message');
+ if (messageBox) {
+ messageBox.textContent = message;
+ messageBox.style.display = 'block';
+ }
+}
+
+function subscribe(reg) {
+ // Get the Subscription or register one
+ reg.pushManager.getSubscription().then(
+ function(subscription) {
+ var metaObj, applicationServerKey, options;
+ // Check if Subscription is available
+ if (subscription) {
+ return subscription;
+ }
+
+ metaObj = document.querySelector('meta[name="django-webpush-vapid-key"]');
+ applicationServerKey = metaObj.content;
+ options = {
+ userVisibleOnly: true
+ };
+ if (applicationServerKey){
+ options.applicationServerKey = urlB64ToUint8Array(applicationServerKey)
+ }
+ // If not, register one
+ reg.pushManager.subscribe(options)
+ .then(
+ function(subscription) {
+ postSubscribeObj('subscribe', subscription,
+ function(response) {
+ // Check the information is saved successfully into server
+ if (response.status === 201) {
+ // Show unsubscribe button instead
+ subBtn.textContent = gettext('Unsubscribe from Push Messaging');
+ subBtn.disabled = false;
+ isPushEnabled = true;
+ showMessage(gettext('Successfully subscribed to push notifications.'));
+ }
+ });
+ })
+ .catch(
+ function() {
+ console.log(gettext('Error while subscribing to push notifications.'), arguments)
+ })
+ }
+ );
+}
+
+function urlB64ToUint8Array(base64String) {
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
+ const base64 = (base64String + padding)
+ .replace(/\-/g, '+')
+ .replace(/_/g, '/');
+
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (var i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+}
+
+function unsubscribe(reg) {
+ // Get the Subscription to unregister
+ reg.pushManager.getSubscription()
+ .then(
+ function(subscription) {
+
+ // Check we have a subscription to unsubscribe
+ if (!subscription) {
+ // No subscription object, so set the state
+ // to allow the user to subscribe to push
+ subBtn.disabled = false;
+ showMessage(gettext('Subscription is not available.'));
+ return;
+ }
+ postSubscribeObj('unsubscribe', subscription,
+ function(response) {
+ // Check if the information is deleted from server
+ if (response.status === 202) {
+ // Get the Subscription
+ // Remove the subscription
+ subscription.unsubscribe()
+ .then(
+ function(successful) {
+ subBtn.textContent = gettext('Subscribe to Push Messaging');
+ showMessage(gettext('Successfully unsubscribed from push notifications.'));
+ isPushEnabled = false;
+ subBtn.disabled = false;
+ }
+ )
+ .catch(
+ function(error) {
+ subBtn.textContent = gettext('Unsubscribe from Push Messaging');
+ showMessage(gettext('Error while unsubscribing from push notifications.'));
+ subBtn.disabled = false;
+ }
+ );
+ }
+ });
+ }
+ )
+}
+
+function postSubscribeObj(statusType, subscription, callback) {
+ // Send the information to the server with fetch API.
+ // the type of the request, the name of the user subscribing,
+ // and the push subscription endpoint + key the server needs
+ // to send push messages
+
+ var browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(),
+ user_agent = navigator.userAgent,
+ data = { status_type: statusType,
+ subscription: subscription.toJSON(),
+ browser: browser,
+ user_agent: user_agent,
+ group: subBtn.dataset.group
+ };
+
+ fetch(subBtn.dataset.url, {
+ method: 'post',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify(data),
+ credentials: 'include'
+ }).then(callback);
+}
diff --git a/auctions/static/webpush/webpush_serviceworker.js b/auctions/static/webpush/webpush_serviceworker.js
new file mode 100644
index 0000000..2eaeaba
--- /dev/null
+++ b/auctions/static/webpush/webpush_serviceworker.js
@@ -0,0 +1,20 @@
+
+// Register event listener for the 'push' event.
+self.addEventListener('push', function(event) {
+ // Retrieve the textual payload from event.data (a PushMessageData object).
+ // Other formats are supported (ArrayBuffer, Blob, JSON), check out the documentation
+ // on https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData.
+ var payload = event.data ? event.data.text() : {"head": "No Content", "Body": "No Content"},
+ data = JSON.parse(payload),
+ head = data.head,
+ body = data.body;
+
+ // Keep the service worker alive until the notification is created.
+ event.waitUntil(
+ // Show a notification with title 'ServiceWorker Cookbook' and use the payload
+ // as the body.
+ self.registration.showNotification(head, {
+ body: body
+ })
+ );
+});
diff --git a/auctions/templates/all_auctions.html b/auctions/templates/all_auctions.html
index a87fcee..998718d 100755
--- a/auctions/templates/all_auctions.html
+++ b/auctions/templates/all_auctions.html
@@ -22,6 +22,7 @@
Auctions
{% if last_auction_used %}
{{ last_auction_used.title }} Your last auction
+ {% if not last_auction_used.promote_this_auction %}Not promoted{% endif %}
{{ last_auction_used.template_lot_link_first_column }}
{{ last_auction_used.template_promo_info }}
diff --git a/auctions/templates/auctions/dynamic_set_lot_winner.html b/auctions/templates/auctions/dynamic_set_lot_winner.html
new file mode 100755
index 0000000..ce48e54
--- /dev/null
+++ b/auctions/templates/auctions/dynamic_set_lot_winner.html
@@ -0,0 +1,191 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% block title %}Set winners for {{ auction }}{% endblock %}
+
+{% block extra_js %}
+
+{% endblock %}
+{% block content %}
+
+{% include 'auction_ribbon.html' %}
+
+Use this page to mark lots as sold. Press tab to move the to next field. Leave feedback about this page here.
+
+
+
+{% endblock %}
+{% block undiv_content %}
+
Promo weight: {{lot.promotion_weight}}
{% endif %}
{% if request.user.is_authenticated %}
- ★ Watching{% else %}>☆ Watch{% endif %}
+ Watching{% else %}> Watch{% endif %}
+ {% if push_notifications_possible %}
+
+ {% if watched and request.user.userdata.push_notifications_when_lots_sell %}
+ You'll get a notification when bidding starts on this lot
+
+ {% endif %}
+
+
+ {% webpush_button with_class="btn btn-primary btn-sm" %} to get a notification when bidding starts on this lot
+
+
+
+
+
+
+
How's this work?
+
+
+
+ You can get a notification on your phone lots you've watched go up for bidding.
+ Note that this will only work if the person running this auction enters the lot number when bidding starts -- Some clubs enter the lot number only after bidding has ended, in which case you'll get a notification only after the lot has sold.
+ You don't need to have auction.fish open to see the notification, it'll come up automatically!
+