mirror of
https://github.com/nikdoof/vapemap.git
synced 2025-12-22 14:19:23 +00:00
Initial Import.
This commit is contained in:
1
app/stores/__init__.py
Normal file
1
app/stores/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = '0.1'
|
||||
46
app/stores/admin.py
Normal file
46
app/stores/admin.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django.contrib import admin
|
||||
from .models import Chain, Store, Address, Brand, ClaimRequest
|
||||
|
||||
|
||||
class ChainAdmin(admin.ModelAdmin):
|
||||
list_filter = ['active']
|
||||
list_display = ['name']
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class StoreAdmin(admin.ModelAdmin):
|
||||
list_filter = ['chain', 'active']
|
||||
list_display = ['name', 'store_type', 'active']
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class ClaimAdmin(admin.ModelAdmin):
|
||||
list_filter = ['status']
|
||||
list_display = ['generic_obj', 'user', 'status', 'note']
|
||||
actions = ['approve_request']
|
||||
|
||||
|
||||
def approve_request(self, request, queryset):
|
||||
qs = queryset.filter(status=ClaimRequest.CLAIM_STATUS_PENDING)
|
||||
for obj in qs:
|
||||
obj.status = ClaimRequest.CLAIM_STATUS_APPROVED
|
||||
target = obj.generic_obj
|
||||
target.editor = obj.user
|
||||
target.save()
|
||||
obj.save()
|
||||
if qs.count() == 1:
|
||||
message_bit = "1 request was"
|
||||
else:
|
||||
message_bit = "%s requests were" % qs.count()
|
||||
self.message_user(request, "%s successfully approved." % message_bit)
|
||||
|
||||
approve_request.short_description = 'Approve selected requests.'
|
||||
|
||||
|
||||
admin.site.register(Chain, ChainAdmin)
|
||||
admin.site.register(Store, StoreAdmin)
|
||||
admin.site.register(Address, admin.ModelAdmin)
|
||||
admin.site.register(Brand, admin.ModelAdmin)
|
||||
admin.site.register(ClaimRequest, ClaimAdmin)
|
||||
16
app/stores/context_processors.py
Normal file
16
app/stores/context_processors.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.contrib.sites.models import Site
|
||||
from .models import ClaimRequest
|
||||
|
||||
def site(request):
|
||||
return {
|
||||
'site': Site.objects.get_current()
|
||||
}
|
||||
|
||||
|
||||
def pending_admin(request):
|
||||
if request.user.is_superuser:
|
||||
pending = ClaimRequest.objects.filter(status=ClaimRequest.CLAIM_STATUS_PENDING).count()
|
||||
return {
|
||||
'admin_pending_requests': pending
|
||||
}
|
||||
return {}
|
||||
1
app/stores/fixtures/counties.json
Normal file
1
app/stores/fixtures/counties.json
Normal file
File diff suppressed because one or more lines are too long
1
app/stores/fixtures/countries.json
Normal file
1
app/stores/fixtures/countries.json
Normal file
File diff suppressed because one or more lines are too long
55
app/stores/forms.py
Normal file
55
app/stores/forms.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django import forms
|
||||
from extra_views import InlineFormSet
|
||||
from epiceditor.widgets import EpicEditorWidget
|
||||
from .models import ClaimRequest, Store, Address
|
||||
|
||||
|
||||
class BootstrapModelForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BootstrapModelForm, self).__init__(*args, **kwargs)
|
||||
if hasattr(self.Meta, 'classes'):
|
||||
for field, css in self.Meta.classes.items():
|
||||
if field in self.fields:
|
||||
self.fields[field].widget.attrs['class'] = css
|
||||
|
||||
|
||||
class ClaimRequestForm(BootstrapModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ClaimRequestForm, self).__init__(*args, **kwargs)
|
||||
for field, css in self.Meta.classes.items():
|
||||
if field in self.fields:
|
||||
self.fields[field].widget.attrs['class'] = css
|
||||
|
||||
class Meta:
|
||||
model = ClaimRequest
|
||||
fields = ('note',)
|
||||
classes = {
|
||||
'note': 'input-xxlarge',
|
||||
}
|
||||
|
||||
|
||||
class StoreForm(BootstrapModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Store
|
||||
exclude = ('slug', 'address', 'chain', 'editor')
|
||||
classes = {
|
||||
'name': 'input-xxlarge',
|
||||
'long_description': 'input-xxlarge',
|
||||
}
|
||||
widgets = {
|
||||
'long_description': EpicEditorWidget(attrs={'rows': 40}, themes={'editor':'epic-light.css'})
|
||||
}
|
||||
|
||||
|
||||
class AddressForm(BootstrapModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Address
|
||||
exclude = ('name', 'geo_latitude', 'geo_longitude')
|
||||
|
||||
|
||||
class AddressInline(InlineFormSet):
|
||||
model = Address
|
||||
207
app/stores/models.py
Normal file
207
app/stores/models.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import re
|
||||
from django.db import models
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes import generic
|
||||
from .utils import caching_geo_lookup
|
||||
|
||||
USER_MODEL = get_user_model()
|
||||
|
||||
|
||||
class Chain(models.Model):
|
||||
name = models.CharField('Name', max_length=200)
|
||||
slug = models.SlugField('URL Slug', max_length=200, blank=True)
|
||||
head_office = models.ForeignKey('stores.Address', blank=True, null=True)
|
||||
website = models.URLField('Website', blank=True, null=True)
|
||||
editor = models.ForeignKey(USER_MODEL, related_name='editable_chains', blank=True, null=True)
|
||||
active = models.BooleanField('Active?', default=True)
|
||||
|
||||
long_description = models.TextField('Description', blank=True)
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.slug == '':
|
||||
self.slug = re.sub(r'\W+', '-', str(self.name).lower())
|
||||
return super(Chain, self).save(**kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('chain-detail', args=[self.slug])
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class Store(models.Model):
|
||||
|
||||
STORE_TYPE_RETAIL = 1
|
||||
STORE_TYPE_ONLINE = 2
|
||||
STORE_TYPE_BOTH = 3
|
||||
|
||||
STORE_TYPE_CHOICES = (
|
||||
(STORE_TYPE_RETAIL, 'Retail Only'),
|
||||
(STORE_TYPE_ONLINE, 'Online Only'),
|
||||
(STORE_TYPE_BOTH, 'Retail & Online'),
|
||||
)
|
||||
|
||||
name = models.CharField('Name', max_length=200, help_text="Store's full name")
|
||||
slug = models.SlugField('URL Slug', max_length=200, blank=True)
|
||||
address = models.ForeignKey('stores.Address', related_name='stores')
|
||||
store_type = models.IntegerField('Store Type', choices=STORE_TYPE_CHOICES)
|
||||
chain = models.ForeignKey(Chain, related_name='stores', null=True, blank=True)
|
||||
editor = models.ForeignKey(USER_MODEL, related_name='editable_stores', null=True, blank=True)
|
||||
active = models.BooleanField('Active?', default=True)
|
||||
|
||||
website = website = models.URLField('Website', null=True, blank=True)
|
||||
email = models.EmailField('Email', null=True, blank=True, help_text="Contact email address for the store.")
|
||||
phone = models.CharField('Phone', max_length=25, blank=True, help_text="Contact phone number for the store.")
|
||||
|
||||
long_description = models.TextField('Description', blank=True, help_text="Full description of the store, including any marketing material. Markdown supported.")
|
||||
brands = models.ManyToManyField('stores.Brand', null=True, blank=True, help_text="Brands that are sold by this store.")
|
||||
|
||||
def get_full_address(self):
|
||||
if self.address:
|
||||
return self.address.full_address
|
||||
|
||||
def get_location(self):
|
||||
if self.address:
|
||||
return self.address.geo_location
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.slug or self.slug == '':
|
||||
self.slug = re.sub(r'\W+', '-', str(self.name).lower())
|
||||
return super(Store, self).save(**kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('store-detail', args=[self.slug])
|
||||
|
||||
def __unicode__(self):
|
||||
if not self.name and self.chain:
|
||||
return self.chain.name
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class Brand(models.Model):
|
||||
name = models.CharField('Brand Name', max_length=200)
|
||||
website = models.URLField('Brand Website', null=True, blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class County(models.Model):
|
||||
"""
|
||||
UK Counties
|
||||
"""
|
||||
name = models.CharField('Name', max_length=100)
|
||||
|
||||
@property
|
||||
def stores(self):
|
||||
return Store.objects.filter(address__county__pk=self.pk)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class Country(models.Model):
|
||||
"""
|
||||
ISO Countries
|
||||
"""
|
||||
iso_code = models.CharField('ISO Code', max_length=3)
|
||||
name = models.CharField('Name', max_length=100)
|
||||
|
||||
@property
|
||||
def stores(self):
|
||||
return Store.objects.filter(address__country__pk=self.pk)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
class Address(models.Model):
|
||||
"""
|
||||
Represents a location or postal address
|
||||
"""
|
||||
name = models.CharField('Address Name', max_length=200)
|
||||
address1 = models.CharField('Address Line 1', max_length=200, help_text="First line of the address. e.g. <em>1 Station Road</em>")
|
||||
address2 = models.CharField('Address Line 2', max_length=200, blank=True, help_text="(Optional) Second line of the address")
|
||||
address3 = models.CharField('Address Line 3', max_length=200, blank=True, help_text="(Optional) Third line of the address")
|
||||
city = models.CharField('Town/City', max_length='50', help_text="City or town")
|
||||
county = models.ForeignKey('stores.County', related_name='addresses', null=True, blank=True, help_text="County or suburban area.")
|
||||
country = models.ForeignKey('stores.Country', related_name='addresses')
|
||||
postcode = models.CharField('Postcode', max_length=20, help_text="Post Code, e.g. <em>M1 1AA</em>")
|
||||
|
||||
geo_latitude = models.FloatField('Latitude', null=True, blank=True)
|
||||
geo_longitude = models.FloatField('Longitude', null=True, blank=True)
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
return u', '.join([
|
||||
self.address1,
|
||||
self.address2,
|
||||
self.address3,
|
||||
self.city,
|
||||
unicode(self.county),
|
||||
self.postcode,
|
||||
unicode(self.country),
|
||||
])
|
||||
|
||||
@property
|
||||
def address_string(self):
|
||||
return u', '.join([
|
||||
self.address1,
|
||||
self.postcode,
|
||||
unicode(self.country),
|
||||
])
|
||||
|
||||
@property
|
||||
def geo_location(self):
|
||||
return Point(self.geo_longitude, self.geo_latitude)
|
||||
|
||||
def __unicode__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return self.address_string
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.geo_latitude and not self.geo_longitude:
|
||||
res = caching_geo_lookup(self.address_string)
|
||||
if res:
|
||||
self.geo_latitude, self.geo_longitude = res[1]
|
||||
return super(Address, self).save(**kwargs)
|
||||
|
||||
|
||||
class ClaimRequest(models.Model):
|
||||
|
||||
CLAIM_STATUS_PENDING = 0
|
||||
CLAIM_STATUS_APPROVED = 1
|
||||
CLAIM_STATUS_REJECTED = 2
|
||||
|
||||
CLAIM_STATUS_CHOICES = (
|
||||
(CLAIM_STATUS_PENDING, 'Pending'),
|
||||
(CLAIM_STATUS_APPROVED, 'Approved'),
|
||||
(CLAIM_STATUS_REJECTED, 'Rejected')
|
||||
)
|
||||
|
||||
object_id = models.PositiveIntegerField()
|
||||
object_type = models.ForeignKey(ContentType)
|
||||
generic_obj = generic.GenericForeignKey('object_type', 'object_id')
|
||||
|
||||
user = models.ForeignKey(USER_MODEL, related_name='claims')
|
||||
note = models.TextField('Claim Note')
|
||||
status = models.IntegerField('Status', choices=CLAIM_STATUS_CHOICES, default=CLAIM_STATUS_PENDING)
|
||||
15
app/stores/search_indexes.py
Normal file
15
app/stores/search_indexes.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from haystack import indexes
|
||||
from .models import Store
|
||||
|
||||
|
||||
class StoreIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
text = indexes.CharField(document=True, use_template=True)
|
||||
address = indexes.CharField(model_attr='address__full_address')
|
||||
location = indexes.LocationField(model_attr='address__geo_location')
|
||||
|
||||
def get_model(self):
|
||||
return Store
|
||||
|
||||
def index_queryset(self, using=None):
|
||||
"""Used when the entire index for model is updated."""
|
||||
return self.get_model().objects.filter(active=True)
|
||||
BIN
app/stores/static/img/map_icons/home-2-green.png
Normal file
BIN
app/stores/static/img/map_icons/home-2-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1000 B |
BIN
app/stores/static/img/map_icons/home-2.png
Normal file
BIN
app/stores/static/img/map_icons/home-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/stores/static/img/map_icons/wifi.png
Normal file
BIN
app/stores/static/img/map_icons/wifi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
17
app/stores/static/js/geolocation.js
Normal file
17
app/stores/static/js/geolocation.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
function supports_geolocation() {
|
||||
return 'geolocation' in navigator;
|
||||
}
|
||||
|
||||
function init_geolocation() {
|
||||
if (supports_geolocation()) {
|
||||
navigator.geolocation.getCurrentPosition(handle_geolocation_event)
|
||||
}
|
||||
}
|
||||
|
||||
function handle_geolocation_event(position) {
|
||||
$('#geolocation a').attr('href', '/stores/search/?lat=' + position.coords.latitude + '&lng=' + position.coords.longitude + '&distance=10');
|
||||
$('#geolocation').fadeIn(1000);
|
||||
}
|
||||
|
||||
$(document).ready(function() {init_geolocation();})
|
||||
45
app/stores/static/js/gmap.js
Normal file
45
app/stores/static/js/gmap.js
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
var store_icon = '/static/img/map_icons/home-2.png'
|
||||
var online_icon = '/static/img/map_icons/wifi.png'
|
||||
var geolocation_icon = '/static/img/map_icons/home-2-green.png'
|
||||
|
||||
function lookup_marker(id) {
|
||||
if (id == null || id == 1) {
|
||||
return store_icon
|
||||
} else if (id == 2) {
|
||||
return online_icon
|
||||
} else if (id == 999) {
|
||||
return geolocation_icon
|
||||
} else {
|
||||
return store_icon
|
||||
}
|
||||
}
|
||||
|
||||
function initialize_map(markers, element) {
|
||||
var mapOptions = {
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
};
|
||||
var map = new google.maps.Map(element, mapOptions);
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
for (var i = 0; i < markers.length; i++) {
|
||||
marker = markers[i];
|
||||
if (marker[1] != null && marker[2] != null) {
|
||||
var latlng = new google.maps.LatLng(marker[1], marker[2]);
|
||||
bounds.extend(latlng);
|
||||
var marker_obj = new google.maps.Marker({
|
||||
position: latlng,
|
||||
map: map,
|
||||
title: marker[0],
|
||||
icon: lookup_marker(marker[3])
|
||||
});
|
||||
if (marker[4] != '') {
|
||||
google.maps.event.addListener(marker_obj, 'click', (function(marker) {
|
||||
return function() {
|
||||
window.location = marker[4];
|
||||
}
|
||||
})(marker));
|
||||
}
|
||||
}
|
||||
}
|
||||
map.fitBounds(bounds)
|
||||
}
|
||||
1
app/stores/static/js/infobox.js
Normal file
1
app/stores/static/js/infobox.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
{{ object.name }}
|
||||
{{ object.address.city }}
|
||||
{{ object.address.county }}
|
||||
{{ object.address.postcode }}
|
||||
{{ object.long_description }}
|
||||
72
app/stores/templates/stores/chain_detail.html
Normal file
72
app/stores/templates/stores/chain_detail.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% load markdown_deux_tags %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
{{ chain.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
#map-canvas-stores {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<script type="text/javascript" src="{% static "js/gmap.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
var stores = [
|
||||
{% for store in chain.stores.all %}{% if store.address.geo_latitude %}['{{ store }}', {{ store.address.geo_latitude }}, {{ store.address.geo_longitude }}],{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
$(document).ready(function(){initialize_map(stores, document.getElementById('map-canvas-stores'))});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ chain }}</h1>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
{% if user.is_authenticated or not store.editor or user.is_superuser %}
|
||||
<p>
|
||||
{% if not chain.editor %}<a href="{% url "chain-claim" chain.slug %}" class="btn btn-small">Claim Chain</a>{% endif %}
|
||||
{% if is_editor %}<a href="#" class="btn btn-small">Edit Chain</a>{% endif %}
|
||||
{% if user.is_superuser %}<a href="{% url "admin:stores_chain_change" chain.pk %}" class="btn btn-small">Edit in Admin</a>{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if chain.website %}
|
||||
<p><b>Website</b>: <a href="{{ chain.website }}" target="_blank">{{ chain.website }}</a></p>
|
||||
{% endif %}
|
||||
{{ chain.long_description|markdown }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<h3>Stores</h3>
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
{% for store in chain.stores.all %}
|
||||
<tr>
|
||||
<td><a href="{% url "store-detail" store.slug %}">{{ store }}</a></td>
|
||||
<td>{{ store.address.city }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div id="map-canvas-stores" class="map">
|
||||
<noscript>
|
||||
You need Javascript enabled to view the map.
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
30
app/stores/templates/stores/chain_list.html
Normal file
30
app/stores/templates/stores/chain_list.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Chains
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Chains</h1>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr><th>Name</th><th># of Stores</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for chain in chain_list %}
|
||||
<tr>
|
||||
<td><a href="{% url "chain-detail" chain.slug %}">{{ chain }}</a></td>
|
||||
<td>{{ chain.stores.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "stores/paginator.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
app/stores/templates/stores/claim_form.html
Normal file
25
app/stores/templates/stores/claim_form.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Claim Request - {{ target_obj }}</h1>
|
||||
</div>
|
||||
|
||||
<p>This form is to submit a request to claim a store/chain and assign it to your login. If you are a store or chain manager for {{ target_obj }} please fill in the following note with proof of your claim. Once you submit the request it'll be reviewed by {{ site.name }} staff who may need to contact you to confirm your request.</p>
|
||||
|
||||
<p>In the note please submit as many of the following details you can.</p>
|
||||
|
||||
<ul>
|
||||
<li>Full name.</li>
|
||||
<li>Contact phone number.</li>
|
||||
<li>Email address.</li>
|
||||
<li>Links to your store's website or Facebook page with the store's details listed.</li>
|
||||
</ul>
|
||||
|
||||
<form class="form form-horizontal" method="post">
|
||||
{{ form|bootstrap }}
|
||||
{% csrf_token %}
|
||||
<input class="btn" type="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
28
app/stores/templates/stores/paginator.html
Normal file
28
app/stores/templates/stores/paginator.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% if is_paginated %}
|
||||
{% load i18n %}
|
||||
<div class="pagination">
|
||||
<ul>
|
||||
{% if page_obj.has_previous %}
|
||||
<li><a href="?page={{ page_obj.previous_page_number }}{{ getvars }}{{ hashtag }}" class="prev">‹‹ {% trans "previous" %}</a></li>
|
||||
{% else %}
|
||||
<li class="disabled prev"><a href="#">‹‹ {% trans "previous" %}</a></li>
|
||||
{% endif %}
|
||||
{% for page in paginator.page_range %}
|
||||
{% if page %}
|
||||
{% ifequal page page_obj.number %}
|
||||
<li class="current page active"><a href="#">{{ page }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ page }}{{ getvars }}{{ hashtag }}" class="page">{{ page }}</a></li>
|
||||
{% endifequal %}
|
||||
{% else %}
|
||||
...
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if page_obj.has_next %}
|
||||
<li><a href="?page={{ page_obj.next_page_number }}{{ getvars }}{{ hashtag }}" class="next">{% trans "next" %} ››</a></li>
|
||||
{% else %}
|
||||
<li class="disabled next"><a href="#">{% trans "next" %} ››</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
101
app/stores/templates/stores/store_detail.html
Normal file
101
app/stores/templates/stores/store_detail.html
Normal file
@@ -0,0 +1,101 @@
|
||||
{% extends "base.html" %}
|
||||
{% load markdown_deux_tags %}
|
||||
|
||||
{% block title %}
|
||||
{{ store.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
.vcard ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
#map-canvas-store {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<script type="text/javascript">
|
||||
function initialize_map_store() {
|
||||
var latlng = new google.maps.LatLng({{ store.address.geo_latitude }},{{ store.address.geo_longitude }});
|
||||
var mapElem = document.getElementById("map-canvas-store");
|
||||
|
||||
var mapOptions = {
|
||||
zoom: 16,
|
||||
center: latlng,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
};
|
||||
|
||||
var map = new google.maps.Map(mapElem, mapOptions);
|
||||
|
||||
var marker = new google.maps.Marker({
|
||||
position: latlng,
|
||||
map: map,
|
||||
title: "{{ map.address }}"
|
||||
});
|
||||
}
|
||||
$(document).ready(function(){initialize_map_store()});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ store.name }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
{{ store.long_description|markdown }}
|
||||
|
||||
{% if store.brands.count %}
|
||||
<h3>Brands Stocked</h3>
|
||||
<ul>
|
||||
{% for brand in store.brands.all %}
|
||||
<li>{% if brand.website %}<a href="{{ brand.website }}">{{ brand }}</a>{% else %}{{ brand }}{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="span4">
|
||||
{% if user.is_authenticated and not store.editor or user.is_superuser %}
|
||||
<p>
|
||||
{% if not store.editor %}<a href="{% url "store-claim" store.slug %}" class="btn btn-small">Claim Store</a>{% endif %}
|
||||
{% if is_editor %}<a href="{% url "store-update" store.slug %}" class="btn btn-small">Edit Store</a>{% endif %}
|
||||
{% if user.is_superuser %}<a href="{% url "admin:stores_store_change" store.pk %}" class="btn btn-small">Edit in Admin</a>{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if store.chain %}<p><b>Chain</b>: <a href="{% url "chain-detail" store.chain.slug %}">{{ store.chain }}</a></p>{% endif %}
|
||||
<p><b>Type</b>: {{ store.get_store_type_display }}</p>
|
||||
<div class="vcard">
|
||||
<h3>Address</h3>
|
||||
<ul class="adr">
|
||||
<li class="fn">{{ store.name }}</li>
|
||||
<li class="street-address">{{ store.address.address1 }}</li>
|
||||
{% if store.address.address2 %}<li>{{ store.address.address2 }}</li>{% endif %}
|
||||
{% if store.address.address3 %}<li>{{ store.address.address3 }}</li>{% endif %}
|
||||
<li class="locality">{{ store.address.city }}</li>
|
||||
<li class="region">{{ store.address.county }}</li>
|
||||
<li class="postal-code">{{ store.address.postcode }}</li>
|
||||
<li class="country-name">{{ store.address.country }}</li>
|
||||
</ul>
|
||||
|
||||
<h3>Contact Details</h3>
|
||||
<ul>
|
||||
{% if store.website %}<li>Website: <a class="url" target="_new" href="{{ store.website }}">{{ store.website }}</a></li>{% endif %}
|
||||
{% if store.email %}<li>Email: <a class="email" href="mailto:{{ store.email }}">{{ store.email }}</a></li>{% endif %}
|
||||
{% if store.phone %}<li>Phone: <span class="tel">{{ store.phone }}</span></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="map-canvas-store" style="width: 300px; height: 300px;" class="map">
|
||||
<noscript>
|
||||
<img alt="Map of {{ store.address.address_string }}" src="https://maps.google.com/maps/api/staticmap?center={{ store.address.geo_latitude }},{{ store.address.geo_longitude }}&zoom=16&markers={{ store.address.geo_latitude }},{{ store.address.geo_longitude }}&size=300x300&sensor=false">
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
app/stores/templates/stores/store_form.html
Normal file
18
app/stores/templates/stores/store_form.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Edit Store - {{ object }}</h1>
|
||||
</div>
|
||||
|
||||
<form class="form form-horizontal" method="post">
|
||||
{{ form|bootstrap }}
|
||||
{% csrf_token %}
|
||||
<input class="btn" type="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
76
app/stores/templates/stores/store_list.html
Normal file
76
app/stores/templates/stores/store_list.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Stores
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css" xmlns="http://www.w3.org/1999/html">
|
||||
#map-canvas-stores {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<script type="text/javascript" src="{% static "js/gmap.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
var stores = [
|
||||
{% for store in store_list %}{% if store.address.geo_latitude %}['{{ store }}', {{ store.address.geo_latitude }}, {{ store.address.geo_longitude }}, {{ store.store_type }}],{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
$(document).ready(function(){initialize_map(stores, document.getElementById("map-canvas-stores"))});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Stores</h1>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
<form method="get">
|
||||
<input type="text" name="q" class="search-query" placeholder="Search" value="{{ search_query }}">
|
||||
<a href="#" class="btn btn-small">Advanced Search</a>
|
||||
</form>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<a href="{% url "store-create" %}" class="btn btn-small pull-right">Submit A Store</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if store_list.count %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Town/City</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for store in store_list %}
|
||||
<tr>
|
||||
<td><a href="{% url "store-detail" store.slug %}">{{ store }}</a></td>
|
||||
<td>{{ store.address.city }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "stores/paginator.html" %}
|
||||
{% else %}
|
||||
{% if search_query %}
|
||||
<p>No results found for the search "{{ search_query }}".</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div id="map-canvas-stores" class="map">
|
||||
<noscript>
|
||||
You need Javascript enabled to view the map.
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
app/stores/templates/stores/store_map.html
Normal file
49
app/stores/templates/stores/store_map.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Stores
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
#map-canvas-stores {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
}
|
||||
#geolocation {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<script type="text/javascript" src="{% static "js/gmap.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/geolocation.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
var stores = [
|
||||
{% for store in store_list %}{% if store.address.geo_latitude %}['{{ store }}', {{ store.address.geo_latitude }}, {{ store.address.geo_longitude }}, {{ store.store_type }}, '{% url "store-detail" store.slug %}'],{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
$(document).ready(function(){initialize_map(stores, document.getElementById('map-canvas-stores'))});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<p class="lead">
|
||||
{{ site.name }} lists {{ store_count }} stores, and {{ chain_count }} chains across the UK and Ireland.
|
||||
</p>
|
||||
<p id="geolocation"><a class="btn btn-large">Find Stores Near Me.</a></p>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span8 offset2">
|
||||
<div id="map-canvas-stores" class="map">
|
||||
<noscript>
|
||||
You need Javascript enabled to view the map.
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
73
app/stores/templates/stores/store_search.html
Normal file
73
app/stores/templates/stores/store_search.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}
|
||||
Stores
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
#map-canvas-stores {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<script type="text/javascript" src="{% static "js/gmap.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
var stores = [
|
||||
{% for store in object_list %}{% if store.object.address.geo_latitude %}['{{ store }}', {{ store.object.address.geo_latitude }}, {{ store.object.address.geo_longitude }}, {{ store.object.store_type }}, '{% url "store-detail" store.object.pk %}'],{% endif %}
|
||||
{% endfor %}
|
||||
{% if location_geo %}['Search Location', {{ location_geo.0 }}, {{ location_geo.1 }}, 999, ''],{% endif %}
|
||||
];
|
||||
$(document).ready(function(){ initialize_map(stores, document.getElementById("map-canvas-stores")); });
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Stores</h1>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span7">
|
||||
<form method="get">
|
||||
<input type="text" name="location" class="search-query" placeholder="Location" value="{{ location }}">
|
||||
<select name="distance">
|
||||
<option value="10">10km</option>
|
||||
<option value="30">20km</option>
|
||||
<option value="30">30km</option>
|
||||
</select>
|
||||
</form>
|
||||
{% if object_list.count %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Town/City</th><th>Distance</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for store in object_list %}
|
||||
<tr>
|
||||
<td><a href="{% url "store-detail" store.object.slug %}">{{ store.object }}</a></td>
|
||||
<td>{{ store.object.address.city }}</td>
|
||||
<td>{{ store.distance }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "stores/paginator.html" %}
|
||||
{% else %}
|
||||
<p>No results found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div id="map-canvas-stores" class="map">
|
||||
<noscript>
|
||||
You need Javascript enabled to view the map.
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
23
app/stores/templates/stores/wizard/store_wizard.html
Normal file
23
app/stores/templates/stores/wizard/store_wizard.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block style %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Add Store ({{ wizard.steps.step1 }} of {{ wizard.steps.count }})</h1>
|
||||
</div>
|
||||
|
||||
<form class="form form-horizontal" method="post">
|
||||
{{ wizard.management_form|bootstrap }}
|
||||
{{ wizard.form|bootstrap }}
|
||||
{% csrf_token %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" class="btn" type="submit" value="{{ wizard.steps.first }}">First Step</button>
|
||||
<button name="wizard_goto_step" class="btn" type="submit" value="{{ wizard.steps.prev }}">Previous Step</button>
|
||||
{% endif %}
|
||||
<button name="wizard_goto_step" class="btn" type="submit" value="{{ wizard.steps.next }}">Next Step</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
22
app/stores/urls.py
Normal file
22
app/stores/urls.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from .views import *
|
||||
from .forms import AddressForm, StoreForm
|
||||
from .models import Store, Chain
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', MapView.as_view(), name='map'),
|
||||
|
||||
url(r'^chains/$', ChainListView.as_view(), name='chain-list'),
|
||||
url(r'^chains/(?P<slug>.*)/claim/$', ClaimCreateView.as_view(target_model=Chain), name='chain-claim'),
|
||||
url(r'^chains/(?P<pk>\d+)/$', ChainDetailView.as_view(), name='chain-detail-pk'),
|
||||
url(r'^chains/(?P<slug>.*)/$', ChainDetailView.as_view(), name='chain-detail'),
|
||||
|
||||
url(r'^stores/$', StoreListView.as_view(), name='store-list'),
|
||||
url(r'^stores/create/$', StoreCreateView.as_view([AddressForm, StoreForm]), name='store-create'),
|
||||
url(r'^stores/search/$', DistanceSearchView.as_view(), name='store-search'),
|
||||
url(r'^stores/(?P<slug>.*)/claim/$', ClaimCreateView.as_view(target_model=Store), name='store-claim'),
|
||||
url(r'^stores/(?P<slug>.*)/update/$', StoreUpdateView.as_view(), name='store-update'),
|
||||
url(r'^stores/(?P<pk>\d+)/$', StoreDetailView.as_view(), name='store-detail-pk'),
|
||||
url(r'^stores/(?P<slug>.*)/$', StoreDetailView.as_view(), name='store-detail'),
|
||||
|
||||
)
|
||||
36
app/stores/utils.py
Normal file
36
app/stores/utils.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from math import radians, cos, sin, asin, sqrt
|
||||
from django.core.cache import cache
|
||||
from geopy.geocoders import GoogleV3
|
||||
|
||||
|
||||
def haversine(lon1, lat1, lon2, lat2):
|
||||
"""
|
||||
Calculate the great circle distance between two points
|
||||
on the earth (specified in decimal degrees)
|
||||
"""
|
||||
# convert decimal degrees to radians
|
||||
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
|
||||
# haversine formula
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
|
||||
c = 2 * asin(sqrt(a))
|
||||
km = 6367 * c
|
||||
return km
|
||||
|
||||
|
||||
def caching_geo_lookup(address):
|
||||
"""
|
||||
Preforms a geo lookup against Google V3 and caches the results
|
||||
"""
|
||||
if not address:
|
||||
return None
|
||||
slug = address.lower().replace(',', '').replace(' ', '-')
|
||||
geo = cache.get('geo_%s' % slug)
|
||||
if not geo:
|
||||
try:
|
||||
geo = GoogleV3().geocode(address)
|
||||
except ValueError:
|
||||
return None
|
||||
cache.set('geo_%s' % slug, geo, 3600)
|
||||
return geo
|
||||
5
app/stores/views/__init__.py
Normal file
5
app/stores/views/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .stores import StoreListView, StoreDetailView, StoreUpdateView, StoreCreateView
|
||||
from .chains import ChainListView, ChainDetailView
|
||||
from .search import DistanceSearchView
|
||||
from .claims import ClaimCreateView
|
||||
from .misc import MapView
|
||||
20
app/stores/views/chains.py
Normal file
20
app/stores/views/chains.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.views.generic import ListView, DetailView
|
||||
from .mixins import EditorCheckMixin
|
||||
from ..models import Chain
|
||||
|
||||
|
||||
class ChainListView(ListView):
|
||||
model = Chain
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(ChainListView, self).get_queryset()
|
||||
return qs.filter(active=True).prefetch_related('stores')
|
||||
|
||||
|
||||
class ChainDetailView(EditorCheckMixin, DetailView):
|
||||
model = Chain
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(ChainDetailView, self).get_queryset()
|
||||
return qs.filter(active=True).prefetch_related('stores', 'stores__address')
|
||||
44
app/stores/views/claims.py
Normal file
44
app/stores/views/claims.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from ..forms import ClaimRequestForm
|
||||
from ..models import ClaimRequest
|
||||
|
||||
|
||||
class ClaimCreateView(CreateView):
|
||||
model = ClaimRequest
|
||||
target_model = None
|
||||
form_class = ClaimRequestForm
|
||||
template_name = 'stores/claim_form.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.target_obj = self.get_target_object()
|
||||
return super(ClaimCreateView, self).get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.target_obj = self.get_target_object()
|
||||
return super(ClaimCreateView, self).post(request, *args, **kwargs)
|
||||
|
||||
def get_target_object(self):
|
||||
obj_slug = self.kwargs.get('slug')
|
||||
return self.target_model.objects.get(slug=obj_slug)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(ClaimCreateView, self).get_context_data(**kwargs)
|
||||
ctx.update({
|
||||
'target_obj': self.target_obj,
|
||||
})
|
||||
return ctx
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.object_id = self.target_obj.pk
|
||||
obj.object_type = ContentType.objects.get_for_model(self.target_model)
|
||||
obj.user = self.request.user
|
||||
obj.save()
|
||||
messages.success(self.request, 'Your claim request for %s has been successfully submitted for review.' % self.target_obj)
|
||||
return super(ClaimCreateView, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('store-detail', args=[self.target_obj.slug])
|
||||
32
app/stores/views/misc.py
Normal file
32
app/stores/views/misc.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.core.cache import cache
|
||||
from django.views.generic import ListView
|
||||
from ..models import Chain, Store
|
||||
|
||||
|
||||
class MapView(ListView):
|
||||
model = Store
|
||||
template_name_suffix = '_map'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(MapView, self).get_context_data(**kwargs)
|
||||
|
||||
stores = cache.get('store_count')
|
||||
chains = cache.get('chain_count')
|
||||
|
||||
if not stores:
|
||||
stores = Store.objects.filter(active=True).count()
|
||||
cache.set('store_count', stores, 600)
|
||||
|
||||
if not chains:
|
||||
chains = Chain.objects.filter(active=True).count()
|
||||
cache.set('chain_count', chains, 600)
|
||||
|
||||
ctx.update({
|
||||
'store_count': stores,
|
||||
'chain_count': chains,
|
||||
})
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(MapView, self).get_queryset()
|
||||
return qs.filter(active=True).select_related('address')
|
||||
55
app/stores/views/mixins.py
Normal file
55
app/stores/views/mixins.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django.http import Http404
|
||||
from haystack.query import SearchQuerySet
|
||||
from haystack.inputs import AutoQuery
|
||||
|
||||
class EditorCheckMixin(object):
|
||||
"""
|
||||
A mixin to check if the object is inactive to only show it to editors or superusers
|
||||
"""
|
||||
|
||||
def is_editor(self, object):
|
||||
if self.request.user.is_superuser or self.request.user == object.editor:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
obj = super(EditorCheckMixin, self).get_object(queryset)
|
||||
if not obj.active:
|
||||
if not self.is_editor(obj):
|
||||
raise Http404
|
||||
return obj
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(EditorCheckMixin, self).get_context_data(**kwargs)
|
||||
ctx.update({
|
||||
'is_editor': self.is_editor(self.object)
|
||||
})
|
||||
return ctx
|
||||
|
||||
|
||||
class HaystackSearchListMixin(object):
|
||||
"""
|
||||
Adds searching via Haystack to a regular ListView
|
||||
"""
|
||||
|
||||
search_parameter = 'q'
|
||||
|
||||
def get_search_terms(self):
|
||||
return self.request.GET.get(self.search_parameter, None)
|
||||
|
||||
def get_search_filter(self):
|
||||
return {
|
||||
'content': AutoQuery(self.get_search_terms())
|
||||
}
|
||||
|
||||
def haystack_search(self):
|
||||
return SearchQuerySet().filter(**self.get_search_filter()).models(self.model)
|
||||
|
||||
def get_queryset(self):
|
||||
if self.get_search_terms():
|
||||
res = self.haystack_search()
|
||||
if res.count() == 0:
|
||||
return self.model.objects.none()
|
||||
return self.model.objects.filter(pk__in=[r.object.pk for r in res.load_all()])
|
||||
else:
|
||||
return super(HaystackSearchListMixin, self).get_queryset()
|
||||
42
app/stores/views/search.py
Normal file
42
app/stores/views/search.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.views.generic import ListView
|
||||
from haystack.query import SearchQuerySet
|
||||
from haystack.utils.geo import Point, D
|
||||
from ..models import Store
|
||||
from ..utils import caching_geo_lookup
|
||||
|
||||
|
||||
class DistanceSearchView(ListView):
|
||||
|
||||
template_name = 'stores/store_search.html'
|
||||
distance = 25
|
||||
|
||||
def get_location(self):
|
||||
# TODO: geopy the location based on kwargs
|
||||
location = self.request.GET.get('location')
|
||||
lat = self.request.GET.get('lat')
|
||||
lng = self.request.GET.get('lng')
|
||||
if location:
|
||||
name, geo = caching_geo_lookup(location)
|
||||
elif lat and lng:
|
||||
name, geo = caching_geo_lookup('%s,%s' % (lat, lng))
|
||||
print name
|
||||
self.location_geo = geo
|
||||
|
||||
return Point(geo[1], geo[0])
|
||||
|
||||
def get_distance(self):
|
||||
return D(km=self.request.GET.get('distance', self.distance))
|
||||
|
||||
def get_queryset(self):
|
||||
location = self.get_location()
|
||||
distance = self.get_distance()
|
||||
print location, distance
|
||||
return SearchQuerySet().dwithin('location', location, distance).distance('location', location).order_by('-distance')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(DistanceSearchView, self).get_context_data(**kwargs)
|
||||
ctx.update({
|
||||
'location': self.request.GET.get('location'),
|
||||
'location_geo': self.location_geo,
|
||||
})
|
||||
return ctx
|
||||
68
app/stores/views/stores.py
Normal file
68
app/stores/views/stores.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.views.generic import ListView, DetailView, UpdateView
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib import messages
|
||||
from django.contrib.formtools.wizard.views import SessionWizardView
|
||||
from .mixins import EditorCheckMixin, HaystackSearchListMixin
|
||||
from ..forms import AddressInline, StoreForm, AddressForm
|
||||
from ..models import Store
|
||||
|
||||
|
||||
class StoreListView(HaystackSearchListMixin, ListView):
|
||||
model = Store
|
||||
paginate_by = 10
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(StoreListView, self).get_context_data(**kwargs)
|
||||
|
||||
search = self.get_search_terms()
|
||||
if search:
|
||||
ctx.update({
|
||||
'search_query': search,
|
||||
})
|
||||
return ctx
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(StoreListView, self).get_queryset()
|
||||
return qs.filter(active=True).select_related('address')
|
||||
|
||||
|
||||
class StoreDetailView(EditorCheckMixin, DetailView):
|
||||
model = Store
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super(StoreDetailView, self).get_queryset()
|
||||
return qs.filter(active=True).select_related('address', 'address__county', 'address__country', 'chain').prefetch_related('brands')
|
||||
|
||||
|
||||
class StoreUpdateView(UpdateView):
|
||||
model = Store
|
||||
form_class = StoreForm
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, "%s updated successfully." % self.object)
|
||||
return super(UpdateView, self).form_valid(form)
|
||||
|
||||
|
||||
class StoreCreateView(SessionWizardView):
|
||||
form_list = [AddressForm, StoreForm]
|
||||
|
||||
def done(self, form_list, **kwargs):
|
||||
|
||||
address, store = form_list
|
||||
|
||||
addr_obj = address.save(commit=False)
|
||||
store_obj = store.save(commit=False)
|
||||
|
||||
addr_obj.name = store_obj.name
|
||||
addr_obj.save()
|
||||
store_obj.address = addr_obj
|
||||
store_obj.active = False
|
||||
store_obj.save()
|
||||
|
||||
messages.success(self.request, "%s has been sumbitted for moderation and should be visible within the next 24 hours." % store_obj)
|
||||
|
||||
return HttpResponseRedirect(reverse('store-map'))
|
||||
|
||||
def get_template_names(self):
|
||||
return 'stores/wizard/store_wizard.html'
|
||||
Reference in New Issue
Block a user