mirror of
https://github.com/nikdoof/vapemap.git
synced 2025-12-13 06:22:16 +00:00
Initial Import.
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
10
app/manage.py
Normal file
10
app/manage.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vapemap.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
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'
|
||||
0
app/vapemap/__init__.py
Normal file
0
app/vapemap/__init__.py
Normal file
142
app/vapemap/settings.py
Normal file
142
app/vapemap/settings.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(os.path.dirname(__file__), '..', '..', 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'127.0.0.1',
|
||||
]
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
SITE_ID = 1
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
MEDIA_ROOT = os.path.join(os.path.dirname(__file__), '..', '..', 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
STATIC_ROOT = os.path.join(os.path.dirname(__file__), '..', '..', 'static')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(os.path.dirname(__file__), 'static'),
|
||||
]
|
||||
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
)
|
||||
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
)
|
||||
|
||||
TEMPLATE_DIRS = [
|
||||
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
]
|
||||
|
||||
|
||||
SECRET_KEY = '4-jz1w*@m**o4dk6!%e22xq3aj!r^9+y(s+_8)v-+)v4fz1lsa'
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.flatpages',
|
||||
'south',
|
||||
'markdown_deux',
|
||||
'epiceditor',
|
||||
'bootstrapform',
|
||||
'registration',
|
||||
'haystack',
|
||||
'stores',
|
||||
]
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
||||
]
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS += (
|
||||
'django.core.context_processors.request',
|
||||
'stores.context_processors.site',
|
||||
'stores.context_processors.pending_admin',
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append('debug_toolbar')
|
||||
MIDDLEWARE_CLASSES.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
|
||||
def custom_show_toolbar(request):
|
||||
return True # Always show toolbar, for example purposes only.
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'INTERCEPT_REDIRECTS': False,
|
||||
'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
|
||||
'EXTRA_SIGNALS': [],
|
||||
'HIDE_DJANGO_SQL': True,
|
||||
'TAG': 'div',
|
||||
'ENABLE_STACKTRACES' : True,
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'vapemap.urls'
|
||||
WSGI_APPLICATION = 'vapemap.wsgi.application'
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'filters': {
|
||||
'require_debug_false': {
|
||||
'()': 'django.utils.log.RequireDebugFalse'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'django.request': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
HAYSTACK_CONNECTIONS = {
|
||||
'default': {
|
||||
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
|
||||
'URL': 'http://127.0.0.1:9200/',
|
||||
'INDEX_NAME': 'haystack',
|
||||
},
|
||||
}
|
||||
15
app/vapemap/static/css/base.css
Normal file
15
app/vapemap/static/css/base.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.epiceditor-container {
|
||||
-webkit-box-shadow: inset 0 1px 1px;
|
||||
-moz-box-shadow: inset 0 1px 1px;
|
||||
box-shadow: inset 0 1px 1px;
|
||||
-webkit-transition: border linear .2s,box-shadow linear .2s;
|
||||
-moz-transition: border linear .2s,box-shadow linear .2s;
|
||||
-o-transitiion: border linear .2s,box-shadow linear .2s;
|
||||
transition: border linear .2s,box-shadow linear .2s;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.map label { width: auto; display:inline; }
|
||||
.map img { max-width: none; }
|
||||
.jumbotron { text-align: center }
|
||||
9
app/vapemap/static/css/bootstrap-responsive.min.css
vendored
Normal file
9
app/vapemap/static/css/bootstrap-responsive.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
app/vapemap/static/css/bootstrap.min.css
vendored
Normal file
9
app/vapemap/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
app/vapemap/static/img/glyphicons-halflings-white.png
Normal file
BIN
app/vapemap/static/img/glyphicons-halflings-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
app/vapemap/static/img/glyphicons-halflings.png
Normal file
BIN
app/vapemap/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
6
app/vapemap/static/js/bootstrap.min.js
vendored
Normal file
6
app/vapemap/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
app/vapemap/static/js/html5shiv.js
vendored
Normal file
8
app/vapemap/static/js/html5shiv.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
HTML5 Shiv v3.6.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||
*/
|
||||
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
|
||||
a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}</style>";
|
||||
c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
|
||||
"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video",version:"3.6.2",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);if(g)return a.createDocumentFragment();
|
||||
for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);
|
||||
79
app/vapemap/templates/base.html
Normal file
79
app/vapemap/templates/base.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% load staticfiles %}
|
||||
{% load flatpages %}
|
||||
{% get_flatpages as flatpages %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ site.name }} - {% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
|
||||
<link href="{% static "css/bootstrap-responsive.min.css" %}" rel="stylesheet">
|
||||
<link href="{% static "css/base.css" %}" rel="stylesheet">
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="{% static "js/html5shiv.js" %}"></script>
|
||||
<![endif]-->
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
//
|
||||
</script>
|
||||
{% block style %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="masthead">
|
||||
<h2>{{ site.name }}</h2>
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<ul class="nav">
|
||||
<li><a href="{% url "map" %}">Map</a></li>
|
||||
<li><a href="{% url "store-list" %}">Stores</a></li>
|
||||
<li><a href="{% url "chain-list" %}">Chains</a></li>
|
||||
<li><a href="#">Brands</a></li>
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
{% if not user.is_authenticated %}
|
||||
<li><a href="#login-modal" data-toggle="modal">Login</a></li>
|
||||
{% else %}
|
||||
{% if user.is_superuser %}
|
||||
{% if admin_pending_requests > 0 %}<li><a href="{% url "admin:stores_claimrequest_changelist" %}?status__exact=0"><span class="badge badge-important">{{ admin_pending_requests }}</span></a></li>{% endif %}
|
||||
<li><a href="{% url "admin:index" %}">Admin</a></li>
|
||||
{% endif %}
|
||||
<li><a href="#">{{ user.username }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div{% if message.tags %} class="alert alert-{{ message.tags }}"{% endif %}>
|
||||
<a class="close" data-dismiss="alert" href="#">×</a>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<hr/>
|
||||
<div class="footer">
|
||||
<p class="muted">© tensixtyone 2013{% for page in flatpages %} | <a href="{{ page.url }}">{{ page.title }}</a>{% endfor %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if not user.is_authenticated %}
|
||||
{% include "login_form.html" %}
|
||||
{% endif %}
|
||||
</body>
|
||||
<script src="{% static "js/bootstrap.min.js" %}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</html>
|
||||
14
app/vapemap/templates/flatpages/default.html
Normal file
14
app/vapemap/templates/flatpages/default.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% load markdown_deux_tags %}
|
||||
|
||||
{% block title %}
|
||||
{{ flatpage.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ flatpage.title }}</h1>
|
||||
</div>
|
||||
|
||||
{{ flatpage.content|markdown }}
|
||||
{% endblock %}
|
||||
31
app/vapemap/templates/login_form.html
Normal file
31
app/vapemap/templates/login_form.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<style type="text/css" xmlns="http://www.w3.org/1999/html">
|
||||
#login-modal {
|
||||
text-align: center;
|
||||
width: 400px;
|
||||
margin-left: -200px;
|
||||
}
|
||||
.form-signin .form-signin-heading,
|
||||
.form-signin .checkbox {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.form-signin input[type="text"],
|
||||
.form-signin input[type="password"] {
|
||||
font-size: 16px;
|
||||
height: auto;
|
||||
margin-bottom: 15px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="login-modal" class="modal hide fade">
|
||||
<div class="modal-body">
|
||||
<form class="form-signin" action="{% url "login" %}?next={{ request.get_full_path }}" method="post">
|
||||
<h2 class="form-signin-heading">Please sign in</h2>
|
||||
<input name="username" type="text" class="input-block-level" placeholder="Username">
|
||||
<input name="password" type="password" class="input-block-level" placeholder="Password">
|
||||
{% csrf_token %}
|
||||
<p><button class="btn btn-large btn-primary" type="submit" data-loading-text="Submitted...">Sign in</button></p>
|
||||
<p class="muted">Don't have an account? Then <a href="{% url "registration_register" %}">sign up</a>.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
18
app/vapemap/templates/registration/activate.html
Normal file
18
app/vapemap/templates/registration/activate.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if account %}
|
||||
|
||||
<p>{% trans "Account successfully activated" %}</p>
|
||||
|
||||
<p><a href="{% url auth_login %}">{% trans "Log in" %}</a></p>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>{% trans "Account activation failed" %}</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
6
app/vapemap/templates/registration/activation_email.txt
Normal file
6
app/vapemap/templates/registration/activation_email.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% trans "Activate account at" %} {{ site.name }}:
|
||||
|
||||
http://{{ site.domain }}{% url registration_activate activation_key %}
|
||||
|
||||
{% blocktrans %}Link is valid for {{ expiration_days }} days.{% endblocktrans %}
|
||||
@@ -0,0 +1 @@
|
||||
{% load i18n %}{% trans "Account activation on" %} {{ site.name }}
|
||||
14
app/vapemap/templates/registration/login.html
Normal file
14
app/vapemap/templates/registration/login.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action=".">
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% trans 'Log in' %}" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</form>
|
||||
|
||||
<p>{% trans "Forgot password" %}? <a href="{% url "auth_password_reset" %}">{% trans "Reset it" %}</a>!</p>
|
||||
<p>{% trans "Not member" %}? <a href="{% url "registration_register" %}">{% trans "Register" %}</a>!</p>
|
||||
{% endblock %}
|
||||
6
app/vapemap/templates/registration/logout.html
Normal file
6
app/vapemap/templates/registration/logout.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% trans "Logged out" %}</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% trans "Password changed" %}</p>
|
||||
{% endblock %}
|
||||
10
app/vapemap/templates/registration/password_change_form.html
Normal file
10
app/vapemap/templates/registration/password_change_form.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action=".">
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% trans 'Submit' %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>{% trans "Password reset successfully" %}</p>
|
||||
|
||||
<p><a href="{% url auth_login %}">{% trans "Log in" %}</a></p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if validlink %}
|
||||
|
||||
<form method="post" action=".">
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% trans 'Submit' %}" />
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>{% trans "Password reset failed" %}</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% trans "Email with password reset instructions has been sent." %}</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Reset password at {{ site_name }}{% endblocktrans %}:
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}{% url auth_password_reset_confirm uidb36=uid, token=token %}
|
||||
{% endblock %}
|
||||
10
app/vapemap/templates/registration/password_reset_form.html
Normal file
10
app/vapemap/templates/registration/password_reset_form.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action=".">
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% trans 'Submit' %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% trans "You are now registered. Activation email sent." %}</p>
|
||||
{% endblock %}
|
||||
10
app/vapemap/templates/registration/registration_form.html
Normal file
10
app/vapemap/templates/registration/registration_form.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action=".">
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="{% trans 'Submit' %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
39
app/vapemap/templates/search/search.html
Normal file
39
app/vapemap/templates/search/search.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Search</h2>
|
||||
|
||||
<form method="get" action=".">
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input type="submit" value="Search">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if query %}
|
||||
<h3>Results</h3>
|
||||
|
||||
{% for result in page.object_list %}
|
||||
<p>
|
||||
<a href="{{ result.object.get_absolute_url }}">{{ result.object }}</a>
|
||||
</p>
|
||||
{% empty %}
|
||||
<p>No results found.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if page.has_previous or page.has_next %}
|
||||
<div>
|
||||
{% if page.has_previous %}<a href="?q={{ query }}&page={{ page.previous_page_number }}">{% endif %}« Previous{% if page.has_previous %}</a>{% endif %}
|
||||
|
|
||||
{% if page.has_next %}<a href="?q={{ query }}&page={{ page.next_page_number }}">{% endif %}Next »{% if page.has_next %}</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Show some example queries to run, maybe query syntax, something else? #}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
14
app/vapemap/urls.py
Normal file
14
app/vapemap/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.contrib import admin
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'user/', include('registration.backends.default.urls')),
|
||||
url(r'user/', include('django.contrib.auth.urls')),
|
||||
url(r'^search/', include('haystack.urls')),
|
||||
url('', include('stores.urls'))
|
||||
)
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
32
app/vapemap/wsgi.py
Normal file
32
app/vapemap/wsgi.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
WSGI config for vapemap project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
|
||||
# if running multiple sites in the same mod_wsgi process. To fix this, use
|
||||
# mod_wsgi daemon mode with each site in its own daemon process, or use
|
||||
# os.environ["DJANGO_SETTINGS_MODULE"] = "vapemap.settings"
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vapemap.settings")
|
||||
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
||||
|
||||
# Apply WSGI middleware here.
|
||||
# from helloworld.wsgi import HelloWorldApplication
|
||||
# application = HelloWorldApplication(application)
|
||||
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
django>=1.5.1,<1.6
|
||||
django-debug-toolbar>=0.9.4
|
||||
south>=0.7.6
|
||||
geopy>=0.95
|
||||
django-markdown-deux>=1.0.4
|
||||
django-bootstrap-form>=1.4
|
||||
django-extra-views>=0.6.2
|
||||
-e hg+https://bitbucket.org/ubernostrum/django-registration@b3c41b3#egg=django_registration
|
||||
requests
|
||||
pyelasticsearch
|
||||
-e git+git://github.com/toastdriven/django-haystack.git@0e8bd20c18ce3133b3a4f285a4c420bb621ac49b#egg=django_haystack
|
||||
django-epiceditor
|
||||
Reference in New Issue
Block a user