├── searchDemo ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-35.pyc │ │ ├── 0001_initial.cpython-35.pyc │ │ ├── 0002_product_slug.cpython-35.pyc │ │ └── 0003_product_timestamp.cpython-35.pyc │ ├── 0003_product_timestamp.py │ ├── 0002_product_slug.py │ └── 0001_initial.py ├── __pycache__ │ ├── admin.cpython-35.pyc │ ├── forms.cpython-35.pyc │ ├── models.cpython-35.pyc │ ├── urls.cpython-35.pyc │ ├── views.cpython-35.pyc │ ├── wsgi.cpython-35.pyc │ ├── __init__.cpython-35.pyc │ ├── settings.cpython-35.pyc │ └── search_indexes.cpython-35.pyc ├── admin.py ├── wsgi.py ├── urls.py ├── models.py ├── search_indexes.py ├── views.py ├── forms.py └── settings.py ├── db.sqlite3 ├── templates ├── search │ └── indexes │ │ └── product_text.txt ├── partials │ └── search_form.html ├── base.html ├── layout.html ├── home.html ├── product.html └── search_result.html ├── searchdemo.png ├── product_images ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 6.jpg ├── no-img.jpg ├── 1_7fIVkXl.jpg ├── 1_AXH5dJ3.jpg ├── 1_B1surJo.jpg ├── 1_vA16Gpz.jpg ├── 2_bk06DHx.jpg ├── 4_EBs9gs9.jpg └── 4_QSI773C.jpg ├── requirements.txt ├── static ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.svg ├── css │ └── autocomplete.css └── js │ ├── our_search_code.js │ ├── jquery.autocomplete.js │ └── bootstrap.js ├── README.md ├── manage.py └── .gitignore /searchDemo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /searchDemo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /templates/search/indexes/product_text.txt: -------------------------------------------------------------------------------- 1 | {{object.title}} 2 | {{ object.description|default:"" }} -------------------------------------------------------------------------------- /searchdemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchdemo.png -------------------------------------------------------------------------------- /product_images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/1.jpg -------------------------------------------------------------------------------- /product_images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/2.jpg -------------------------------------------------------------------------------- /product_images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/3.jpg -------------------------------------------------------------------------------- /product_images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/4.jpg -------------------------------------------------------------------------------- /product_images/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/6.jpg -------------------------------------------------------------------------------- /product_images/no-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/no-img.jpg -------------------------------------------------------------------------------- /product_images/1_7fIVkXl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/1_7fIVkXl.jpg -------------------------------------------------------------------------------- /product_images/1_AXH5dJ3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/1_AXH5dJ3.jpg -------------------------------------------------------------------------------- /product_images/1_B1surJo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/1_B1surJo.jpg -------------------------------------------------------------------------------- /product_images/1_vA16Gpz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/1_vA16Gpz.jpg -------------------------------------------------------------------------------- /product_images/2_bk06DHx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/2_bk06DHx.jpg -------------------------------------------------------------------------------- /product_images/4_EBs9gs9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/4_EBs9gs9.jpg -------------------------------------------------------------------------------- /product_images/4_QSI773C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/product_images/4_QSI773C.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10.5 2 | django-haystack==2.5.1 3 | elasticsearch==5.0.1 4 | olefile==0.44 5 | Pillow==4.0.0 6 | urllib3>=1.23 7 | -------------------------------------------------------------------------------- /searchDemo/__pycache__/admin.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/admin.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/forms.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/forms.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/models.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/models.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/urls.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/urls.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/views.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/views.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/wsgi.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/wsgi.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/__pycache__/settings.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/settings.cpython-35.pyc -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /searchDemo/__pycache__/search_indexes.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/__pycache__/search_indexes.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/migrations/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/migrations/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/migrations/__pycache__/0001_initial.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/migrations/__pycache__/0001_initial.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/migrations/__pycache__/0002_product_slug.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/migrations/__pycache__/0002_product_slug.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/migrations/__pycache__/0003_product_timestamp.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhaskar-c/django-haystack-elasticsearch/HEAD/searchDemo/migrations/__pycache__/0003_product_timestamp.cpython-35.pyc -------------------------------------------------------------------------------- /searchDemo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Product, Category 4 | 5 | 6 | admin.site.register(Category) 7 | 8 | 9 | 10 | class ProductAdmin(admin.ModelAdmin): 11 | prepopulated_fields = {"slug": ("title",)} 12 | 13 | admin.site.register(Product, ProductAdmin) 14 | 15 | -------------------------------------------------------------------------------- /searchDemo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for searchDemo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "searchDemo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /templates/partials/search_form.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /searchDemo/migrations/0003_product_timestamp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-01-14 17:15 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('searchDemo', '0002_product_slug'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='product', 17 | name='timestamp', 18 | field=models.DateTimeField(auto_now=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /searchDemo/migrations/0002_product_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-01-14 08:53 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('searchDemo', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='product', 17 | name='slug', 18 | field=models.SlugField(blank=True, max_length=255, null=True, unique=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /searchDemo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | from .views import HomeView, ProductView, FacetedSearchView, autocomplete 4 | from .settings import MEDIA_ROOT, MEDIA_URL 5 | from django.conf.urls.static import static 6 | 7 | urlpatterns = [ 8 | url(r'^$', HomeView.as_view()), 9 | url(r'^admin/', admin.site.urls), 10 | url(r'^product/(?P[\w-]+)/$', ProductView.as_view(), name='product'), 11 | url(r'^search/autocomplete/$', autocomplete), 12 | url(r'^find/', FacetedSearchView.as_view(), name='haystack_search'), 13 | 14 | ] + static(MEDIA_URL, document_root=MEDIA_ROOT) 15 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | Haystack ElasticSearch Facet Autocomplete Search Tutorial 5 | 6 | 7 | 8 | {% block head_injections %} {% endblock %} 9 | 10 | 11 | {# Main content goes in this 'layout' block #} 12 | {% block layout %}{% endblock %} 13 | 14 | {% block footer_injections %} 15 | 16 | {% endblock %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Haystack + Elasticsearch + Autocomplete + Faceting Tutorial 2 | 3 | Code accompanying Django Haystack + Elasticsearch + Autocomplete + Faceting Demo 4 | 5 | ![Django Haystack + Elasticsearch + Autocomplete + Faceting Tutorial](/searchdemo.png?raw=true "") 6 | 7 | 8 | 9 | 10 | ### Installation 11 | 12 | 1) Download and Unzip the files 13 | 14 | 2) Install Elasticsearch and start the server. 15 | 16 | 3) create a virtualenv 17 | 18 | virtualenv venv 19 | 20 | 4) Install dependencies 21 | 22 | pip install -r requirements.txt 23 | 24 | 5) Run following Django commands: 25 | 26 | python manage.py makemigrations 27 | python manage.py migrate 28 | python manage.py rebuild_index 29 | python manage.py runserver 30 | 31 | 6) Visit home page 127.0.0.1:8000 and try out some searches. 32 | -------------------------------------------------------------------------------- /static/css/autocomplete.css: -------------------------------------------------------------------------------- 1 | .autocomplete-suggestions { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | border: 1px solid #999; 6 | background: #FFF; 7 | cursor: default; 8 | overflow: auto; 9 | -webkit-box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); 10 | -moz-box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); 11 | box-shadow: 1px 4px 3px rgba(50, 50, 50, 0.64); } 12 | .autocomplete-suggestion { padding: 2px 5px; white-space: nowrap; overflow: hidden; } 13 | .autocomplete-no-suggestion { padding: 2px 5px;} 14 | .autocomplete-selected { background: #F0F0F0; } 15 | .autocomplete-suggestions strong { font-weight: bold; color: #000; } 16 | .autocomplete-group { padding: 2px 5px; } 17 | .autocomplete-group strong { font-weight: bold; font-size: 16px; color: #000; display: block; border-bottom: 1px solid #000; } 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "searchDemo.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /searchDemo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls.base import reverse 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=255, db_index=True, unique=True) 6 | 7 | def __str__(self): 8 | return self.name 9 | 10 | 11 | class Product(models.Model): 12 | title = models.CharField(unique=True, max_length=255, db_index=True, default='') 13 | slug = models.SlugField(null=True, blank=True, unique=True, max_length=255) 14 | description = models.TextField(db_index=True) 15 | brand = models.CharField(db_index=True, max_length=255) 16 | category = models.ForeignKey(Category, related_name='category') 17 | image = models.ImageField(upload_to = 'product_images/', default = 'product_images/no-img.jpg') 18 | timestamp = models.DateTimeField(auto_now=True) 19 | 20 | def get_absolute_url(self): 21 | return reverse('product', 22 | kwargs={'slug': self.slug}) 23 | 24 | 25 | def __str__(self): 26 | return self.title 27 | 28 | 29 | -------------------------------------------------------------------------------- /searchDemo/search_indexes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.utils import timezone 3 | from haystack import indexes 4 | from haystack.fields import CharField 5 | 6 | from .models import Product 7 | 8 | 9 | class ProductIndex(indexes.SearchIndex, indexes.Indexable): 10 | text = indexes.EdgeNgramField( 11 | document=True, use_template=True, 12 | template_name='/home/gublu/Desktop/DjangoHasytackElasticSearchDemo/searchDemo/templates/search/indexes/product_text.txt') 13 | title = indexes.EdgeNgramField(model_attr='title') 14 | description = indexes.EdgeNgramField(model_attr="description", null=True) 15 | 16 | category = indexes.CharField(model_attr='category', faceted=True) 17 | 18 | brand = indexes.CharField(model_attr='brand', faceted=True) 19 | 20 | # for auto complete 21 | content_auto = indexes.EdgeNgramField(model_attr='title') 22 | 23 | # Spelling suggestions 24 | suggestions = indexes.FacetCharField() 25 | 26 | def get_model(self): 27 | return Product 28 | 29 | def index_queryset(self, using=None): 30 | """Used when the entire index for model is updated.""" 31 | return self.get_model().objects.filter(timestamp__lte=timezone.now()) 32 | -------------------------------------------------------------------------------- /searchDemo/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | from django.views.generic.detail import DetailView 3 | from django.http import JsonResponse 4 | from haystack.generic_views import FacetedSearchView as BaseFacetedSearchView 5 | from haystack.query import SearchQuerySet 6 | 7 | from .models import Product 8 | from .forms import FacetedProductSearchForm 9 | 10 | 11 | class HomeView(TemplateView): 12 | template_name = "home.html" 13 | 14 | 15 | class ProductView(DetailView): 16 | template_name = "product.html" 17 | model = Product 18 | 19 | 20 | def autocomplete(request): 21 | sqs = SearchQuerySet().autocomplete( 22 | content_auto=request.GET.get( 23 | 'query', 24 | ''))[ 25 | :5] 26 | s = [] 27 | for result in sqs: 28 | d = {"value": result.title, "data": result.object.slug} 29 | s.append(d) 30 | output = {'suggestions': s} 31 | return JsonResponse(output) 32 | 33 | 34 | class FacetedSearchView(BaseFacetedSearchView): 35 | 36 | form_class = FacetedProductSearchForm 37 | facet_fields = ['category', 'brand'] 38 | template_name = 'search_result.html' 39 | paginate_by = 3 40 | context_object_name = 'object_list' 41 | -------------------------------------------------------------------------------- /searchDemo/forms.py: -------------------------------------------------------------------------------- 1 | from haystack.forms import FacetedSearchForm 2 | 3 | 4 | class FacetedProductSearchForm(FacetedSearchForm): 5 | 6 | def __init__(self, *args, **kwargs): 7 | data = dict(kwargs.get("data", [])) 8 | self.categories = data.get('category', []) 9 | self.brands = data.get('brand', []) 10 | super(FacetedProductSearchForm, self).__init__(*args, **kwargs) 11 | 12 | def search(self): 13 | sqs = super(FacetedProductSearchForm, self).search() 14 | if self.categories: 15 | query = None 16 | for category in self.categories: 17 | if query: 18 | query += u' OR ' 19 | else: 20 | query = u'' 21 | query += u'"%s"' % sqs.query.clean(category) 22 | sqs = sqs.narrow(u'category_exact:%s' % query) 23 | if self.brands: 24 | query = None 25 | for brand in self.brands: 26 | if query: 27 | query += u' OR ' 28 | else: 29 | query = u'' 30 | query += u'"%s"' % sqs.query.clean(brand) 31 | sqs = sqs.narrow(u'brand_exact:%s' % query) 32 | return sqs 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block head_injections %} 5 | {{ block.super }} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block layout %} 11 | 12 | 18 | 19 | {% block content_wrapper %} 20 |
21 |
22 |
23 |
24 | {% block content %}{% endblock %} 25 |
26 |
27 |
28 |
29 | {% endblock %} 30 | {% endblock %} 31 | 32 | {% block footer_injections %} 33 | {{ block.super }} 34 | 35 | 36 | 37 | {% endblock %} 38 | 39 | -------------------------------------------------------------------------------- /searchDemo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-01-14 08:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Category', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(db_index=True, max_length=255, unique=True)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Product', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('title', models.CharField(db_index=True, default='', max_length=255, unique=True)), 29 | ('description', models.TextField(db_index=True)), 30 | ('brand', models.CharField(db_index=True, max_length=255)), 31 | ('image', models.ImageField(default='product_images/no-img.jpg', upload_to='product_images/')), 32 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category', to='searchDemo.Category')), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |
5 |

