├── README.md ├── goto ├── __init__.py ├── views.py~ ├── urls.py ├── tests.py └── views.py ├── learn ├── __init__.py └── Collab.py ├── concierge ├── __init__.py ├── urls.py ├── wsgi.py ├── views.py └── settings.py ├── utility ├── __init__.py ├── match.py ├── g_utility.py ├── models.py └── fb_utility.py ├── static ├── img │ ├── .gitkeep │ ├── fb.png │ └── logo.png ├── empowered.png ├── js │ ├── app.js │ ├── foundation │ │ ├── foundation.alerts.js │ │ ├── foundation.cookie.js │ │ ├── foundation.dropdown.js │ │ ├── foundation.magellan.js │ │ ├── foundation.placeholder.js │ │ ├── foundation.tooltips.js │ │ ├── foundation.topbar.js │ │ ├── foundation.reveal.js │ │ ├── foundation.section.js │ │ ├── foundation.js │ │ ├── foundation.clearing.js │ │ ├── foundation.orbit.js │ │ ├── foundation.forms.js │ │ └── foundation.joyride.js │ └── vendor │ │ └── custom.modernizr.js └── css │ ├── style.css │ └── normalize.css ├── user_profile ├── __init__.py ├── admin.py ├── urls.py ├── tests.py ├── views.py ├── models.py ├── managers.py └── singly.py ├── recommendation_item ├── __init__.py ├── tests.py ├── views.py └── models.py ├── requirements.txt ├── manage.py ├── .gitignore └── templates ├── findasong.html ├── findafriend.html ├── gotos.html ├── resrecos.html └── index.html /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /goto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /learn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /concierge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utility/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /user_profile/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /recommendation_item/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /goto/views.py~: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /static/img/fb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/concierge-web/HEAD/static/img/fb.png -------------------------------------------------------------------------------- /static/empowered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/concierge-web/HEAD/static/empowered.png -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/concierge-web/HEAD/static/img/logo.png -------------------------------------------------------------------------------- /user_profile/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import UserProfile 3 | 4 | admin.site.register(UserProfile) 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.4.3 2 | django-tastypie==0.9.11 3 | mimeparse==0.1.3 4 | psycopg2==2.4.6 5 | python-dateutil==1.5 6 | requests==1.1.0 7 | simplejson==3.0.7 8 | wsgiref==0.1.2 9 | -------------------------------------------------------------------------------- /goto/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | urlpatterns = patterns('goto.views', 3 | url(r'^pickgotos/', 'pickgotos', 4 | name='pickgotos'), 5 | url(r'^savegotos/', 'savegotos', 6 | name='savegotos'), 7 | ) 8 | -------------------------------------------------------------------------------- /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", "concierge.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /user_profile/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | urlpatterns = patterns('user_profile.views', 3 | url(r'^authenticate/(?P[a-z]+)/$', 'authenticate_redirect', 4 | name='authenticate_redirect'), 5 | url(r'^authorize/callback/$', 'authorize_callback', 6 | name='authorize_callback'), 7 | ) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | *.db 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | lib 17 | include 18 | develop-eggs 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | .Python 28 | 29 | .venv 30 | .DS_Store 31 | */.DS_Store 32 | *.pyc 33 | *.swp 34 | *~ 35 | -------------------------------------------------------------------------------- /goto/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /user_profile/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /recommendation_item/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /concierge/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | (r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'}), 8 | ) 9 | 10 | urlpatterns += patterns('concierge.views', 11 | (r'^admin/', include(admin.site.urls)), 12 | url(r'', include('user_profile.urls')), 13 | url(r'', include('goto.urls')), 14 | url(r'^$', 'index', name='index'), 15 | 16 | ) 17 | 18 | urlpatterns += patterns('recommendation_item.views', 19 | url(r'^getFactual/', 'testFactual')) 20 | 21 | urlpatterns += patterns('recommendation_item.views', 22 | url(r'^recRestaurants/', 23 | 'recommendRestaurants')) 24 | -------------------------------------------------------------------------------- /utility/match.py: -------------------------------------------------------------------------------- 1 | from factual import Factual 2 | from factual.utils import circle 3 | 4 | FACTUAL_KEY = "koqW7kcnT4qXVi6ruJC25LSVZhhpEwnxTJnDT0Kn" 5 | FACTUAL_SECRET = "JgofM3V440POwypFRGgtgkbDuOGbPbTCoTw0M4Gv" 6 | 7 | factual = Factual(FACTUAL_KEY, FACTUAL_SECRET) 8 | 9 | def matchRestaurant(restaurant_name, city="", state=""): 10 | 11 | address = city + " " + state 12 | s = factual.table("restaurants").search(restaurant_name + " " + address).data() 13 | if not s: 14 | s = factual.table("restaurants").search(restaurant_name + " " + state).data() 15 | 16 | return s 17 | 18 | def matchCoordinates(address): 19 | query = factual.table("world-geographies").search(address).limit(1) 20 | query = query.select("longitude,latitude") 21 | query = query.geo(circle(42.350933, -71.069209, 5000)) 22 | s = query.data() 23 | if len(s): 24 | return (s[0][unicode('latitude')], s[0][unicode('longitude')]) 25 | return (42.350933, -71.069209) 26 | -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | // The URL of the Singly API endpoint 2 | var apiBaseUrl = 'https://api.singly.com'; 3 | 4 | // A small wrapper for getting data from the Singly API 5 | var singly = { 6 | get: function(url, options, callback) { 7 | if (options === undefined || 8 | options === null) { 9 | options = {}; 10 | } 11 | 12 | options.access_token = accessToken; 13 | $.getJSON(apiBaseUrl + url, options, callback); 14 | } 15 | }; 16 | 17 | // Runs after the page has loaded 18 | $(function() { 19 | // If there was no access token defined then return 20 | if (accessToken === 'undefined' || 21 | accessToken === undefined) { 22 | return; 23 | } 24 | 25 | // Get the 5 latest items from the user's Twitter feed 26 | singly.get('/services/twitter/tweets', { limit: 5 }, function(tweets) { 27 | for (var i = 0; i < tweets.length; i++) { 28 | $('#twitter').append('
  • Tweet: '+ tweets[i].data.text +'
  • '); 29 | } 30 | }); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /templates/findasong.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Concierge 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 |
    23 |
    24 |

    Here's a song you might like

    25 | {% if request.user.is_authenticated %} 26 |
    27 |
    28 | {{ suggested_song }} 29 |
    30 |
    31 | {% endif %} 32 |
    33 |
    34 | 35 | 36 | -------------------------------------------------------------------------------- /user_profile/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect, HttpResponse 2 | from django.contrib.auth import authenticate, login as auth_login 3 | from django.contrib.auth.models import User 4 | 5 | from singly import SinglyHelper 6 | from models import UserProfile 7 | from utility import fb_utility 8 | 9 | def authenticate_redirect(request, service): 10 | url = SinglyHelper.get_authorize_url(service) 11 | return HttpResponseRedirect(url) 12 | 13 | def authorize_callback(request): 14 | code = request.GET.get('code') 15 | content = SinglyHelper.get_access_token(code) 16 | user_profile = UserProfile.objects.get_or_create_user(content['account'], content['access_token']) 17 | user_profile.fb_data = fb_utility.fill_profile(user_profile, content['account'], content['access_token']) 18 | user_profile.save() 19 | if not request.user.is_authenticated(): 20 | user = authenticate(username=user_profile.user.username, password='fakepassword') 21 | auth_login(request, user) 22 | return HttpResponseRedirect('/') 23 | 24 | -------------------------------------------------------------------------------- /templates/findafriend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Concierge 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 |
    23 |
    24 |

    Here's your new friend!

    25 | {% if request.user.is_authenticated %} 26 |
    27 |
    28 | {{ suggested_user.first_name }} 29 |
    30 |
    31 | {% endif %} 32 |
    33 |
    34 | 35 | 36 | -------------------------------------------------------------------------------- /user_profile/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from managers import UserProfileManager 4 | 5 | from recommendation_item.models import Restaurant, Address 6 | 7 | # Create your models here. 8 | class UserProfile(models.Model): 9 | user = models.ForeignKey(User, related_name='profile') 10 | access_token = models.CharField(max_length=260, null=True, blank=True) 11 | singly_id = models.CharField(max_length=260, null=True, blank=True) 12 | profiles = models.TextField(null=True, blank=True) 13 | gotos = models.ManyToManyField(Restaurant, null=True, blank=True) 14 | fb_data = models.CharField(max_length=10000) 15 | objects = UserProfileManager() 16 | 17 | class Meta: 18 | db_table = 'user_profile' 19 | 20 | # static Facebook vector, fb_data, holds: 21 | """ 22 | Where you live 23 | Schools 24 | Birthday/Age 25 | Gender 26 | Sexual orientation 27 | Languages 28 | Likes 29 | (Count for each like category) 30 | Map tags 31 | Friend Count 32 | """ 33 | -------------------------------------------------------------------------------- /concierge/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for concierge project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "concierge.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /user_profile/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from singly import * 3 | from django.contrib.auth.models import User 4 | from django.core.exceptions import ObjectDoesNotExist 5 | 6 | 7 | class UserProfileManager(models.Manager): 8 | 9 | def get_or_create_user(self, singly_id, access_token): 10 | endpoint = '/profiles' 11 | request = {'auth': 'true'} 12 | profiles = Singly(access_token=access_token).make_request(endpoint, request=request) 13 | singly_id = profiles['id'] 14 | try: 15 | user_profile = self.get(singly_id=singly_id) 16 | user_profile.profiles = profiles 17 | user_profile.save() 18 | 19 | except ObjectDoesNotExist: 20 | try: 21 | user = User.objects.get(username=singly_id) 22 | except ObjectDoesNotExist: 23 | # Made-up email address included due to convention 24 | user = User.objects.create_user(singly_id, singly_id + '@singly.com', 'fakepassword') 25 | user_profile = self.model( 26 | access_token=access_token, 27 | singly_id=singly_id, 28 | profiles=profiles, 29 | user=user 30 | ) 31 | user_profile.save() 32 | return user_profile 33 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.alerts.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.alerts = { 7 | name : 'alerts', 8 | 9 | version : '4.0.0', 10 | 11 | settings : { 12 | speed: 300, // fade out speed 13 | callback: function (){} 14 | }, 15 | 16 | init : function (scope, method, options) { 17 | this.scope = scope || this.scope; 18 | 19 | if (typeof method === 'object') { 20 | $.extend(true, this.settings, method); 21 | } 22 | 23 | if (typeof method != 'string') { 24 | if (!this.settings.init) this.events(); 25 | 26 | return this.settings.init; 27 | } else { 28 | return this[method].call(this, options); 29 | } 30 | }, 31 | 32 | events : function () { 33 | var self = this; 34 | 35 | $(this.scope).on('click.fndtn.alerts', '[data-alert] a.close', function (e) { 36 | e.preventDefault(); 37 | $(this).closest("[data-alert]").fadeOut(self.speed, function () { 38 | $(this).remove(); 39 | self.settings.callback(); 40 | }); 41 | }); 42 | 43 | this.settings.init = true; 44 | }, 45 | 46 | off : function () { 47 | $(this.scope).off('.fndtn.alerts'); 48 | } 49 | }; 50 | }(Foundation.zj, this, this.document)); 51 | -------------------------------------------------------------------------------- /concierge/views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.shortcuts import render_to_response 3 | from django.template.context import RequestContext 4 | from django.http import HttpResponseRedirect 5 | from django.contrib import auth 6 | import simplejson 7 | from learn.Collab import Collab 8 | 9 | def index(request, template='index.html'): 10 | services = [ 11 | 'Facebook', 12 | ] 13 | if request.user.is_authenticated(): 14 | print(request.user) 15 | user_profile = request.user.get_profile() 16 | print(user_profile.profiles) 17 | if user_profile.profiles and user_profile.profiles != "": 18 | # We replace single quotes with double quotes b/c of python's strict json requirements 19 | profiles = simplejson.loads(user_profile.profiles.replace("'", '"')) 20 | else: 21 | profiles = "" 22 | response = render_to_response(template, locals(), context_instance=RequestContext(request)) 23 | return response 24 | 25 | def suggest_user(request): 26 | template = 'findafriend.html' # put a template here 27 | if request.user.is_authenticated(): 28 | user_profile = request.user.get_profile() 29 | suggested_user = Collab().suggest_users(user_profile, 1)[0].user 30 | return render_to_response(template, locals(), context_instance=RequestContext(request)) 31 | return HttpResponseRedirect(reverse('concierge.views.index')) 32 | 33 | def logout_user(request): 34 | auth.logout(request) 35 | return HttpResponseRedirect(reverse('concierge.views.index')) 36 | -------------------------------------------------------------------------------- /templates/gotos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Concierge 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 |

    Where do you eat?

    20 | 21 | {% if error_message %}

    {{ error_message }}

    {% endif %} 22 |
    23 |
    24 | {% csrf_token %} 25 | {% for r in raw_gotos %} 26 | 27 | 28 | 29 | 30 | , 31 | 32 | 33 |
    34 | {% endfor %} 35 | Skip this step 36 | 37 |
    38 |
    39 |
    40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/resrecos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Concierge 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 |

    Here's a bunch of restaurants you might like

    20 | Back 21 | 22 |
    23 | {% csrf_token %} 24 | Location: 25 | 26 |
    27 | {% if request.user.is_authenticated %} 28 | {% for r, cuisines, img in recs %} 29 |
    30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    {{ r.title }}
    37 |
    {{ r.address.street_address }} {{r.address.city}}, {{r.address.state}}
    38 |
    39 | {% for c in cuisines %} 40 |
    {{c}}
    41 | 42 | {% endfor %} 43 |
    44 | 45 | 46 |
    47 |
    48 | {% endfor %} 49 | {% endif %} 50 | 51 |
    52 | 53 | 54 | -------------------------------------------------------------------------------- /goto/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from django.shortcuts import get_object_or_404, render, render_to_response 3 | from django.http import HttpResponseRedirect, HttpResponse 4 | from django.core.urlresolvers import reverse 5 | from django.template.context import RequestContext 6 | 7 | from recommendation_item.models import Restaurant, Address 8 | from recommendation_item.views import restaurantFromFactual, addressFromFactual 9 | 10 | from utility import match 11 | import json 12 | 13 | def pickgotos(request, template = "gotos.html"): 14 | 15 | raw_gotos = [] 16 | for count in range(5): 17 | raw_gotos.append(["", ""]) 18 | 19 | return render_to_response(template, locals(), context_instance=RequestContext(request)) 20 | 21 | def savegotos(request): 22 | if request.user.is_authenticated(): 23 | user_profile = request.user.get_profile() 24 | rawgotos = request.POST.getlist(unicode('rawgotos'),'') 25 | rawcities = request.POST.getlist(unicode('rawcities'),'') 26 | rawstates = request.POST.getlist(unicode('rawstates'),'') 27 | fgotos = [] 28 | for r in range(len(rawgotos)): 29 | if rawgotos[r]: 30 | fgotos.append(match.matchRestaurant(rawgotos[r], 31 | rawcities[r], 32 | rawstates[r])) 33 | gotos = [] 34 | 35 | for f in fgotos: 36 | if f: 37 | datum = f[0] 38 | 39 | sources = "{'factual':["+datum.get('factual_id', 0)+"]}" 40 | 41 | a, a_created = addressFromFactual(datum) 42 | r, r_created = restaurantFromFactual(datum, a, sources) 43 | 44 | if not r_created and (not r in user_profile.gotos.all()): 45 | gotos.append(r) 46 | 47 | [user_profile.gotos.add(i) for i in gotos] 48 | user_profile.save() 49 | 50 | return HttpResponseRedirect('../recRestaurants') 51 | -------------------------------------------------------------------------------- /utility/g_utility.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | import simplejson 3 | 4 | from recommendation_item.models import Restaurant, Address 5 | # The request also includes the userip parameter which provides the end 6 | # user's IP address. Doing so will help distinguish this legitimate 7 | # server-side traffic from traffic which doesn't come from an end-user. 8 | 9 | google_key = 'AIzaSyBFD-5MKrHvcu2vtCI630fhGWpzBPEZqdk' 10 | 11 | def gImageSearch(restaurant): 12 | 13 | query = (restaurant.title + " " + 14 | restaurant.address.city + " " + 15 | restaurant.address.state) 16 | query = query.replace(" ","%20") 17 | 18 | url = ("https://maps.googleapis.com/maps/api/place/textsearch/json?query=" + query + 19 | "&key=" + google_key + "&sensor=false") 20 | 21 | 22 | request = urllib2.Request( 23 | url, None, {'Referer': 'Noone'}) 24 | rawResponse = urllib2.urlopen(request) 25 | response = simplejson.load(rawResponse) 26 | print response 27 | if unicode('results') in response and len(response[unicode('results')]) > 0: 28 | if unicode('photos') in response[unicode('results')][0]: 29 | photo_reference = response[unicode('results')][0][unicode('photos')][0][unicode('photo_reference')] 30 | print photo_reference 31 | image_url = ("https://maps.googleapis.com/maps/api/place/photo?photoreference=" + photo_reference + 32 | "&sensor=false" + 33 | "&maxheight=1600&maxwidth=1600&key=" + google_key) 34 | return image_url 35 | 36 | url = ("https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q="+query+ 37 | "&safe=active") 38 | 39 | request = urllib2.Request( 40 | url, None, {'Referer': 'Noone'}) 41 | rawResponse = urllib2.urlopen(request) 42 | response = simplejson.load(rawResponse) 43 | 44 | 45 | 46 | if unicode('responseData') in response and len(response[unicode('responseData')][unicode('results')]) > 0: 47 | results = response[unicode('responseData')][unicode('results')] 48 | else: 49 | return '' 50 | 51 | 52 | 53 | 54 | return results[0][unicode('url')] 55 | -------------------------------------------------------------------------------- /user_profile/singly.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | import requests 3 | from concierge import settings 4 | from concierge.settings import SINGLY_CLIENT_ID, SINGLY_CLIENT_SECRET, SINGLY_REDIRECT_URI 5 | 6 | 7 | class Singly(object): 8 | 9 | api_base = 'https://api.singly.com' 10 | 11 | def __init__(self, client_id=None, client_secret=None, access_token=None): 12 | self.client_id = client_id 13 | self.client_secret = client_secret 14 | self.access_token = access_token 15 | 16 | def make_request(self, endpoint, method='GET', request={}): 17 | url = self.api_base + endpoint 18 | 19 | if method == 'GET': 20 | if self.access_token is not None: 21 | request['access_token'] = self.access_token 22 | response = requests.get(url, params=request) 23 | 24 | elif method == 'POST': 25 | response = requests.post(url, request) 26 | 27 | else: 28 | raise ApiError("Unsupported protocol") 29 | 30 | if response.status_code == 200: 31 | return simplejson.loads(response.content) 32 | else: 33 | if settings.DEBUG: 34 | raise ApiError("%s: %s" % (response.status_code, response.content)) 35 | else: 36 | raise ApiError("Error returned from API") 37 | 38 | def authorize(self, code): 39 | endpoint = '/oauth/access_token' 40 | request = { 41 | 'client_id': self.client_id, 42 | 'client_secret': self.client_secret, 43 | 'code': code 44 | } 45 | content = self.make_request(endpoint, 'POST', request) 46 | return content 47 | 48 | 49 | class SinglyHelper(object): 50 | 51 | @classmethod 52 | def get_authorize_url(cls, service, redirect_uri=None): 53 | 54 | url = '%s/oauth/authorize?client_id=%s&redirect_uri=%s&service=%s' % ( 55 | Singly.api_base, SINGLY_CLIENT_ID, SINGLY_REDIRECT_URI, service 56 | ) 57 | return url 58 | 59 | @classmethod 60 | def get_access_token(cls, code): 61 | api = Singly(SINGLY_CLIENT_ID, SINGLY_CLIENT_SECRET) 62 | content = api.authorize(code) 63 | return content 64 | 65 | 66 | class ApiError(Exception): 67 | pass 68 | -------------------------------------------------------------------------------- /utility/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | class IntegerRangeField(models.IntegerField): 5 | def __init__(self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs): 6 | self.min_value, self.max_value = min_value, max_value 7 | models.IntegerField.__init__(self, verbose_name, name, **kwargs) 8 | def formfield(self, **kwargs): 9 | defaults = {'min_value': self.min_value, 'max_value':self.max_value} 10 | defaults.update(kwargs) 11 | return super(IntegerRangeField, self).formfield(**defaults) 12 | 13 | class SeparatedValuesTimeField(models.TextField): 14 | __metaclass__ = models.SubfieldBase 15 | 16 | def __init__(self, *args, **kwargs): 17 | self.token = kwargs.pop('token', ',') 18 | super(SeparatedValuesTimeField, self).__init__(*args, **kwargs) 19 | 20 | def to_python(self, value): 21 | if not value: return 22 | if isinstance(value, list): 23 | return value 24 | return value.split(self.token) 25 | 26 | def get_db_prep_value(self, value): 27 | if not value: return 28 | assert(isinstance(value, list) or isinstance(value, tuple)) 29 | return self.token.join([unicode(s) for s in value]) 30 | 31 | def value_to_string(self, obj): 32 | value = self._get_val_from_obj(obj) 33 | 34 | class SeparatedValuesField(models.TextField): 35 | __metaclass__ = models.SubfieldBase 36 | 37 | def __init__(self, *args, **kwargs): 38 | self.token = kwargs.pop('token', ',') 39 | super(SeparatedValuesField, self).__init__(*args, **kwargs) 40 | 41 | def to_python(self, value): 42 | if not value: return 43 | if isinstance(value, list): 44 | return value 45 | return value.split(self.token) 46 | 47 | def get_db_prep_value(self, value): 48 | if not value: return 49 | assert(isinstance(value, list) or isinstance(value, tuple)) 50 | return self.token.join([unicode(s) for s in value]) 51 | 52 | def get_db_prep_save(self,value): 53 | print('in db_prep_save with: ', value) 54 | if not value: return '' 55 | else: return value 56 | 57 | def value_to_string(self, obj): 58 | value = self._get_val_from_obj(obj) 59 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2011, Klaus Hartl 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.opensource.org/licenses/GPL-2.0 9 | * 10 | * Modified to work with Zepto.js by ZURB 11 | */ 12 | (function ($, document, undefined) { 13 | 14 | var pluses = /\+/g; 15 | 16 | function raw(s) { 17 | return s; 18 | } 19 | 20 | function decoded(s) { 21 | return decodeURIComponent(s.replace(pluses, ' ')); 22 | } 23 | 24 | var config = $.cookie = function (key, value, options) { 25 | 26 | // write 27 | if (value !== undefined) { 28 | options = $.extend({}, config.defaults, options); 29 | 30 | if (value === null) { 31 | options.expires = -1; 32 | } 33 | 34 | if (typeof options.expires === 'number') { 35 | var days = options.expires, t = options.expires = new Date(); 36 | t.setDate(t.getDate() + days); 37 | } 38 | 39 | value = config.json ? JSON.stringify(value) : String(value); 40 | 41 | return (document.cookie = [ 42 | encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value), 43 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 44 | options.path ? '; path=' + options.path : '', 45 | options.domain ? '; domain=' + options.domain : '', 46 | options.secure ? '; secure' : '' 47 | ].join('')); 48 | } 49 | 50 | // read 51 | var decode = config.raw ? raw : decoded; 52 | var cookies = document.cookie.split('; '); 53 | for (var i = 0, l = cookies.length; i < l; i++) { 54 | var parts = cookies[i].split('='); 55 | if (decode(parts.shift()) === key) { 56 | var cookie = decode(parts.join('=')); 57 | return config.json ? JSON.parse(cookie) : cookie; 58 | } 59 | } 60 | 61 | return null; 62 | }; 63 | 64 | config.defaults = {}; 65 | 66 | $.removeCookie = function (key, options) { 67 | if ($.cookie(key) !== null) { 68 | $.cookie(key, null, options); 69 | return true; 70 | } 71 | return false; 72 | }; 73 | 74 | })(Foundation.zj, document); 75 | -------------------------------------------------------------------------------- /utility/fb_utility.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from user_profile.singly import * 3 | from django.contrib.auth.models import User 4 | from recommendation_item.models import Restaurant, Address, restaurantFromFactual, addressFromFactual 5 | from utility import match 6 | import json 7 | 8 | def fill_profile(user_profile, singly_id, access_token): 9 | discovery_endpoint = '/services/facebook' 10 | self_endpoint = '/services/facebook/self' 11 | page_likes_endpoint = '/services/facebook/page_likes?limit=1000' 12 | request = {'auth': 'true'} 13 | fb_discovery = Singly(access_token=access_token).make_request(discovery_endpoint, request=request) 14 | fb_self = Singly(access_token=access_token).make_request(self_endpoint, request=request) 15 | fb_page_likes = Singly(access_token=access_token).make_request(page_likes_endpoint, request=request) 16 | 17 | udata = unicode("data") 18 | fb_profile = {} 19 | 20 | self_attributes = ["location", "education", "birthday", "gender", 21 | "interested_in", "languages"] 22 | 23 | for a in self_attributes: 24 | if unicode(a) in fb_self[0][udata]: 25 | fb_profile[a] = fb_self[0][udata][unicode(a)] 26 | 27 | fb_profile["friend_count"] = fb_discovery[unicode("friends")] 28 | fb_profile["likes"] = [] 29 | for i in fb_page_likes: 30 | like = [i[udata][unicode("name")],i[udata][unicode("category")]] 31 | if(like[1] == "Restaurant/cafe"): 32 | fill_restaurant_likes(user_profile, like[0], i) 33 | 34 | return json.dumps(fb_profile) 35 | 36 | def fill_restaurant_likes(user_profile, name, full_like): 37 | 38 | udata = unicode("data") 39 | ulocation = unicode('location') 40 | 41 | city = "" 42 | state = "" 43 | #print full_like 44 | 45 | if ulocation in full_like[udata]: 46 | if unicode('city') in full_like[udata][ulocation]: 47 | city = full_like[udata][ulocation][unicode('city')] 48 | if unicode('state') in full_like[udata][ulocation]: 49 | state = full_like[udata][ulocation][unicode('state')] 50 | 51 | frestaurant = match.matchRestaurant(name, city, state) 52 | if frestaurant: 53 | sources = "{'factual':["+frestaurant[0].get('factual_id', 0)+"]}" 54 | 55 | 56 | a, a_created = addressFromFactual(frestaurant[0]) 57 | r, r_created = restaurantFromFactual(frestaurant[0], a, sources) 58 | if not r in user_profile.gotos.all(): 59 | user_profile.gotos.add(r) 60 | user_profile.save() 61 | 62 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Concierge 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 18 | 19 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% if not request.user.is_authenticated %} 43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 |
    50 | {% else %} 51 |
    52 |
    53 | 54 | 59 |
    60 |
    61 | 62 | 63 | 64 | 65 | 66 | {% endif %} 67 | 68 | 69 | -------------------------------------------------------------------------------- /recommendation_item/views.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from django.http import HttpResponseRedirect, HttpResponse 4 | from django.shortcuts import render_to_response 5 | from django.template.context import RequestContext 6 | 7 | from factual import Factual 8 | from factual.utils import circle 9 | 10 | from utility import g_utility 11 | from utility import match 12 | from learn.Collab import Collab 13 | 14 | 15 | from recommendation_item.models import Restaurant, Address, restaurantFromFactual, addressFromFactual 16 | 17 | FACTUAL_KEY = "koqW7kcnT4qXVi6ruJC25LSVZhhpEwnxTJnDT0Kn" 18 | FACTUAL_SECRET = "JgofM3V440POwypFRGgtgkbDuOGbPbTCoTw0M4Gv" 19 | 20 | factual = Factual(FACTUAL_KEY, FACTUAL_SECRET) 21 | 22 | # location is a circle around the location you want results from 23 | def getRestaurantDataFromFactual(location): 24 | added_names = [] 25 | 26 | query = factual.table("restaurants") 27 | query = query.filters({"country":"US"}) 28 | query = query.geo(location) 29 | 30 | query_data = query.data() 31 | print(len(query_data)) 32 | 33 | for datum in query_data: 34 | print (datum.get('name', 'No name given')) 35 | print(datum.get('cuisine',"No cuisine given")) 36 | # dictionary to hold current item's descriptors 37 | sources = "{'factual':["+datum.get('factual_id', 0)+"]}" 38 | 39 | a, a_created = addressFromFactual(datum) 40 | r, r_created = restaurantFromFactual(datum, a, sources) 41 | 42 | if r_created: 43 | added_names.append(datum.get('name',None)) 44 | 45 | return str(added_names) 46 | 47 | def testFactual(request): 48 | LENGTH_OF_ONE_LAT = 111081.59847784671 49 | LENGTH_OF_ONE_LON = 82291.40843937476 50 | lat = 42.418837 51 | lon = -71.130553 52 | end_lat = 42.33 53 | end_lon = -71.033440 54 | 55 | while lat >= end_lat: 56 | while lon <= end_lon: 57 | print(str(lat) + "," + str(lon)) 58 | LOC = circle(lat, lon, 100) 59 | getRestaurantDataFromFactual(LOC) 60 | lon += (100/LENGTH_OF_ONE_LON) 61 | sleep(1) 62 | lat -= (100/LENGTH_OF_ONE_LAT) 63 | lon = -71.130553 64 | return HttpResponse("OK") 65 | 66 | 67 | def recommendRestaurants(request, template = "resrecos.html"): 68 | c = Collab() 69 | user_profile = request.user.get_profile() 70 | location = request.POST.get(unicode('location'),'') 71 | if location: 72 | latitude, longitude = match.matchCoordinates(location) 73 | print latitude, longitude 74 | request.COOKIES['coords'] = ("("+str(latitude)+","+ 75 | str(longitude)+")") 76 | else: 77 | latitude, longitude = eval(request.COOKIES['coords']) 78 | recommendations = c.ensemble_suggestion(4,user_profile,latitude,longitude) 79 | print recommendations 80 | #recommendations = user_profile.gotos 81 | recs = [] 82 | for r in recommendations: 83 | cuisines = eval(str(r.cuisines)) 84 | if not r.image: 85 | r.image = g_utility.gImageSearch(r) 86 | if len(r.image) < 399: 87 | r.save() 88 | 89 | recs.append((r, cuisines, r.image)) 90 | 91 | 92 | 93 | 94 | return render_to_response(template, locals(), context_instance=RequestContext(request)) 95 | 96 | 97 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | #header{ 3 | margin-top:100px; 4 | } 5 | 6 | #rec-wrap { 7 | margin-top:75px; 8 | } 9 | 10 | #rec-links{ 11 | padding-top:250px; 12 | } 13 | 14 | h1 { 15 | font-size:10em; 16 | text-align:center; 17 | } 18 | 19 | #facebook-login { 20 | width:100px; 21 | height:30px; 22 | } 23 | 24 | * { 25 | font-family: Helvetica, Arial; 26 | } 27 | 28 | body { 29 | padding: 4em; 30 | padding-top: 1em; 31 | } 32 | 33 | #access-token { 34 | width: 200px; 35 | } 36 | 37 | a, a:visited { 38 | color: #25aae1; 39 | text-decoration: none; 40 | } 41 | 42 | a:hover { 43 | text-decoration: underline; 44 | } 45 | 46 | li { 47 | list-style-type: none; 48 | } 49 | 50 | .check { 51 | color: #25aae1; 52 | margin-right: 4px; 53 | } 54 | 55 | #empowered { 56 | margin-left: 14px; 57 | width: 900px; 58 | } 59 | 60 | #auth { 61 | width: 180px; 62 | } 63 | 64 | #headerText{ 65 | font-size: 40px; 66 | } 67 | 68 | #tagline{ 69 | font-size: 16px; 70 | color:#1e1e1e; 71 | font-family:Courier; 72 | margin-left: 15px; 73 | margin-top: 23px; 74 | } 75 | 76 | #contentWrapper{ 77 | clear:both; 78 | } 79 | .recLeft{ 80 | float: left; 81 | } 82 | .recRight{ 83 | float: right; 84 | } 85 | .recClear{ 86 | clear: both; 87 | } 88 | .recTitle{ 89 | font-size: 30pt; 90 | } 91 | .recAddress{ 92 | margin-top: 30px; 93 | margin-left: 8px; 94 | } 95 | .recDesc{ 96 | font-size: 15pt; 97 | margin-left: 20px 98 | } 99 | 100 | .recWrapper{ 101 | width:1000px; 102 | margin-left: auto; 103 | margin-right: auto; 104 | } 105 | .innerRecWrapper{ 106 | float: left; 107 | width: 100%; 108 | border-top: solid 2px orange; 109 | } 110 | 111 | .singleRec{ 112 | border-top-width: 5px; 113 | border-top-color: black; 114 | border-top-style: solid; 115 | padding-bottom: 5px; 116 | float:left; 117 | width: 100%; 118 | margin-bottom: 10px; 119 | 120 | } 121 | .imageWrapper{ 122 | float: left; 123 | } 124 | .recImage{ 125 | float: left; 126 | width: 250px; 127 | } 128 | 129 | .recText{ 130 | padding:5px; 131 | float: left; 132 | } 133 | 134 | 135 | #innerWrapper{ 136 | float: left; 137 | margin-left: auto; 138 | margin-right: auto; 139 | width:900px; 140 | height:100px; 141 | } 142 | 143 | .survey{ 144 | font-size: 30pt; 145 | } 146 | 147 | .formWrapper{ 148 | font-size: 30pt; 149 | } 150 | .surveyInput{ 151 | border: 1px rgba(0,0,0,0.42) solid; 152 | border-radius: 2px; 153 | font-size: 14pt; 154 | } 155 | 156 | .resto{width:400px; margin-top: 5px;padding:3px;} 157 | 158 | .city{width:200px;} 159 | 160 | .state{width:50px;} 161 | 162 | ::-webkit-input-placeholder { 163 | color: orange; 164 | opacity: .3; 165 | } 166 | 167 | :-moz-placeholder { /* Firefox 18- */ 168 | color: orange; 169 | opacity: .3; 170 | } 171 | 172 | ::-moz-placeholder { /* Firefox 19+ */ 173 | color: orange; 174 | opacity: .3; 175 | } 176 | 177 | :-ms-input-placeholder { 178 | color: orange; 179 | opacity: .3; 180 | } 181 | .submitButton{ 182 | display: inline-block; 183 | padding: 5px 10px 6px; 184 | color: #fff; 185 | text-decoration: none; 186 | font-weight: bold; 187 | line-height: 1; 188 | -moz-border-radius: 5px; 189 | -webkit-border-radius: 5px; 190 | -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5); 191 | -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5); 192 | text-shadow: 0 -1px 1px rgba(0,0,0,0.25); 193 | border-bottom: 1px solid rgba(0,0,0,0.25); 194 | position: relative; 195 | cursor: pointer; 196 | background-color: #ff5c00; 197 | } 198 | 199 | 200 | #leftPop{ 201 | margin-top: 40px; 202 | } 203 | 204 | #rightPop{ 205 | margin-top: 40px; 206 | } 207 | 208 | 209 | .popOutWithBorder{ 210 | border-width: 0px 0px 2px 0px; 211 | border-color: #25aae1; 212 | background-color: white; 213 | border-style: solid; 214 | padding:5px; 215 | box-shadow: 0px 100px 1000px #ddd; 216 | /* height:400px; */ 217 | width: 280px; 218 | } 219 | 220 | .left{float: left;} 221 | .right{float: right;} 222 | 223 | .login{ 224 | font-size: 18px; 225 | color:#17529c; 226 | } 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.dropdown.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.dropdown = { 7 | name : 'dropdown', 8 | 9 | version : '4.1.0', 10 | 11 | settings : { 12 | activeClass: 'open' 13 | }, 14 | 15 | init : function (scope, method, options) { 16 | this.scope = scope || this.scope; 17 | Foundation.inherit(this, 'throttle scrollLeft'); 18 | 19 | if (typeof method === 'object') { 20 | $.extend(true, this.settings, method); 21 | } 22 | 23 | if (typeof method != 'string') { 24 | 25 | if (!this.settings.init) { 26 | this.events(); 27 | } 28 | 29 | return this.settings.init; 30 | } else { 31 | return this[method].call(this, options); 32 | } 33 | }, 34 | 35 | events : function () { 36 | var self = this; 37 | 38 | $(this.scope).on('click.fndtn.dropdown', '[data-dropdown]', function (e) { 39 | e.preventDefault(); 40 | e.stopPropagation(); 41 | self.toggle($(this)); 42 | }); 43 | 44 | $('*, html, body').on('click.fndtn.dropdown', function (e) { 45 | if (!$(e.target).data('dropdown')) { 46 | $('[data-dropdown-content]') 47 | .css(Foundation.rtl ? 'right':'left', '-99999px') 48 | .removeClass(self.settings.activeClass); 49 | } 50 | }); 51 | 52 | $(window).on('resize.fndtn.dropdown', self.throttle(function () { 53 | self.resize.call(self); 54 | }, 50)).trigger('resize'); 55 | 56 | this.settings.init = true; 57 | }, 58 | 59 | toggle : function (target, resize) { 60 | var dropdown = $('#' + target.data('dropdown')); 61 | 62 | $('[data-dropdown-content]').not(dropdown).css(Foundation.rtl ? 'right':'left', '-99999px').removeClass(this.settings.activeClass); 63 | 64 | if (dropdown.hasClass(this.settings.activeClass)) { 65 | dropdown 66 | .css(Foundation.rtl ? 'right':'left', '-99999px') 67 | .removeClass(this.settings.activeClass); 68 | } else { 69 | this 70 | .css(dropdown 71 | .addClass(this.settings.activeClass), target); 72 | } 73 | }, 74 | 75 | resize : function () { 76 | var dropdown = $('[data-dropdown-content].open'), 77 | target = $("[data-dropdown='" + dropdown.attr('id') + "']"); 78 | 79 | if (dropdown.length && target.length) { 80 | this.css(dropdown, target); 81 | } 82 | }, 83 | 84 | css : function (dropdown, target) { 85 | var position = target.position(); 86 | position.top += target.offsetParent().offset().top; 87 | position.left += target.offsetParent().offset().left; 88 | 89 | if (this.small()) { 90 | dropdown.css({ 91 | position : 'absolute', 92 | width: '95%', 93 | left: '2.5%', 94 | 'max-width': 'none', 95 | top: position.top + this.outerHeight(target) 96 | }); 97 | } else { 98 | if (!Foundation.rtl && $(window).width() > this.outerWidth(dropdown) + target.offset().left) { 99 | var left = position.left; 100 | } else { 101 | if (!dropdown.hasClass('right')) { 102 | dropdown.addClass('right'); 103 | } 104 | var left = position.left - (this.outerWidth(dropdown) - this.outerWidth(target)); 105 | } 106 | 107 | dropdown.attr('style', '').css({ 108 | position : 'absolute', 109 | top: position.top + this.outerHeight(target), 110 | left: left 111 | }); 112 | } 113 | 114 | return dropdown; 115 | }, 116 | 117 | small : function () { 118 | return $(window).width() < 768 || $('html').hasClass('lt-ie9'); 119 | }, 120 | 121 | off: function () { 122 | $(this.scope).off('.fndtn.dropdown'); 123 | $('html, body').off('.fndtn.dropdown'); 124 | $(window).off('.fndtn.dropdown'); 125 | $('[data-dropdown-content]').off('.fndtn.dropdown'); 126 | this.settings.init = false; 127 | } 128 | }; 129 | }(Foundation.zj, this, this.document)); 130 | -------------------------------------------------------------------------------- /recommendation_item/models.py: -------------------------------------------------------------------------------- 1 | import math 2 | from django.db import models 3 | from django.contrib.gis.db import models as geo_models 4 | from django.contrib.localflavor.us.models import USStateField 5 | 6 | from django.contrib.auth.models import User 7 | 8 | 9 | # address model from: https://docs.djangoproject.com/en/dev/ref/contrib/gis/model-api/ 10 | class Address(geo_models.Model): 11 | longitude = models.FloatField() 12 | latitude = models.FloatField() 13 | street_address = models.CharField(max_length=200) 14 | city = models.CharField(max_length=100) 15 | state = USStateField() 16 | zipcode = models.CharField(max_length=10) 17 | objects = geo_models.GeoManager() 18 | 19 | # holds a set of recommendation_items and their ratings for a particular user 20 | class RecommendationList(models.Model): 21 | user = models.ForeignKey(User) 22 | 23 | # a recommendation item abstracts the item being recommended. And example of a 24 | # recommendation item is a restaurant, an event, or a song. 25 | # 26 | # Important features of a recommendation item (LIVE LIST) 27 | # - Has one or more data sources to get information from 28 | # - Has an address that can be filtered based on proximity to a location 29 | class RecommendationItem(models.Model): 30 | recommendation_list = models.ManyToManyField(RecommendationList) 31 | date_added = models.DateTimeField(auto_now=True) 32 | data_sources = models.TextField(null=True, blank=True) 33 | address = models.ForeignKey(Address) 34 | title = models.CharField(max_length=120, default="") 35 | 36 | # takes in a destination and returns the distance from it in miles 37 | def distanceFromRestaurant(self, dest): 38 | lat1 = self.address.latitude 39 | lon1 = self.address.longitude 40 | lat2 = dest.address.latitude 41 | lon2 = dest.address.longitude 42 | radius = 6371 # km of earth 43 | 44 | dlat = math.radians(lat2-lat1) 45 | dlon = math.radians(lon2-lon1) 46 | a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1))\ 47 | * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2) 48 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) 49 | d = radius * c 50 | 51 | # convert to miles 52 | d = d * 0.621371 53 | 54 | return d 55 | 56 | def distanceFromPoint(self, point): 57 | lat1 = self.address.latitude 58 | lon1 = self.address.longitude 59 | lat2_u, lon2_u = point 60 | lat2 = float(lat2_u) 61 | lon2 = float(lon2_u) 62 | radius = 6371 # km of earth 63 | 64 | dlat = math.radians(lat2-lat1) 65 | dlon = math.radians(lon2-lon1) 66 | a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1))\ 67 | * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2) 68 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) 69 | d = radius * c 70 | 71 | # convert to miles 72 | d = d * 0.621371 73 | 74 | return d 75 | 76 | # specific recommendation item extensions 77 | class Restaurant(RecommendationItem): 78 | cuisines = models.CharField(max_length=400, null=True, blank=True) 79 | # rating, dangerous assumption that this is always out of 5 80 | rating = models.FloatField(null=True, blank=True) 81 | # price from 1-5 82 | price = models.IntegerField() 83 | image = models.CharField(max_length=600, null=True, blank=True) 84 | 85 | def __unicode__(self): 86 | return self.title 87 | 88 | def addressFromFactual(datum): 89 | a, a_created = Address.objects.get_or_create(street_address=datum.get('address',""), 90 | city=datum.get('locality',""), 91 | state=datum.get('region',""), 92 | zipcode=datum.get('postcode',""), 93 | longitude=datum.get('longitude',-1), 94 | latitude=datum.get('latitude',-1)) 95 | return [a, a_created] 96 | 97 | def restaurantFromFactual(datum, a, sources): 98 | r, r_created = Restaurant.objects.get_or_create( title=datum.get('name',None), 99 | cuisines=datum.get('cuisine',None), 100 | rating=(datum.get('rating', 3) - 3) / 2, 101 | price=datum.get('price', -1), 102 | address=a, 103 | data_sources=sources) 104 | return [r, r_created] 105 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.magellan.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.magellan = { 7 | name : 'magellan', 8 | 9 | version : '4.0.0', 10 | 11 | settings : { 12 | activeClass: 'active' 13 | }, 14 | 15 | init : function (scope, method, options) { 16 | this.scope = scope || this.scope; 17 | Foundation.inherit(this, 'data_options'); 18 | 19 | if (typeof method === 'object') { 20 | $.extend(true, this.settings, method); 21 | } 22 | 23 | if (typeof method != 'string') { 24 | if (!this.settings.init) { 25 | this.fixed_magellan = $("[data-magellan-expedition]"); 26 | this.set_threshold(); 27 | this.last_destination = $('[data-magellan-destination]').last(); 28 | this.events(); 29 | } 30 | 31 | return this.settings.init; 32 | } else { 33 | return this[method].call(this, options); 34 | } 35 | }, 36 | 37 | events : function () { 38 | var self = this; 39 | $(this.scope).on('arrival.fndtn.magellan', '[data-magellan-arrival]', function (e) { 40 | var $destination = $(this), 41 | $expedition = $destination.closest('[data-magellan-expedition]'), 42 | activeClass = $expedition.attr('data-magellan-active-class') 43 | || self.settings.activeClass; 44 | 45 | $destination 46 | .closest('[data-magellan-expedition]') 47 | .find('[data-magellan-arrival]') 48 | .not($destination) 49 | .removeClass(activeClass); 50 | $destination.addClass(activeClass); 51 | }); 52 | 53 | this.fixed_magellan 54 | .on('update-position.fndtn.magellan', function(){ 55 | var $el = $(this); 56 | // $el.data("magellan-fixed-position",""); 57 | //$el.data("magellan-top-offset", ""); 58 | }) 59 | .trigger('update-position'); 60 | 61 | $(window) 62 | .on('resize.fndtn.magellan', function() { 63 | this.fixed_magellan.trigger('update-position'); 64 | }.bind(this)) 65 | 66 | .on('scroll.fndtn.magellan', function() { 67 | var windowScrollTop = $(window).scrollTop(); 68 | self.fixed_magellan.each(function() { 69 | var $expedition = $(this); 70 | if (typeof $expedition.data('magellan-top-offset') === 'undefined') { 71 | $expedition.data('magellan-top-offset', $expedition.offset().top); 72 | } 73 | if (typeof $expedition.data('magellan-fixed-position') === 'undefined') { 74 | $expedition.data('magellan-fixed-position', false) 75 | } 76 | var fixed_position = (windowScrollTop + self.settings.threshold) > $expedition.data("magellan-top-offset"); 77 | var attr = $expedition.attr('data-magellan-top-offset'); 78 | 79 | if ($expedition.data("magellan-fixed-position") != fixed_position) { 80 | $expedition.data("magellan-fixed-position", fixed_position); 81 | if (fixed_position) { 82 | $expedition.css({position:"fixed", top:0}); 83 | } else { 84 | $expedition.css({position:"", top:""}); 85 | } 86 | if (fixed_position && typeof attr != 'undefined' && attr != false) { 87 | $expedition.css({position:"fixed", top:attr + "px"}); 88 | } 89 | } 90 | }); 91 | }); 92 | 93 | 94 | if (this.last_destination.length > 0) { 95 | $(window).on('scroll.fndtn.magellan', function (e) { 96 | var windowScrollTop = $(window).scrollTop(), 97 | scrolltopPlusHeight = windowScrollTop + $(window).height(), 98 | lastDestinationTop = Math.ceil(self.last_destination.offset().top); 99 | 100 | $('[data-magellan-destination]').each(function () { 101 | var $destination = $(this), 102 | destination_name = $destination.attr('data-magellan-destination'), 103 | topOffset = $destination.offset().top - windowScrollTop; 104 | 105 | if (topOffset <= self.settings.threshold) { 106 | $("[data-magellan-arrival='" + destination_name + "']").trigger('arrival'); 107 | } 108 | // In large screens we may hit the bottom of the page and dont reach the top of the last magellan-destination, so lets force it 109 | if (scrolltopPlusHeight >= $(self.scope).height() && lastDestinationTop > windowScrollTop && lastDestinationTop < scrolltopPlusHeight) { 110 | $('[data-magellan-arrival]').last().trigger('arrival'); 111 | } 112 | }); 113 | }); 114 | } 115 | 116 | this.settings.init = true; 117 | }, 118 | 119 | set_threshold : function () { 120 | if (!this.settings.threshold) { 121 | this.settings.threshold = (this.fixed_magellan.length > 0) ? 122 | this.outerHeight(this.fixed_magellan, true) : 0; 123 | } 124 | }, 125 | 126 | off : function () { 127 | $(this.scope).off('.fndtn.magellan'); 128 | } 129 | }; 130 | }(Foundation.zj, this, this.document)); 131 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.placeholder.js: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/placeholder v2.0.7 by @mathias 2 | Modified to work with Zepto.js by ZURB 3 | */ 4 | ;(function(window, document, $) { 5 | 6 | var isInputSupported = 'placeholder' in document.createElement('input'), 7 | isTextareaSupported = 'placeholder' in document.createElement('textarea'), 8 | prototype = $.fn, 9 | valHooks = $.valHooks, 10 | hooks, 11 | placeholder; 12 | 13 | if (isInputSupported && isTextareaSupported) { 14 | 15 | placeholder = prototype.placeholder = function() { 16 | return this; 17 | }; 18 | 19 | placeholder.input = placeholder.textarea = true; 20 | 21 | } else { 22 | 23 | placeholder = prototype.placeholder = function() { 24 | var $this = this; 25 | $this 26 | .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]') 27 | .not('.placeholder') 28 | .bind({ 29 | 'focus.placeholder': clearPlaceholder, 30 | 'blur.placeholder': setPlaceholder 31 | }) 32 | .data('placeholder-enabled', true) 33 | .trigger('blur.placeholder'); 34 | return $this; 35 | }; 36 | 37 | placeholder.input = isInputSupported; 38 | placeholder.textarea = isTextareaSupported; 39 | 40 | hooks = { 41 | 'get': function(element) { 42 | var $element = $(element); 43 | return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value; 44 | }, 45 | 'set': function(element, value) { 46 | var $element = $(element); 47 | if (!$element.data('placeholder-enabled')) { 48 | return element.value = value; 49 | } 50 | if (value == '') { 51 | element.value = value; 52 | // Issue #56: Setting the placeholder causes problems if the element continues to have focus. 53 | if (element != document.activeElement) { 54 | // We can't use `triggerHandler` here because of dummy text/password inputs :( 55 | setPlaceholder.call(element); 56 | } 57 | } else if ($element.hasClass('placeholder')) { 58 | clearPlaceholder.call(element, true, value) || (element.value = value); 59 | } else { 60 | element.value = value; 61 | } 62 | // `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363 63 | return $element; 64 | } 65 | }; 66 | 67 | isInputSupported || (valHooks.input = hooks); 68 | isTextareaSupported || (valHooks.textarea = hooks); 69 | 70 | $(function() { 71 | // Look for forms 72 | $(document).delegate('form', 'submit.placeholder', function() { 73 | // Clear the placeholder values so they don't get submitted 74 | var $inputs = $('.placeholder', this).each(clearPlaceholder); 75 | setTimeout(function() { 76 | $inputs.each(setPlaceholder); 77 | }, 10); 78 | }); 79 | }); 80 | 81 | // Clear placeholder values upon page reload 82 | $(window).bind('beforeunload.placeholder', function() { 83 | $('.placeholder').each(function() { 84 | this.value = ''; 85 | }); 86 | }); 87 | 88 | } 89 | 90 | function args(elem) { 91 | // Return an object of element attributes 92 | var newAttrs = {}, 93 | rinlinejQuery = /^jQuery\d+$/; 94 | $.each(elem.attributes, function(i, attr) { 95 | if (attr.specified && !rinlinejQuery.test(attr.name)) { 96 | newAttrs[attr.name] = attr.value; 97 | } 98 | }); 99 | return newAttrs; 100 | } 101 | 102 | function clearPlaceholder(event, value) { 103 | var input = this, 104 | $input = $(input); 105 | if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) { 106 | if ($input.data('placeholder-password')) { 107 | $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id')); 108 | // If `clearPlaceholder` was called from `$.valHooks.input.set` 109 | if (event === true) { 110 | return $input[0].value = value; 111 | } 112 | $input.focus(); 113 | } else { 114 | input.value = ''; 115 | $input.removeClass('placeholder'); 116 | input == document.activeElement && input.select(); 117 | } 118 | } 119 | } 120 | 121 | function setPlaceholder() { 122 | var $replacement, 123 | input = this, 124 | $input = $(input), 125 | $origInput = $input, 126 | id = this.id; 127 | if (input.value == '') { 128 | if (input.type == 'password') { 129 | if (!$input.data('placeholder-textinput')) { 130 | try { 131 | $replacement = $input.clone().attr({ 'type': 'text' }); 132 | } catch(e) { 133 | $replacement = $('').attr($.extend(args(this), { 'type': 'text' })); 134 | } 135 | $replacement 136 | .removeAttr('name') 137 | .data({ 138 | 'placeholder-password': true, 139 | 'placeholder-id': id 140 | }) 141 | .bind('focus.placeholder', clearPlaceholder); 142 | $input 143 | .data({ 144 | 'placeholder-textinput': $replacement, 145 | 'placeholder-id': id 146 | }) 147 | .before($replacement); 148 | } 149 | $input = $input.removeAttr('id').hide().prev().attr('id', id).show(); 150 | // Note: `$input[0] != input` now! 151 | } 152 | $input.addClass('placeholder'); 153 | $input[0].value = $input.attr('placeholder'); 154 | } else { 155 | $input.removeClass('placeholder'); 156 | } 157 | } 158 | 159 | }(this, document, Foundation.zj)); 160 | -------------------------------------------------------------------------------- /concierge/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for concierge project. 2 | import os 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 7 | 8 | SINGLY_CLIENT_ID = 'bf19470d3e2edbc464bdb28b628af1b5' 9 | SINGLY_CLIENT_SECRET = 'b421eab26644737aa82d7fca62a46f27' 10 | # lvh.me is just a domain name for localhost 11 | SINGLY_REDIRECT_URI = 'http://localhost:8000/authorize/callback' 12 | 13 | AUTH_PROFILE_MODULE = "user_profile.UserProfile" 14 | 15 | DEBUG = True 16 | TEMPLATE_DEBUG = DEBUG 17 | 18 | ADMINS = ( 19 | # ('Your Name', 'your_email@example.com'), 20 | ) 21 | 22 | MANAGERS = ADMINS 23 | 24 | DATABASES = { 25 | 'default': { 26 | 'ENGINE': 'django.contrib.gis.db.backends.postgis', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 27 | 'NAME': 'concierge', # Or path to database file if using sqlite3. 28 | 'USER': 'concierge', # Not used with sqlite3. 29 | 'PASSWORD': 'concierge', # Not used with sqlite3. 30 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 31 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 32 | } 33 | } 34 | 35 | # Local time zone for this installation. Choices can be found here: 36 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 37 | # although not all choices may be available on all operating systems. 38 | # In a Windows environment this must be set to your system time zone. 39 | TIME_ZONE = 'America/Chicago' 40 | 41 | # Language code for this installation. All choices can be found here: 42 | # http://www.i18nguy.com/unicode/language-identifiers.html 43 | LANGUAGE_CODE = 'en-us' 44 | 45 | SITE_ID = 1 46 | 47 | # If you set this to False, Django will make some optimizations so as not 48 | # to load the internationalization machinery. 49 | USE_I18N = True 50 | 51 | # If you set this to False, Django will not format dates, numbers and 52 | # calendars according to the current locale. 53 | USE_L10N = True 54 | 55 | # If you set this to False, Django will not use timezone-aware datetimes. 56 | USE_TZ = True 57 | 58 | # Absolute filesystem path to the directory that will hold user-uploaded files. 59 | # Example: "/home/media/media.lawrence.com/media/" 60 | MEDIA_ROOT = '' 61 | 62 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 63 | # trailing slash. 64 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 65 | MEDIA_URL = '' 66 | 67 | # Absolute path to the directory static files should be collected to. 68 | # Don't put anything in this directory yourself; store your static files 69 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 70 | # Example: "/home/media/media.lawrence.com/static/" 71 | STATIC_ROOT = '' 72 | 73 | # URL prefix for static files. 74 | # Example: "http://media.lawrence.com/static/" 75 | STATIC_URL = '/static/' 76 | 77 | # Additional locations of static files 78 | STATICFILES_DIRS = ( 79 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 80 | # Always use forward slashes, even on Windows. 81 | # Don't forget to use absolute paths, not relative paths. 82 | os.path.join(PROJECT_ROOT, "static"), 83 | ) 84 | 85 | # List of finder classes that know how to find static files in 86 | # various locations. 87 | STATICFILES_FINDERS = ( 88 | 'django.contrib.staticfiles.finders.FileSystemFinder', 89 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 90 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 91 | ) 92 | 93 | # Make this unique, and don't share it with anybody. 94 | SECRET_KEY = '5&b@4t&ij%_uxjr=x0#luv#znp()i7+qm15c-78&z*2etzru#s' 95 | 96 | # List of callables that know how to import templates from various sources. 97 | TEMPLATE_LOADERS = ( 98 | 'django.template.loaders.filesystem.Loader', 99 | 'django.template.loaders.app_directories.Loader', 100 | # 'django.template.loaders.eggs.Loader', 101 | ) 102 | 103 | MIDDLEWARE_CLASSES = ( 104 | 'django.middleware.common.CommonMiddleware', 105 | 'django.contrib.sessions.middleware.SessionMiddleware', 106 | 'django.middleware.csrf.CsrfViewMiddleware', 107 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 108 | 'django.contrib.messages.middleware.MessageMiddleware', 109 | # Uncomment the next line for simple clickjacking protection: 110 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 111 | ) 112 | 113 | ROOT_URLCONF = 'concierge.urls' 114 | 115 | # Python dotted path to the WSGI application used by Django's runserver. 116 | WSGI_APPLICATION = 'concierge.wsgi.application' 117 | 118 | TEMPLATE_DIRS = ( 119 | os.path.join(PROJECT_ROOT, "templates"), 120 | ) 121 | 122 | INSTALLED_APPS = ( 123 | 'django.contrib.auth', 124 | 'django.contrib.contenttypes', 125 | 'django.contrib.sessions', 126 | 'django.contrib.sites', 127 | 'django.contrib.messages', 128 | 'django.contrib.staticfiles', 129 | 'django.contrib.admin', 130 | 'django.contrib.gis', 131 | 'user_profile', 132 | 'recommendation_item', 133 | ) 134 | 135 | # A sample logging configuration. The only tangible logging 136 | # performed by this configuration is to send an email to 137 | # the site admins on every HTTP 500 error when DEBUG=False. 138 | # See http://docs.djangoproject.com/en/dev/topics/logging for 139 | # more details on how to customize your logging configuration. 140 | LOGGING = { 141 | 'version': 1, 142 | 'disable_existing_loggers': False, 143 | 'filters': { 144 | 'require_debug_false': { 145 | '()': 'django.utils.log.RequireDebugFalse' 146 | } 147 | }, 148 | 'handlers': { 149 | 'mail_admins': { 150 | 'level': 'ERROR', 151 | 'filters': ['require_debug_false'], 152 | 'class': 'django.utils.log.AdminEmailHandler' 153 | } 154 | }, 155 | 'loggers': { 156 | 'django.request': { 157 | 'handlers': ['mail_admins'], 158 | 'level': 'ERROR', 159 | 'propagate': True, 160 | }, 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /learn/Collab.py: -------------------------------------------------------------------------------- 1 | import cPickle, json, math 2 | from datetime import date 3 | from collections import Counter 4 | 5 | from user_profile.models import UserProfile 6 | from recommendation_item.models import RecommendationItem, Restaurant 7 | 8 | # Collab is based on the memory-based collaboration filtering 9 | # algorithm found at http://en.wikipedia.org/wiki/Collaborative_filtering 10 | 11 | class Collab(object): 12 | 13 | ######################################## 14 | # FUNCTIONS FOR OUTSIDE USE (PUBLIC) 15 | ######################################## 16 | 17 | # predicts the rating of the reccomendation item at index i 18 | # by the user whose index is u 19 | # returns actual rating if that item is already rated 20 | # this rating is not stored in the rating matrix 21 | def predict_rating(self, i, u): 22 | 23 | # find top n users most similar to u that have rated item i 24 | relevant_users = [ x for x in UserProfile.objects.all() 25 | if not x == u ] 26 | top_n_most_sim = sorted(relevant_users, 27 | key=lambda x: abs(self._user_user_sim(u,x)), 28 | reverse=True)[:10] 29 | 30 | # return a weighted average of the ratings of the item in question 31 | weighted_sum = 0.0 32 | sum_of_weights = 0.0 33 | for user in top_n_most_sim: 34 | weight = self._user_user_sim(u, user) 35 | sum_of_weights += weight 36 | weighted_sum += weight * self._get_rating(i, user) 37 | if isinstance(i, Restaurant): 38 | weight = self._user_expert_sim(u) 39 | sum_of_weights += weight 40 | weighted_sum += weight * i.rating 41 | if sum_of_weights == 0: 42 | return 0 43 | return (weighted_sum / sum_of_weights) 44 | 45 | # returns a sorted list of the top n items that user u has not rated 46 | # that the user is most likely to like 47 | def suggest_items(self, n, u): 48 | item_rating_pairs = [] 49 | for i in RecommendationItem.objects.all(): 50 | if i not in u.gotos.all(): 51 | item_rating_pairs.append((i, self.predict_rating(i, u))) 52 | return zip(*sorted(item_rating_pairs, 53 | key=lambda x: x[1], 54 | reverse=True)[:n])[0] 55 | 56 | def suggest_restaurants(self, n, u): 57 | for x in UserProfile.objects.all(): 58 | print x.user.username, self._user_user_sim(x,u) 59 | print self._user_expert_sim(u) 60 | item_rating_pairs = [] 61 | for i in Restaurant.objects.all(): 62 | if i not in u.gotos.all(): 63 | item_rating_pairs.append((i, self.predict_rating(i, u))) 64 | return zip(*sorted(item_rating_pairs, 65 | key=lambda x: x[1], 66 | reverse=True)[:n])[0] 67 | 68 | def ensemble_suggestion(self, n, u, latitude, longitude, furthest = 0.009): 69 | collab_weight = 1.0 70 | sim_weight = 1.0 71 | weight_sum = collab_weight + sim_weight 72 | restaurant_ratings = [] 73 | 74 | for i in Restaurant.objects.filter( 75 | address__longitude__gt = longitude - furthest, 76 | address__longitude__lt = longitude + furthest, 77 | address__latitude__gt = latitude - furthest, 78 | address__latitude__lt = latitude + furthest): 79 | if (i not in u.gotos.all()): 80 | restaurant_ratings.append((i, 81 | (collab_weight * self.predict_rating(i, u) + sim_weight * self.user_restaurant_sim(i, u)) / weight_sum)) 82 | if len(restaurant_ratings) < n: 83 | return self.ensemble_suggestion(n, u, latitude, longitude, furthest+0.009) 84 | top_n = zip(*sorted(restaurant_ratings, 85 | key=lambda x: x[1], 86 | reverse=True)[:n])[0] 87 | return top_n 88 | 89 | 90 | def user_restaurant_sim(self, r, u): 91 | user_cuisine_counter = Counter() 92 | gotos = u.gotos.all() 93 | for goto in gotos: 94 | if goto.cuisines: 95 | user_cuisine_counter.update(eval(goto.cuisines)) 96 | u_vec = [] 97 | r_vec = [] 98 | r_cuisines = eval(r.cuisines) if r.cuisines else [] 99 | keys = set(user_cuisine_counter.keys() + r_cuisines) 100 | for c in keys: 101 | u_vec.append(user_cuisine_counter[c] / float(len(gotos))) 102 | r_vec.append(1 if c in r_cuisines else 0) 103 | sim = Collab.cosine(u_vec, r_vec) 104 | return sim 105 | 106 | # returns true if a user u has recommended reccomendation item i 107 | # else returns false 108 | def has_reccomended(self, i, u): 109 | if self._get_rating(i, u): 110 | return True 111 | return False 112 | 113 | # retuns a list of the n users most similar to the given user_id 114 | def suggest_users(self, u, n): 115 | return sorted(UserProfile.objects.all(), 116 | key=lambda x: abs(self._user_user_sim(u,x)), 117 | reverse=True)[:n] 118 | 119 | ######################################### 120 | # FUNCTIONS NOT FOR OUTSIDE USE (PRIVATE) 121 | ######################################### 122 | 123 | def __init__(self): 124 | self.user_user_sim_cache = {} 125 | self.feature_vector_cache = {} 126 | self.user_expert_sim_cache = {} 127 | 128 | # user-user similarity metric 129 | # returns a scaler between 0 and 1 representing the similarity 130 | # between two given UserProfiles u1 and u2. Closer to 1 is more similar. 131 | def _user_user_sim(self, u1, u2): 132 | if (u1,u2) in self.user_user_sim_cache: 133 | return self.user_user_sim_cache[(u1,u2)] 134 | if (u2,u1) in self.user_user_sim_cache: 135 | return self.user_user_sim_cache[(u2,u1)] 136 | u1_features = self._get_feature_vector(u1) 137 | u2_features = self._get_feature_vector(u2) 138 | u1_vec = [] 139 | u2_vec = [] 140 | keys = set(u1_features.keys()) | set(u2_features.keys()) 141 | for key in keys: 142 | u1_vec.append(u1_features[key] if key in u1_features else 0) 143 | u2_vec.append(u2_features[key] if key in u2_features else 0) 144 | sim = Collab.cosine(u1_vec, u2_vec) 145 | self.user_user_sim_cache[(u1,u2)] = sim 146 | return sim 147 | 148 | def _user_expert_sim(self, u): 149 | if u in self.user_expert_sim_cache: 150 | return self.user_expert_sim_cache[u] 151 | u_vec = [] 152 | exp_vec = [] 153 | gotos = u.gotos.all() 154 | for i in gotos: 155 | u_vec.append(1) 156 | exp_vec.append(i.rating) 157 | sim = Collab.cosine(u_vec, exp_vec) 158 | self.user_expert_sim_cache[u] = sim 159 | return sim 160 | 161 | @staticmethod 162 | def cosine(v1, v2): 163 | prod_sum, v1_sq_sum, v2_sq_sum = 0.0, 0.0, 0.0 164 | for f1, f2 in zip(v1, v2): 165 | prod_sum += f1 * f2 166 | v1_sq_sum += f1 * f1 167 | v2_sq_sum += f2 * f2 168 | if v1_sq_sum == 0 or v2_sq_sum == 0: 169 | return 0.0 170 | return prod_sum / ( math.sqrt(v1_sq_sum) * math.sqrt(v2_sq_sum) ) 171 | 172 | # returns the rating of item i by user u. If user u has not rated item i, 173 | # returns None 174 | def _get_rating(self, i, u): 175 | fv = self._get_feature_vector(u) 176 | if i.title in fv: 177 | return fv[i.title] 178 | return None 179 | 180 | # returns the feature vector corresponding to that UserProfile. missing items 181 | # have the value None 182 | def _get_feature_vector(self, u): 183 | if u in self.feature_vector_cache: 184 | return self.feature_vector_cache[u] 185 | feature_vector = {} 186 | gotos = [ i.title for i in u.gotos.all() ] 187 | for r in RecommendationItem.objects.all(): 188 | feature_vector[r.title] = 1 if r.title in gotos else 0 189 | self.feature_vector_cache[u] = feature_vector 190 | return feature_vector 191 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.tooltips.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.tooltips = { 7 | name: 'tooltips', 8 | 9 | version : '4.1.0', 10 | 11 | settings : { 12 | selector : '.has-tip', 13 | additionalInheritableClasses : [], 14 | tooltipClass : '.tooltip', 15 | tipTemplate : function (selector, content) { 16 | return '' + content + ''; 19 | } 20 | }, 21 | 22 | cache : {}, 23 | 24 | init : function (scope, method, options) { 25 | var self = this; 26 | this.scope = scope || this.scope; 27 | 28 | if (typeof method === 'object') { 29 | $.extend(true, this.settings, method); 30 | } 31 | 32 | if (typeof method != 'string') { 33 | if (Modernizr.touch) { 34 | $(this.scope) 35 | .on('click.fndtn.tooltip touchstart.fndtn.tooltip touchend.fndtn.tooltip', 36 | '[data-tooltip]', function (e) { 37 | e.preventDefault(); 38 | $(self.settings.tooltipClass).hide(); 39 | self.showOrCreateTip($(this)); 40 | }) 41 | .on('click.fndtn.tooltip touchstart.fndtn.tooltip touchend.fndtn.tooltip', 42 | this.settings.tooltipClass, function (e) { 43 | e.preventDefault(); 44 | $(this).fadeOut(150); 45 | }); 46 | } else { 47 | $(this.scope) 48 | .on('mouseenter.fndtn.tooltip mouseleave.fndtn.tooltip', 49 | '[data-tooltip]', function (e) { 50 | var $this = $(this); 51 | 52 | if (e.type === 'mouseover' || e.type === 'mouseenter') { 53 | self.showOrCreateTip($this); 54 | } else if (e.type === 'mouseout' || e.type === 'mouseleave') { 55 | self.hide($this); 56 | } 57 | }); 58 | } 59 | 60 | // $(this.scope).data('fndtn-tooltips', true); 61 | } else { 62 | return this[method].call(this, options); 63 | } 64 | 65 | }, 66 | 67 | showOrCreateTip : function ($target) { 68 | var $tip = this.getTip($target); 69 | 70 | if ($tip && $tip.length > 0) { 71 | return this.show($target); 72 | } 73 | 74 | return this.create($target); 75 | }, 76 | 77 | getTip : function ($target) { 78 | var selector = this.selector($target), 79 | tip = null; 80 | 81 | if (selector) { 82 | tip = $('span[data-selector=' + selector + ']' + this.settings.tooltipClass); 83 | } 84 | 85 | return (typeof tip === 'object') ? tip : false; 86 | }, 87 | 88 | selector : function ($target) { 89 | var id = $target.attr('id'), 90 | dataSelector = $target.attr('data-tooltip') || $target.attr('data-selector'); 91 | 92 | if ((id && id.length < 1 || !id) && typeof dataSelector != 'string') { 93 | dataSelector = 'tooltip' + Math.random().toString(36).substring(7); 94 | $target.attr('data-selector', dataSelector); 95 | } 96 | 97 | return (id && id.length > 0) ? id : dataSelector; 98 | }, 99 | 100 | create : function ($target) { 101 | var $tip = $(this.settings.tipTemplate(this.selector($target), $('
    ').html($target.attr('title')).html())), 102 | classes = this.inheritable_classes($target); 103 | 104 | $tip.addClass(classes).appendTo('body'); 105 | if (Modernizr.touch) { 106 | $tip.append('tap to close '); 107 | } 108 | $target.removeAttr('title').attr('title',''); 109 | this.show($target); 110 | }, 111 | 112 | reposition : function (target, tip, classes) { 113 | var width, nub, nubHeight, nubWidth, column, objPos; 114 | 115 | tip.css('visibility', 'hidden').show(); 116 | 117 | width = target.data('width'); 118 | nub = tip.children('.nub'); 119 | nubHeight = this.outerHeight(nub); 120 | nubWidth = this.outerHeight(nub); 121 | 122 | objPos = function (obj, top, right, bottom, left, width) { 123 | return obj.css({ 124 | 'top' : (top) ? top : 'auto', 125 | 'bottom' : (bottom) ? bottom : 'auto', 126 | 'left' : (left) ? left : 'auto', 127 | 'right' : (right) ? right : 'auto', 128 | 'width' : (width) ? width : 'auto' 129 | }).end(); 130 | }; 131 | 132 | objPos(tip, (target.offset().top + this.outerHeight(target) + 10), 'auto', 'auto', target.offset().left, width); 133 | 134 | if ($(window).width() < 767) { 135 | objPos(tip, (target.offset().top + this.outerHeight(target) + 10), 'auto', 'auto', 12.5, $(this.scope).width()); 136 | tip.addClass('tip-override'); 137 | objPos(nub, -nubHeight, 'auto', 'auto', target.offset().left); 138 | } else { 139 | var left = target.offset().left; 140 | if (Foundation.rtl) { 141 | left = target.offset().left + target.offset().width - this.outerWidth(tip); 142 | } 143 | objPos(tip, (target.offset().top + this.outerHeight(target) + 10), 'auto', 'auto', left, width); 144 | tip.removeClass('tip-override'); 145 | if (classes && classes.indexOf('tip-top') > -1) { 146 | objPos(tip, (target.offset().top - this.outerHeight(tip)), 'auto', 'auto', left, width) 147 | .removeClass('tip-override'); 148 | } else if (classes && classes.indexOf('tip-left') > -1) { 149 | objPos(tip, (target.offset().top + (this.outerHeight(target) / 2) - nubHeight*2.5), 'auto', 'auto', (target.offset().left - this.outerWidth(tip) - nubHeight), width) 150 | .removeClass('tip-override'); 151 | } else if (classes && classes.indexOf('tip-right') > -1) { 152 | objPos(tip, (target.offset().top + (this.outerHeight(target) / 2) - nubHeight*2.5), 'auto', 'auto', (target.offset().left + this.outerWidth(target) + nubHeight), width) 153 | .removeClass('tip-override'); 154 | } 155 | } 156 | 157 | tip.css('visibility', 'visible').hide(); 158 | }, 159 | 160 | inheritable_classes : function (target) { 161 | var inheritables = ['tip-top', 'tip-left', 'tip-bottom', 'tip-right', 'noradius'].concat(this.settings.additionalInheritableClasses), 162 | classes = target.attr('class'), 163 | filtered = classes ? $.map(classes.split(' '), function (el, i) { 164 | if ($.inArray(el, inheritables) !== -1) { 165 | return el; 166 | } 167 | }).join(' ') : ''; 168 | 169 | return $.trim(filtered); 170 | }, 171 | 172 | show : function ($target) { 173 | var $tip = this.getTip($target); 174 | 175 | this.reposition($target, $tip, $target.attr('class')); 176 | $tip.fadeIn(150); 177 | }, 178 | 179 | hide : function ($target) { 180 | var $tip = this.getTip($target); 181 | 182 | $tip.fadeOut(150); 183 | }, 184 | 185 | // deprecate reload 186 | reload : function () { 187 | var $self = $(this); 188 | 189 | return ($self.data('fndtn-tooltips')) ? $self.foundationTooltips('destroy').foundationTooltips('init') : $self.foundationTooltips('init'); 190 | }, 191 | 192 | off : function () { 193 | $(this.scope).off('.fndtn.tooltip'); 194 | $(this.settings.tooltipClass).each(function (i) { 195 | $('[data-tooltip]').get(i).attr('title', $(this).text()); 196 | }).remove(); 197 | } 198 | }; 199 | }(Foundation.zj, this, this.document)); 200 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.topbar.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.topbar = { 7 | name : 'topbar', 8 | 9 | version : '4.1.0', 10 | 11 | settings : { 12 | index : 0, 13 | stickyClass : 'sticky', 14 | custom_back_text: true, 15 | back_text: 'Back', 16 | init : false 17 | }, 18 | 19 | init : function (method, options) { 20 | var self = this; 21 | 22 | if (typeof method === 'object') { 23 | $.extend(true, this.settings, method); 24 | } 25 | 26 | if (typeof method != 'string') { 27 | 28 | $('.top-bar').each(function () { 29 | self.settings.$w = $(window); 30 | self.settings.$topbar = $(this); 31 | self.settings.$section = self.settings.$topbar.find('section'); 32 | self.settings.$titlebar = self.settings.$topbar.children('ul').first(); 33 | 34 | 35 | self.settings.$topbar.data('index', 0); 36 | 37 | var breakpoint = $("
    ").insertAfter(self.settings.$topbar); 38 | self.settings.breakPoint = breakpoint.width(); 39 | breakpoint.remove(); 40 | 41 | self.assemble(); 42 | 43 | if (self.settings.$topbar.parent().hasClass('fixed')) { 44 | $('body').css('padding-top', self.outerHeight(self.settings.$topbar)); 45 | } 46 | }); 47 | 48 | if (!self.settings.init) { 49 | this.events(); 50 | } 51 | 52 | return this.settings.init; 53 | } else { 54 | // fire method 55 | return this[method].call(this, options); 56 | } 57 | }, 58 | 59 | events : function () { 60 | var self = this; 61 | var offst = this.outerHeight($('.top-bar')); 62 | $(this.scope) 63 | .on('click.fndtn.topbar', '.top-bar .toggle-topbar', function (e) { 64 | var topbar = $(this).closest('.top-bar'), 65 | section = topbar.find('section, .section'), 66 | titlebar = topbar.children('ul').first(); 67 | 68 | if (!topbar.data('height')) self.largestUL(); 69 | 70 | e.preventDefault(); 71 | 72 | if (self.breakpoint()) { 73 | topbar 74 | .toggleClass('expanded') 75 | .css('min-height', ''); 76 | } 77 | 78 | if (!topbar.hasClass('expanded')) { 79 | if (!self.rtl) { 80 | section.css({left: '0%'}); 81 | section.find('>.name').css({left: '100%'}); 82 | } else { 83 | section.css({right: '0%'}); 84 | section.find('>.name').css({right: '100%'}); 85 | } 86 | section.find('li.moved').removeClass('moved'); 87 | topbar.data('index', 0); 88 | } 89 | 90 | if (topbar.parent().hasClass('fixed')) { 91 | topbar.parent().removeClass('fixed'); 92 | $('body').css('padding-top','0'); 93 | window.scrollTo(0,0); 94 | } else if (topbar.hasClass('fixed expanded')) { 95 | topbar.parent().addClass('fixed'); 96 | $('body').css('padding-top',offst); 97 | } 98 | 99 | }) 100 | 101 | .on('click.fndtn.topbar', '.top-bar .has-dropdown>a', function (e) { 102 | var topbar = $(this).closest('.top-bar'), 103 | section = topbar.find('section, .section'), 104 | titlebar = topbar.children('ul').first(); 105 | 106 | if (Modernizr.touch || self.breakpoint()) { 107 | e.preventDefault(); 108 | } 109 | 110 | if (self.breakpoint()) { 111 | var $this = $(this), 112 | $selectedLi = $this.closest('li'); 113 | 114 | topbar.data('index', topbar.data('index') + 1); 115 | $selectedLi.addClass('moved'); 116 | if (!self.rtl) { 117 | section.css({left: -(100 * topbar.data('index')) + '%'}); 118 | section.find('>.name').css({left: 100 * topbar.data('index') + '%'}); 119 | } else { 120 | section.css({right: -(100 * topbar.data('index')) + '%'}); 121 | section.find('>.name').css({right: 100 * topbar.data('index') + '%'}); 122 | } 123 | 124 | $this.siblings('ul') 125 | .height(topbar.data('height') + self.outerHeight(titlebar, true)); 126 | topbar 127 | .css('min-height', topbar.data('height') + self.outerHeight(titlebar, true) * 2) 128 | } 129 | }); 130 | 131 | $(window).on('resize.fndtn.topbar', function () { 132 | if (!self.breakpoint()) { 133 | $('.top-bar') 134 | .css('min-height', '') 135 | .removeClass('expanded'); 136 | } 137 | }.bind(this)); 138 | 139 | // Go up a level on Click 140 | $(this.scope).on('click.fndtn', '.top-bar .has-dropdown .back', function (e) { 141 | e.preventDefault(); 142 | 143 | var $this = $(this), 144 | topbar = $this.closest('.top-bar'), 145 | section = topbar.find('section, .section'), 146 | $movedLi = $this.closest('li.moved'), 147 | $previousLevelUl = $movedLi.parent(); 148 | 149 | topbar.data('index', topbar.data('index') - 1); 150 | if (!self.rtl) { 151 | section.css({left: -(100 * topbar.data('index')) + '%'}); 152 | section.find('>.name').css({left: 100 * topbar.data('index') + '%'}); 153 | } else { 154 | section.css({right: -(100 * topbar.data('index')) + '%'}); 155 | section.find('>.name').css({right: 100 * topbar.data('index') + '%'}); 156 | } 157 | 158 | if (topbar.data('index') === 0) { 159 | topbar.css('min-height', 0); 160 | } 161 | 162 | setTimeout(function () { 163 | $movedLi.removeClass('moved'); 164 | }, 300); 165 | }); 166 | }, 167 | 168 | breakpoint : function () { 169 | return $(window).width() <= this.settings.breakPoint || $('html').hasClass('lt-ie9'); 170 | }, 171 | 172 | assemble : function () { 173 | var self = this; 174 | // Pull element out of the DOM for manipulation 175 | this.settings.$section.detach(); 176 | 177 | this.settings.$section.find('.has-dropdown>a').each(function () { 178 | var $link = $(this), 179 | $dropdown = $link.siblings('.dropdown'), 180 | $titleLi = $('
  • '); 181 | 182 | // Copy link to subnav 183 | if (self.settings.custom_back_text == true) { 184 | $titleLi.find('h5>a').html('« ' + self.settings.back_text); 185 | } else { 186 | $titleLi.find('h5>a').html('« ' + $link.html()); 187 | } 188 | $dropdown.prepend($titleLi); 189 | }); 190 | 191 | // Put element back in the DOM 192 | this.settings.$section.appendTo(this.settings.$topbar); 193 | 194 | // check for sticky 195 | this.sticky(); 196 | }, 197 | 198 | largestUL : function () { 199 | var uls = this.settings.$topbar.find('section ul ul'), 200 | largest = uls.first(), 201 | total = 0, 202 | self = this; 203 | 204 | uls.each(function () { 205 | if ($(this).children('li').length > largest.children('li').length) { 206 | largest = $(this); 207 | } 208 | }); 209 | 210 | largest.children('li').each(function () { total += self.outerHeight($(this), true); }); 211 | 212 | this.settings.$topbar.data('height', total); 213 | }, 214 | 215 | sticky : function () { 216 | var klass = '.' + this.settings.stickyClass; 217 | if ($(klass).length > 0) { 218 | var distance = $(klass).length ? $(klass).offset().top: 0, 219 | $window = $(window); 220 | var offst = this.outerHeight($('.top-bar')); 221 | 222 | $window.scroll(function() { 223 | if ($window.scrollTop() >= (distance)) { 224 | $(klass).addClass("fixed"); 225 | $('body').css('padding-top',offst); 226 | } 227 | 228 | else if ($window.scrollTop() < distance) { 229 | $(klass).removeClass("fixed"); 230 | $('body').css('padding-top','0'); 231 | } 232 | }); 233 | } 234 | }, 235 | 236 | off : function () { 237 | $(this.scope).off('.fndtn.topbar'); 238 | $(window).off('.fndtn.topbar'); 239 | } 240 | }; 241 | }(Foundation.zj, this, this.document)); 242 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.reveal.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.reveal = { 7 | name: 'reveal', 8 | 9 | version : '4.0.9', 10 | 11 | locked : false, 12 | 13 | settings : { 14 | animation: 'fadeAndPop', 15 | animationSpeed: 250, 16 | closeOnBackgroundClick: true, 17 | dismissModalClass: 'close-reveal-modal', 18 | bgClass: 'reveal-modal-bg', 19 | open: function(){}, 20 | opened: function(){}, 21 | close: function(){}, 22 | closed: function(){}, 23 | bg : $('.reveal-modal-bg'), 24 | css : { 25 | open : { 26 | 'opacity': 0, 27 | 'visibility': 'visible', 28 | 'display' : 'block' 29 | }, 30 | close : { 31 | 'opacity': 1, 32 | 'visibility': 'hidden', 33 | 'display': 'none' 34 | } 35 | } 36 | }, 37 | 38 | init : function (scope, method, options) { 39 | this.scope = scope || this.scope; 40 | Foundation.inherit(this, 'data_options delay'); 41 | 42 | if (typeof method === 'object') { 43 | $.extend(true, this.settings, method); 44 | } 45 | 46 | if (typeof method != 'string') { 47 | this.events(); 48 | 49 | return this.settings.init; 50 | } else { 51 | return this[method].call(this, options); 52 | } 53 | }, 54 | 55 | events : function () { 56 | var self = this; 57 | 58 | $(this.scope) 59 | .off('.fndtn.reveal') 60 | .on('click.fndtn.reveal', '[data-reveal-id]', function (e) { 61 | e.preventDefault(); 62 | if (!self.locked) { 63 | self.locked = true; 64 | self.open.call(self, $(this)); 65 | } 66 | }) 67 | .on('click.fndtn.reveal touchend.click.fndtn.reveal', this.close_targets(), function (e) { 68 | e.preventDefault(); 69 | if (!self.locked) { 70 | self.locked = true; 71 | self.close.call(self, $(this).closest('.reveal-modal')); 72 | } 73 | }) 74 | .on('open.fndtn.reveal', '.reveal-modal', this.settings.open) 75 | .on('opened.fndtn.reveal', '.reveal-modal', this.settings.opened) 76 | .on('opened.fndtn.reveal', '.reveal-modal', this.open_video) 77 | .on('close.fndtn.reveal', '.reveal-modal', this.settings.close) 78 | .on('closed.fndtn.reveal', '.reveal-modal', this.settings.closed) 79 | .on('closed.fndtn.reveal', '.reveal-modal', this.close_video); 80 | 81 | return true; 82 | }, 83 | 84 | open : function (target) { 85 | if (target) { 86 | var modal = $('#' + target.data('reveal-id')); 87 | } else { 88 | var modal = $(this.scope); 89 | } 90 | 91 | if (!modal.hasClass('open')) { 92 | var open_modal = $('.reveal-modal.open'); 93 | 94 | if (typeof modal.data('css-top') === 'undefined') { 95 | modal.data('css-top', parseInt(modal.css('top'), 10)) 96 | .data('offset', this.cache_offset(modal)); 97 | } 98 | 99 | modal.trigger('open'); 100 | 101 | if (open_modal.length < 1) { 102 | this.toggle_bg(modal); 103 | } 104 | this.hide(open_modal, this.settings.css.open); 105 | this.show(modal, this.settings.css.open); 106 | } 107 | }, 108 | 109 | close : function (modal) { 110 | 111 | var modal = modal || $(this.scope), 112 | open_modals = $('.reveal-modal.open'); 113 | 114 | if (open_modals.length > 0) { 115 | this.locked = true; 116 | modal.trigger('close'); 117 | this.toggle_bg(modal); 118 | this.hide(open_modals, this.settings.css.close); 119 | } 120 | }, 121 | 122 | close_targets : function () { 123 | var base = '.' + this.settings.dismissModalClass; 124 | 125 | if (this.settings.closeOnBackgroundClick) { 126 | return base + ', .' + this.settings.bgClass; 127 | } 128 | 129 | return base; 130 | }, 131 | 132 | toggle_bg : function (modal) { 133 | if ($('.reveal-modal-bg').length === 0) { 134 | this.settings.bg = $('
    ', {'class': this.settings.bgClass}) 135 | .appendTo('body'); 136 | } 137 | 138 | if (this.settings.bg.filter(':visible').length > 0) { 139 | this.hide(this.settings.bg); 140 | } else { 141 | this.show(this.settings.bg); 142 | } 143 | }, 144 | 145 | show : function (el, css) { 146 | // is modal 147 | if (css) { 148 | if (/pop/i.test(this.settings.animation)) { 149 | css.top = $(window).scrollTop() - el.data('offset') + 'px'; 150 | var end_css = { 151 | top: $(window).scrollTop() + el.data('css-top') + 'px', 152 | opacity: 1 153 | } 154 | 155 | return this.delay(function () { 156 | return el 157 | .css(css) 158 | .animate(end_css, this.settings.animationSpeed, 'linear', function () { 159 | this.locked = false; 160 | el.trigger('opened'); 161 | }.bind(this)) 162 | .addClass('open'); 163 | }.bind(this), this.settings.animationSpeed / 2); 164 | } 165 | 166 | if (/fade/i.test(this.settings.animation)) { 167 | var end_css = {opacity: 1}; 168 | 169 | return this.delay(function () { 170 | return el 171 | .css(css) 172 | .animate(end_css, this.settings.animationSpeed, 'linear', function () { 173 | this.locked = false; 174 | el.trigger('opened'); 175 | }.bind(this)) 176 | .addClass('open'); 177 | }.bind(this), this.settings.animationSpeed / 2); 178 | } 179 | 180 | return el.css(css).show().css({opacity: 1}).addClass('open').trigger('opened'); 181 | } 182 | 183 | // should we animate the background? 184 | if (/fade/i.test(this.settings.animation)) { 185 | return el.fadeIn(this.settings.animationSpeed / 2); 186 | } 187 | 188 | return el.show(); 189 | }, 190 | 191 | hide : function (el, css) { 192 | // is modal 193 | if (css) { 194 | if (/pop/i.test(this.settings.animation)) { 195 | var end_css = { 196 | top: - $(window).scrollTop() - el.data('offset') + 'px', 197 | opacity: 0 198 | }; 199 | 200 | return this.delay(function () { 201 | return el 202 | .animate(end_css, this.settings.animationSpeed, 'linear', function () { 203 | this.locked = false; 204 | el.css(css).trigger('closed'); 205 | }.bind(this)) 206 | .removeClass('open'); 207 | }.bind(this), this.settings.animationSpeed / 2); 208 | } 209 | 210 | if (/fade/i.test(this.settings.animation)) { 211 | var end_css = {opacity: 0}; 212 | 213 | return this.delay(function () { 214 | return el 215 | .animate(end_css, this.settings.animationSpeed, 'linear', function () { 216 | this.locked = false; 217 | el.css(css).trigger('closed'); 218 | }.bind(this)) 219 | .removeClass('open'); 220 | }.bind(this), this.settings.animationSpeed / 2); 221 | } 222 | 223 | return el.hide().css(css).removeClass('open').trigger('closed'); 224 | } 225 | 226 | // should we animate the background? 227 | if (/fade/i.test(this.settings.animation)) { 228 | return el.fadeOut(this.settings.animationSpeed / 2); 229 | } 230 | 231 | return el.hide(); 232 | }, 233 | 234 | close_video : function (e) { 235 | var video = $(this).find('.flex-video'), 236 | iframe = video.find('iframe'); 237 | 238 | if (iframe.length > 0) { 239 | iframe.attr('data-src', iframe[0].src); 240 | iframe.attr('src', 'about:blank'); 241 | video.fadeOut(100).hide(); 242 | } 243 | }, 244 | 245 | open_video : function (e) { 246 | var video = $(this).find('.flex-video'), 247 | iframe = video.find('iframe'); 248 | 249 | if (iframe.length > 0) { 250 | var data_src = iframe.attr('data-src'); 251 | if (typeof data_src === 'string') { 252 | iframe[0].src = iframe.attr('data-src'); 253 | } 254 | video.show().fadeIn(100); 255 | } 256 | }, 257 | 258 | cache_offset : function (modal) { 259 | var offset = modal.show().height() + parseInt(modal.css('top'), 10); 260 | 261 | modal.hide(); 262 | 263 | return offset; 264 | }, 265 | 266 | off : function () { 267 | $(this.scope).off('.fndtn.reveal'); 268 | } 269 | }; 270 | }(Foundation.zj, this, this.document)); 271 | -------------------------------------------------------------------------------- /static/js/vendor/custom.modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-inlinesvg-svg-svgclippaths-touch-shiv-mq-cssclasses-teststyles-prefixes-ie8compat-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function y(a){j.cssText=a}function z(a,b){return y(m.join(a+";")+(b||""))}function A(a,b){return typeof a===b}function B(a,b){return!!~(""+a).indexOf(b)}function C(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:A(f,"function")?f.bind(d||b):f}return!1}var d="2.6.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n={svg:"http://www.w3.org/2000/svg"},o={},p={},q={},r=[],s=r.slice,t,u=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},v=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return u("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},w={}.hasOwnProperty,x;!A(w,"undefined")&&!A(w.call,"undefined")?x=function(a,b){return w.call(a,b)}:x=function(a,b){return b in a&&A(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=s.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(s.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(s.call(arguments)))};return e}),o.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:u(["@media (",m.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},o.svg=function(){return!!b.createElementNS&&!!b.createElementNS(n.svg,"svg").createSVGRect},o.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==n.svg},o.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(l.call(b.createElementNS(n.svg,"clipPath")))};for(var D in o)x(o,D)&&(t=D.toLowerCase(),e[t]=o[D](),r.push((e[t]?"":"no-")+t));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)x(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},y(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=m,e.mq=v,e.testStyles=u,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+r.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f 0) { 76 | e.preventDefault(); 77 | } 78 | 79 | if (section.hasClass('active')) { 80 | if (self.small(parent) 81 | || self.is_vertical(parent) 82 | || self.is_horizontal(parent) 83 | || self.is_accordion(parent)) { 84 | section 85 | .removeClass('active') 86 | .attr('style', ''); 87 | } 88 | } else { 89 | var prev_active_section = null, 90 | title_height = self.outerHeight(section.find('.title')); 91 | 92 | if (self.small(parent) || settings.one_up) { 93 | prev_active_section = $this.closest('[data-section]').find('section.active, .section.active'); 94 | 95 | if (self.small(parent)) { 96 | prev_active_section.attr('style', ''); 97 | } else { 98 | prev_active_section.attr('style', 'visibility: hidden; padding-top: '+title_height+'px;'); 99 | } 100 | } 101 | 102 | if (self.small(parent)) { 103 | section.attr('style', ''); 104 | } else { 105 | section.css('padding-top', title_height); 106 | } 107 | 108 | section.addClass('active'); 109 | 110 | if (prev_active_section !== null) { 111 | prev_active_section.removeClass('active').attr('style', ''); 112 | } 113 | } 114 | 115 | setTimeout(function () { 116 | self.settings.toggled = false; 117 | }, 300); 118 | 119 | settings.callback(); 120 | }, 121 | 122 | resize : function () { 123 | var sections = $('[data-section]'), 124 | self = Foundation.libs.section; 125 | 126 | sections.each(function() { 127 | var $this = $(this), 128 | active_section = $this.find('section.active, .section.active'), 129 | settings = $.extend({}, self.settings, self.data_options($this)); 130 | 131 | if (active_section.length > 1) { 132 | active_section 133 | .not(':first') 134 | .removeClass('active') 135 | .attr('style', ''); 136 | } else if (active_section.length < 1 137 | && !self.is_vertical($this) 138 | && !self.is_horizontal($this) 139 | && !self.is_accordion($this)) { 140 | 141 | var first = $this.find('section, .section').first(); 142 | first.addClass('active'); 143 | 144 | if (self.small($this)) { 145 | first.attr('style', ''); 146 | } else { 147 | first.css('padding-top', self.outerHeight(first.find('.title'))); 148 | } 149 | } 150 | 151 | if (self.small($this)) { 152 | active_section.attr('style', ''); 153 | } else { 154 | active_section.css('padding-top', self.outerHeight(active_section.find('.title'))); 155 | } 156 | 157 | self.position_titles($this); 158 | 159 | if (self.is_horizontal($this) && !self.small($this)) { 160 | self.position_content($this); 161 | } else { 162 | self.position_content($this, false); 163 | } 164 | }); 165 | }, 166 | 167 | is_vertical : function (el) { 168 | return /vertical-nav/i.test(el.data('section')); 169 | }, 170 | 171 | is_horizontal : function (el) { 172 | return /horizontal-nav/i.test(el.data('section')); 173 | }, 174 | 175 | is_accordion : function (el) { 176 | return /accordion/i.test(el.data('section')); 177 | }, 178 | 179 | is_tabs : function (el) { 180 | return /tabs/i.test(el.data('section')); 181 | }, 182 | 183 | set_active_from_hash : function () { 184 | var hash = window.location.hash.substring(1), 185 | sections = $('[data-section]'), 186 | self = this; 187 | 188 | sections.each(function () { 189 | var section = $(this), 190 | settings = $.extend({}, self.settings, self.data_options(section)); 191 | 192 | if (hash.length > 0 && settings.deep_linking) { 193 | section 194 | .find('section, .section') 195 | .attr('style', '') 196 | .removeClass('active'); 197 | section 198 | .find('.content[data-slug="' + hash + '"]') 199 | .closest('section, .section') 200 | .addClass('active'); 201 | } 202 | }); 203 | }, 204 | 205 | position_titles : function (section, off) { 206 | var titles = section.find('.title'), 207 | previous_width = 0, 208 | self = this; 209 | 210 | if (typeof off === 'boolean') { 211 | titles.attr('style', ''); 212 | 213 | } else { 214 | titles.each(function () { 215 | if (!self.rtl) { 216 | $(this).css('left', previous_width); 217 | } else { 218 | $(this).css('right', previous_width); 219 | } 220 | previous_width += self.outerWidth($(this)); 221 | }); 222 | } 223 | }, 224 | 225 | position_content : function (section, off) { 226 | var titles = section.find('.title'), 227 | content = section.find('.content'), 228 | self = this; 229 | 230 | if (typeof off === 'boolean') { 231 | content.attr('style', ''); 232 | section.attr('style', ''); 233 | } else { 234 | section.find('section, .section').each(function () { 235 | var title = $(this).find('.title'), 236 | content = $(this).find('.content'); 237 | if (!self.rtl) { 238 | content.css({left: title.position().left - 1, top: self.outerHeight(title) - 2}); 239 | } else { 240 | content.css({right: self.position_right(title) + 1, top: self.outerHeight(title) - 2}); 241 | } 242 | }); 243 | 244 | // temporary work around for Zepto outerheight calculation issues. 245 | if (typeof Zepto === 'function') { 246 | section.height(this.outerHeight(titles.first())); 247 | } else { 248 | section.height(this.outerHeight(titles.first()) - 2); 249 | } 250 | } 251 | 252 | }, 253 | 254 | position_right : function (el) { 255 | var section = el.closest('[data-section]'), 256 | section_width = el.closest('[data-section]').width(), 257 | offset = section.find('.title').length; 258 | return (section_width - el.position().left - el.width() * (el.index() + 1) - offset); 259 | }, 260 | 261 | reflow : function () { 262 | $('[data-section]').trigger('resize'); 263 | }, 264 | 265 | small : function (el) { 266 | var settings = $.extend({}, this.settings, this.data_options(el)); 267 | if (this.is_tabs(el)) { 268 | return false; 269 | } 270 | if (el && this.is_accordion(el)) { 271 | return true; 272 | } 273 | if ($('html').hasClass('lt-ie9')) { 274 | return true; 275 | } 276 | if ($('html').hasClass('ie8compat')) { 277 | return true; 278 | } 279 | return $(this.scope).width() < 768; 280 | }, 281 | 282 | off : function () { 283 | $(this.scope).off('.fndtn.section'); 284 | $(window).off('.fndtn.section'); 285 | $(document).off('.fndtn.section') 286 | } 287 | }; 288 | }(Foundation.zj, this, this.document)); 289 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Foundation Responsive Library 3 | * http://foundation.zurb.com 4 | * Copyright 2013, ZURB 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | */ 8 | 9 | /*jslint unparam: true, browser: true, indent: 2 */ 10 | 11 | // Accommodate running jQuery or Zepto in noConflict() mode by 12 | // using an anonymous function to redefine the $ shorthand name. 13 | // See http://docs.jquery.com/Using_jQuery_with_Other_Libraries 14 | // and http://zeptojs.com/ 15 | var libFuncName = null; 16 | if (typeof jQuery === "undefined" && 17 | typeof Zepto === "undefined" && 18 | typeof $ === "function") { 19 | libFuncName = $; 20 | } else if (typeof jQuery === "function") { 21 | libFuncName = jQuery; 22 | } else if (typeof Zepto === "function") { 23 | libFuncName = Zepto; 24 | } else { 25 | throw new TypeError(); 26 | } 27 | 28 | (function ($) { 29 | 30 | (function () { 31 | // add dusty browser stuff 32 | if (!Array.prototype.filter) { 33 | Array.prototype.filter = function(fun /*, thisp */) { 34 | "use strict"; 35 | 36 | if (this == null) { 37 | throw new TypeError(); 38 | } 39 | 40 | var t = Object(this), 41 | len = t.length >>> 0; 42 | if (typeof fun != "function") { 43 | try { 44 | throw new TypeError(); 45 | } catch (e) { 46 | return; 47 | } 48 | } 49 | 50 | var res = [], 51 | thisp = arguments[1]; 52 | for (var i = 0; i < len; i++) { 53 | if (i in t) { 54 | var val = t[i]; // in case fun mutates this 55 | if (fun && fun.call(thisp, val, i, t)) { 56 | res.push(val); 57 | } 58 | } 59 | } 60 | 61 | return res; 62 | }; 63 | 64 | if (!Function.prototype.bind) { 65 | Function.prototype.bind = function (oThis) { 66 | if (typeof this !== "function") { 67 | // closest thing possible to the ECMAScript 5 internal IsCallable function 68 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 69 | } 70 | 71 | var aArgs = Array.prototype.slice.call(arguments, 1), 72 | fToBind = this, 73 | fNOP = function () {}, 74 | fBound = function () { 75 | return fToBind.apply(this instanceof fNOP && oThis 76 | ? this 77 | : oThis, 78 | aArgs.concat(Array.prototype.slice.call(arguments))); 79 | }; 80 | 81 | fNOP.prototype = this.prototype; 82 | fBound.prototype = new fNOP(); 83 | 84 | return fBound; 85 | }; 86 | } 87 | } 88 | 89 | // fake stop() for zepto. 90 | $.fn.stop = $.fn.stop || function() { 91 | return this; 92 | }; 93 | }()); 94 | 95 | ;(function (window, document, undefined) { 96 | 'use strict'; 97 | 98 | window.Foundation = { 99 | name : 'Foundation', 100 | 101 | version : '4.1.0', 102 | 103 | // global Foundation cache object 104 | cache : {}, 105 | 106 | init : function (scope, libraries, method, options, response, /* internal */ nc) { 107 | var library_arr, 108 | args = [scope, method, options, response], 109 | responses = [], 110 | nc = nc || false; 111 | 112 | // disable library error catching, 113 | // used for development only 114 | if (nc) this.nc = nc; 115 | 116 | 117 | // check RTL 118 | this.rtl = /rtl/i.test($('html').attr('dir')); 119 | 120 | // set foundation global scope 121 | this.scope = scope || this.scope; 122 | 123 | if (libraries && typeof libraries === 'string') { 124 | if (/off/i.test(libraries)) return this.off(); 125 | 126 | library_arr = libraries.split(' '); 127 | 128 | if (library_arr.length > 0) { 129 | for (var i = library_arr.length - 1; i >= 0; i--) { 130 | responses.push(this.init_lib(library_arr[i], args)); 131 | } 132 | } 133 | } else { 134 | for (var lib in this.libs) { 135 | responses.push(this.init_lib(lib, args)); 136 | } 137 | } 138 | 139 | // if first argument is callback, add to args 140 | if (typeof libraries === 'function') { 141 | args.unshift(libraries); 142 | } 143 | 144 | return this.response_obj(responses, args); 145 | }, 146 | 147 | response_obj : function (response_arr, args) { 148 | for (var callback in args) { 149 | if (typeof args[callback] === 'function') { 150 | return args[callback]({ 151 | errors: response_arr.filter(function (s) { 152 | if (typeof s === 'string') return s; 153 | }) 154 | }); 155 | } 156 | } 157 | 158 | return response_arr; 159 | }, 160 | 161 | init_lib : function (lib, args) { 162 | return this.trap(function () { 163 | if (this.libs.hasOwnProperty(lib)) { 164 | this.patch(this.libs[lib]); 165 | return this.libs[lib].init.apply(this.libs[lib], args); 166 | } 167 | }.bind(this), lib); 168 | }, 169 | 170 | trap : function (fun, lib) { 171 | if (!this.nc) { 172 | try { 173 | return fun(); 174 | } catch (e) { 175 | return this.error({name: lib, message: 'could not be initialized', more: e.name + ' ' + e.message}); 176 | } 177 | } 178 | 179 | return fun(); 180 | }, 181 | 182 | patch : function (lib) { 183 | this.fix_outer(lib); 184 | lib.scope = this.scope; 185 | lib.rtl = this.rtl; 186 | }, 187 | 188 | inherit : function (scope, methods) { 189 | var methods_arr = methods.split(' '); 190 | 191 | for (var i = methods_arr.length - 1; i >= 0; i--) { 192 | if (this.lib_methods.hasOwnProperty(methods_arr[i])) { 193 | this.libs[scope.name][methods_arr[i]] = this.lib_methods[methods_arr[i]]; 194 | } 195 | } 196 | }, 197 | 198 | random_str : function (length) { 199 | var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split(''); 200 | 201 | if (!length) { 202 | length = Math.floor(Math.random() * chars.length); 203 | } 204 | 205 | var str = ''; 206 | for (var i = 0; i < length; i++) { 207 | str += chars[Math.floor(Math.random() * chars.length)]; 208 | } 209 | return str; 210 | }, 211 | 212 | libs : {}, 213 | 214 | // methods that can be inherited in libraries 215 | lib_methods : { 216 | set_data : function (node, data) { 217 | // this.name references the name of the library calling this method 218 | var id = [this.name,+new Date(),Foundation.random_str(5)].join('-'); 219 | 220 | Foundation.cache[id] = data; 221 | node.attr('data-' + this.name + '-id', id); 222 | return data; 223 | }, 224 | 225 | get_data : function (node) { 226 | return Foundation.cache[node.attr('data-' + this.name + '-id')]; 227 | }, 228 | 229 | remove_data : function (node) { 230 | if (node) { 231 | delete Foundation.cache[node.attr('data-' + this.name + '-id')]; 232 | node.attr('data-' + this.name + '-id', ''); 233 | } else { 234 | $('[data-' + this.name + '-id]').each(function () { 235 | delete Foundation.cache[$(this).attr('data-' + this.name + '-id')]; 236 | $(this).attr('data-' + this.name + '-id', ''); 237 | }); 238 | } 239 | }, 240 | 241 | throttle : function(fun, delay) { 242 | var timer = null; 243 | return function () { 244 | var context = this, args = arguments; 245 | clearTimeout(timer); 246 | timer = setTimeout(function () { 247 | fun.apply(context, args); 248 | }, delay); 249 | }; 250 | }, 251 | 252 | // parses data-options attribute on nodes and turns 253 | // them into an object 254 | data_options : function (el) { 255 | var opts = {}, ii, p, 256 | opts_arr = (el.attr('data-options') || ':').split(';'), 257 | opts_len = opts_arr.length; 258 | 259 | function isNumber (o) { 260 | return ! isNaN (o-0) && o !== null && o !== "" && o !== false && o !== true; 261 | } 262 | 263 | function trim(str) { 264 | if (typeof str === 'string') return $.trim(str); 265 | return str; 266 | } 267 | 268 | // parse options 269 | for (ii = opts_len - 1; ii >= 0; ii--) { 270 | p = opts_arr[ii].split(':'); 271 | 272 | if (/true/i.test(p[1])) p[1] = true; 273 | if (/false/i.test(p[1])) p[1] = false; 274 | if (isNumber(p[1])) p[1] = parseInt(p[1], 10); 275 | 276 | if (p.length === 2 && p[0].length > 0) { 277 | opts[trim(p[0])] = trim(p[1]); 278 | } 279 | } 280 | 281 | return opts; 282 | }, 283 | 284 | delay : function (fun, delay) { 285 | return setTimeout(fun, delay); 286 | }, 287 | 288 | // animated scrolling 289 | scrollTo : function (el, to, duration) { 290 | if (duration < 0) return; 291 | var difference = to - $(window).scrollTop(); 292 | var perTick = difference / duration * 10; 293 | 294 | this.scrollToTimerCache = setTimeout(function() { 295 | if (!isNaN(parseInt(perTick, 10))) { 296 | window.scrollTo(0, $(window).scrollTop() + perTick); 297 | this.scrollTo(el, to, duration - 10); 298 | } 299 | }.bind(this), 10); 300 | }, 301 | 302 | // not supported in core Zepto 303 | scrollLeft : function (el) { 304 | if (!el.length) return; 305 | return ('scrollLeft' in el[0]) ? el[0].scrollLeft : el[0].pageXOffset; 306 | }, 307 | 308 | // test for empty object or array 309 | empty : function (obj) { 310 | if (obj.length && obj.length > 0) return false; 311 | if (obj.length && obj.length === 0) return true; 312 | 313 | for (var key in obj) { 314 | if (hasOwnProperty.call(obj, key)) return false; 315 | } 316 | 317 | return true; 318 | } 319 | }, 320 | 321 | fix_outer : function (lib) { 322 | lib.outerHeight = function (el, bool) { 323 | if (typeof Zepto === 'function') { 324 | return el.height(); 325 | } 326 | 327 | if (typeof bool !== 'undefined') { 328 | return el.outerHeight(bool); 329 | } 330 | 331 | return el.outerHeight(); 332 | }; 333 | 334 | lib.outerWidth = function (el) { 335 | if (typeof Zepto === 'function') { 336 | return el.width(); 337 | } 338 | 339 | if (typeof bool !== 'undefined') { 340 | return el.outerWidth(bool); 341 | } 342 | 343 | return el.outerWidth(); 344 | }; 345 | }, 346 | 347 | error : function (error) { 348 | return error.name + ' ' + error.message + '; ' + error.more; 349 | }, 350 | 351 | // remove all foundation events. 352 | off: function () { 353 | $(this.scope).off('.fndtn'); 354 | $(window).off('.fndtn'); 355 | return true; 356 | }, 357 | 358 | zj : function () { 359 | try { 360 | return Zepto; 361 | } catch (e) { 362 | return jQuery; 363 | } 364 | }() 365 | }, 366 | 367 | $.fn.foundation = function () { 368 | var args = Array.prototype.slice.call(arguments, 0); 369 | 370 | return this.each(function () { 371 | Foundation.init.apply(Foundation, [this].concat(args)); 372 | return this; 373 | }); 374 | }; 375 | 376 | }(this, this.document)); 377 | 378 | })(libFuncName); 379 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.clearing.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.clearing = { 7 | name : 'clearing', 8 | 9 | version : '4.1.0', 10 | 11 | settings : { 12 | templates : { 13 | viewing : '×' + 14 | '' 17 | }, 18 | 19 | // comma delimited list of selectors that, on click, will close clearing, 20 | // add 'div.clearing-blackout, div.visible-img' to close on background click 21 | close_selectors : '.clearing-close', 22 | 23 | // event initializers and locks 24 | init : false, 25 | locked : false 26 | }, 27 | 28 | init : function (method, options) { 29 | Foundation.inherit(this, 'set_data get_data remove_data throttle'); 30 | 31 | if (typeof method === 'object') { 32 | options = $.extend(true, this.settings, method); 33 | } 34 | 35 | if (typeof method != 'string') { 36 | $(this.scope).find('ul[data-clearing]').each(function () { 37 | var self = Foundation.libs.clearing, 38 | $el = $(this), 39 | options = options || {}, 40 | settings = self.get_data($el); 41 | 42 | if (!settings) { 43 | options.$parent = $el.parent(); 44 | 45 | self.set_data($el, $.extend(true, self.settings, options)); 46 | 47 | self.assemble($el.find('li')); 48 | 49 | if (!self.settings.init) { 50 | self.events().swipe_events(); 51 | } 52 | } 53 | }); 54 | 55 | return this.settings.init; 56 | } else { 57 | // fire method 58 | return this[method].call(this, options); 59 | } 60 | }, 61 | 62 | // event binding and initial setup 63 | 64 | events : function () { 65 | var self = this; 66 | 67 | $(this.scope) 68 | .on('click.fndtn.clearing', 'ul[data-clearing] li', 69 | function (e, current, target) { 70 | var current = current || $(this), 71 | target = target || current, 72 | settings = self.get_data(current.parent()); 73 | 74 | e.preventDefault(); 75 | if (!settings) self.init(); 76 | 77 | // set current and target to the clicked li if not otherwise defined. 78 | self.open($(e.target), current, target); 79 | self.update_paddles(target); 80 | }) 81 | 82 | .on('click.fndtn.clearing', '.clearing-main-next', 83 | function (e) { this.nav(e, 'next') }.bind(this)) 84 | .on('click.fndtn.clearing', '.clearing-main-prev', 85 | function (e) { this.nav(e, 'prev') }.bind(this)) 86 | .on('click.fndtn.clearing', this.settings.close_selectors, 87 | function (e) { Foundation.libs.clearing.close(e, this) }) 88 | .on('keydown.fndtn.clearing', 89 | function (e) { this.keydown(e) }.bind(this)); 90 | 91 | $(window).on('resize.fndtn.clearing', 92 | function (e) { this.resize() }.bind(this)); 93 | 94 | this.settings.init = true; 95 | return this; 96 | }, 97 | 98 | swipe_events : function () { 99 | var self = this; 100 | 101 | $(this.scope) 102 | .on('touchstart.fndtn.clearing', '.visible-img', function(e) { 103 | if (!e.touches) { e = e.originalEvent; } 104 | var data = { 105 | start_page_x: e.touches[0].pageX, 106 | start_page_y: e.touches[0].pageY, 107 | start_time: (new Date()).getTime(), 108 | delta_x: 0, 109 | is_scrolling: undefined 110 | }; 111 | 112 | $(this).data('swipe-transition', data); 113 | e.stopPropagation(); 114 | }) 115 | .on('touchmove.fndtn.clearing', '.visible-img', function(e) { 116 | if (!e.touches) { e = e.originalEvent; } 117 | // Ignore pinch/zoom events 118 | if(e.touches.length > 1 || e.scale && e.scale !== 1) return; 119 | 120 | var data = $(this).data('swipe-transition'); 121 | 122 | if (typeof data === 'undefined') { 123 | data = {}; 124 | } 125 | 126 | data.delta_x = e.touches[0].pageX - data.start_page_x; 127 | 128 | if ( typeof data.is_scrolling === 'undefined') { 129 | data.is_scrolling = !!( data.is_scrolling || Math.abs(data.delta_x) < Math.abs(e.touches[0].pageY - data.start_page_y) ); 130 | } 131 | 132 | if (!data.is_scrolling && !data.active) { 133 | e.preventDefault(); 134 | var direction = (data.delta_x < 0) ? 'next' : 'prev'; 135 | data.active = true; 136 | self.nav(e, direction); 137 | } 138 | }) 139 | .on('touchend.fndtn.clearing', '.visible-img', function(e) { 140 | $(this).data('swipe-transition', {}); 141 | e.stopPropagation(); 142 | }); 143 | }, 144 | 145 | assemble : function ($li) { 146 | var $el = $li.parent(), 147 | settings = this.get_data($el), 148 | grid = $el.detach(), 149 | data = { 150 | grid: '', 151 | viewing: settings.templates.viewing 152 | }, 153 | wrapper = '
    ' + data.viewing + 154 | data.grid + '
    '; 155 | 156 | return settings.$parent.append(wrapper); 157 | }, 158 | 159 | // event callbacks 160 | 161 | open : function ($image, current, target) { 162 | var root = target.closest('.clearing-assembled'), 163 | container = root.find('div').first(), 164 | visible_image = container.find('.visible-img'), 165 | image = visible_image.find('img').not($image); 166 | 167 | if (!this.locked()) { 168 | // set the image to the selected thumbnail 169 | image.attr('src', this.load($image)); 170 | 171 | this.loaded(image, function () { 172 | // toggle the gallery 173 | root.addClass('clearing-blackout'); 174 | container.addClass('clearing-container'); 175 | visible_image.show(); 176 | this.fix_height(target) 177 | .caption(visible_image.find('.clearing-caption'), $image) 178 | .center(image) 179 | .shift(current, target, function () { 180 | target.siblings().removeClass('visible'); 181 | target.addClass('visible'); 182 | }); 183 | }.bind(this)); 184 | } 185 | }, 186 | 187 | close : function (e, el) { 188 | e.preventDefault(); 189 | 190 | var root = (function (target) { 191 | if (/blackout/.test(target.selector)) { 192 | return target; 193 | } else { 194 | return target.closest('.clearing-blackout'); 195 | } 196 | }($(el))), container, visible_image; 197 | 198 | if (el === e.target && root) { 199 | container = root.find('div').first(), 200 | visible_image = container.find('.visible-img'); 201 | this.settings.prev_index = 0; 202 | root.find('ul[data-clearing]') 203 | .attr('style', '').closest('.clearing-blackout') 204 | .removeClass('clearing-blackout'); 205 | container.removeClass('clearing-container'); 206 | visible_image.hide(); 207 | } 208 | 209 | return false; 210 | }, 211 | 212 | keydown : function (e) { 213 | var clearing = $('.clearing-blackout').find('ul[data-clearing]'); 214 | 215 | if (e.which === 39) this.go(clearing, 'next'); 216 | if (e.which === 37) this.go(clearing, 'prev'); 217 | if (e.which === 27) $('a.clearing-close').trigger('click'); 218 | }, 219 | 220 | nav : function (e, direction) { 221 | var clearing = $('.clearing-blackout').find('ul[data-clearing]'); 222 | 223 | e.preventDefault(); 224 | this.go(clearing, direction); 225 | }, 226 | 227 | resize : function () { 228 | var image = $('.clearing-blackout .visible-img').find('img'); 229 | 230 | if (image.length) { 231 | this.center(image); 232 | } 233 | }, 234 | 235 | // visual adjustments 236 | fix_height : function (target) { 237 | var lis = target.parent().children(), 238 | self = this; 239 | 240 | lis.each(function () { 241 | var li = $(this), 242 | image = li.find('img'); 243 | 244 | if (li.height() > self.outerHeight(image)) { 245 | li.addClass('fix-height'); 246 | } 247 | }) 248 | .closest('ul') 249 | .width(lis.length * 100 + '%'); 250 | 251 | return this; 252 | }, 253 | 254 | update_paddles : function (target) { 255 | var visible_image = target 256 | .closest('.carousel') 257 | .siblings('.visible-img'); 258 | 259 | if (target.next().length) { 260 | visible_image 261 | .find('.clearing-main-right') 262 | .removeClass('disabled'); 263 | } else { 264 | visible_image 265 | .find('.clearing-main-right') 266 | .addClass('disabled'); 267 | } 268 | 269 | if (target.prev().length) { 270 | visible_image 271 | .find('.clearing-main-prev') 272 | .removeClass('disabled'); 273 | } else { 274 | visible_image 275 | .find('.clearing-main-prev') 276 | .addClass('disabled'); 277 | } 278 | }, 279 | 280 | center : function (target) { 281 | if (!this.rtl) { 282 | target.css({ 283 | marginLeft : -(this.outerWidth(target) / 2), 284 | marginTop : -(this.outerHeight(target) / 2) 285 | }); 286 | } else { 287 | target.css({ 288 | marginRight : -(this.outerWidth(target) / 2), 289 | marginTop : -(this.outerHeight(target) / 2) 290 | }); 291 | } 292 | return this; 293 | }, 294 | 295 | // image loading and preloading 296 | 297 | load : function ($image) { 298 | var href = $image.parent().attr('href'); 299 | 300 | this.preload($image); 301 | 302 | if (href) return href; 303 | return $image.attr('src'); 304 | }, 305 | 306 | preload : function ($image) { 307 | this 308 | .img($image.closest('li').next()) 309 | .img($image.closest('li').prev()); 310 | }, 311 | 312 | loaded : function (image, callback) { 313 | // based on jquery.imageready.js 314 | // @weblinc, @jsantell, (c) 2012 315 | 316 | function loaded () { 317 | callback(); 318 | } 319 | 320 | function bindLoad () { 321 | this.one('load', loaded); 322 | 323 | if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { 324 | var src = this.attr( 'src' ), 325 | param = src.match( /\?/ ) ? '&' : '?'; 326 | 327 | param += 'random=' + (new Date()).getTime(); 328 | this.attr('src', src + param); 329 | } 330 | } 331 | 332 | if (!image.attr('src')) { 333 | loaded(); 334 | return; 335 | } 336 | 337 | if (image[0].complete || image[0].readyState === 4) { 338 | loaded(); 339 | } else { 340 | bindLoad.call(image); 341 | } 342 | }, 343 | 344 | img : function (img) { 345 | if (img.length) { 346 | var new_img = new Image(), 347 | new_a = img.find('a'); 348 | 349 | if (new_a.length) { 350 | new_img.src = new_a.attr('href'); 351 | } else { 352 | new_img.src = img.find('img').attr('src'); 353 | } 354 | } 355 | return this; 356 | }, 357 | 358 | // image caption 359 | 360 | caption : function (container, $image) { 361 | var caption = $image.data('caption'); 362 | 363 | if (caption) { 364 | container 365 | .text(caption) 366 | .show(); 367 | } else { 368 | container 369 | .text('') 370 | .hide(); 371 | } 372 | return this; 373 | }, 374 | 375 | // directional methods 376 | 377 | go : function ($ul, direction) { 378 | var current = $ul.find('.visible'), 379 | target = current[direction](); 380 | 381 | if (target.length) { 382 | target 383 | .find('img') 384 | .trigger('click', [current, target]); 385 | } 386 | }, 387 | 388 | shift : function (current, target, callback) { 389 | var clearing = target.parent(), 390 | old_index = this.settings.prev_index || target.index(), 391 | direction = this.direction(clearing, current, target), 392 | left = parseInt(clearing.css('left'), 10), 393 | width = this.outerWidth(target), 394 | skip_shift; 395 | 396 | // we use jQuery animate instead of CSS transitions because we 397 | // need a callback to unlock the next animation 398 | if (target.index() !== old_index && !/skip/.test(direction)){ 399 | if (/left/.test(direction)) { 400 | this.lock(); 401 | clearing.animate({left : left + width}, 300, this.unlock()); 402 | } else if (/right/.test(direction)) { 403 | this.lock(); 404 | clearing.animate({left : left - width}, 300, this.unlock()); 405 | } 406 | } else if (/skip/.test(direction)) { 407 | // the target image is not adjacent to the current image, so 408 | // do we scroll right or not 409 | skip_shift = target.index() - this.settings.up_count; 410 | this.lock(); 411 | 412 | if (skip_shift > 0) { 413 | clearing.animate({left : -(skip_shift * width)}, 300, this.unlock()); 414 | } else { 415 | clearing.animate({left : 0}, 300, this.unlock()); 416 | } 417 | } 418 | 419 | callback(); 420 | }, 421 | 422 | direction : function ($el, current, target) { 423 | var lis = $el.find('li'), 424 | li_width = this.outerWidth(lis) + (this.outerWidth(lis) / 4), 425 | up_count = Math.floor(this.outerWidth($('.clearing-container')) / li_width) - 1, 426 | target_index = lis.index(target), 427 | response; 428 | 429 | this.settings.up_count = up_count; 430 | 431 | if (this.adjacent(this.settings.prev_index, target_index)) { 432 | if ((target_index > up_count) 433 | && target_index > this.settings.prev_index) { 434 | response = 'right'; 435 | } else if ((target_index > up_count - 1) 436 | && target_index <= this.settings.prev_index) { 437 | response = 'left'; 438 | } else { 439 | response = false; 440 | } 441 | } else { 442 | response = 'skip'; 443 | } 444 | 445 | this.settings.prev_index = target_index; 446 | 447 | return response; 448 | }, 449 | 450 | adjacent : function (current_index, target_index) { 451 | for (var i = target_index + 1; i >= target_index - 1; i--) { 452 | if (i === current_index) return true; 453 | } 454 | return false; 455 | }, 456 | 457 | // lock management 458 | 459 | lock : function () { 460 | this.settings.locked = true; 461 | }, 462 | 463 | unlock : function () { 464 | this.settings.locked = false; 465 | }, 466 | 467 | locked : function () { 468 | return this.settings.locked; 469 | }, 470 | 471 | // plugin management/browser quirks 472 | 473 | outerHTML : function (el) { 474 | // support FireFox < 11 475 | return el.outerHTML || new XMLSerializer().serializeToString(el); 476 | }, 477 | 478 | off : function () { 479 | $(this.scope).off('.fndtn.clearing'); 480 | $(window).off('.fndtn.clearing'); 481 | this.remove_data(); // empty settings cache 482 | this.settings.init = false; 483 | } 484 | }; 485 | 486 | }(Foundation.zj, this, this.document)); 487 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.orbit.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs = Foundation.libs || {}; 5 | 6 | Foundation.libs.orbit = { 7 | name: 'orbit', 8 | 9 | version: '4.1.0', 10 | 11 | settings: { 12 | timer_speed: 10000, 13 | animation_speed: 500, 14 | bullets: true, 15 | stack_on_small: true, 16 | container_class: 'orbit-container', 17 | stack_on_small_class: 'orbit-stack-on-small', 18 | next_class: 'orbit-next', 19 | prev_class: 'orbit-prev', 20 | timer_container_class: 'orbit-timer', 21 | timer_paused_class: 'paused', 22 | timer_progress_class: 'orbit-progress', 23 | slides_container_class: 'orbit-slides-container', 24 | bullets_container_class: 'orbit-bullets', 25 | bullets_active_class: 'active', 26 | slide_number_class: 'orbit-slide-number', 27 | caption_class: 'orbit-caption', 28 | active_slide_class: 'active', 29 | orbit_transition_class: 'orbit-transitioning' 30 | }, 31 | 32 | init: function (scope, method, options) { 33 | var self = this; 34 | Foundation.inherit(self, 'data_options'); 35 | 36 | if (typeof method === 'object') { 37 | $.extend(true, self.settings, method); 38 | } 39 | 40 | $('[data-orbit]', scope).each(function(idx, el) { 41 | var scoped_self = $.extend(true, {}, self); 42 | scoped_self._init(idx, el); 43 | }); 44 | }, 45 | 46 | _container_html: function() { 47 | var self = this; 48 | return '
    '; 49 | }, 50 | 51 | _bullets_container_html: function($slides) { 52 | var self = this, 53 | $list = $('
      '); 54 | $slides.each(function(idx, slide) { 55 | var $item = $('
    1. '); 56 | if (idx === 0) { 57 | $item.addClass(self.settings.bullets_active_class); 58 | } 59 | $list.append($item); 60 | }); 61 | return $list; 62 | }, 63 | 64 | _slide_number_html: function(slide_number, total_slides) { 65 | var self = this, 66 | $container = $('
      '); 67 | $container.append('' + slide_number + ' of ' + total_slides + ''); 68 | return $container; 69 | }, 70 | 71 | _timer_html: function() { 72 | var self = this; 73 | if (typeof self.settings.timer_speed === 'number' && self.settings.timer_speed > 0) { 74 | return '
      '; 77 | } else { 78 | return ''; 79 | } 80 | }, 81 | 82 | _next_html: function() { 83 | var self = this; 84 | return 'Next '; 85 | }, 86 | 87 | _prev_html: function() { 88 | var self = this; 89 | return 'Prev '; 90 | }, 91 | 92 | _init: function (idx, slider) { 93 | var self = this, 94 | $slides_container = $(slider), 95 | $container = $slides_container.wrap(self._container_html()).parent(), 96 | $slides = $slides_container.children(); 97 | 98 | $.extend(true, self.settings, self.data_options($slides_container)); 99 | 100 | $container.append(self._prev_html()); 101 | $container.append(self._next_html()); 102 | $slides_container.addClass(self.settings.slides_container_class); 103 | if (self.settings.stack_on_small) { 104 | $container.addClass(self.settings.stack_on_small_class); 105 | } 106 | $container.append(self._slide_number_html(1, $slides.length)); 107 | $container.append(self._timer_html()); 108 | if (self.settings.bullets) { 109 | $container.after(self._bullets_container_html($slides)); 110 | } 111 | // To better support the "sliding" effect it's easier 112 | // if we just clone the first and last slides 113 | $slides_container.append($slides.first().clone().attr('data-orbit-slide','')); 114 | $slides_container.prepend($slides.last().clone().attr('data-orbit-slide','')); 115 | // Make the first "real" slide active 116 | $slides_container.css('marginLeft', '-100%'); 117 | $slides.first().addClass(self.settings.active_slide_class); 118 | 119 | self._init_events($slides_container); 120 | self._init_dimensions($slides_container); 121 | self._start_timer($slides_container); 122 | }, 123 | 124 | _init_events: function ($slides_container) { 125 | var self = this, 126 | $container = $slides_container.parent(); 127 | 128 | $(window) 129 | .on('load.fndtn.orbit', function() { 130 | $slides_container.height(''); 131 | $slides_container.height($slides_container.height($container.height())); 132 | $slides_container.trigger('orbit:ready'); 133 | }) 134 | .on('resize.fndtn.orbit', function() { 135 | $slides_container.height(''); 136 | $slides_container.height($slides_container.height($container.height())); 137 | }); 138 | 139 | $(document).on('click.fndtn.orbit', '[data-orbit-link]', function(e) { 140 | e.preventDefault(); 141 | var id = $(e.currentTarget).attr('data-orbit-link'), 142 | $slide = $slides_container.find('[data-orbit-slide=' + id + ']').first(); 143 | 144 | if ($slide.length === 1) { 145 | self._reset_timer($slides_container, true); 146 | self._goto($slides_container, $slide.index(), function() {}); 147 | } 148 | }); 149 | 150 | $container.siblings('.' + self.settings.bullets_container_class) 151 | .on('click.fndtn.orbit', '[data-orbit-slide-number]', function(e) { 152 | e.preventDefault(); 153 | self._reset_timer($slides_container, true); 154 | self._goto($slides_container, $(e.currentTarget).data('orbit-slide-number'),function() {}); 155 | }); 156 | 157 | $container 158 | .on('orbit:after-slide-change.fndtn.orbit', function(e, orbit) { 159 | var $slide_number = $container.find('.' + self.settings.slide_number_class); 160 | 161 | if ($slide_number.length === 1) { 162 | $slide_number.replaceWith(self._slide_number_html(orbit.slide_number, orbit.total_slides)); 163 | } 164 | }) 165 | .on('orbit:next-slide.fndtn.orbit click.fndtn.orbit', '.' + self.settings.next_class, function(e) { 166 | e.preventDefault(); 167 | self._reset_timer($slides_container, true); 168 | self._goto($slides_container, 'next', function() {}); 169 | }) 170 | .on('orbit:prev-slide.fndtn.orbit click.fndtn.orbit', '.' + self.settings.prev_class, function(e) { 171 | e.preventDefault(); 172 | self._reset_timer($slides_container, true); 173 | self._goto($slides_container, 'prev', function() {}); 174 | }) 175 | .on('orbit:toggle-play-pause.fndtn.orbit click.fndtn.orbit touchstart.fndtn.orbit', '.' + self.settings.timer_container_class, function(e) { 176 | e.preventDefault(); 177 | var $timer = $(e.currentTarget).toggleClass(self.settings.timer_paused_class), 178 | $slides_container = $timer.closest('.' + self.settings.container_class) 179 | .find('.' + self.settings.slides_container_class); 180 | 181 | if ($timer.hasClass(self.settings.timer_paused_class)) { 182 | self._stop_timer($slides_container); 183 | } else { 184 | self._start_timer($slides_container); 185 | } 186 | }) 187 | .on('touchstart.fndtn.orbit', function(e) { 188 | if (!e.touches) { e = e.originalEvent; } 189 | var data = { 190 | start_page_x: e.touches[0].pageX, 191 | start_page_y: e.touches[0].pageY, 192 | start_time: (new Date()).getTime(), 193 | delta_x: 0, 194 | is_scrolling: undefined 195 | }; 196 | $container.data('swipe-transition', data); 197 | e.stopPropagation(); 198 | }) 199 | .on('touchmove.fndtn.orbit', function(e) { 200 | if (!e.touches) { e = e.originalEvent; } 201 | // Ignore pinch/zoom events 202 | if(e.touches.length > 1 || e.scale && e.scale !== 1) return; 203 | 204 | var data = $container.data('swipe-transition'); 205 | if (typeof data === 'undefined') { 206 | data = {}; 207 | } 208 | 209 | data.delta_x = e.touches[0].pageX - data.start_page_x; 210 | 211 | if ( typeof data.is_scrolling === 'undefined') { 212 | data.is_scrolling = !!( data.is_scrolling || Math.abs(data.delta_x) < Math.abs(e.touches[0].pageY - data.start_page_y) ); 213 | } 214 | 215 | if (!data.is_scrolling && !data.active) { 216 | e.preventDefault(); 217 | self._stop_timer($slides_container); 218 | var direction = (data.delta_x < 0) ? 'next' : 'prev'; 219 | data.active = true; 220 | self._goto($slides_container, direction, function() {}); 221 | } 222 | }) 223 | .on('touchend.fndtn.orbit', function(e) { 224 | $container.data('swipe-transition', {}); 225 | e.stopPropagation(); 226 | }); 227 | }, 228 | 229 | _init_dimensions: function ($slides_container) { 230 | var $container = $slides_container.parent(), 231 | $slides = $slides_container.children(); 232 | 233 | $slides_container.css('width', $slides.length * 100 + '%'); 234 | $slides.css('width', 100 / $slides.length + '%'); 235 | $slides_container.height($container.height()); 236 | $slides_container.css('width', $slides.length * 100 + '%'); 237 | }, 238 | 239 | _start_timer: function ($slides_container) { 240 | var self = this, 241 | $container = $slides_container.parent(); 242 | 243 | var callback = function() { 244 | self._reset_timer($slides_container, false); 245 | self._goto($slides_container, 'next', function() { 246 | self._start_timer($slides_container); 247 | }); 248 | }; 249 | 250 | var $timer = $container.find('.' + self.settings.timer_container_class), 251 | $progress = $timer.find('.' + self.settings.timer_progress_class), 252 | progress_pct = ($progress.width() / $timer.width()), 253 | delay = self.settings.timer_speed - (progress_pct * self.settings.timer_speed); 254 | 255 | $progress.animate({'width': '100%'}, delay, 'linear', callback); 256 | $slides_container.trigger('orbit:timer-started'); 257 | }, 258 | 259 | _stop_timer: function ($slides_container) { 260 | var self = this, 261 | $container = $slides_container.parent(), 262 | $timer = $container.find('.' + self.settings.timer_container_class), 263 | $progress = $timer.find('.' + self.settings.timer_progress_class), 264 | progress_pct = $progress.width() / $timer.width() 265 | self._rebuild_timer($container, progress_pct * 100 + '%'); 266 | // $progress.stop(); 267 | $slides_container.trigger('orbit:timer-stopped'); 268 | $timer = $container.find('.' + self.settings.timer_container_class); 269 | $timer.addClass(self.settings.timer_paused_class); 270 | }, 271 | 272 | _reset_timer: function($slides_container, is_paused) { 273 | var self = this, 274 | $container = $slides_container.parent(); 275 | self._rebuild_timer($container, '0%'); 276 | if (typeof is_paused === 'boolean' && is_paused) { 277 | var $timer = $container.find('.' + self.settings.timer_container_class); 278 | $timer.addClass(self.settings.timer_paused_class); 279 | } 280 | }, 281 | 282 | _rebuild_timer: function ($container, width_pct) { 283 | // Zepto is unable to stop animations since they 284 | // are css-based. This is a workaround for that 285 | // limitation, which rebuilds the dom element 286 | // thus stopping the animation 287 | var self = this, 288 | $timer = $container.find('.' + self.settings.timer_container_class), 289 | $new_timer = $(self._timer_html()), 290 | $new_timer_progress = $new_timer.find('.' + self.settings.timer_progress_class); 291 | 292 | if (typeof Zepto === 'function') { 293 | $timer.remove(); 294 | $container.append($new_timer); 295 | $new_timer_progress.css('width', width_pct); 296 | } else if (typeof jQuery === 'function') { 297 | var $progress = $timer.find('.' + self.settings.timer_progress_class); 298 | $progress.css('width', width_pct); 299 | $progress.stop(); 300 | } 301 | }, 302 | 303 | _goto: function($slides_container, index_or_direction, callback) { 304 | var self = this, 305 | $container = $slides_container.parent(), 306 | $slides = $slides_container.children(), 307 | $active_slide = $slides_container.find('.' + self.settings.active_slide_class), 308 | active_index = $active_slide.index(), 309 | margin_position = Foundation.rtl ? 'marginRight' : 'marginLeft'; 310 | 311 | if ($container.hasClass(self.settings.orbit_transition_class)) { 312 | return false; 313 | } 314 | 315 | if (index_or_direction === 'prev') { 316 | if (active_index === 0) { 317 | active_index = $slides.length - 1; 318 | } 319 | else { 320 | active_index--; 321 | } 322 | } 323 | else if (index_or_direction === 'next') { 324 | active_index = (active_index+1) % $slides.length; 325 | } 326 | else if (typeof index_or_direction === 'number') { 327 | active_index = (index_or_direction % $slides.length); 328 | } 329 | if (active_index === ($slides.length - 1) && index_or_direction === 'next') { 330 | $slides_container.css(margin_position, '0%'); 331 | active_index = 1; 332 | } 333 | else if (active_index === 0 && index_or_direction === 'prev') { 334 | $slides_container.css(margin_position, '-' + ($slides.length - 1) * 100 + '%'); 335 | active_index = $slides.length - 2; 336 | } 337 | // Start transition, make next slide active 338 | $container.addClass(self.settings.orbit_transition_class); 339 | $active_slide.removeClass(self.settings.active_slide_class); 340 | $($slides[active_index]).addClass(self.settings.active_slide_class); 341 | // Make next bullet active 342 | var $bullets = $container.siblings('.' + self.settings.bullets_container_class); 343 | if ($bullets.length === 1) { 344 | $bullets.children().removeClass(self.settings.bullets_active_class); 345 | $($bullets.children()[active_index-1]).addClass(self.settings.bullets_active_class); 346 | } 347 | var new_margin_left = '-' + (active_index * 100) + '%'; 348 | // Check to see if animation will occur, otherwise perform 349 | // callbacks manually 350 | $slides_container.trigger('orbit:before-slide-change'); 351 | if ($slides_container.css(margin_position) === new_margin_left) { 352 | $container.removeClass(self.settings.orbit_transition_class); 353 | $slides_container.trigger('orbit:after-slide-change', [{slide_number: active_index, total_slides: $slides_container.children().length - 2}]); 354 | callback(); 355 | } else { 356 | var properties = {}; 357 | properties[margin_position] = new_margin_left; 358 | 359 | $slides_container.animate(properties, self.settings.animation_speed, 'linear', function() { 360 | $container.removeClass(self.settings.orbit_transition_class); 361 | $slides_container.trigger('orbit:after-slide-change', [{slide_number: active_index, total_slides: $slides_container.children().length - 2}]); 362 | callback(); 363 | }); 364 | } 365 | } 366 | }; 367 | }(Foundation.zj, this, this.document)); 368 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.forms.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.forms = { 7 | name : 'forms', 8 | 9 | version : '4.0.4', 10 | 11 | settings : { 12 | disable_class: 'no-custom' 13 | }, 14 | 15 | init : function (scope, method, options) { 16 | this.scope = scope || this.scope; 17 | 18 | if (typeof method === 'object') { 19 | $.extend(true, this.settings, method); 20 | } 21 | 22 | if (typeof method != 'string') { 23 | if (!this.settings.init) { 24 | this.events(); 25 | } 26 | 27 | this.assemble(); 28 | 29 | return this.settings.init; 30 | } else { 31 | return this[method].call(this, options); 32 | } 33 | }, 34 | 35 | assemble : function () { 36 | $('form.custom input[type="radio"]', $(this.scope)).not('[data-customforms="disabled"]') 37 | .each(this.append_custom_markup); 38 | $('form.custom input[type="checkbox"]', $(this.scope)).not('[data-customforms="disabled"]') 39 | .each(this.append_custom_markup); 40 | $('form.custom select', $(this.scope)).not('[data-customforms="disabled"]') 41 | .each(this.append_custom_select); 42 | }, 43 | 44 | events : function () { 45 | var self = this; 46 | 47 | $(this.scope) 48 | .on('click.fndtn.forms', 'form.custom span.custom.checkbox', function (e) { 49 | e.preventDefault(); 50 | e.stopPropagation(); 51 | self.toggle_checkbox($(this)); 52 | }) 53 | .on('click.fndtn.forms', 'form.custom span.custom.radio', function (e) { 54 | e.preventDefault(); 55 | e.stopPropagation(); 56 | self.toggle_radio($(this)); 57 | }) 58 | .on('change.fndtn.forms', 'form.custom select:not([data-customforms="disabled"])', function (e) { 59 | self.refresh_custom_select($(this)); 60 | }) 61 | .on('click.fndtn.forms', 'form.custom label', function (e) { 62 | var $associatedElement = $('#' + self.escape($(this).attr('for')) + ':not([data-customforms="disabled"])'), 63 | $customCheckbox, 64 | $customRadio; 65 | if ($associatedElement.length !== 0) { 66 | if ($associatedElement.attr('type') === 'checkbox') { 67 | e.preventDefault(); 68 | $customCheckbox = $(this).find('span.custom.checkbox'); 69 | //the checkbox might be outside after the label or inside of another element 70 | if ($customCheckbox.length == 0) { 71 | $customCheckbox = $associatedElement.add(this).siblings('span.custom.checkbox').first(); 72 | } 73 | self.toggle_checkbox($customCheckbox); 74 | } else if ($associatedElement.attr('type') === 'radio') { 75 | e.preventDefault(); 76 | $customRadio = $(this).find('span.custom.radio'); 77 | //the radio might be outside after the label or inside of another element 78 | if ($customRadio.length == 0) { 79 | $customRadio = $associatedElement.add(this).siblings('span.custom.radio').first(); 80 | } 81 | self.toggle_radio($customRadio); 82 | } 83 | } 84 | }) 85 | .on('click.fndtn.forms', 'form.custom div.custom.dropdown a.current, form.custom div.custom.dropdown a.selector', function (e) { 86 | var $this = $(this), 87 | $dropdown = $this.closest('div.custom.dropdown'), 88 | $select = $dropdown.prev(); 89 | 90 | // make sure other dropdowns close 91 | if(!$dropdown.hasClass('open')) 92 | $(self.scope).trigger('click'); 93 | 94 | e.preventDefault(); 95 | if (false === $select.is(':disabled')) { 96 | $dropdown.toggleClass('open'); 97 | 98 | if ($dropdown.hasClass('open')) { 99 | $(self.scope).on('click.fndtn.forms.customdropdown', function () { 100 | $dropdown.removeClass('open'); 101 | $(self.scope).off('.fndtn.forms.customdropdown'); 102 | }); 103 | } else { 104 | $(self.scope).on('.fndtn.forms.customdropdown'); 105 | } 106 | return false; 107 | } 108 | }) 109 | .on('click.fndtn.forms touchend.fndtn.forms', 'form.custom div.custom.dropdown li', function (e) { 110 | var $this = $(this), 111 | $customDropdown = $this.closest('div.custom.dropdown'), 112 | $select = $customDropdown.prev(), 113 | selectedIndex = 0; 114 | 115 | e.preventDefault(); 116 | e.stopPropagation(); 117 | 118 | if ( ! $(this).hasClass('disabled')) { 119 | $('div.dropdown').not($customDropdown).removeClass('open'); 120 | 121 | var $oldThis= $this 122 | .closest('ul') 123 | .find('li.selected'); 124 | $oldThis.removeClass('selected'); 125 | 126 | $this.addClass('selected'); 127 | 128 | $customDropdown 129 | .removeClass('open') 130 | .find('a.current') 131 | .html($this.html()); 132 | 133 | $this.closest('ul').find('li').each(function (index) { 134 | if ($this[0] == this) { 135 | selectedIndex = index; 136 | } 137 | 138 | }); 139 | $select[0].selectedIndex = selectedIndex; 140 | 141 | //store the old value in data 142 | $select.data('prevalue', $oldThis.html()); 143 | $select.trigger('change'); 144 | } 145 | }); 146 | 147 | $(window).on('keydown', function (e) { 148 | var focus = document.activeElement, 149 | dropdown = $('.custom.dropdown.open'); 150 | 151 | if (dropdown.length > 0) { 152 | e.preventDefault(); 153 | 154 | if (e.which === 13) { 155 | dropdown.find('li.selected').trigger('click'); 156 | } 157 | 158 | if (e.which === 38) { 159 | var current = dropdown.find('li.selected'), 160 | prev = current.prev(':not(.disabled)'); 161 | 162 | if (prev.length > 0) { 163 | current.removeClass('selected'); 164 | prev.addClass('selected'); 165 | } 166 | } else if (e.which === 40) { 167 | var current = dropdown.find('li.selected'), 168 | next = current.next(':not(.disabled)'); 169 | 170 | if (next.length > 0) { 171 | current.removeClass('selected'); 172 | next.addClass('selected'); 173 | } 174 | } 175 | } 176 | }); 177 | 178 | this.settings.init = true; 179 | }, 180 | 181 | append_custom_markup : function (idx, sel) { 182 | var $this = $(sel).hide(), 183 | type = $this.attr('type'), 184 | $span = $this.next('span.custom.' + type); 185 | 186 | if ($span.length === 0) { 187 | $span = $('').insertAfter($this); 188 | } 189 | 190 | $span.toggleClass('checked', $this.is(':checked')); 191 | $span.toggleClass('disabled', $this.is(':disabled')); 192 | }, 193 | 194 | append_custom_select : function (idx, sel) { 195 | var self = Foundation.libs.forms, 196 | $this = $( sel ), 197 | $customSelect = $this.next( 'div.custom.dropdown' ), 198 | $customList = $customSelect.find( 'ul' ), 199 | $selectCurrent = $customSelect.find( ".current" ), 200 | $selector = $customSelect.find( ".selector" ), 201 | $options = $this.find( 'option' ), 202 | $selectedOption = $options.filter( ':selected' ), 203 | copyClasses = $this.attr('class') ? $this.attr('class').split(' ') : [], 204 | maxWidth = 0, 205 | liHtml = '', 206 | $listItems, 207 | $currentSelect = false; 208 | 209 | if ($this.hasClass(self.settings.disable_class)) return; 210 | 211 | if ($customSelect.length === 0) { 212 | var customSelectSize = $this.hasClass( 'small' ) ? 'small' : 213 | $this.hasClass( 'medium' ) ? 'medium' : 214 | $this.hasClass( 'large' ) ? 'large' : 215 | $this.hasClass( 'expand' ) ? 'expand' : ''; 216 | 217 | $customSelect = $('
        '); 218 | $selector = $customSelect.find(".selector"); 219 | $customList = $customSelect.find("ul"); 220 | liHtml = $options.map(function() { return "
      • " + $( this ).html() + "
      • "; } ).get().join( '' ); 221 | $customList.append(liHtml); 222 | $currentSelect = $customSelect.prepend('' + $selectedOption.html() + '' ).find( ".current" ); 223 | $this 224 | .after( $customSelect ) 225 | .hide(); 226 | 227 | } else { 228 | liHtml = $options.map(function() { 229 | return "
      • " + $( this ).html() + "
      • "; 230 | }) 231 | .get().join(''); 232 | $customList 233 | .html('') 234 | .append(liHtml); 235 | 236 | } // endif $customSelect.length === 0 237 | $customSelect.toggleClass('disabled', $this.is( ':disabled' ) ); 238 | $listItems = $customList.find( 'li' ); 239 | 240 | $options.each( function ( index ) { 241 | if ( this.selected ) { 242 | $listItems.eq( index ).addClass( 'selected' ); 243 | 244 | if ($currentSelect) { 245 | $currentSelect.html( $( this ).html() ); 246 | } 247 | 248 | } 249 | if ($(this).is(':disabled')) { 250 | $listItems.eq( index ).addClass( 'disabled' ); 251 | } 252 | }); 253 | 254 | // 255 | // If we're not specifying a predetermined form size. 256 | // 257 | if (!$customSelect.is('.small, .medium, .large, .expand')) { 258 | 259 | // ------------------------------------------------------------------------------------ 260 | // This is a work-around for when elements are contained within hidden parents. 261 | // For example, when custom-form elements are inside of a hidden reveal modal. 262 | // 263 | // We need to display the current custom list element as well as hidden parent elements 264 | // in order to properly calculate the list item element's width property. 265 | // ------------------------------------------------------------------------------------- 266 | 267 | $customSelect.addClass( 'open' ); 268 | // 269 | // Quickly, display all parent elements. 270 | // This should help us calcualate the width of the list item's within the drop down. 271 | // 272 | var self = Foundation.libs.forms; 273 | self.hidden_fix.adjust( $customList ); 274 | 275 | maxWidth = ( self.outerWidth($listItems) > maxWidth ) ? self.outerWidth($listItems) : maxWidth; 276 | 277 | Foundation.libs.forms.hidden_fix.reset(); 278 | 279 | $customSelect.removeClass( 'open' ); 280 | 281 | } // endif 282 | 283 | }, 284 | 285 | refresh_custom_select : function ($select) { 286 | var self = this; 287 | var maxWidth = 0, 288 | $customSelect = $select.next(), 289 | $options = $select.find('option'); 290 | 291 | $customSelect.find('ul').html(''); 292 | 293 | $options.each(function () { 294 | var $li = $('
      • ' + $(this).html() + '
      • '); 295 | $customSelect.find('ul').append($li); 296 | }); 297 | 298 | // re-populate 299 | $options.each(function (index) { 300 | if (this.selected) { 301 | $customSelect.find('li').eq(index).addClass('selected'); 302 | $customSelect.find('.current').html($(this).html()); 303 | } 304 | if ($(this).is(':disabled')) { 305 | $customSelect.find('li').eq(index).addClass('disabled'); 306 | } 307 | }); 308 | 309 | // fix width 310 | $customSelect.removeAttr('style') 311 | .find('ul').removeAttr('style'); 312 | $customSelect.find('li').each(function () { 313 | $customSelect.addClass('open'); 314 | if (self.outerWidth($(this)) > maxWidth) { 315 | maxWidth = self.outerWidth($(this)); 316 | } 317 | $customSelect.removeClass('open'); 318 | }); 319 | }, 320 | 321 | toggle_checkbox : function ($element) { 322 | var $input = $element.prev(), 323 | input = $input[0]; 324 | 325 | if (false === $input.is(':disabled')) { 326 | input.checked = ((input.checked) ? false : true); 327 | $element.toggleClass('checked'); 328 | 329 | $input.trigger('change'); 330 | } 331 | }, 332 | 333 | toggle_radio : function ($element) { 334 | var $input = $element.prev(), 335 | $form = $input.closest('form.custom'), 336 | input = $input[0]; 337 | 338 | if (false === $input.is(':disabled')) { 339 | $form.find('input[type="radio"][name="' + this.escape($input.attr('name')) + '"]').next().not($element).removeClass('checked'); 340 | if ( !$element.hasClass('checked') ) { 341 | $element.toggleClass('checked'); 342 | } 343 | input.checked = $element.hasClass('checked'); 344 | 345 | $input.trigger('change'); 346 | } 347 | }, 348 | 349 | escape : function (text) { 350 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 351 | }, 352 | 353 | hidden_fix : { 354 | /** 355 | * Sets all hidden parent elements and self to visibile. 356 | * 357 | * @method adjust 358 | * @param {jQuery Object} $child 359 | */ 360 | 361 | // We'll use this to temporarily store style properties. 362 | tmp : [], 363 | 364 | // We'll use this to set hidden parent elements. 365 | hidden : null, 366 | 367 | adjust : function( $child ) { 368 | // Internal reference. 369 | var _self = this; 370 | 371 | // Set all hidden parent elements, including this element. 372 | _self.hidden = $child.parents().andSelf().filter( ":hidden" ); 373 | 374 | // Loop through all hidden elements. 375 | _self.hidden.each( function() { 376 | 377 | // Cache the element. 378 | var $elem = $( this ); 379 | 380 | // Store the style attribute. 381 | // Undefined if element doesn't have a style attribute. 382 | _self.tmp.push( $elem.attr( 'style' ) ); 383 | 384 | // Set the element's display property to block, 385 | // but ensure it's visibility is hidden. 386 | $elem.css( { 'visibility' : 'hidden', 'display' : 'block' } ); 387 | }); 388 | 389 | }, // end adjust 390 | 391 | /** 392 | * Resets the elements previous state. 393 | * 394 | * @method reset 395 | */ 396 | reset : function() { 397 | // Internal reference. 398 | var _self = this; 399 | // Loop through our hidden element collection. 400 | _self.hidden.each( function( i ) { 401 | // Cache this element. 402 | var $elem = $( this ), 403 | _tmp = _self.tmp[ i ]; // Get the stored 'style' value for this element. 404 | 405 | // If the stored value is undefined. 406 | if( _tmp === undefined ) 407 | // Remove the style attribute. 408 | $elem.removeAttr( 'style' ); 409 | else 410 | // Otherwise, reset the element style attribute. 411 | $elem.attr( 'style', _tmp ); 412 | 413 | }); 414 | // Reset the tmp array. 415 | _self.tmp = []; 416 | // Reset the hidden elements variable. 417 | _self.hidden = null; 418 | 419 | } // end reset 420 | 421 | }, 422 | 423 | off : function () { 424 | $(this.scope).off('.fndtn.forms'); 425 | } 426 | }; 427 | }(Foundation.zj, this, this.document)); 428 | -------------------------------------------------------------------------------- /static/js/foundation/foundation.joyride.js: -------------------------------------------------------------------------------- 1 | /*jslint unparam: true, browser: true, indent: 2 */ 2 | 3 | ;(function ($, window, document, undefined) { 4 | 'use strict'; 5 | 6 | Foundation.libs.joyride = { 7 | name: 'joyride', 8 | 9 | version : '4.0.0', 10 | 11 | defaults : { 12 | tipLocation : 'bottom', // 'top' or 'bottom' in relation to parent 13 | nubPosition : 'auto', // override on a per tooltip bases 14 | scrollSpeed : 300, // Page scrolling speed in milliseconds 15 | timer : 0, // 0 = no timer , all other numbers = timer in milliseconds 16 | startTimerOnClick : true, // true or false - true requires clicking the first button start the timer 17 | startOffset : 0, // the index of the tooltip you want to start on (index of the li) 18 | nextButton : true, // true or false to control whether a next button is used 19 | tipAnimation : 'fade', // 'pop' or 'fade' in each tip 20 | pauseAfter : [], // array of indexes where to pause the tour after 21 | tipAnimationFadeSpeed: 300, // when tipAnimation = 'fade' this is speed in milliseconds for the transition 22 | cookieMonster : false, // true or false to control whether cookies are used 23 | cookieName : 'joyride', // Name the cookie you'll use 24 | cookieDomain : false, // Will this cookie be attached to a domain, ie. '.notableapp.com' 25 | cookieExpires : 365, // set when you would like the cookie to expire. 26 | tipContainer : 'body', // Where will the tip be attached 27 | postRideCallback : function (){}, // A method to call once the tour closes (canceled or complete) 28 | postStepCallback : function (){}, // A method to call after each step 29 | template : { // HTML segments for tip layout 30 | link : '×', 31 | timer : '
        ', 32 | tip : '
        ', 33 | wrapper : '
        ', 34 | button : '' 35 | } 36 | }, 37 | 38 | settings : {}, 39 | 40 | init : function (scope, method, options) { 41 | this.scope = scope || this.scope; 42 | Foundation.inherit(this, 'throttle data_options scrollTo scrollLeft delay'); 43 | 44 | if (typeof method === 'object') { 45 | $.extend(true, this.settings, this.defaults, method); 46 | } else { 47 | $.extend(true, this.settings, this.defaults, options); 48 | } 49 | 50 | if (typeof method != 'string') { 51 | if (!this.settings.init) this.events(); 52 | 53 | return this.settings.init; 54 | } else { 55 | return this[method].call(this, options); 56 | } 57 | }, 58 | 59 | events : function () { 60 | var self = this; 61 | 62 | $(this.scope) 63 | .on('click.joyride', '.joyride-next-tip, .joyride-modal-bg', function (e) { 64 | e.preventDefault(); 65 | 66 | if (this.settings.$li.next().length < 1) { 67 | this.end(); 68 | } else if (this.settings.timer > 0) { 69 | clearTimeout(this.settings.automate); 70 | this.hide(); 71 | this.show(); 72 | this.startTimer(); 73 | } else { 74 | this.hide(); 75 | this.show(); 76 | } 77 | 78 | }.bind(this)) 79 | 80 | .on('click.joyride', '.joyride-close-tip', function (e) { 81 | e.preventDefault(); 82 | this.end(); 83 | }.bind(this)); 84 | 85 | $(window).on('resize.fndtn.joyride', self.throttle(function () { 86 | if ($('[data-joyride]').length > 0 && self.settings.$next_tip) { 87 | if (self.is_phone()) { 88 | self.pos_phone(); 89 | } else { 90 | self.pos_default(false, true); 91 | } 92 | } 93 | }, 100)); 94 | 95 | this.settings.init = true; 96 | }, 97 | 98 | start : function () { 99 | var self = this, 100 | $this = $(this.scope).find('[data-joyride]'), 101 | integer_settings = ['timer', 'scrollSpeed', 'startOffset', 'tipAnimationFadeSpeed', 'cookieExpires'], 102 | int_settings_count = integer_settings.length; 103 | 104 | if (!this.settings.init) this.init(); 105 | 106 | // non configureable settings 107 | this.settings.$content_el = $this; 108 | this.settings.body_offset = $(this.settings.tipContainer).position(); 109 | this.settings.$tip_content = this.settings.$content_el.find('> li'); 110 | this.settings.paused = false; 111 | this.settings.attempts = 0; 112 | 113 | this.settings.tipLocationPatterns = { 114 | top: ['bottom'], 115 | bottom: [], // bottom should not need to be repositioned 116 | left: ['right', 'top', 'bottom'], 117 | right: ['left', 'top', 'bottom'] 118 | }; 119 | 120 | // can we create cookies? 121 | if (typeof $.cookie !== 'function') { 122 | this.settings.cookieMonster = false; 123 | } 124 | 125 | // generate the tips and insert into dom. 126 | if (!this.settings.cookieMonster || this.settings.cookieMonster && $.cookie(this.settings.cookieName) === null) { 127 | this.settings.$tip_content.each(function (index) { 128 | var $this = $(this); 129 | $.extend(true, self.settings, self.data_options($this)); 130 | // Make sure that settings parsed from data_options are integers where necessary 131 | for (var i = int_settings_count - 1; i >= 0; i--) { 132 | self.settings[integer_settings[i]] = parseInt(self.settings[integer_settings[i]], 10); 133 | } 134 | self.create({$li : $this, index : index}); 135 | }); 136 | 137 | // show first tip 138 | if (!this.settings.startTimerOnClick && this.settings.timer > 0) { 139 | this.show('init'); 140 | this.startTimer(); 141 | } else { 142 | this.show('init'); 143 | } 144 | 145 | } 146 | }, 147 | 148 | resume : function () { 149 | this.set_li(); 150 | this.show(); 151 | }, 152 | 153 | tip_template : function (opts) { 154 | var $blank, content; 155 | 156 | opts.tip_class = opts.tip_class || ''; 157 | 158 | $blank = $(this.settings.template.tip).addClass(opts.tip_class); 159 | content = $.trim($(opts.li).html()) + 160 | this.button_text(opts.button_text) + 161 | this.settings.template.link + 162 | this.timer_instance(opts.index); 163 | 164 | $blank.append($(this.settings.template.wrapper)); 165 | $blank.first().attr('data-index', opts.index); 166 | $('.joyride-content-wrapper', $blank).append(content); 167 | 168 | return $blank[0]; 169 | }, 170 | 171 | timer_instance : function (index) { 172 | var txt; 173 | 174 | if ((index === 0 && this.settings.startTimerOnClick && this.settings.timer > 0) || this.settings.timer === 0) { 175 | txt = ''; 176 | } else { 177 | txt = this.outerHTML($(this.settings.template.timer)[0]); 178 | } 179 | return txt; 180 | }, 181 | 182 | button_text : function (txt) { 183 | if (this.settings.nextButton) { 184 | txt = $.trim(txt) || 'Next'; 185 | txt = this.outerHTML($(this.settings.template.button).append(txt)[0]); 186 | } else { 187 | txt = ''; 188 | } 189 | return txt; 190 | }, 191 | 192 | create : function (opts) { 193 | var buttonText = opts.$li.attr('data-button') || opts.$li.attr('data-text'), 194 | tipClass = opts.$li.attr('class'), 195 | $tip_content = $(this.tip_template({ 196 | tip_class : tipClass, 197 | index : opts.index, 198 | button_text : buttonText, 199 | li : opts.$li 200 | })); 201 | 202 | $(this.settings.tipContainer).append($tip_content); 203 | }, 204 | 205 | show : function (init) { 206 | var $timer = null; 207 | 208 | // are we paused? 209 | if (this.settings.$li === undefined 210 | || ($.inArray(this.settings.$li.index(), this.settings.pauseAfter) === -1)) { 211 | 212 | // don't go to the next li if the tour was paused 213 | if (this.settings.paused) { 214 | this.settings.paused = false; 215 | } else { 216 | this.set_li(init); 217 | } 218 | 219 | this.settings.attempts = 0; 220 | 221 | if (this.settings.$li.length && this.settings.$target.length > 0) { 222 | 223 | this.settings.tipSettings = $.extend(this.settings, this.data_options(this.settings.$li)); 224 | 225 | this.settings.timer = parseInt(this.settings.timer, 10); 226 | 227 | this.settings.tipSettings.tipLocationPattern = this.settings.tipLocationPatterns[this.settings.tipSettings.tipLocation]; 228 | 229 | // scroll if not modal 230 | if (!/body/i.test(this.settings.$target.selector)) { 231 | this.scroll_to(); 232 | } 233 | 234 | if (this.is_phone()) { 235 | this.pos_phone(true); 236 | } else { 237 | this.pos_default(true); 238 | } 239 | 240 | $timer = this.settings.$next_tip.find('.joyride-timer-indicator'); 241 | 242 | if (/pop/i.test(this.settings.tipAnimation)) { 243 | 244 | $timer.width(0); 245 | 246 | if (thsi.settings.timer > 0) { 247 | 248 | this.settings.$next_tip.show(); 249 | 250 | this.delay(function () { 251 | $timer.animate({ 252 | width: $timer.parent().width() 253 | }, this.settings.timer, 'linear'); 254 | }.bind(this), this.settings.tipAnimationFadeSpeed); 255 | 256 | } else { 257 | this.settings.$next_tip.show(); 258 | 259 | } 260 | 261 | 262 | } else if (/fade/i.test(this.settings.tipAnimation)) { 263 | 264 | $timer.width(0); 265 | 266 | if (this.settings.timer > 0) { 267 | 268 | this.settings.$next_tip 269 | .fadeIn(this.settings.tipAnimationFadeSpeed) 270 | .show(); 271 | 272 | this.delay(function () { 273 | $timer.animate({ 274 | width: $timer.parent().width() 275 | }, this.settings.timer, 'linear'); 276 | }.bind(this), this.settings.tipAnimationFadeSpeed); 277 | 278 | } else { 279 | this.settings.$next_tip.fadeIn(this.settings.tipAnimationFadeSpeed); 280 | 281 | } 282 | } 283 | 284 | this.settings.$current_tip = this.settings.$next_tip; 285 | 286 | // skip non-existant targets 287 | } else if (this.settings.$li && this.settings.$target.length < 1) { 288 | 289 | this.show(); 290 | 291 | } else { 292 | 293 | this.end(); 294 | 295 | } 296 | } else { 297 | 298 | this.settings.paused = true; 299 | 300 | } 301 | 302 | }, 303 | 304 | is_phone : function () { 305 | if (Modernizr) { 306 | return Modernizr.mq('only screen and (max-width: 767px)') || $('.lt-ie9').length > 0; 307 | } 308 | 309 | return (this.settings.$window.width() < 767) ? true : false; 310 | }, 311 | 312 | hide : function () { 313 | this.settings.postStepCallback(this.settings.$li.index(), 314 | this.settings.$current_tip); 315 | $('.joyride-modal-bg').hide(); 316 | this.settings.$current_tip.hide(); 317 | }, 318 | 319 | set_li : function (init) { 320 | if (init) { 321 | this.settings.$li = this.settings.$tip_content.eq(this.settings.startOffset); 322 | this.set_next_tip(); 323 | this.settings.$current_tip = this.settings.$next_tip; 324 | } else { 325 | this.settings.$li = this.settings.$li.next(); 326 | this.set_next_tip(); 327 | } 328 | 329 | this.set_target(); 330 | }, 331 | 332 | set_next_tip : function () { 333 | this.settings.$next_tip = $(".joyride-tip-guide[data-index='" + this.settings.$li.index() + "']"); 334 | this.settings.$next_tip.data('closed', ''); 335 | }, 336 | 337 | set_target : function () { 338 | var cl = this.settings.$li.attr('data-class'), 339 | id = this.settings.$li.attr('data-id'), 340 | $sel = function () { 341 | if (id) { 342 | return $(document.getElementById(id)); 343 | } else if (cl) { 344 | return $('.' + cl).first(); 345 | } else { 346 | return $('body'); 347 | } 348 | }; 349 | 350 | this.settings.$target = $sel(); 351 | }, 352 | 353 | scroll_to : function () { 354 | var window_half, tipOffset; 355 | 356 | window_half = $(window).height() / 2; 357 | tipOffset = Math.ceil(this.settings.$target.offset().top - window_half + this.outerHeight(this.settings.$next_tip)); 358 | if (tipOffset > 0) { 359 | this.scrollTo($('html, body'), tipOffset, this.settings.scrollSpeed); 360 | } 361 | }, 362 | 363 | paused : function () { 364 | if (($.inArray((this.settings.$li.index() + 1), this.settings.pauseAfter) === -1)) { 365 | return true; 366 | } 367 | 368 | return false; 369 | }, 370 | 371 | restart : function () { 372 | this.hide(); 373 | this.settings.$li = undefined; 374 | this.show('init'); 375 | }, 376 | 377 | pos_default : function (init, resizing) { 378 | var half_fold = Math.ceil($(window).height() / 2), 379 | tip_position = this.settings.$next_tip.offset(), 380 | $nub = this.settings.$next_tip.find('.joyride-nub'), 381 | nub_height = Math.ceil(this.outerHeight($nub) / 2), 382 | toggle = init || false; 383 | 384 | // tip must not be "display: none" to calculate position 385 | if (toggle) { 386 | this.settings.$next_tip.css('visibility', 'hidden'); 387 | this.settings.$next_tip.show(); 388 | } 389 | 390 | if (typeof resizing === 'undefined') resizing = false; 391 | if (!/body/i.test(this.settings.$target.selector)) { 392 | 393 | if (this.bottom()) { 394 | var leftOffset = this.settings.$target.offset().left; 395 | if (Foundation.rtl) { 396 | leftOffset = this.settings.$target.offset().width - this.settings.$next_tip.width() + leftOffset; 397 | } 398 | this.settings.$next_tip.css({ 399 | top: (this.settings.$target.offset().top + nub_height + this.outerHeight(this.settings.$target)), 400 | left: leftOffset}); 401 | 402 | this.nub_position($nub, this.settings.tipSettings.nubPosition, 'top'); 403 | 404 | } else if (this.top()) { 405 | var leftOffset = this.settings.$target.offset().left; 406 | if (Foundation.rtl) { 407 | leftOffset = this.settings.$target.offset().width - this.settings.$next_tip.width() + leftOffset; 408 | } 409 | this.settings.$next_tip.css({ 410 | top: (this.settings.$target.offset().top - this.outerHeight(this.settings.$next_tip) - nub_height), 411 | left: leftOffset}); 412 | 413 | this.nub_position($nub, this.settings.tipSettings.nubPosition, 'bottom'); 414 | 415 | } else if (this.right()) { 416 | 417 | this.settings.$next_tip.css({ 418 | top: this.settings.$target.offset().top, 419 | left: (this.outerWidth(this.settings.$target) + this.settings.$target.offset().left)}); 420 | 421 | this.nub_position($nub, this.settings.tipSettings.nubPosition, 'left'); 422 | 423 | } else if (this.left()) { 424 | 425 | this.settings.$next_tip.css({ 426 | top: this.settings.$target.offset().top, 427 | left: (this.settings.$target.offset().left - this.outerWidth(this.settings.$next_tip) - nub_height)}); 428 | 429 | this.nub_position($nub, this.settings.tipSettings.nubPosition, 'right'); 430 | 431 | } 432 | 433 | if (!this.visible(this.corners(this.settings.$next_tip)) && this.settings.attempts < this.settings.tipSettings.tipLocationPattern.length) { 434 | 435 | $nub.removeClass('bottom') 436 | .removeClass('top') 437 | .removeClass('right') 438 | .removeClass('left'); 439 | 440 | this.settings.tipSettings.tipLocation = this.settings.tipSettings.tipLocationPattern[this.settings.attempts]; 441 | 442 | this.settings.attempts++; 443 | 444 | this.pos_default(); 445 | 446 | } 447 | 448 | } else if (this.settings.$li.length) { 449 | 450 | this.pos_modal($nub); 451 | 452 | } 453 | 454 | if (toggle) { 455 | this.settings.$next_tip.hide(); 456 | this.settings.$next_tip.css('visibility', 'visible'); 457 | } 458 | 459 | }, 460 | 461 | pos_phone : function (init) { 462 | var tip_height = this.outerHeight(this.settings.$next_tip), 463 | tip_offset = this.settings.$next_tip.offset(), 464 | target_height = this.outerHeight(this.settings.$target), 465 | $nub = $('.joyride-nub', this.settings.$next_tip), 466 | nub_height = Math.ceil(this.outerHeight($nub) / 2), 467 | toggle = init || false; 468 | 469 | $nub.removeClass('bottom') 470 | .removeClass('top') 471 | .removeClass('right') 472 | .removeClass('left'); 473 | 474 | if (toggle) { 475 | this.settings.$next_tip.css('visibility', 'hidden'); 476 | this.settings.$next_tip.show(); 477 | } 478 | 479 | if (!/body/i.test(this.settings.$target.selector)) { 480 | 481 | if (this.top()) { 482 | 483 | this.settings.$next_tip.offset({top: this.settings.$target.offset().top - tip_height - nub_height}); 484 | $nub.addClass('bottom'); 485 | 486 | } else { 487 | 488 | this.settings.$next_tip.offset({top: this.settings.$target.offset().top + target_height + nub_height}); 489 | $nub.addClass('top'); 490 | 491 | } 492 | 493 | } else if (this.settings.$li.length) { 494 | this.pos_modal($nub); 495 | } 496 | 497 | if (toggle) { 498 | this.settings.$next_tip.hide(); 499 | this.settings.$next_tip.css('visibility', 'visible'); 500 | } 501 | }, 502 | 503 | pos_modal : function ($nub) { 504 | this.center(); 505 | $nub.hide(); 506 | if (!this.settings.$next_tip.data('closed')) { 507 | if ($('.joyride-modal-bg').length < 1) { 508 | $('body').append('
        ').show(); 509 | } 510 | 511 | if (/pop/i.test(this.settings.tipAnimation)) { 512 | $('.joyride-modal-bg').show(); 513 | } else { 514 | $('.joyride-modal-bg').fadeIn(this.settings.tipAnimationFadeSpeed); 515 | } 516 | } 517 | }, 518 | 519 | center : function () { 520 | var $w = $(window); 521 | 522 | this.settings.$next_tip.css({ 523 | top : ((($w.height() - this.outerHeight(this.settings.$next_tip)) / 2) + $w.scrollTop()), 524 | left : ((($w.width() - this.outerWidth(this.settings.$next_tip)) / 2) + this.scrollLeft($w)) 525 | }); 526 | 527 | return true; 528 | }, 529 | 530 | bottom : function () { 531 | return /bottom/i.test(this.settings.tipSettings.tipLocation); 532 | }, 533 | 534 | top : function () { 535 | return /top/i.test(this.settings.tipSettings.tipLocation); 536 | }, 537 | 538 | right : function () { 539 | return /right/i.test(this.settings.tipSettings.tipLocation); 540 | }, 541 | 542 | left : function () { 543 | return /left/i.test(this.settings.tipSettings.tipLocation); 544 | }, 545 | 546 | corners : function (el) { 547 | var w = $(window), 548 | right = w.width() + this.scrollLeft(w), 549 | bottom = w.width() + w.scrollTop(); 550 | 551 | return [ 552 | el.offset().top <= w.scrollTop(), 553 | right <= el.offset().left + this.outerWidth(el), 554 | bottom <= el.offset().top + this.outerHeight(el), 555 | this.scrollLeft(w) >= el.offset().left 556 | ]; 557 | }, 558 | 559 | visible : function (hidden_corners) { 560 | var i = hidden_corners.length; 561 | 562 | while (i--) { 563 | if (hidden_corners[i]) return false; 564 | } 565 | 566 | return true; 567 | }, 568 | 569 | nub_position : function (nub, pos, def) { 570 | if (pos === 'auto') { 571 | nub.addClass(def); 572 | } else { 573 | nub.addClass(pos); 574 | } 575 | }, 576 | 577 | startTimer : function () { 578 | if (this.settings.$li.length) { 579 | this.settings.automate = setTimeout(function () { 580 | this.hide(); 581 | this.show(); 582 | this.startTimer(); 583 | }.bind(this), this.settings.timer); 584 | } else { 585 | clearTimeout(this.settings.automate); 586 | } 587 | }, 588 | 589 | end : function () { 590 | if (this.settings.cookieMonster) { 591 | $.cookie(this.settings.cookieName, 'ridden', { expires: this.settings.cookieExpires, domain: this.settings.cookieDomain }); 592 | } 593 | 594 | if (this.settings.timer > 0) { 595 | clearTimeout(this.settings.automate); 596 | } 597 | 598 | this.settings.$next_tip.data('closed', true); 599 | 600 | $('.joyride-modal-bg').hide(); 601 | this.settings.$current_tip.hide(); 602 | this.settings.postStepCallback(this.settings.$li.index(), this.settings.$current_tip); 603 | this.settings.postRideCallback(this.settings.$li.index(), this.settings.$current_tip); 604 | }, 605 | 606 | outerHTML : function (el) { 607 | // support FireFox < 11 608 | return el.outerHTML || new XMLSerializer().serializeToString(el); 609 | }, 610 | 611 | off : function () { 612 | $(this.scope).off('.joyride'); 613 | $(window).off('.joyride'); 614 | $('.joyride-close-tip, .joyride-next-tip, .joyride-modal-bg').off('.joyride'); 615 | $('.joyride-tip-guide, .joyride-modal-bg').remove(); 616 | clearTimeout(this.settings.automate); 617 | this.settings = {}; 618 | } 619 | }; 620 | }(Foundation.zj, this, this.document)); 621 | --------------------------------------------------------------------------------