diff --git a/auctions/filters.py b/auctions/filters.py index ffe43fe..0c3c64f 100755 --- a/auctions/filters.py +++ b/auctions/filters.py @@ -32,6 +32,37 @@ ) +class AuctionFilter(django_filters.FilterSet): + """Filter for the main auctions list""" + + query = django_filters.CharFilter( + method="auction_search", + label="", + widget=TextInput( + attrs={ + "placeholder": "Filter by auction name, or type a number to see nearby auctions", + "hx-get": "", + "hx-target": "div.table-container", + "hx-trigger": "keyup changed delay:300ms", + "hx-swap": "outerHTML", + "hx-indicator": ".progress", + } + ), + ) + + class Meta: + model = Auction + fields = [] # nothing here so no buttons show up + + def auction_search(self, queryset, name, value): + if value == "joined": + return queryset.exclude(joined=False).exclude(joined=0) + if value.isnumeric(): + return queryset.filter(distance__lte=int(value)) + else: + return queryset.filter(title__icontains=value) + + class AuctionTOSFilter(django_filters.FilterSet): """This filter is used on any admin views that allow adding users to an auction and on lot creation/winner screens""" diff --git a/auctions/models.py b/auctions/models.py index 955bfe9..2c7f812 100755 --- a/auctions/models.py +++ b/auctions/models.py @@ -1073,12 +1073,12 @@ def template_lot_link(self): @property def template_lot_link_first_column(self): """Shown on small screens only""" - return mark_safe(f'
{self.template_lot_link}
') + return mark_safe(f'
{self.template_lot_link}
') @property def template_lot_link_seperate_column(self): """Shown on big screens only""" - return mark_safe(f'{self.template_lot_link}') + return mark_safe(f'{self.template_lot_link}') @property def can_submit_lots(self): diff --git a/auctions/tables.py b/auctions/tables.py index 731cf8e..591b75e 100644 --- a/auctions/tables.py +++ b/auctions/tables.py @@ -1,8 +1,9 @@ import django_tables2 as tables from django.urls import reverse +from django.utils import formats from django.utils.safestring import mark_safe -from .models import AuctionTOS, Lot +from .models import Auction, AuctionTOS, Lot class AuctionTOSHTMxTable(tables.Table): @@ -159,6 +160,56 @@ class Meta: # } +class AuctionHTMxTable(tables.Table): + hide_string = "d-md-table-cell d-none" + + auction = tables.Column(accessor="title", verbose_name="Auction") + date = tables.Column(accessor="date_start", verbose_name="Status") + lots = tables.Column( + accessor="template_lot_link_seperate_column", + verbose_name="Lots", + orderable=False, + attrs={"th": {"class": hide_string}, "cell": {"class": hide_string}}, + ) + + def render_date(self, value, record): + localized_date = formats.date_format(record.template_date_timestamp, use_l10n=True) + return mark_safe(f"{record.template_status}{localized_date}{record.ended_badge}") + + def render_auction(self, value, record): + auction = record + result = f"{auction.title}
" + if auction.is_last_used: + result += " Your last auction" + if auction.is_online: + result += " Online" + if not auction.promote_this_auction: + result += " Not promoted" + if auction.distance: + result += f" {int(auction.distance)} miles from you" + if auction.joined: + result += " Joined" + result += auction.template_lot_link_first_column + auction.template_promo_info + return mark_safe(result) + + class Meta: + model = Auction + template_name = "tables/bootstrap_htmx.html" + fields = ( + "auction", + "date", + "lots", + ) + row_attrs = { + # 'class': lambda record: str(record.table_class), + # 'style':'cursor:pointer;', + # 'hx-get': lambda record: "/api/lot/" + str(record.pk), + # 'hx-target':"#modals-here", + # 'hx-trigger':"click", + # '_':"on htmx:afterOnLoad wait 10ms then add .show to #modal then add .show to #modal-backdrop" + } + + class LotHTMxTableForUsers(tables.Table): hide_string = "d-md-table-cell d-none" # seller = tables.Column(accessor='auctiontos_seller', verbose_name="Seller") diff --git a/auctions/templates/all_auctions.html b/auctions/templates/all_auctions.html index 998718d..7a28201 100755 --- a/auctions/templates/all_auctions.html +++ b/auctions/templates/all_auctions.html @@ -1,64 +1,27 @@ {% extends "base.html" %} +{% load render_table from django_tables2 %} {% load crispy_forms_tags %} - {% block title %}Auctions {% endblock %} {% load static %} {% block content %} +