Django Haystack + Faceting + Autocomplete Tutorial

6 |

Type 'shoes' (we have lots of 'shoes' in our database) in the search box above

7 | 8 |
9 |
10 |
11 |

SERVICES

12 |

What we offer

13 |
14 |
15 |
16 | 17 |

POWER

18 |

Lorem ipsum dolor sit amet..

19 |
20 |
21 | 22 |

LOVE

23 |

Lorem ipsum dolor sit amet..

24 |
25 |
26 | 27 |

JOB DONE

28 |

Lorem ipsum dolor sit amet..

29 |
30 |
31 |

32 |
33 |
34 | 35 |

GREEN

36 |

Lorem ipsum dolor sit amet..

37 |
38 |
39 | 40 |

CERTIFIED

41 |

Lorem ipsum dolor sit amet..

42 |
43 |
44 | 45 |

HARD WORK

46 |

Lorem ipsum dolor sit amet..

47 |
48 |
49 |
50 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /static/js/our_search_code.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 'use strict'; 3 | 4 | $('#q').autocomplete({ 5 | serviceUrl: "http://127.0.0.1:8000/search/autocomplete/", 6 | minChars: 2, 7 | dataType: 'json', 8 | type: 'GET', 9 | onSelect: function (suggestion) { 10 | console.log( suggestion.value + ', data :' + suggestion.data); 11 | } 12 | }); 13 | 14 | }); 15 | 16 | 17 | function getParameterByName(name, url) { 18 | if (!url) { 19 | url = window.location.href; 20 | } 21 | name = name.replace(/[\[\]]/g, "\\$&"); 22 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 23 | results = regex.exec(url); 24 | if (!results) return null; 25 | if (!results[2]) return ''; 26 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 27 | } 28 | 29 | 30 | 31 | 32 | function onFacetChangeApplied(){ 33 | var url = window.location.href.split("?")[0]; 34 | var search_query = getParameterByName('q'); 35 | var url_with_search_query = url + '?q=' + search_query 36 | $('input:checkbox.facet').each(function () { 37 | var sThisVal = (this.checked ? $(this).val() : null); 38 | var sThisName = (this.checked ? $(this).attr('name') : null); 39 | if(sThisVal !== null){ 40 | url_with_search_query += '&'+encodeURIComponent(sThisName)+'='+encodeURIComponent(sThisVal); 41 | } 42 | }); 43 | location.href = url_with_search_query; 44 | return true; 45 | } 46 | 47 | 48 | function getQueryParams(){ 49 | var vars = {}, hash; 50 | var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 51 | for(var i = 0; i < hashes.length; i++) 52 | { 53 | hash = hashes[i].split('='); 54 | vars[hash[1]] = hash[0] ; 55 | } 56 | return vars; 57 | } 58 | 59 | 60 | $( document ).ready(function() { 61 | var all_params = getQueryParams(); 62 | console.log(); 63 | $.each( all_params, function( key, value ) { 64 | id = decodeURIComponent(key).replace(/\s/g,''); 65 | $('#'+id).attr('checked', 'checked'); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /templates/product.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block head_injections %} 5 | {{ block.super }} 6 | 14 | {% endblock %} 15 | 16 | 17 | {% block content %} 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |

{{object.title}}

26 | 27 |
28 | 29 | ₹ 430.00 30 |

₹ 359.00

31 |
32 | 33 | {{ object.description|safe }} 34 |
    35 |
  • Availability:In stock
  • 36 |
  • Amount in stock: 5
  • 37 |
38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 |
56 | 60 | 61 | 65 |
66 | 67 |
68 | 69 | {% endblock %} 70 | 71 | 72 | 73 | {% block extrascripts %} 74 | {% endblock %} 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /searchDemo/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = '^e$@2k08pt0z6hgpvgao&+lh!mm9t4u+0#=v8&9we!$e4q_0$(' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = [] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 'haystack', 29 | 'searchDemo' 30 | ] 31 | 32 | 33 | HAYSTACK_CONNECTIONS = { 34 | 'default': { 35 | 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', 36 | 'URL': 'http://127.0.0.1:9200/', 37 | 'INDEX_NAME': 'products_tutorial', 38 | }, 39 | } 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | ROOT_URLCONF = 'searchDemo.urls' 52 | TEMPLATES = [ 53 | { 54 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 55 | 'DIRS': [ 56 | os.path.join(BASE_DIR, 'templates') 57 | ], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'searchDemo.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 119 | STATIC_ROOT = '' 120 | STATIC_URL = '/static/' 121 | STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static'), ) 122 | MEDIA_ROOT = BASE_DIR 123 | MEDIA_URL = '/media/' 124 | -------------------------------------------------------------------------------- /templates/search_result.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 | "{{ query }}" | {{ block.super }} 4 | {% endblock %} 5 | 6 | 7 | 8 | {% block content %} 9 |
10 |
11 | 12 | {% if page_obj.object_list %} 13 |
14 |

Filters

15 |
16 | {% if facets.fields.category %} 17 |
Filter by Category
18 | {% for category in facets.fields.category %} 19 | {% if category.1 != 0 %} 20 |
21 | {{ category.0 }} ({{ category.1 }}) 23 |
24 | {% endif %} 25 | {% endfor %} 26 | {% endif %} 27 |
28 |
29 | 30 |
31 |
32 | 33 | {% if facets.fields.brand %} 34 |
Filter by Brand
35 | {% for brand in facets.fields.brand %} 36 | {% if brand.1 != 0 %} 37 |
38 | {{ brand.0 }} ({{ brand.1 }}) 39 |
40 | {% endif %} 41 | {% endfor %} 42 | {% endif %} 43 |
44 |
45 | 46 | 47 |
48 |
49 | {% endif %} 50 |
51 |
52 |
53 | Search result for: 54 |
55 |
56 | 57 | Showing {{ page_obj.start_index }} - {{ page_obj.end_index }} of total 58 | {{ page_obj.paginator.count }} 59 | results on page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} 60 |
61 |
62 |
63 | {% if page_obj.object_list %} 64 |
    65 | 66 | {% for result in page_obj.object_list %} 67 | 68 |
    69 | 70 |

    {{result.object.title}}

    71 | 72 |
    73 |

    {{result.object.brand}}

    74 |
    75 | {% endfor %} 76 |
77 |
78 | {% if is_paginated %} 79 |
    80 | {% if page_obj.has_previous %} 81 |
  • «
  • 82 | {% else %} 83 |
  • «
  • 84 | {% endif %} 85 | {% for i in paginator.page_range %} 86 | {% if page_obj.number == i %} 87 |
  • {{ i }} (current)
  • 88 | {% else %} 89 |
  • {{ i }}
  • 90 | {% endif %} 91 | {% endfor %} 92 | {% if page_obj.has_next %} 93 |
  • »
  • 94 | {% else %} 95 |
  • »
  • 96 | {% endif %} 97 |
98 | {% endif %} 99 | {% else %} 100 |

Sorry, no result found for the search term {{query}}

101 | {% endif %} 102 |
103 |
104 |
105 | 106 | {% endblock %} 107 | 108 | -------------------------------------------------------------------------------- /static/js/jquery.autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ajax Autocomplete for jQuery, version %version% 3 | * (c) 2015 Tomas Kirda 4 | * 5 | * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. 6 | * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete 7 | */ 8 | 9 | /*jslint browser: true, white: true, single: true, this: true, multivar: true */ 10 | /*global define, window, document, jQuery, exports, require */ 11 | 12 | // Expose plugin as an AMD module if AMD loader is present: 13 | (function (factory) { 14 | "use strict"; 15 | if (typeof define === 'function' && define.amd) { 16 | // AMD. Register as an anonymous module. 17 | define(['jquery'], factory); 18 | } else if (typeof exports === 'object' && typeof require === 'function') { 19 | // Browserify 20 | factory(require('jquery')); 21 | } else { 22 | // Browser globals 23 | factory(jQuery); 24 | } 25 | }(function ($) { 26 | 'use strict'; 27 | 28 | var 29 | utils = (function () { 30 | return { 31 | escapeRegExChars: function (value) { 32 | return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); 33 | }, 34 | createNode: function (containerClass) { 35 | var div = document.createElement('div'); 36 | div.className = containerClass; 37 | div.style.position = 'absolute'; 38 | div.style.display = 'none'; 39 | return div; 40 | } 41 | }; 42 | }()), 43 | 44 | keys = { 45 | ESC: 27, 46 | TAB: 9, 47 | RETURN: 13, 48 | LEFT: 37, 49 | UP: 38, 50 | RIGHT: 39, 51 | DOWN: 40 52 | }; 53 | 54 | function Autocomplete(el, options) { 55 | var noop = $.noop, 56 | that = this, 57 | defaults = { 58 | ajaxSettings: {}, 59 | autoSelectFirst: false, 60 | appendTo: document.body, 61 | serviceUrl: null, 62 | lookup: null, 63 | onSelect: null, 64 | width: 'auto', 65 | minChars: 1, 66 | maxHeight: 300, 67 | deferRequestBy: 0, 68 | params: {}, 69 | formatResult: Autocomplete.formatResult, 70 | delimiter: null, 71 | zIndex: 9999, 72 | type: 'GET', 73 | noCache: false, 74 | onSearchStart: noop, 75 | onSearchComplete: noop, 76 | onSearchError: noop, 77 | preserveInput: false, 78 | containerClass: 'autocomplete-suggestions', 79 | tabDisabled: false, 80 | dataType: 'text', 81 | currentRequest: null, 82 | triggerSelectOnValidInput: true, 83 | preventBadQueries: true, 84 | lookupFilter: function (suggestion, originalQuery, queryLowerCase) { 85 | return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; 86 | }, 87 | paramName: 'query', 88 | transformResult: function (response) { 89 | return typeof response === 'string' ? $.parseJSON(response) : response; 90 | }, 91 | showNoSuggestionNotice: false, 92 | noSuggestionNotice: 'No results', 93 | orientation: 'bottom', 94 | forceFixPosition: false 95 | }; 96 | 97 | // Shared variables: 98 | that.element = el; 99 | that.el = $(el); 100 | that.suggestions = []; 101 | that.badQueries = []; 102 | that.selectedIndex = -1; 103 | that.currentValue = that.element.value; 104 | that.intervalId = 0; 105 | that.cachedResponse = {}; 106 | that.onChangeInterval = null; 107 | that.onChange = null; 108 | that.isLocal = false; 109 | that.suggestionsContainer = null; 110 | that.noSuggestionsContainer = null; 111 | that.options = $.extend({}, defaults, options); 112 | that.classes = { 113 | selected: 'autocomplete-selected', 114 | suggestion: 'autocomplete-suggestion' 115 | }; 116 | that.hint = null; 117 | that.hintValue = ''; 118 | that.selection = null; 119 | 120 | // Initialize and set options: 121 | that.initialize(); 122 | that.setOptions(options); 123 | } 124 | 125 | Autocomplete.utils = utils; 126 | 127 | $.Autocomplete = Autocomplete; 128 | 129 | Autocomplete.formatResult = function (suggestion, currentValue) { 130 | // Do not replace anything if there current value is empty 131 | if (!currentValue) { 132 | return suggestion.value; 133 | } 134 | 135 | var pattern = '(' + utils.escapeRegExChars(currentValue) + ')'; 136 | 137 | return suggestion.value 138 | .replace(new RegExp(pattern, 'gi'), '$1<\/strong>') 139 | .replace(/&/g, '&') 140 | .replace(//g, '>') 142 | .replace(/"/g, '"') 143 | .replace(/<(\/?strong)>/g, '<$1>'); 144 | }; 145 | 146 | Autocomplete.prototype = { 147 | 148 | killerFn: null, 149 | 150 | initialize: function () { 151 | var that = this, 152 | suggestionSelector = '.' + that.classes.suggestion, 153 | selected = that.classes.selected, 154 | options = that.options, 155 | container; 156 | 157 | // Remove autocomplete attribute to prevent native suggestions: 158 | that.element.setAttribute('autocomplete', 'off'); 159 | 160 | that.killerFn = function (e) { 161 | if (!$(e.target).closest('.' + that.options.containerClass).length) { 162 | that.killSuggestions(); 163 | that.disableKillerFn(); 164 | } 165 | }; 166 | 167 | // html() deals with many types: htmlString or Element or Array or jQuery 168 | that.noSuggestionsContainer = $('
') 169 | .html(this.options.noSuggestionNotice).get(0); 170 | 171 | that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); 172 | 173 | container = $(that.suggestionsContainer); 174 | 175 | container.appendTo(options.appendTo); 176 | 177 | // Only set width if it was provided: 178 | if (options.width !== 'auto') { 179 | container.css('width', options.width); 180 | } 181 | 182 | // Listen for mouse over event on suggestions list: 183 | container.on('mouseover.autocomplete', suggestionSelector, function () { 184 | that.activate($(this).data('index')); 185 | }); 186 | 187 | // Deselect active element when mouse leaves suggestions container: 188 | container.on('mouseout.autocomplete', function () { 189 | that.selectedIndex = -1; 190 | container.children('.' + selected).removeClass(selected); 191 | }); 192 | 193 | // Listen for click event on suggestions list: 194 | container.on('click.autocomplete', suggestionSelector, function () { 195 | that.select($(this).data('index')); 196 | return false; 197 | }); 198 | 199 | that.fixPositionCapture = function () { 200 | if (that.visible) { 201 | that.fixPosition(); 202 | } 203 | }; 204 | 205 | $(window).on('resize.autocomplete', that.fixPositionCapture); 206 | 207 | that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); }); 208 | that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); }); 209 | that.el.on('blur.autocomplete', function () { that.onBlur(); }); 210 | that.el.on('focus.autocomplete', function () { that.onFocus(); }); 211 | that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); }); 212 | that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); }); 213 | }, 214 | 215 | onFocus: function () { 216 | var that = this; 217 | 218 | that.fixPosition(); 219 | 220 | if (that.el.val().length >= that.options.minChars) { 221 | that.onValueChange(); 222 | } 223 | }, 224 | 225 | onBlur: function () { 226 | this.enableKillerFn(); 227 | }, 228 | 229 | abortAjax: function () { 230 | var that = this; 231 | if (that.currentRequest) { 232 | that.currentRequest.abort(); 233 | that.currentRequest = null; 234 | } 235 | }, 236 | 237 | setOptions: function (suppliedOptions) { 238 | var that = this, 239 | options = that.options; 240 | 241 | $.extend(options, suppliedOptions); 242 | 243 | that.isLocal = $.isArray(options.lookup); 244 | 245 | if (that.isLocal) { 246 | options.lookup = that.verifySuggestionsFormat(options.lookup); 247 | } 248 | 249 | options.orientation = that.validateOrientation(options.orientation, 'bottom'); 250 | 251 | // Adjust height, width and z-index: 252 | $(that.suggestionsContainer).css({ 253 | 'max-height': options.maxHeight + 'px', 254 | 'width': options.width + 'px', 255 | 'z-index': options.zIndex 256 | }); 257 | }, 258 | 259 | 260 | clearCache: function () { 261 | this.cachedResponse = {}; 262 | this.badQueries = []; 263 | }, 264 | 265 | clear: function () { 266 | this.clearCache(); 267 | this.currentValue = ''; 268 | this.suggestions = []; 269 | }, 270 | 271 | disable: function () { 272 | var that = this; 273 | that.disabled = true; 274 | clearInterval(that.onChangeInterval); 275 | that.abortAjax(); 276 | }, 277 | 278 | enable: function () { 279 | this.disabled = false; 280 | }, 281 | 282 | fixPosition: function () { 283 | // Use only when container has already its content 284 | 285 | var that = this, 286 | $container = $(that.suggestionsContainer), 287 | containerParent = $container.parent().get(0); 288 | // Fix position automatically when appended to body. 289 | // In other cases force parameter must be given. 290 | if (containerParent !== document.body && !that.options.forceFixPosition) { 291 | return; 292 | } 293 | 294 | // Choose orientation 295 | var orientation = that.options.orientation, 296 | containerHeight = $container.outerHeight(), 297 | height = that.el.outerHeight(), 298 | offset = that.el.offset(), 299 | styles = { 'top': offset.top, 'left': offset.left }; 300 | 301 | if (orientation === 'auto') { 302 | var viewPortHeight = $(window).height(), 303 | scrollTop = $(window).scrollTop(), 304 | topOverflow = -scrollTop + offset.top - containerHeight, 305 | bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); 306 | 307 | orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom'; 308 | } 309 | 310 | if (orientation === 'top') { 311 | styles.top += -containerHeight; 312 | } else { 313 | styles.top += height; 314 | } 315 | 316 | // If container is not positioned to body, 317 | // correct its position using offset parent offset 318 | if(containerParent !== document.body) { 319 | var opacity = $container.css('opacity'), 320 | parentOffsetDiff; 321 | 322 | if (!that.visible){ 323 | $container.css('opacity', 0).show(); 324 | } 325 | 326 | parentOffsetDiff = $container.offsetParent().offset(); 327 | styles.top -= parentOffsetDiff.top; 328 | styles.left -= parentOffsetDiff.left; 329 | 330 | if (!that.visible){ 331 | $container.css('opacity', opacity).hide(); 332 | } 333 | } 334 | 335 | if (that.options.width === 'auto') { 336 | styles.width = that.el.outerWidth() + 'px'; 337 | } 338 | 339 | $container.css(styles); 340 | }, 341 | 342 | enableKillerFn: function () { 343 | var that = this; 344 | $(document).on('click.autocomplete', that.killerFn); 345 | }, 346 | 347 | disableKillerFn: function () { 348 | var that = this; 349 | $(document).off('click.autocomplete', that.killerFn); 350 | }, 351 | 352 | killSuggestions: function () { 353 | var that = this; 354 | that.stopKillSuggestions(); 355 | that.intervalId = window.setInterval(function () { 356 | if (that.visible) { 357 | // No need to restore value when 358 | // preserveInput === true, 359 | // because we did not change it 360 | if (!that.options.preserveInput) { 361 | that.el.val(that.currentValue); 362 | } 363 | 364 | that.hide(); 365 | } 366 | 367 | that.stopKillSuggestions(); 368 | }, 50); 369 | }, 370 | 371 | stopKillSuggestions: function () { 372 | window.clearInterval(this.intervalId); 373 | }, 374 | 375 | isCursorAtEnd: function () { 376 | var that = this, 377 | valLength = that.el.val().length, 378 | selectionStart = that.element.selectionStart, 379 | range; 380 | 381 | if (typeof selectionStart === 'number') { 382 | return selectionStart === valLength; 383 | } 384 | if (document.selection) { 385 | range = document.selection.createRange(); 386 | range.moveStart('character', -valLength); 387 | return valLength === range.text.length; 388 | } 389 | return true; 390 | }, 391 | 392 | onKeyPress: function (e) { 393 | var that = this; 394 | 395 | // If suggestions are hidden and user presses arrow down, display suggestions: 396 | if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) { 397 | that.suggest(); 398 | return; 399 | } 400 | 401 | if (that.disabled || !that.visible) { 402 | return; 403 | } 404 | 405 | switch (e.which) { 406 | case keys.ESC: 407 | that.el.val(that.currentValue); 408 | that.hide(); 409 | break; 410 | case keys.RIGHT: 411 | if (that.hint && that.options.onHint && that.isCursorAtEnd()) { 412 | that.selectHint(); 413 | break; 414 | } 415 | return; 416 | case keys.TAB: 417 | if (that.hint && that.options.onHint) { 418 | that.selectHint(); 419 | return; 420 | } 421 | if (that.selectedIndex === -1) { 422 | that.hide(); 423 | return; 424 | } 425 | that.select(that.selectedIndex); 426 | if (that.options.tabDisabled === false) { 427 | return; 428 | } 429 | break; 430 | case keys.RETURN: 431 | if (that.selectedIndex === -1) { 432 | that.hide(); 433 | return; 434 | } 435 | that.select(that.selectedIndex); 436 | break; 437 | case keys.UP: 438 | that.moveUp(); 439 | break; 440 | case keys.DOWN: 441 | that.moveDown(); 442 | break; 443 | default: 444 | return; 445 | } 446 | 447 | // Cancel event if function did not return: 448 | e.stopImmediatePropagation(); 449 | e.preventDefault(); 450 | }, 451 | 452 | onKeyUp: function (e) { 453 | var that = this; 454 | 455 | if (that.disabled) { 456 | return; 457 | } 458 | 459 | switch (e.which) { 460 | case keys.UP: 461 | case keys.DOWN: 462 | return; 463 | } 464 | 465 | clearInterval(that.onChangeInterval); 466 | 467 | if (that.currentValue !== that.el.val()) { 468 | that.findBestHint(); 469 | if (that.options.deferRequestBy > 0) { 470 | // Defer lookup in case when value changes very quickly: 471 | that.onChangeInterval = setInterval(function () { 472 | that.onValueChange(); 473 | }, that.options.deferRequestBy); 474 | } else { 475 | that.onValueChange(); 476 | } 477 | } 478 | }, 479 | 480 | onValueChange: function () { 481 | var that = this, 482 | options = that.options, 483 | value = that.el.val(), 484 | query = that.getQuery(value); 485 | 486 | if (that.selection && that.currentValue !== query) { 487 | that.selection = null; 488 | (options.onInvalidateSelection || $.noop).call(that.element); 489 | } 490 | 491 | clearInterval(that.onChangeInterval); 492 | that.currentValue = value; 493 | that.selectedIndex = -1; 494 | 495 | // Check existing suggestion for the match before proceeding: 496 | if (options.triggerSelectOnValidInput && that.isExactMatch(query)) { 497 | that.select(0); 498 | return; 499 | } 500 | 501 | if (query.length < options.minChars) { 502 | that.hide(); 503 | } else { 504 | that.getSuggestions(query); 505 | } 506 | }, 507 | 508 | isExactMatch: function (query) { 509 | var suggestions = this.suggestions; 510 | 511 | return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase()); 512 | }, 513 | 514 | getQuery: function (value) { 515 | var delimiter = this.options.delimiter, 516 | parts; 517 | 518 | if (!delimiter) { 519 | return value; 520 | } 521 | parts = value.split(delimiter); 522 | return $.trim(parts[parts.length - 1]); 523 | }, 524 | 525 | getSuggestionsLocal: function (query) { 526 | var that = this, 527 | options = that.options, 528 | queryLowerCase = query.toLowerCase(), 529 | filter = options.lookupFilter, 530 | limit = parseInt(options.lookupLimit, 10), 531 | data; 532 | 533 | data = { 534 | suggestions: $.grep(options.lookup, function (suggestion) { 535 | return filter(suggestion, query, queryLowerCase); 536 | }) 537 | }; 538 | 539 | if (limit && data.suggestions.length > limit) { 540 | data.suggestions = data.suggestions.slice(0, limit); 541 | } 542 | 543 | return data; 544 | }, 545 | 546 | getSuggestions: function (q) { 547 | var response, 548 | that = this, 549 | options = that.options, 550 | serviceUrl = options.serviceUrl, 551 | params, 552 | cacheKey, 553 | ajaxSettings; 554 | 555 | options.params[options.paramName] = q; 556 | params = options.ignoreParams ? null : options.params; 557 | 558 | if (options.onSearchStart.call(that.element, options.params) === false) { 559 | return; 560 | } 561 | 562 | if ($.isFunction(options.lookup)){ 563 | options.lookup(q, function (data) { 564 | that.suggestions = data.suggestions; 565 | that.suggest(); 566 | options.onSearchComplete.call(that.element, q, data.suggestions); 567 | }); 568 | return; 569 | } 570 | 571 | if (that.isLocal) { 572 | response = that.getSuggestionsLocal(q); 573 | } else { 574 | if ($.isFunction(serviceUrl)) { 575 | serviceUrl = serviceUrl.call(that.element, q); 576 | } 577 | cacheKey = serviceUrl + '?' + $.param(params || {}); 578 | response = that.cachedResponse[cacheKey]; 579 | } 580 | 581 | if (response && $.isArray(response.suggestions)) { 582 | that.suggestions = response.suggestions; 583 | that.suggest(); 584 | options.onSearchComplete.call(that.element, q, response.suggestions); 585 | } else if (!that.isBadQuery(q)) { 586 | that.abortAjax(); 587 | 588 | ajaxSettings = { 589 | url: serviceUrl, 590 | data: params, 591 | type: options.type, 592 | dataType: options.dataType 593 | }; 594 | 595 | $.extend(ajaxSettings, options.ajaxSettings); 596 | 597 | that.currentRequest = $.ajax(ajaxSettings).done(function (data) { 598 | var result; 599 | that.currentRequest = null; 600 | result = options.transformResult(data, q); 601 | that.processResponse(result, q, cacheKey); 602 | options.onSearchComplete.call(that.element, q, result.suggestions); 603 | }).fail(function (jqXHR, textStatus, errorThrown) { 604 | options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); 605 | }); 606 | } else { 607 | options.onSearchComplete.call(that.element, q, []); 608 | } 609 | }, 610 | 611 | isBadQuery: function (q) { 612 | if (!this.options.preventBadQueries){ 613 | return false; 614 | } 615 | 616 | var badQueries = this.badQueries, 617 | i = badQueries.length; 618 | 619 | while (i--) { 620 | if (q.indexOf(badQueries[i]) === 0) { 621 | return true; 622 | } 623 | } 624 | 625 | return false; 626 | }, 627 | 628 | hide: function () { 629 | var that = this, 630 | container = $(that.suggestionsContainer); 631 | 632 | if ($.isFunction(that.options.onHide) && that.visible) { 633 | that.options.onHide.call(that.element, container); 634 | } 635 | 636 | that.visible = false; 637 | that.selectedIndex = -1; 638 | clearInterval(that.onChangeInterval); 639 | $(that.suggestionsContainer).hide(); 640 | that.signalHint(null); 641 | }, 642 | 643 | suggest: function () { 644 | if (!this.suggestions.length) { 645 | if (this.options.showNoSuggestionNotice) { 646 | this.noSuggestions(); 647 | } else { 648 | this.hide(); 649 | } 650 | return; 651 | } 652 | 653 | var that = this, 654 | options = that.options, 655 | groupBy = options.groupBy, 656 | formatResult = options.formatResult, 657 | value = that.getQuery(that.currentValue), 658 | className = that.classes.suggestion, 659 | classSelected = that.classes.selected, 660 | container = $(that.suggestionsContainer), 661 | noSuggestionsContainer = $(that.noSuggestionsContainer), 662 | beforeRender = options.beforeRender, 663 | html = '', 664 | category, 665 | formatGroup = function (suggestion, index) { 666 | var currentCategory = suggestion.data[groupBy]; 667 | 668 | if (category === currentCategory){ 669 | return ''; 670 | } 671 | 672 | category = currentCategory; 673 | 674 | return '
' + category + '
'; 675 | }; 676 | 677 | if (options.triggerSelectOnValidInput && that.isExactMatch(value)) { 678 | that.select(0); 679 | return; 680 | } 681 | 682 | // Build suggestions inner HTML: 683 | $.each(that.suggestions, function (i, suggestion) { 684 | if (groupBy){ 685 | html += formatGroup(suggestion, value, i); 686 | } 687 | 688 | html += '
' + formatResult(suggestion, value, i) + '
'; 689 | }); 690 | 691 | this.adjustContainerWidth(); 692 | 693 | noSuggestionsContainer.detach(); 694 | container.html(html); 695 | 696 | if ($.isFunction(beforeRender)) { 697 | beforeRender.call(that.element, container, that.suggestions); 698 | } 699 | 700 | that.fixPosition(); 701 | container.show(); 702 | 703 | // Select first value by default: 704 | if (options.autoSelectFirst) { 705 | that.selectedIndex = 0; 706 | container.scrollTop(0); 707 | container.children('.' + className).first().addClass(classSelected); 708 | } 709 | 710 | that.visible = true; 711 | that.findBestHint(); 712 | }, 713 | 714 | noSuggestions: function() { 715 | var that = this, 716 | container = $(that.suggestionsContainer), 717 | noSuggestionsContainer = $(that.noSuggestionsContainer); 718 | 719 | this.adjustContainerWidth(); 720 | 721 | // Some explicit steps. Be careful here as it easy to get 722 | // noSuggestionsContainer removed from DOM if not detached properly. 723 | noSuggestionsContainer.detach(); 724 | container.empty(); // clean suggestions if any 725 | container.append(noSuggestionsContainer); 726 | 727 | that.fixPosition(); 728 | 729 | container.show(); 730 | that.visible = true; 731 | }, 732 | 733 | adjustContainerWidth: function() { 734 | var that = this, 735 | options = that.options, 736 | width, 737 | container = $(that.suggestionsContainer); 738 | 739 | // If width is auto, adjust width before displaying suggestions, 740 | // because if instance was created before input had width, it will be zero. 741 | // Also it adjusts if input width has changed. 742 | if (options.width === 'auto') { 743 | width = that.el.outerWidth(); 744 | container.css('width', width > 0 ? width : 300); 745 | } 746 | }, 747 | 748 | findBestHint: function () { 749 | var that = this, 750 | value = that.el.val().toLowerCase(), 751 | bestMatch = null; 752 | 753 | if (!value) { 754 | return; 755 | } 756 | 757 | $.each(that.suggestions, function (i, suggestion) { 758 | var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0; 759 | if (foundMatch) { 760 | bestMatch = suggestion; 761 | } 762 | return !foundMatch; 763 | }); 764 | 765 | that.signalHint(bestMatch); 766 | }, 767 | 768 | signalHint: function (suggestion) { 769 | var hintValue = '', 770 | that = this; 771 | if (suggestion) { 772 | hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length); 773 | } 774 | if (that.hintValue !== hintValue) { 775 | that.hintValue = hintValue; 776 | that.hint = suggestion; 777 | (this.options.onHint || $.noop)(hintValue); 778 | } 779 | }, 780 | 781 | verifySuggestionsFormat: function (suggestions) { 782 | // If suggestions is string array, convert them to supported format: 783 | if (suggestions.length && typeof suggestions[0] === 'string') { 784 | return $.map(suggestions, function (value) { 785 | return { value: value, data: null }; 786 | }); 787 | } 788 | 789 | return suggestions; 790 | }, 791 | 792 | validateOrientation: function(orientation, fallback) { 793 | orientation = $.trim(orientation || '').toLowerCase(); 794 | 795 | if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){ 796 | orientation = fallback; 797 | } 798 | 799 | return orientation; 800 | }, 801 | 802 | processResponse: function (result, originalQuery, cacheKey) { 803 | var that = this, 804 | options = that.options; 805 | 806 | result.suggestions = that.verifySuggestionsFormat(result.suggestions); 807 | 808 | // Cache results if cache is not disabled: 809 | if (!options.noCache) { 810 | that.cachedResponse[cacheKey] = result; 811 | if (options.preventBadQueries && !result.suggestions.length) { 812 | that.badQueries.push(originalQuery); 813 | } 814 | } 815 | 816 | // Return if originalQuery is not matching current query: 817 | if (originalQuery !== that.getQuery(that.currentValue)) { 818 | return; 819 | } 820 | 821 | that.suggestions = result.suggestions; 822 | that.suggest(); 823 | }, 824 | 825 | activate: function (index) { 826 | var that = this, 827 | activeItem, 828 | selected = that.classes.selected, 829 | container = $(that.suggestionsContainer), 830 | children = container.find('.' + that.classes.suggestion); 831 | 832 | container.find('.' + selected).removeClass(selected); 833 | 834 | that.selectedIndex = index; 835 | 836 | if (that.selectedIndex !== -1 && children.length > that.selectedIndex) { 837 | activeItem = children.get(that.selectedIndex); 838 | $(activeItem).addClass(selected); 839 | return activeItem; 840 | } 841 | 842 | return null; 843 | }, 844 | 845 | selectHint: function () { 846 | var that = this, 847 | i = $.inArray(that.hint, that.suggestions); 848 | 849 | that.select(i); 850 | }, 851 | 852 | select: function (i) { 853 | var that = this; 854 | that.hide(); 855 | that.onSelect(i); 856 | that.disableKillerFn(); 857 | }, 858 | 859 | moveUp: function () { 860 | var that = this; 861 | 862 | if (that.selectedIndex === -1) { 863 | return; 864 | } 865 | 866 | if (that.selectedIndex === 0) { 867 | $(that.suggestionsContainer).children().first().removeClass(that.classes.selected); 868 | that.selectedIndex = -1; 869 | that.el.val(that.currentValue); 870 | that.findBestHint(); 871 | return; 872 | } 873 | 874 | that.adjustScroll(that.selectedIndex - 1); 875 | }, 876 | 877 | moveDown: function () { 878 | var that = this; 879 | 880 | if (that.selectedIndex === (that.suggestions.length - 1)) { 881 | return; 882 | } 883 | 884 | that.adjustScroll(that.selectedIndex + 1); 885 | }, 886 | 887 | adjustScroll: function (index) { 888 | var that = this, 889 | activeItem = that.activate(index); 890 | 891 | if (!activeItem) { 892 | return; 893 | } 894 | 895 | var offsetTop, 896 | upperBound, 897 | lowerBound, 898 | heightDelta = $(activeItem).outerHeight(); 899 | 900 | offsetTop = activeItem.offsetTop; 901 | upperBound = $(that.suggestionsContainer).scrollTop(); 902 | lowerBound = upperBound + that.options.maxHeight - heightDelta; 903 | 904 | if (offsetTop < upperBound) { 905 | $(that.suggestionsContainer).scrollTop(offsetTop); 906 | } else if (offsetTop > lowerBound) { 907 | $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta); 908 | } 909 | 910 | if (!that.options.preserveInput) { 911 | that.el.val(that.getValue(that.suggestions[index].value)); 912 | } 913 | that.signalHint(null); 914 | }, 915 | 916 | onSelect: function (index) { 917 | var that = this, 918 | onSelectCallback = that.options.onSelect, 919 | suggestion = that.suggestions[index]; 920 | 921 | that.currentValue = that.getValue(suggestion.value); 922 | 923 | if (that.currentValue !== that.el.val() && !that.options.preserveInput) { 924 | that.el.val(that.currentValue); 925 | } 926 | 927 | that.signalHint(null); 928 | that.suggestions = []; 929 | that.selection = suggestion; 930 | 931 | if ($.isFunction(onSelectCallback)) { 932 | onSelectCallback.call(that.element, suggestion); 933 | } 934 | }, 935 | 936 | getValue: function (value) { 937 | var that = this, 938 | delimiter = that.options.delimiter, 939 | currentValue, 940 | parts; 941 | 942 | if (!delimiter) { 943 | return value; 944 | } 945 | 946 | currentValue = that.currentValue; 947 | parts = currentValue.split(delimiter); 948 | 949 | if (parts.length === 1) { 950 | return value; 951 | } 952 | 953 | return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value; 954 | }, 955 | 956 | dispose: function () { 957 | var that = this; 958 | that.el.off('.autocomplete').removeData('autocomplete'); 959 | that.disableKillerFn(); 960 | $(window).off('resize.autocomplete', that.fixPositionCapture); 961 | $(that.suggestionsContainer).remove(); 962 | } 963 | }; 964 | 965 | // Create chainable jQuery plugin: 966 | $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { 967 | var dataKey = 'autocomplete'; 968 | // If function invoked without argument return 969 | // instance of the first matched element: 970 | if (!arguments.length) { 971 | return this.first().data(dataKey); 972 | } 973 | 974 | return this.each(function () { 975 | var inputElement = $(this), 976 | instance = inputElement.data(dataKey); 977 | 978 | if (typeof options === 'string') { 979 | if (instance && typeof instance[options] === 'function') { 980 | instance[options](args); 981 | } 982 | } else { 983 | // If instance already exists, destroy it: 984 | if (instance && instance.dispose) { 985 | instance.dispose(); 986 | } 987 | instance = new Autocomplete(this, options); 988 | inputElement.data(dataKey, instance); 989 | } 990 | }); 991 | }; 992 | })); 993 | -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | if (typeof jQuery === "undefined") { throw new Error("Bootstrap requires jQuery") } 8 | 9 | /* ======================================================================== 10 | * Bootstrap: transition.js v3.0.3 11 | * http://getbootstrap.com/javascript/#transitions 12 | * ======================================================================== 13 | * Copyright 2013 Twitter, Inc. 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | * ======================================================================== */ 27 | 28 | 29 | +function ($) { "use strict"; 30 | 31 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 32 | // ============================================================ 33 | 34 | function transitionEnd() { 35 | var el = document.createElement('bootstrap') 36 | 37 | var transEndEventNames = { 38 | 'WebkitTransition' : 'webkitTransitionEnd' 39 | , 'MozTransition' : 'transitionend' 40 | , 'OTransition' : 'oTransitionEnd otransitionend' 41 | , 'transition' : 'transitionend' 42 | } 43 | 44 | for (var name in transEndEventNames) { 45 | if (el.style[name] !== undefined) { 46 | return { end: transEndEventNames[name] } 47 | } 48 | } 49 | } 50 | 51 | // http://blog.alexmaccaw.com/css-transitions 52 | $.fn.emulateTransitionEnd = function (duration) { 53 | var called = false, $el = this 54 | $(this).one($.support.transition.end, function () { called = true }) 55 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 56 | setTimeout(callback, duration) 57 | return this 58 | } 59 | 60 | $(function () { 61 | $.support.transition = transitionEnd() 62 | }) 63 | 64 | }(jQuery); 65 | 66 | /* ======================================================================== 67 | * Bootstrap: alert.js v3.0.3 68 | * http://getbootstrap.com/javascript/#alerts 69 | * ======================================================================== 70 | * Copyright 2013 Twitter, Inc. 71 | * 72 | * Licensed under the Apache License, Version 2.0 (the "License"); 73 | * you may not use this file except in compliance with the License. 74 | * You may obtain a copy of the License at 75 | * 76 | * http://www.apache.org/licenses/LICENSE-2.0 77 | * 78 | * Unless required by applicable law or agreed to in writing, software 79 | * distributed under the License is distributed on an "AS IS" BASIS, 80 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 81 | * See the License for the specific language governing permissions and 82 | * limitations under the License. 83 | * ======================================================================== */ 84 | 85 | 86 | +function ($) { "use strict"; 87 | 88 | // ALERT CLASS DEFINITION 89 | // ====================== 90 | 91 | var dismiss = '[data-dismiss="alert"]' 92 | var Alert = function (el) { 93 | $(el).on('click', dismiss, this.close) 94 | } 95 | 96 | Alert.prototype.close = function (e) { 97 | var $this = $(this) 98 | var selector = $this.attr('data-target') 99 | 100 | if (!selector) { 101 | selector = $this.attr('href') 102 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 103 | } 104 | 105 | var $parent = $(selector) 106 | 107 | if (e) e.preventDefault() 108 | 109 | if (!$parent.length) { 110 | $parent = $this.hasClass('alert') ? $this : $this.parent() 111 | } 112 | 113 | $parent.trigger(e = $.Event('close.bs.alert')) 114 | 115 | if (e.isDefaultPrevented()) return 116 | 117 | $parent.removeClass('in') 118 | 119 | function removeElement() { 120 | $parent.trigger('closed.bs.alert').remove() 121 | } 122 | 123 | $.support.transition && $parent.hasClass('fade') ? 124 | $parent 125 | .one($.support.transition.end, removeElement) 126 | .emulateTransitionEnd(150) : 127 | removeElement() 128 | } 129 | 130 | 131 | // ALERT PLUGIN DEFINITION 132 | // ======================= 133 | 134 | var old = $.fn.alert 135 | 136 | $.fn.alert = function (option) { 137 | return this.each(function () { 138 | var $this = $(this) 139 | var data = $this.data('bs.alert') 140 | 141 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 142 | if (typeof option == 'string') data[option].call($this) 143 | }) 144 | } 145 | 146 | $.fn.alert.Constructor = Alert 147 | 148 | 149 | // ALERT NO CONFLICT 150 | // ================= 151 | 152 | $.fn.alert.noConflict = function () { 153 | $.fn.alert = old 154 | return this 155 | } 156 | 157 | 158 | // ALERT DATA-API 159 | // ============== 160 | 161 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 162 | 163 | }(jQuery); 164 | 165 | /* ======================================================================== 166 | * Bootstrap: button.js v3.0.3 167 | * http://getbootstrap.com/javascript/#buttons 168 | * ======================================================================== 169 | * Copyright 2013 Twitter, Inc. 170 | * 171 | * Licensed under the Apache License, Version 2.0 (the "License"); 172 | * you may not use this file except in compliance with the License. 173 | * You may obtain a copy of the License at 174 | * 175 | * http://www.apache.org/licenses/LICENSE-2.0 176 | * 177 | * Unless required by applicable law or agreed to in writing, software 178 | * distributed under the License is distributed on an "AS IS" BASIS, 179 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 180 | * See the License for the specific language governing permissions and 181 | * limitations under the License. 182 | * ======================================================================== */ 183 | 184 | 185 | +function ($) { "use strict"; 186 | 187 | // BUTTON PUBLIC CLASS DEFINITION 188 | // ============================== 189 | 190 | var Button = function (element, options) { 191 | this.$element = $(element) 192 | this.options = $.extend({}, Button.DEFAULTS, options) 193 | } 194 | 195 | Button.DEFAULTS = { 196 | loadingText: 'loading...' 197 | } 198 | 199 | Button.prototype.setState = function (state) { 200 | var d = 'disabled' 201 | var $el = this.$element 202 | var val = $el.is('input') ? 'val' : 'html' 203 | var data = $el.data() 204 | 205 | state = state + 'Text' 206 | 207 | if (!data.resetText) $el.data('resetText', $el[val]()) 208 | 209 | $el[val](data[state] || this.options[state]) 210 | 211 | // push to event loop to allow forms to submit 212 | setTimeout(function () { 213 | state == 'loadingText' ? 214 | $el.addClass(d).attr(d, d) : 215 | $el.removeClass(d).removeAttr(d); 216 | }, 0) 217 | } 218 | 219 | Button.prototype.toggle = function () { 220 | var $parent = this.$element.closest('[data-toggle="buttons"]') 221 | var changed = true 222 | 223 | if ($parent.length) { 224 | var $input = this.$element.find('input') 225 | if ($input.prop('type') === 'radio') { 226 | // see if clicking on current one 227 | if ($input.prop('checked') && this.$element.hasClass('active')) 228 | changed = false 229 | else 230 | $parent.find('.active').removeClass('active') 231 | } 232 | if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') 233 | } 234 | 235 | if (changed) this.$element.toggleClass('active') 236 | } 237 | 238 | 239 | // BUTTON PLUGIN DEFINITION 240 | // ======================== 241 | 242 | var old = $.fn.button 243 | 244 | $.fn.button = function (option) { 245 | return this.each(function () { 246 | var $this = $(this) 247 | var data = $this.data('bs.button') 248 | var options = typeof option == 'object' && option 249 | 250 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 251 | 252 | if (option == 'toggle') data.toggle() 253 | else if (option) data.setState(option) 254 | }) 255 | } 256 | 257 | $.fn.button.Constructor = Button 258 | 259 | 260 | // BUTTON NO CONFLICT 261 | // ================== 262 | 263 | $.fn.button.noConflict = function () { 264 | $.fn.button = old 265 | return this 266 | } 267 | 268 | 269 | // BUTTON DATA-API 270 | // =============== 271 | 272 | $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { 273 | var $btn = $(e.target) 274 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 275 | $btn.button('toggle') 276 | e.preventDefault() 277 | }) 278 | 279 | }(jQuery); 280 | 281 | /* ======================================================================== 282 | * Bootstrap: carousel.js v3.0.3 283 | * http://getbootstrap.com/javascript/#carousel 284 | * ======================================================================== 285 | * Copyright 2013 Twitter, Inc. 286 | * 287 | * Licensed under the Apache License, Version 2.0 (the "License"); 288 | * you may not use this file except in compliance with the License. 289 | * You may obtain a copy of the License at 290 | * 291 | * http://www.apache.org/licenses/LICENSE-2.0 292 | * 293 | * Unless required by applicable law or agreed to in writing, software 294 | * distributed under the License is distributed on an "AS IS" BASIS, 295 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 296 | * See the License for the specific language governing permissions and 297 | * limitations under the License. 298 | * ======================================================================== */ 299 | 300 | 301 | +function ($) { "use strict"; 302 | 303 | // CAROUSEL CLASS DEFINITION 304 | // ========================= 305 | 306 | var Carousel = function (element, options) { 307 | this.$element = $(element) 308 | this.$indicators = this.$element.find('.carousel-indicators') 309 | this.options = options 310 | this.paused = 311 | this.sliding = 312 | this.interval = 313 | this.$active = 314 | this.$items = null 315 | 316 | this.options.pause == 'hover' && this.$element 317 | .on('mouseenter', $.proxy(this.pause, this)) 318 | .on('mouseleave', $.proxy(this.cycle, this)) 319 | } 320 | 321 | Carousel.DEFAULTS = { 322 | interval: 5000 323 | , pause: 'hover' 324 | , wrap: true 325 | } 326 | 327 | Carousel.prototype.cycle = function (e) { 328 | e || (this.paused = false) 329 | 330 | this.interval && clearInterval(this.interval) 331 | 332 | this.options.interval 333 | && !this.paused 334 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 335 | 336 | return this 337 | } 338 | 339 | Carousel.prototype.getActiveIndex = function () { 340 | this.$active = this.$element.find('.item.active') 341 | this.$items = this.$active.parent().children() 342 | 343 | return this.$items.index(this.$active) 344 | } 345 | 346 | Carousel.prototype.to = function (pos) { 347 | var that = this 348 | var activeIndex = this.getActiveIndex() 349 | 350 | if (pos > (this.$items.length - 1) || pos < 0) return 351 | 352 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) 353 | if (activeIndex == pos) return this.pause().cycle() 354 | 355 | return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) 356 | } 357 | 358 | Carousel.prototype.pause = function (e) { 359 | e || (this.paused = true) 360 | 361 | if (this.$element.find('.next, .prev').length && $.support.transition.end) { 362 | this.$element.trigger($.support.transition.end) 363 | this.cycle(true) 364 | } 365 | 366 | this.interval = clearInterval(this.interval) 367 | 368 | return this 369 | } 370 | 371 | Carousel.prototype.next = function () { 372 | if (this.sliding) return 373 | return this.slide('next') 374 | } 375 | 376 | Carousel.prototype.prev = function () { 377 | if (this.sliding) return 378 | return this.slide('prev') 379 | } 380 | 381 | Carousel.prototype.slide = function (type, next) { 382 | var $active = this.$element.find('.item.active') 383 | var $next = next || $active[type]() 384 | var isCycling = this.interval 385 | var direction = type == 'next' ? 'left' : 'right' 386 | var fallback = type == 'next' ? 'first' : 'last' 387 | var that = this 388 | 389 | if (!$next.length) { 390 | if (!this.options.wrap) return 391 | $next = this.$element.find('.item')[fallback]() 392 | } 393 | 394 | this.sliding = true 395 | 396 | isCycling && this.pause() 397 | 398 | var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) 399 | 400 | if ($next.hasClass('active')) return 401 | 402 | if (this.$indicators.length) { 403 | this.$indicators.find('.active').removeClass('active') 404 | this.$element.one('slid.bs.carousel', function () { 405 | var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) 406 | $nextIndicator && $nextIndicator.addClass('active') 407 | }) 408 | } 409 | 410 | if ($.support.transition && this.$element.hasClass('slide')) { 411 | this.$element.trigger(e) 412 | if (e.isDefaultPrevented()) return 413 | $next.addClass(type) 414 | $next[0].offsetWidth // force reflow 415 | $active.addClass(direction) 416 | $next.addClass(direction) 417 | $active 418 | .one($.support.transition.end, function () { 419 | $next.removeClass([type, direction].join(' ')).addClass('active') 420 | $active.removeClass(['active', direction].join(' ')) 421 | that.sliding = false 422 | setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) 423 | }) 424 | .emulateTransitionEnd(600) 425 | } else { 426 | this.$element.trigger(e) 427 | if (e.isDefaultPrevented()) return 428 | $active.removeClass('active') 429 | $next.addClass('active') 430 | this.sliding = false 431 | this.$element.trigger('slid.bs.carousel') 432 | } 433 | 434 | isCycling && this.cycle() 435 | 436 | return this 437 | } 438 | 439 | 440 | // CAROUSEL PLUGIN DEFINITION 441 | // ========================== 442 | 443 | var old = $.fn.carousel 444 | 445 | $.fn.carousel = function (option) { 446 | return this.each(function () { 447 | var $this = $(this) 448 | var data = $this.data('bs.carousel') 449 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 450 | var action = typeof option == 'string' ? option : options.slide 451 | 452 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 453 | if (typeof option == 'number') data.to(option) 454 | else if (action) data[action]() 455 | else if (options.interval) data.pause().cycle() 456 | }) 457 | } 458 | 459 | $.fn.carousel.Constructor = Carousel 460 | 461 | 462 | // CAROUSEL NO CONFLICT 463 | // ==================== 464 | 465 | $.fn.carousel.noConflict = function () { 466 | $.fn.carousel = old 467 | return this 468 | } 469 | 470 | 471 | // CAROUSEL DATA-API 472 | // ================= 473 | 474 | $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { 475 | var $this = $(this), href 476 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 477 | var options = $.extend({}, $target.data(), $this.data()) 478 | var slideIndex = $this.attr('data-slide-to') 479 | if (slideIndex) options.interval = false 480 | 481 | $target.carousel(options) 482 | 483 | if (slideIndex = $this.attr('data-slide-to')) { 484 | $target.data('bs.carousel').to(slideIndex) 485 | } 486 | 487 | e.preventDefault() 488 | }) 489 | 490 | $(window).on('load', function () { 491 | $('[data-ride="carousel"]').each(function () { 492 | var $carousel = $(this) 493 | $carousel.carousel($carousel.data()) 494 | }) 495 | }) 496 | 497 | }(jQuery); 498 | 499 | /* ======================================================================== 500 | * Bootstrap: collapse.js v3.0.3 501 | * http://getbootstrap.com/javascript/#collapse 502 | * ======================================================================== 503 | * Copyright 2013 Twitter, Inc. 504 | * 505 | * Licensed under the Apache License, Version 2.0 (the "License"); 506 | * you may not use this file except in compliance with the License. 507 | * You may obtain a copy of the License at 508 | * 509 | * http://www.apache.org/licenses/LICENSE-2.0 510 | * 511 | * Unless required by applicable law or agreed to in writing, software 512 | * distributed under the License is distributed on an "AS IS" BASIS, 513 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 514 | * See the License for the specific language governing permissions and 515 | * limitations under the License. 516 | * ======================================================================== */ 517 | 518 | 519 | +function ($) { "use strict"; 520 | 521 | // COLLAPSE PUBLIC CLASS DEFINITION 522 | // ================================ 523 | 524 | var Collapse = function (element, options) { 525 | this.$element = $(element) 526 | this.options = $.extend({}, Collapse.DEFAULTS, options) 527 | this.transitioning = null 528 | 529 | if (this.options.parent) this.$parent = $(this.options.parent) 530 | if (this.options.toggle) this.toggle() 531 | } 532 | 533 | Collapse.DEFAULTS = { 534 | toggle: true 535 | } 536 | 537 | Collapse.prototype.dimension = function () { 538 | var hasWidth = this.$element.hasClass('width') 539 | return hasWidth ? 'width' : 'height' 540 | } 541 | 542 | Collapse.prototype.show = function () { 543 | if (this.transitioning || this.$element.hasClass('in')) return 544 | 545 | var startEvent = $.Event('show.bs.collapse') 546 | this.$element.trigger(startEvent) 547 | if (startEvent.isDefaultPrevented()) return 548 | 549 | var actives = this.$parent && this.$parent.find('> .panel > .in') 550 | 551 | if (actives && actives.length) { 552 | var hasData = actives.data('bs.collapse') 553 | if (hasData && hasData.transitioning) return 554 | actives.collapse('hide') 555 | hasData || actives.data('bs.collapse', null) 556 | } 557 | 558 | var dimension = this.dimension() 559 | 560 | this.$element 561 | .removeClass('collapse') 562 | .addClass('collapsing') 563 | [dimension](0) 564 | 565 | this.transitioning = 1 566 | 567 | var complete = function () { 568 | this.$element 569 | .removeClass('collapsing') 570 | .addClass('in') 571 | [dimension]('auto') 572 | this.transitioning = 0 573 | this.$element.trigger('shown.bs.collapse') 574 | } 575 | 576 | if (!$.support.transition) return complete.call(this) 577 | 578 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 579 | 580 | this.$element 581 | .one($.support.transition.end, $.proxy(complete, this)) 582 | .emulateTransitionEnd(350) 583 | [dimension](this.$element[0][scrollSize]) 584 | } 585 | 586 | Collapse.prototype.hide = function () { 587 | if (this.transitioning || !this.$element.hasClass('in')) return 588 | 589 | var startEvent = $.Event('hide.bs.collapse') 590 | this.$element.trigger(startEvent) 591 | if (startEvent.isDefaultPrevented()) return 592 | 593 | var dimension = this.dimension() 594 | 595 | this.$element 596 | [dimension](this.$element[dimension]()) 597 | [0].offsetHeight 598 | 599 | this.$element 600 | .addClass('collapsing') 601 | .removeClass('collapse') 602 | .removeClass('in') 603 | 604 | this.transitioning = 1 605 | 606 | var complete = function () { 607 | this.transitioning = 0 608 | this.$element 609 | .trigger('hidden.bs.collapse') 610 | .removeClass('collapsing') 611 | .addClass('collapse') 612 | } 613 | 614 | if (!$.support.transition) return complete.call(this) 615 | 616 | this.$element 617 | [dimension](0) 618 | .one($.support.transition.end, $.proxy(complete, this)) 619 | .emulateTransitionEnd(350) 620 | } 621 | 622 | Collapse.prototype.toggle = function () { 623 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 624 | } 625 | 626 | 627 | // COLLAPSE PLUGIN DEFINITION 628 | // ========================== 629 | 630 | var old = $.fn.collapse 631 | 632 | $.fn.collapse = function (option) { 633 | return this.each(function () { 634 | var $this = $(this) 635 | var data = $this.data('bs.collapse') 636 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 637 | 638 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 639 | if (typeof option == 'string') data[option]() 640 | }) 641 | } 642 | 643 | $.fn.collapse.Constructor = Collapse 644 | 645 | 646 | // COLLAPSE NO CONFLICT 647 | // ==================== 648 | 649 | $.fn.collapse.noConflict = function () { 650 | $.fn.collapse = old 651 | return this 652 | } 653 | 654 | 655 | // COLLAPSE DATA-API 656 | // ================= 657 | 658 | $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { 659 | var $this = $(this), href 660 | var target = $this.attr('data-target') 661 | || e.preventDefault() 662 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 663 | var $target = $(target) 664 | var data = $target.data('bs.collapse') 665 | var option = data ? 'toggle' : $this.data() 666 | var parent = $this.attr('data-parent') 667 | var $parent = parent && $(parent) 668 | 669 | if (!data || !data.transitioning) { 670 | if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') 671 | $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') 672 | } 673 | 674 | $target.collapse(option) 675 | }) 676 | 677 | }(jQuery); 678 | 679 | /* ======================================================================== 680 | * Bootstrap: dropdown.js v3.0.3 681 | * http://getbootstrap.com/javascript/#dropdowns 682 | * ======================================================================== 683 | * Copyright 2013 Twitter, Inc. 684 | * 685 | * Licensed under the Apache License, Version 2.0 (the "License"); 686 | * you may not use this file except in compliance with the License. 687 | * You may obtain a copy of the License at 688 | * 689 | * http://www.apache.org/licenses/LICENSE-2.0 690 | * 691 | * Unless required by applicable law or agreed to in writing, software 692 | * distributed under the License is distributed on an "AS IS" BASIS, 693 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 694 | * See the License for the specific language governing permissions and 695 | * limitations under the License. 696 | * ======================================================================== */ 697 | 698 | 699 | +function ($) { "use strict"; 700 | 701 | // DROPDOWN CLASS DEFINITION 702 | // ========================= 703 | 704 | var backdrop = '.dropdown-backdrop' 705 | var toggle = '[data-toggle=dropdown]' 706 | var Dropdown = function (element) { 707 | $(element).on('click.bs.dropdown', this.toggle) 708 | } 709 | 710 | Dropdown.prototype.toggle = function (e) { 711 | var $this = $(this) 712 | 713 | if ($this.is('.disabled, :disabled')) return 714 | 715 | var $parent = getParent($this) 716 | var isActive = $parent.hasClass('open') 717 | 718 | clearMenus() 719 | 720 | if (!isActive) { 721 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 722 | // if mobile we use a backdrop because click events don't delegate 723 | $('