From f9615934bbb47d88c37bff3ffa92fd211f3e30ca Mon Sep 17 00:00:00 2001 From: Jan-Pascal van Best Date: Mon, 1 Feb 2016 21:39:41 +0100 Subject: [PATCH] Work in progress --- .gitignore | 6 + dependencies | 4 + manage.py | 10 + tweet/__init__.py | 1 + tweet/admin.py | 8 + tweet/apps.py | 7 + tweet/config.py | 36 + tweet/jobrunner.py | 112 + tweet/migrations/0001_initial.py | 33 + tweet/migrations/0002_auto_20160129_1351.py | 25 + tweet/migrations/0003_auto_20160129_1353.py | 30 + tweet/migrations/0004_auto_20160201_0937.py | 57 + tweet/migrations/__init__.py | 0 tweet/models.py | 106 + .../static/tweet/javascript/bootstrap.min.js | 6 + .../tweet/javascript/jquery-1.9.0.min.js | 4 + .../javascript/jquery-ui-1.10.3.custom.min.js | 7 + tweet/static/tweet/stylesheets/bootstrap.css | 2470 +++++++++++++++++ .../tweet/stylesheets/bootstrap.min.css | 9 + tweet/static/tweet/stylesheets/main.css | 90 + .../ui-lightness/images/animated-overlay.gif | Bin 0 -> 1738 bytes .../ui-bg_diagonals-thick_18_b81900_40x40.png | Bin 0 -> 418 bytes .../ui-bg_diagonals-thick_20_666666_40x40.png | Bin 0 -> 312 bytes .../images/ui-bg_flat_10_000000_40x100.png | Bin 0 -> 205 bytes .../images/ui-bg_glass_100_f6f6f6_1x400.png | Bin 0 -> 262 bytes .../images/ui-bg_glass_100_fdf5ce_1x400.png | Bin 0 -> 348 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 207 bytes .../ui-bg_gloss-wave_35_f6a828_500x100.png | Bin 0 -> 5815 bytes .../ui-bg_highlight-soft_100_eeeeee_1x100.png | Bin 0 -> 278 bytes .../ui-bg_highlight-soft_75_ffe45c_1x100.png | Bin 0 -> 328 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 6922 bytes .../images/ui-icons_228ef1_256x240.png | Bin 0 -> 4549 bytes .../images/ui-icons_ef8c08_256x240.png | Bin 0 -> 4549 bytes .../images/ui-icons_ffd27a_256x240.png | Bin 0 -> 4549 bytes .../images/ui-icons_ffffff_256x240.png | Bin 0 -> 6299 bytes .../ui-lightness/jquery-ui-1.10.3.custom.css | 1177 ++++++++ .../jquery-ui-1.10.3.custom.min.css | 5 + tweet/templates/tweet/base.html | 44 + tweet/templates/tweet/index.html | 19 + tweet/templates/tweet/list_jobs.html | 30 + tweet/templates/tweet/selectconfig.html | 15 + tweet/tests.py | 5 + tweet/urls.py | 13 + tweet/views.py | 45 + tweet_django/__init__.py | 0 tweet_django/local.ini.example | 15 + tweet_django/settings.py | 138 + tweet_django/urls.py | 22 + tweet_django/wsgi.py | 16 + 49 files changed, 4565 insertions(+) create mode 100644 .gitignore create mode 100644 dependencies create mode 100755 manage.py create mode 100644 tweet/__init__.py create mode 100644 tweet/admin.py create mode 100644 tweet/apps.py create mode 100644 tweet/config.py create mode 100644 tweet/jobrunner.py create mode 100644 tweet/migrations/0001_initial.py create mode 100644 tweet/migrations/0002_auto_20160129_1351.py create mode 100644 tweet/migrations/0003_auto_20160129_1353.py create mode 100644 tweet/migrations/0004_auto_20160201_0937.py create mode 100644 tweet/migrations/__init__.py create mode 100644 tweet/models.py create mode 100644 tweet/static/tweet/javascript/bootstrap.min.js create mode 100644 tweet/static/tweet/javascript/jquery-1.9.0.min.js create mode 100644 tweet/static/tweet/javascript/jquery-ui-1.10.3.custom.min.js create mode 100644 tweet/static/tweet/stylesheets/bootstrap.css create mode 100644 tweet/static/tweet/stylesheets/bootstrap.min.css create mode 100644 tweet/static/tweet/stylesheets/main.css create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/animated-overlay.gif create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_flat_10_000000_40x100.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-icons_222222_256x240.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-icons_228ef1_256x240.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-icons_ef8c08_256x240.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-icons_ffd27a_256x240.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/images/ui-icons_ffffff_256x240.png create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/jquery-ui-1.10.3.custom.css create mode 100644 tweet/static/tweet/stylesheets/ui-lightness/jquery-ui-1.10.3.custom.min.css create mode 100644 tweet/templates/tweet/base.html create mode 100644 tweet/templates/tweet/index.html create mode 100644 tweet/templates/tweet/list_jobs.html create mode 100644 tweet/templates/tweet/selectconfig.html create mode 100644 tweet/tests.py create mode 100644 tweet/urls.py create mode 100644 tweet/views.py create mode 100644 tweet_django/__init__.py create mode 100644 tweet_django/local.ini.example create mode 100644 tweet_django/settings.py create mode 100644 tweet_django/urls.py create mode 100644 tweet_django/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b6bd05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/db.sqlite3 +/manage.py +/out.txt +/results +__pycache__ +*.pyc diff --git a/dependencies b/dependencies new file mode 100644 index 0000000..806a860 --- /dev/null +++ b/dependencies @@ -0,0 +1,4 @@ +python3-django +python3-mysqldb +python3-requests-oauthlib +python3-twython diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..ee56cff --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tweet_django.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/tweet/__init__.py b/tweet/__init__.py new file mode 100644 index 0000000..5785fa0 --- /dev/null +++ b/tweet/__init__.py @@ -0,0 +1 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tweet/admin.py b/tweet/admin.py new file mode 100644 index 0000000..6ce31d0 --- /dev/null +++ b/tweet/admin.py @@ -0,0 +1,8 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +from django.contrib import admin + +from .models import Tweet, Job + +admin.site.register(Tweet) +admin.site.register(Job) diff --git a/tweet/apps.py b/tweet/apps.py new file mode 100644 index 0000000..0f2ed1d --- /dev/null +++ b/tweet/apps.py @@ -0,0 +1,7 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +from django.apps import AppConfig + + +class TweetConfig(AppConfig): + name = 'tweet' diff --git a/tweet/config.py b/tweet/config.py new file mode 100644 index 0000000..4587d99 --- /dev/null +++ b/tweet/config.py @@ -0,0 +1,36 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +import configparser + +class Config: + page_size = 100 + max_pages = 500 + + terms = {} + excels = {} + + def __init__(self, f): + self.load(f) + + def load(self, f): + config = configparser.ConfigParser() + config.read_file(f) + + self.page_size = config.getint('general', 'pagesize', fallback=100) + self.max_pages = config.getint('general', 'maxpages', fallback=500) + + self.terms = dict(config['search']) + + for key in config['excel']: + terms = config['excel'][key].split(",") + self.excels[key] = terms + + print("terms: " + str(self.terms)) + print("excels: " + str(self.excels)) + + def query_names(self): + return self.terms.keys() + + def all_search_terms(self): + return self.terms.values() + diff --git a/tweet/jobrunner.py b/tweet/jobrunner.py new file mode 100644 index 0000000..67f581b --- /dev/null +++ b/tweet/jobrunner.py @@ -0,0 +1,112 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +import time +from concurrent.futures import ThreadPoolExecutor + +from django.conf import settings + +from twython import Twython + +_running = [] +_pool = ThreadPoolExecutor(max_workers=15) + +def _handle_tweet(query_name, status): + print("==========") + print(status) + +def _get_rate_status(twitter): + rate_status = None + try: + rate_status = twitter.get_application_rate_limit_status(resources="search") + rate_status = rate_status["resources"]["search"]["/search/tweets"] + print(rate_status) + except TwythonError as e: + print("get_rate_limit exception, error code = {}: {}".format(e.error_code, e)) + if e.error_code != 130: + raise e + print("Over capacity message, sleeping for five seconds...") + for i in range(4, 0, -1): + time.sleep(1) + print("Sleep time remaining: {} seconds".format(i)) + + print("A") + return rate_status + + +def _fetch(query_name, terms, max_pages, page_size): + print("_fetch...") + # TODO use global twitter object? + twitter = Twython(settings.TWEET_OAUTH_CONSUMER_KEY, + settings.TWEET_OAUTH_CONSUMER_SECRET, + settings.TWEET_OAUTH_ACCESS_TOKEN, + settings.TWEET_OAUTH_ACCESS_TOKEN_SECRET) + + rate_status = _get_rate_status(twitter) + print("B") + remaining = rate_status["remaining"] + print("C") + + max_id = -1 + previous_last_id = float('inf') + + print("max_pages: {}".format(max_pages)) + for k in range(max_pages): + print("k: {}".format(k)) + while remaining<10: + seconds = rate_status["reset"] - time.time() + print("Running into Twitter rate limiting, need to wait {} seconds...".format(seconds)) + for i in range(max(60, seconds), 0, -1): + print("still waiting: {}".format(i)) + time.sleep(1) + rate_status = _get_rate_status(twitter) + remaining = rate_status.remaining + remaining -= 1 + query = {'q':terms, 'result_type':'recent', 'count': page_size} + if max_id>0: + query["max_id"]= max_id + + try: + results = twitter.search(**query) + except TwythonError as e: + print("search exception, error code = {}: {}".format(e.error_code, e)) + # FIXME + break + + remaining = int(twitter.get_lastfunction_header('x-rate-limit-remaining')) + print("remaining: {}".format(remaining)) + + max_id = results["search_metadata"]["max_id"] + print("Number of results: {}".format(results["search_metadata"]["count"])) + last_id = float('inf') + for status in results["statuses"]: + _handle_tweet(query_name, status) + if status["id"]=previous_last_id: + print("max_id not descending, quitting") + break + previous_last_id = max_id + if k >= max_pages-1: + print("Warning, more than {} pages of results, ignoring the rest!!!".format(max_pages)); + + #results = twitter.search(q='nlalert', result_type='recent') # since_id=, max_id=,count=, + #for result in results: + # print(results[result]) + +# Runs in a thread pool thread +def _execute_job(job): + try: + _fetch("testing", "amsterdam", 4, 10) + except Exception as e: + print("Exception in _execute_job:") + print(str(e)) + +def run_job(job): + _running.append(job.id) + _pool.submit(_execute_job, job) diff --git a/tweet/migrations/0001_initial.py b/tweet/migrations/0001_initial.py new file mode 100644 index 0000000..7afc2b8 --- /dev/null +++ b/tweet/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-01-29 12:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Tweet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='date')), + ('from_user', models.CharField(max_length=20, verbose_name='sender screen name')), + ('from_user_id', models.BigIntegerField(verbose_name='sender user id')), + ('in_reply_to', models.CharField(max_length=20, verbose_name='in reply to screen name')), + ('text', models.TextField(max_length=140, verbose_name='tweet text')), + ('latitude', models.FloatField(verbose_name='latitude')), + ('longitude', models.FloatField(verbose_name='longitude')), + ('conforms_to_terms', models.BooleanField(verbose_name='tweet confirms to filters')), + ], + options={ + 'db_table': 'tweet', + }, + ), + ] diff --git a/tweet/migrations/0002_auto_20160129_1351.py b/tweet/migrations/0002_auto_20160129_1351.py new file mode 100644 index 0000000..a2fb613 --- /dev/null +++ b/tweet/migrations/0002_auto_20160129_1351.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-01-29 12:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('tweet', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='tweet', + name='date', + ), + migrations.AddField( + model_name='tweet', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date and time'), + ), + ] diff --git a/tweet/migrations/0003_auto_20160129_1353.py b/tweet/migrations/0003_auto_20160129_1353.py new file mode 100644 index 0000000..1f560a4 --- /dev/null +++ b/tweet/migrations/0003_auto_20160129_1353.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-01-29 12:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tweet', '0002_auto_20160129_1351'), + ] + + operations = [ + migrations.AlterField( + model_name='tweet', + name='in_reply_to', + field=models.CharField(blank=True, max_length=20, verbose_name='in reply to screen name'), + ), + migrations.AlterField( + model_name='tweet', + name='latitude', + field=models.FloatField(null=True, verbose_name='latitude'), + ), + migrations.AlterField( + model_name='tweet', + name='longitude', + field=models.FloatField(null=True, verbose_name='longitude'), + ), + ] diff --git a/tweet/migrations/0004_auto_20160201_0937.py b/tweet/migrations/0004_auto_20160201_0937.py new file mode 100644 index 0000000..55a620e --- /dev/null +++ b/tweet/migrations/0004_auto_20160201_0937.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-02-01 08:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('tweet', '0003_auto_20160129_1353'), + ] + + operations = [ + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=80, verbose_name='name')), + ('start_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='start time')), + ('num_tweets', models.BigIntegerField(default=0, verbose_name='number of tweets found')), + ('status', models.IntegerField(default=0, verbose_name='job status')), + ('seconds_to_wait', models.IntegerField(default=0, verbose_name='number of seconds to wait')), + ], + ), + migrations.CreateModel( + name='JobDescription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=80, verbose_name='job name')), + ], + ), + migrations.CreateModel( + name='JobOutput', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('filename', models.CharField(max_length=1024, verbose_name='file name')), + ('job_description', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tweet.JobDescription')), + ], + ), + migrations.CreateModel( + name='JobQuery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=80, verbose_name='name')), + ('terms', models.TextField(verbose_name='search terms')), + ('job_description', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tweet.JobDescription')), + ], + ), + migrations.AddField( + model_name='joboutput', + name='queries', + field=models.ManyToManyField(to='tweet.JobQuery'), + ), + ] diff --git a/tweet/migrations/__init__.py b/tweet/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tweet/models.py b/tweet/models.py new file mode 100644 index 0000000..db8bc80 --- /dev/null +++ b/tweet/models.py @@ -0,0 +1,106 @@ +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +from django.conf import settings +from django.db import models +from django.utils import timezone +import os.path +from enum import Enum + +from . import config + +class Tweet(models.Model): + created_at = models.DateTimeField('date and time', default=timezone.now) + from_user = models.CharField('sender screen name', max_length=20) + from_user_id = models.BigIntegerField('sender user id') + in_reply_to = models.CharField('in reply to screen name', max_length=20, blank=True) + text = models.TextField('tweet text', max_length=140) + latitude = models.FloatField('latitude', null=True) + longitude = models.FloatField('longitude', null=True) + conforms_to_terms = models.BooleanField('tweet confirms to filters') + + def __str__(self): + return self.text + + class Meta: + db_table = "tweet" + +class JobDescription(models.Model): + name = models.CharField("job name", max_length=80) + + def __str__(self): + return self.name + + +class JobQuery(models.Model): + job_description = models.ForeignKey(JobDescription, on_delete=models.CASCADE) + name = models.CharField("name", max_length=80) + terms = models.TextField("search terms") + + def __str__(self): + return self.name + +class JobOutput(models.Model): + job_description = models.ForeignKey(JobDescription, on_delete=models.CASCADE) + filename = models.CharField("file name", max_length=1024) + queries = models.ManyToManyField(JobQuery) + + def __str__(self): + return self.filename + +class Job(models.Model): + STATUS_INIT = 0 + STATUS_RUNNING = 1 + STATUS_WAITING = 2 + STATUS_FAILED = 3 + STATUS_DONE = 4 + + name = models.CharField("name", max_length=80) + start_time = models.DateTimeField("start time", default=timezone.now) + num_tweets = models.BigIntegerField("number of tweets found", default=0) + status = models.IntegerField("job status", default=STATUS_INIT) + seconds_to_wait = models.IntegerField("number of seconds to wait", default=0) + + def __init__(self, *args, **kwargs): + models.Model.__init__(self, *args, **kwargs) + self.inifile = None + self.xls = None + self.zipfile = None + self.logfile = None + self.logwriter = None + self.loaded = False + self.config = None + + def __str__(self): + return self.name + + def job_path(self): + base_path = settings.TWEET_BASEPATH + path = os.path.join(settings.TWEET_BASEPATH, str(self.id)) + os.makedirs(path) + return path + + def add_config(self, f): + self.inifile = os.path.join(self.job_path(), "config.ini") + with open(self.inifile, 'wb+') as destination: + for chunk in f.chunks(): + destination.write(chunk) + with open(self.inifile, 'r') as ini: + self.config = config.Config(ini) + + def get_config(self): + if self.config is not None: + return self.config + inifile = os.path.join(self.job_path(), "config.ini") + self.config = Config(inifile) + return self.config + + def get_queries(self): + result = set() + try: + config = self.get_config() + result = config.all_search_terms() + except Exception as e: + print("Exception getting config:") + print(e) + return result + diff --git a/tweet/static/tweet/javascript/bootstrap.min.js b/tweet/static/tweet/javascript/bootstrap.min.js new file mode 100644 index 0000000..f9cbdae --- /dev/null +++ b/tweet/static/tweet/javascript/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! +* Bootstrap.js by @fat & @mdo +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(".dropdown-backdrop").remove(),e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||("ontouchstart"in document.documentElement&&e('