pycountry integration?

This commit is contained in:
pacnpal
2024-10-30 20:04:04 +00:00
parent 2d98c5b896
commit 77aa70f611
10 changed files with 154 additions and 133 deletions

View File

@@ -8,6 +8,8 @@ urlpatterns = [
path('', views.ParkListView.as_view(), name='park_list'),
path('create/', views.ParkCreateView.as_view(), name='park_create'),
path('rides/', RideListView.as_view(), name='all_rides'), # Global rides list
path('countries/search/', views.search_countries, name='search_countries'),
path('countries/select/', views.select_country, name='select_country'),
path('<slug:slug>/', views.ParkDetailView.as_view(), name='park_detail'),
path('<slug:park_slug>/rides/', include('rides.urls', namespace='rides')),
]

View File

@@ -1,15 +1,17 @@
from django.views.generic import DetailView, ListView, CreateView
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render
from django.core.serializers.json import DjangoJSONEncoder
from django.urls import reverse
from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.http import JsonResponse, HttpResponseRedirect
from django.http import JsonResponse, HttpResponseRedirect, HttpResponse
from .models import Park, ParkArea
from rides.models import Ride
from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin
from moderation.models import EditSubmission
import pycountry
class ParkCreateView(LoginRequiredMixin, CreateView):
model = Park
@@ -42,6 +44,37 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
def get_success_url(self):
return reverse('park_detail', kwargs={'slug': self.object.slug})
def search_countries(request):
query = request.GET.get('q', '').strip()
countries = []
if query:
# Use pycountry's search functionality for fuzzy matching
try:
# Try exact search first
country = pycountry.countries.get(name=query)
if country:
countries = [country]
else:
# If no exact match, try fuzzy search
countries = pycountry.countries.search_fuzzy(query)
except LookupError:
# If search fails, fallback to manual filtering
countries = [
country for country in pycountry.countries
if query.lower() in country.name.lower()
]
return render(request, 'parks/partials/country_search_results.html', {
'countries': countries[:10] # Limit to top 10 results
})
def select_country(request):
if request.method == 'POST':
country = request.POST.get('country', '')
return HttpResponse(country)
return HttpResponse('Invalid request', status=400)
class ParkDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, InlineEditMixin, HistoryMixin, DetailView):
model = Park
template_name = 'parks/park_detail.html'
@@ -106,13 +139,15 @@ class ParkListView(ListView):
def get_queryset(self):
queryset = Park.objects.select_related('owner').prefetch_related('photos', 'rides')
# Apply filters
search = self.request.GET.get('search', '').strip()
location = self.request.GET.get('location', '').strip()
status = self.request.GET.get('status', '').strip()
search = self.request.GET.get('search', '').strip() or None
location = self.request.GET.get('location', '').strip() or None
status = self.request.GET.get('status', '').strip() or None
if search:
queryset = queryset.filter(name__icontains=search) | queryset.filter(location__icontains=search)
queryset = queryset.filter(
Q(name__icontains=search) |
Q(location__icontains=search)
)
if location:
queryset = queryset.filter(location=location)
if status:

View File

@@ -17,7 +17,21 @@
<label for="{{ field.id_for_label }}" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
</label>
{% if field.field.widget.input_type == 'text' or field.field.widget.input_type == 'date' %}
{% if field.name == 'country' %}
<div class="relative">
<input type="text"
name="{{ field.name }}"
id="country-input"
value="{{ field.value|default:'' }}"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
hx-get="{% url 'parks:search_countries' %}"
hx-trigger="keyup changed delay:200ms"
hx-target="#country-results"
hx-params="q"
{% if field.field.required %}required{% endif %}>
<div id="country-results" class="relative"></div>
</div>
{% elif field.field.widget.input_type == 'text' or field.field.widget.input_type == 'date' %}
<input type="{{ field.field.widget.input_type }}"
name="{{ field.name }}"
id="{{ field.id_for_label }}"

View File

@@ -16,7 +16,11 @@
<!-- Filters -->
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<form method="get" class="grid grid-cols-1 gap-4 md:grid-cols-4">
<form class="grid grid-cols-1 gap-4 md:grid-cols-3"
hx-get="{% url 'parks:park_list' %}"
hx-trigger="change from:select, input from:input[type='text']"
hx-target="#parks-grid"
hx-push-url="true">
<div>
<label for="search" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
<input type="text" name="search" id="search"
@@ -49,91 +53,12 @@
<option value="RELOCATED" {% if current_filters.status == 'RELOCATED' %}selected{% endif %}>Relocated</option>
</select>
</div>
<div class="flex items-end">
<button type="submit" class="w-full btn-primary">
<i class="mr-2 fas fa-filter"></i>Filter
</button>
</div>
</form>
</div>
<!-- Parks Grid -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for park in parks %}
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
{% if park.photos.exists %}
<div class="aspect-w-16 aspect-h-9">
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="object-cover w-full">
</div>
{% endif %}
<div class="p-4">
<h2 class="mb-2 text-xl font-bold">
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ park.name }}
</a>
</h2>
<p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{{ park.location }}
</p>
<div class="flex flex-wrap gap-2">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
<div class="flex items-center justify-between mt-4">
<a href="{% url 'parks:rides:ride_list' park.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.rides.count }} attractions <i class="ml-1 fas fa-arrow-right"></i>
</a>
{% if park.owner %}
<a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.owner.name }}
</a>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="col-span-3 py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">No parks found matching your criteria.</p>
</div>
{% endfor %}
<div id="parks-grid">
{% include "parks/partials/park_list.html" %}
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="flex justify-center mt-6">
<div class="inline-flex rounded-md shadow-sm">
{% if page_obj.has_previous %}
<a href="?page=1{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">&laquo; First</a>
<a href="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Previous</a>
{% endif %}
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Next</a>
<a href="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Last &raquo;</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<div class="absolute z-10 w-full bg-white border rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600">
{% for country in countries %}
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
hx-post="{% url 'parks:select_country' %}"
hx-target="#country-input"
hx-swap="value"
hx-vals='{"country": "{{ country.name }}"}'>
{{ country.name }}
</div>
{% empty %}
<div class="px-4 py-2 text-gray-500 dark:text-gray-400">
No countries found
</div>
{% endfor %}
</div>