Auctions

+
This is a list of club auctions which have been created on this site.
+ Create a new auction
+
+ {% crispy filter.form %} +
+
+{% render_table table %} -

Auctions

-
This is a listing of club auctions which have been created on this site.
- Create a new auction
- - - - - - - - - - {% if last_auction_used %} - - - - {{ last_auction_used.template_lot_link_seperate_column }} - - {% endif %} - {% for auction in object_list %} - - - - {{ auction.template_lot_link_seperate_column }} - - {% endfor %} - -
AuctionDateLots
{{ 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 }} -
- {{ last_auction_used.template_status }} - {{ last_auction_used.template_date_timestamp }} - {{ last_auction_used.ended_badge }} -
{{ auction.title }}
{% if auction.is_online %}Online{% endif %} - {% if not auction.promote_this_auction %}Not promoted{% endif %} - {% if not location_message and auction.number_of_locations %}{{ auction.distance | floatformat:0 }} miles from you{% endif %} - {{ auction.template_lot_link_first_column }} - {{ auction.template_promo_info }} -
- {{ auction.template_status }} - {{ auction.template_date_timestamp }} - {{ auction.ended_badge }} -
-Note: Auctions you haven't joined won't appear in this list if they: +Auctions you've joined will always show up here. Other auctions will only be listed here if they: - Auctions you've joined will always show up here. + {% endblock %} {% block extra_js %}{% endblock %} diff --git a/auctions/templates/tables/table_generic.html b/auctions/templates/tables/table_generic.html index 29f4999..cbefafe 100644 --- a/auctions/templates/tables/table_generic.html +++ b/auctions/templates/tables/table_generic.html @@ -1,3 +1,8 @@ {% load render_table from django_tables2 %} - +{% if no_results %} +
+ {{ no_results | safe }} +
+{% else %} {% render_table table %} +{% endif %} diff --git a/auctions/urls.py b/auctions/urls.py index 7d47551..b18784d 100755 --- a/auctions/urls.py +++ b/auctions/urls.py @@ -133,8 +133,8 @@ ), path("images//delete/", views.ImageDelete.as_view(), name="delete_image"), path("images//edit", views.ImageUpdateView.as_view(), name="edit_image"), - path("auctions/", views.allAuctions.as_view(), name="auctions"), - path("auctions/all/", views.allAuctions.as_view()), + path("auctions/", views.AllAuctions.as_view(), name="auctions"), + path("auctions/all/", views.AllAuctions.as_view()), # path('auctions/new/', views.createAuction, name='createAuction'), path("auctions/new/", login_required(views.AuctionCreateView.as_view())), path("auctions//edit/", views.AuctionUpdate.as_view(), name="edit_auction"), diff --git a/auctions/views.py b/auctions/views.py index 080d809..81e28e9 100755 --- a/auctions/views.py +++ b/auctions/views.py @@ -26,6 +26,7 @@ from django.core.files.base import ContentFile from django.db.models import ( Avg, + Case, Count, Exists, ExpressionWrapper, @@ -36,6 +37,8 @@ Q, Subquery, Sum, + Value, + When, ) from django.db.models.base import Model as Model from django.db.models.functions import TruncDay @@ -83,6 +86,7 @@ from webpush.models import PushInformation from .filters import ( + AuctionFilter, AuctionTOSFilter, LotAdminFilter, LotFilter, @@ -151,7 +155,7 @@ median_value, nearby_auctions, ) -from .tables import AuctionTOSHTMxTable, LotHTMxTable, LotHTMxTableForUsers +from .tables import AuctionHTMxTable, AuctionTOSHTMxTable, LotHTMxTable, LotHTMxTableForUsers logger = logging.getLogger(__name__) @@ -320,6 +324,39 @@ def is_auction_admin(self): return result +class LocationMixin: + """For location aware views, adds a `get_coordinates()` function which returns a tuple of `latitude, longitude` based on self.request.cookies or userdata + + get_coordinates() should be called before get_context_data + make sure to set `view.no_location_message`""" + + # override this message in your view, it'll be shown to users without a location + no_location_message = "Click here to set your location" + + # don't set this, it'll get set automatically by get_coordinates() if the user does not have a cookie + _location_message = None + + def get_coordinates(self): + try: + latitude = float(self.request.COOKIES.get("latitude", 0)) + longitude = float(self.request.COOKIES.get("longitude", 0)) + except (ValueError, TypeError): + latitude, longitude = 0, 0 + + if latitude == 0 and longitude == 0: + self._location_message = self.no_location_message + + if self.request.user.is_authenticated: + latitude = self.request.user.userdata.latitude + longitude = self.request.user.userdata.longitude + return latitude, longitude + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["location_message"] = self._location_message + return context + + class ClickAd(RedirectView): def get_redirect_url(self, *args, **kwargs): try: @@ -4141,13 +4178,35 @@ def toAccount(request): return redirect(reverse("userpage", kwargs={"slug": request.user.username})) -class allAuctions(ListView): +class AllAuctions(LocationMixin, SingleTableMixin, FilterView): model = Auction - template_name = "all_auctions.html" - ordering = ["-date_end"] + no_location_message = "Set your location to see how far away auctions are" + table_class = AuctionHTMxTable + filterset_class = AuctionFilter + paginate_by = 100 + + def get_template_names(self): + if self.request.htmx: + template_name = "tables/table_generic.html" + else: + template_name = "all_auctions.html" + return template_name def get_queryset(self): - qs = Auction.objects.exclude(is_deleted=True).order_by("-date_start") + last_auction_pk = -1 + if self.request.user.is_authenticated and self.request.user.userdata.last_auction_used: + last_auction_pk = self.request.user.userdata.last_auction_used.pk + qs = ( + Auction.objects.exclude(is_deleted=True) + .annotate( + is_last_used=Case( + When(pk=last_auction_pk, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ) + .order_by("-is_last_used", "-date_start") + ) next_90_days = timezone.now() + timedelta(days=90) two_years_ago = timezone.now() - timedelta(days=365 * 2) standard_filter = Q( @@ -4155,19 +4214,7 @@ def get_queryset(self): date_start__lte=next_90_days, date_posted__gte=two_years_ago, ) - latitude = 0 - longitude = 0 - try: - latitude = self.request.COOKIES["latitude"] - longitude = self.request.COOKIES["longitude"] - except: - if self.request.user.is_authenticated: - userData, created = UserData.objects.get_or_create( - user=self.request.user, - defaults={}, - ) - latitude = userData.latitude - longitude = userData.longitude + latitude, longitude = self.get_coordinates() if latitude and longitude: closest_pickup_location_subquery = ( PickupLocation.objects.filter(auction=OuterRef("pk")) @@ -4176,33 +4223,36 @@ def get_queryset(self): .values("distance")[:1] ) qs = qs.annotate(distance=Subquery(closest_pickup_location_subquery)) - if self.request.user.is_superuser: - return qs.exclude(pk=self.request.user.userdata.last_auction_used.pk) + else: + qs = qs.annotate(distance=Value(0, output_field=FloatField())) if not self.request.user.is_authenticated: - return qs.filter(standard_filter) - qs = qs.filter( - Q(auctiontos__user=self.request.user) - | Q(auctiontos__email=self.request.user.email) - | Q(created_by=self.request.user) - | standard_filter - ).distinct() - if self.request.user.userdata.last_auction_used: - # union messes with ordering - qs = qs.exclude(pk=self.request.user.userdata.last_auction_used.pk) + return qs.filter(standard_filter).annotate(joined=Value(0, output_field=FloatField())).distinct() + qs = ( + qs.filter( + Q(auctiontos__user=self.request.user) + | Q(auctiontos__email=self.request.user.email) + | Q(created_by=self.request.user) + | standard_filter + ) + .annotate( + joined=Exists( + AuctionTOS.objects.filter( + auction=OuterRef("pk"), + user=self.request.user, + ) + ) + ) + .distinct() + ) return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - try: - self.request.COOKIES["longitude"] - except: - if self.request.user.is_authenticated: - context["location_message"] = "Set your location to get notifications about new auctions near you" - else: - context["location_message"] = "Set your location to see how far away auctions are" context["hide_google_login"] = True - if self.request.user.is_authenticated: - context["last_auction_used"] = self.request.user.userdata.last_auction_used + if not self.object_list.exists(): + context["no_results"] = ( + "No auctions found. This only searches club auctions, if you're looking for fish to buy, check out the list of lots for sale" + ) return context