View File

@@ -1,48 +1,78 @@
{% for park in parks %}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden transform transition-transform hover:-translate-y-1">
{% if park.photos.first %}
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="w-full h-48 object-cover">
{% else %}
<div class="w-full h-48 bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span class="text-gray-400">No image available</span>
</div>
{% endif %}
<div class="p-4">
<h3 class="text-xl font-semibold mb-2">
<a href="{% url 'parks:park_detail' slug=park.slug %}"
class="text-blue-600 dark:text-blue-400 hover:underline">{{ park.name }}</a>
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-2">{{ park.location }}</p>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ park.rides.count }} attractions
</span>
{% if park.average_rating %}
<div class="flex items-center">
<span class="text-yellow-400 mr-1"></span>
<span class="text-gray-600 dark:text-gray-400">{{ park.average_rating|floatformat:1 }}/10</span>
<!-- Parks Grid -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for park in parks %}
<div class="overflow-hidden transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1">
{% if park.photos.exists %}
<div class="aspect-w-16 aspect-h-9">
<img src="{{ park.photos.first.image.url }}"
alt="{{ park.name }}"
class="object-cover w-full">
</div>
{% endif %}
</div>
{% if park.status != 'OPERATING' %}
<div class="mt-2">
<span class="px-2 py-1 text-xs rounded-full
{% if park.status == 'CLOSED_TEMP' %}bg-yellow-100 text-yellow-800
{% elif park.status == 'CLOSED_PERM' %}bg-red-100 text-red-800
{% elif park.status == 'UNDER_CONSTRUCTION' %}bg-blue-100 text-blue-800
{% elif park.status == 'DEMOLISHED' %}bg-gray-100 text-gray-800
{% endif %}">
{{ park.get_status_display }}
</span>
<div class="p-4">
<h2 class="mb-2 text-xl font-bold">
<a href="{% url 'parks:park_detail' park.slug %}"
class="text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ park.name }}
</a>
</h2>
<p class="mb-3 text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i>
{{ park.location }}
</p>
<div class="flex flex-wrap gap-2">
<span class="status-badge {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
{% elif park.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif park.status == 'DEMOLISHED' %}status-demolished
{% elif park.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ park.get_status_display }}
</span>
{% if park.average_rating %}
<span class="text-yellow-800 bg-yellow-100 status-badge dark:bg-yellow-400/30 dark:text-yellow-200 dark:ring-1 dark:ring-yellow-400/30">
<i class="mr-1 text-yellow-500 fas fa-star dark:text-yellow-300"></i>
{{ park.average_rating|floatformat:1 }}/10
</span>
{% endif %}
</div>
<div class="flex items-center justify-between mt-4">
<a href="{% url 'parks:rides:ride_list' park.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.rides.count }} attractions <i class="ml-1 fas fa-arrow-right"></i>
</a>
{% if park.owner %}
<a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.owner.name }}
</a>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="col-span-3 py-8 text-center">
<p class="text-gray-500 dark:text-gray-400">No parks found matching your criteria.</p>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if is_paginated %}
<div class="flex justify-center mt-6">
<div class="inline-flex rounded-md shadow-sm">
{% if page_obj.has_previous %}
<a href="?page=1{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">&laquo; First</a>
<a href="?page={{ page_obj.previous_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Previous</a>
{% endif %}
<span class="px-3 py-2 text-gray-700 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border-t border-b border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Next</a>
<a href="?page={{ page_obj.paginator.num_pages }}{{ request.GET.urlencode }}" class="px-3 py-2 text-gray-700 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700">Last &raquo;</a>
{% endif %}
</div>
</div>
{% empty %}
<div class="col-span-full text-center py-8">
<p class="text-gray-500 dark:text-gray-400">No parks found matching your criteria.</p>
</div>
{% endfor %}
{% endif %}

View File

@@ -12,7 +12,7 @@ SECRET_KEY = 'django-insecure-=0)^0#h#k$0@$8$ys=^$0#h#k$0@$8$ys=^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'thrillwiki.com']
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'thrillwiki.com', 'beta.thrillwiki.com']
# Application definition
INSTALLED_APPS = [