├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── __main__.py ├── app ├── __init__.py ├── controller.py ├── decorators.py ├── forms.py ├── models.py ├── processes.py ├── static │ ├── bootstrap-theme.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.js │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ ├── npm.js │ └── style.css ├── tasks.py ├── templates │ ├── _macros.html │ ├── admin_home.html │ ├── admin_login.html │ ├── base.html │ ├── collector.html │ ├── create.html │ ├── home.html │ ├── index.html │ ├── login.html │ ├── network_home.html │ ├── new_collector.html │ ├── setup.html │ └── update_collector.html ├── twitter │ ├── ThreadedCollector.py │ ├── __init__.py │ ├── logging.conf │ ├── mongoBatchInsert.py │ ├── platform.ini │ ├── preprocess.py │ ├── tweetprocessing.py │ └── tweetstream.py └── views.py ├── config.py ├── install ├── license.txt ├── requirements.txt ├── run.py ├── run.wsgi ├── scripts └── data_pull │ ├── ConfigCond.py │ ├── ConfigNoCond.py │ ├── Tweet-QueryScript-Cond.py │ ├── Tweet-QueryScript-NoCond.py │ └── tweetIDs.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # STACKS Files & Directories 2 | *_raw_tweets 3 | raw_tweets_* 4 | raw_tweets 5 | *_insert_queue 6 | insert_queue_* 7 | insert_queue 8 | *_tweet_archive 9 | tweet_archive_* 10 | tweet_archive 11 | error_tweets 12 | error_tweets.txt 13 | error_inserted_tweets 14 | error_inserted_tweets.txt 15 | controller_old.py 16 | test.terms 17 | test.ini 18 | py_modules_list.txt 19 | old_collection.terms 20 | test.py 21 | out 22 | data 23 | logs 24 | stack 25 | 26 | 27 | # Python Files & Dirs 28 | .idea 29 | *.pyc 30 | env 31 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Installing STACK 2 | ========= 3 | 4 | Welcome to STACK! This doc will take you through the installation of STACK and will guide you through the basic setup of a data collection process. 5 | 6 | **_This documentation assumes the following:_** 7 | 8 | * You know how to use ssh. 9 | * Your server has [MongoDB already installed](http://docs.mongodb.org/manual/installation/). 10 | * You understand how to edit files using vim (“vi”). 11 | * You have rights and know how to install Python libraries. 12 | 13 | In addition, this doc is geared towards working on a Linux system (for testing we use Ubuntu). We've tried to link to external documentation where installation diverges if you are using other systems. 14 | 15 | Finally, the instructions below detail how to install STACK. To learn more about STACK semantics, or how to interact with the app in more detail, [refer to our wiki](https://github.com/bitslabsyr/stack/wiki). 16 | 17 | ## Step 1) Download STACK 18 | 19 | First, clone this repo to your local machine: 20 | 21 | sudo git clone https://github.com/bitslabsyr/stack.git 22 | 23 | Next, make sure to install the required Python libraries outlined in the _requirements.txt_ file. We use pip to install and manage dependencies: 24 | 25 | sudo pip install -r requirements.txt 26 | 27 | **Note** - We use Python 2.7.6 for STACK. 28 | 29 | ## Step 2) Configuration & Setup 30 | 31 | STACK is built to work with MongoDB. The app stores most configuration information in Mongo, however we also use a configuration (.ini) file to manage some parts of the collection process from the Streaming API. Before getting started with STACK, you'll need to do the following: 32 | 33 | * Setup a project account 34 | * Edit the master configuration file 35 | * Create & start a collector 36 | 37 | These steps are detailed below. 38 | 39 | **Project Account Setup** 40 | 41 | TODO - wiki link 42 | 43 | STACK uses "project accounts" to maintain ownership over collection processes. A project account can own multiple collection processes that run concurrently. _To learn more about project accounts and STACK configuration, [see the wiki](#)_. 44 | 45 | After cloning the STACK repo to your local machine, move into the main directory and activate the _setup.py_ script: 46 | 47 | cd stack 48 | python setup.py 49 | 50 | The setup script initializes the Mongo database with important configuration information, as well as creates your user account. The script will prompt you for the following information: 51 | 52 | * _Project Name_: A unique account name for your project. STACK calls all login accounts "projects" and allows for multiple projects at once. 53 | * _Password_: A password for your project account. 54 | * _Description_: A short description for your project account. 55 | 56 | If the script returns a successful execution notice, you will be able to start creating and running collection processes for that account. You can rerun the setup.py script to create new accounts. 57 | 58 | **Creating a Collector** 59 | 60 | Each project account can instantiate multiple **collectors** that will scrape data. A collector is defined as a singular instance that collects data for a specific set of user-provided terms. A project can have multiple collectors running for a given network. 61 | 62 | To create a collector, first run the following command from the main STACK diretcory: 63 | 64 | python __main__.py db set_collector_detail 65 | 66 | You will then be prompted to provide the following configuration information for the collector: 67 | 68 | * _Project Account Name_ (required): The name of your project account. 69 | * _Collector Name_ (required): Non-unique name to identify your collector instance. 70 | * _Language(s)_ (optional): A list of [BCP-47](http://tools.ietf.org/html/bcp47) language codes. If this used, the collector will only grab tweets in this language. [Learn more here](https://dev.twitter.com/streaming/overview/request-parameters#language) about Twitter language parameters. 71 | * _Location(s)_ (optional): A list of location coordinates. If used, we will collect all geocoded tweets within the location bounding box. Bounding boxes must consist of four lat/long pairs. [Learn more here](https://dev.twitter.com/streaming/overview/request-parameters#locations) about location formatting for the Twitter API. 72 | * _Terms_ (optiona): A line item list of terms for the collector to stream. 73 | * _API_ (required): Three options: track, follow, or none. Each collector can stream from one part of Twitter's Streaming API: 74 | * **Track**: Collects all mentions (hashtags included) for a given list of terms. 75 | * **Follow**: Collects all tweets, retweets, and replies for a given use handle. Each term must be a valid Twitter screen name. 76 | * **None**: Only choose this option if you have not inputted a terms list and are collecting for a given set of language(s) and/or location(s). If you do not track a terms list, make sure you are tracking at least one language or location. 77 | * _OAuth Information_: Four keys used to authenticate with the Twitter API. To get consumer & access tokens, first register your app on [https://dev.twitter.com/apps/new](https://dev.twitter.com/apps/new). Navigate to Keys and Access Tokens and click "Create my access token." **NOTE** - Each collector needs to have a unique set of access keys, or else the Streaming API will limit your connection. The four keys include: 78 | * Consumer Key 79 | * Consumer Secret 80 | * Access Token 81 | * Access Token Secret 82 | 83 | _A note on location tracking_: Location tracking with Twitter is an OR filter. We will collect all tweets that match other filters (such as a terms list or a language identifier) OR tweets in the given location. Please plan accordingly. 84 | 85 | **Config File** 86 | 87 | As of v1.0, most configuration information has been moved away from .ini files and into Mongo. However, we still use the config file to maintain rollover rates for data collection. First, open the config file: 88 | 89 | sudo vi ./stack/twitter/platform.ini 90 | 91 | Edit the following key line items: 92 | 93 | * _tweets_file_date_frmt_: The rollover rate for the collection file (minutes, hours, or days). By default it is set to hours, our suggest rate for production use. 94 | 95 | ## Step 3) Starting STACK 96 | 97 | There a three processes to start to have STACK running in full: collector, processor, and inserter. As noted above, multiple instances of each process can run at the same time. In turn, an instance of each process need not run for STACK to operate. 98 | 99 | * _Collectors_: A specific collector used to scrape data for a given set of filters. Multiple can be created/run for each project account. 100 | * _Processors_: This processes raw tweet files written by a collector. Only one processor can be run for a given project account. 101 | * _Inserters_: A process that takes processed tweets and inserts them into MongoDB. Only one inserter can be run for a given project account. 102 | 103 | TODO - wiki 104 | 105 | To learn more about STACK processes and architecture, [please consult our wiki](#). 106 | 107 | **Starting a Collector** 108 | 109 | To start a collector, you'll need to pass both a project_id and collector_id to STACK via the console. First, get your project accounts ID: 110 | 111 | $ python __main__.py db auth [project_name] [password] 112 | {"status": 1, "message": "Success", "project_id": "your_id_value"} 113 | 114 | Then, using the project_id returned above, find a list of your collectors and their ID values: 115 | 116 | $ python __main__.py db get_collector_ids [project_id] 117 | {"status": 1, "collectors": [{"collector_name": [your_collector_name], "collector_id": [your_collector_id]}]} 118 | 119 | Finally, using the project_id and collector_id values returned above, start the given collector for the project account of your choice: 120 | 121 | sudo python __main__.py controller collect start [project_id] [collector_id] 122 | 123 | Your collector is now running! 124 | 125 | **Starting a Processor** 126 | 127 | To start a processor, the syntax is very similar to the collector start command above. Here though, you only need to pass a project account ID: 128 | 129 | sudo python __main__.py controller process start [project_id] twitter 130 | 131 | Your processor is now running! 132 | 133 | **Starting an Inserter** 134 | 135 | To start an inserter, follow the syntax for starting a processor, but instead calling the "insert" command instead: 136 | 137 | sudo python __main__.py controller insert start [project_id] twitter 138 | 139 | Your inserter is now running! 140 | 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 SoMe Lab @ UW 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | STACKS - Social Media Tracker, Analyzer, & Collector Toolkit at Syracuse 2 | ========= 3 | 4 | STACKS is an extensible social media research toolkit designed to collect, process, and store data from online social networks. The toolkit is an ongoing project via the [Syracuse University iSchool](http://ischool.syr.edu), and currently supports the [Twitter Streaming API](https://dev.twitter.com/streaming/overview). Collecting from the Twitter search API is under development. The toolkit architecture is modular and supports extending. 5 | 6 | You can cite this repository: 7 | 8 | Jeff Hemsley, Sam Jackson, Sikana Tanupabrungsun, & Billy Ceskavich. (2019). bitslabsyr/stack: STACKS 3.1 (Version 3.1). http://doi.org/10.5281/zenodo.2638848 9 | 10 | **_This documentation assumes the following:_** 11 | 12 | * You know how to use ssh. 13 | * Your server has MongoDB already installed. 14 | * You understand how to edit files using vim (“vi”) or nano. 15 | * You have rights and know how to install Python libraries. 16 | 17 | ## Installation 18 | 19 | Please read through [Install](https://github.com/bitslabsyr/stack/wiki/Installation) to go through the STACK installation process. 20 | 21 | Prior to installing STACK, make sure you have MongoDB installed and running on your server. [Learn how to install MongoDB here](http://docs.mongodb.org/manual/installation/). 22 | 23 | ## Wiki 24 | 25 | To learn more about STACK semantics, logging, and processing parameters, [refer to our wiki](https://github.com/bitslabsyr/stack/wiki). 26 | 27 | ## Ongoing Work + Next Action Items 28 | 29 | This list will be updated soon with more detailed action items. Please note again that we are actively working on this toolkit! 30 | 31 | 1. Full move away from .ini file use 32 | 2. Extensible module format for future social network implementations 33 | 3. Exentesible back-end API 34 | 35 | ## Credits 36 | 37 | Lovingly maintained at Syracuse University by: 38 | 39 | * [Jeff Hemsley](https://github.com/jhemsley) 40 | * [Sam Jackson](https://github.com/sjacks26) 41 | * [Sikana Tanupabrungsun](https://github.com/Sikana) 42 | * [Billy Ceskavich](https://github.com/bceskavich/) 43 | 44 | Distributed under the MIT License: 45 | 46 | The MIT License (MIT) 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 53 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | 5 | from werkzeug import generate_password_hash 6 | 7 | from app.controller import Controller 8 | from app.models import DB 9 | 10 | basedir = os.getcwd() 11 | 12 | if __name__ == "__main__": 13 | 14 | USAGE = 'USAGE: python __main__.py db|controller {db_method}|{controller_method} {params}' 15 | 16 | db_methods = [ 17 | 'create_project', 18 | 'auth', 19 | 'get_project_list', 20 | 'get_project_detail', 21 | 'get_collector_detail', 22 | 'get_network_detail', 23 | 'set_collector_detail', 24 | 'set_network_status', 25 | 'set_collector_status', 26 | 'get_collector_ids', 27 | 'update_collector_detail' 28 | ] 29 | 30 | controller_processes = ['collect', 'process', 'insert'] 31 | controller_commands = ['start', 'stop', 'restart'] 32 | 33 | try: 34 | wrapper = sys.argv[1] 35 | except: 36 | print USAGE 37 | sys.exit() 38 | try: 39 | method = sys.argv[2] 40 | except: 41 | print USAGE 42 | sys.exit() 43 | 44 | if wrapper not in ['db', 'controller']: 45 | print USAGE 46 | sys.exit() 47 | 48 | if wrapper == 'db' and method in db_methods: 49 | db = DB() 50 | 51 | if method == 'create_project': 52 | """ 53 | python __main__.py db create_project 54 | """ 55 | 56 | print 57 | print 'Welcome to STACKS! Please fill out the following information \ 58 | to get started:' 59 | print 60 | print 'Project Name - one word, NO hyphens (-), underscores (_), or \ 61 | spaces' 62 | print 63 | print 'Email - one or more email(s) used for status reports and \ 64 | issue notices.' 65 | print 66 | print 'Password - used for validation down the road' 67 | print 68 | print 'Description - a quick description about your project' 69 | 70 | project_name = raw_input('Project Name: ') 71 | password = raw_input('Password: ') 72 | hashed_password = generate_password_hash(password) 73 | 74 | cont = True 75 | email = [] 76 | while cont: 77 | inut_email = raw_input('Email: ') 78 | email.append(inut_email) 79 | 80 | add_more = raw_input('Add Another Email [y/n]: ') 81 | if add_more is not 'y': 82 | cont = False 83 | 84 | description = raw_input('Description: ') 85 | 86 | resp = db.create(project_name, password, hashed_password, description=description, email=email) 87 | print json.dumps(resp, indent=1) 88 | 89 | elif method == 'auth': 90 | """ 91 | python __main__.py db auth project_name password 92 | """ 93 | project_name = sys.argv[3] 94 | password = sys.argv[4] 95 | resp = db.auth(project_name, password) 96 | print json.dumps(resp, indent=1) 97 | 98 | elif method == 'get_project_list': 99 | """ 100 | python __main__.py db get_project_list 101 | """ 102 | resp = db.get_project_list() 103 | print json.dumps(resp, indent=1) 104 | 105 | elif method == 'get_collector_ids': 106 | """ 107 | python __main__.py db get_collector_ids project_id 108 | """ 109 | project_id = sys.argv[3] 110 | resp = db.get_collector_ids(project_id) 111 | print json.dumps(resp, indent=1) 112 | elif method == 'get_project_detail': 113 | """ 114 | python __main__.py db get_project_detail project_id 115 | """ 116 | project_id = sys.argv[3] 117 | resp = db.get_project_detail(project_id) 118 | print json.dumps(resp, indent=1) 119 | elif method == 'get_collector_detail': 120 | """ 121 | python __main__.py db get_collector_detail project_id collector_id 122 | """ 123 | project_id = sys.argv[3] 124 | collector_id = sys.argv[4] 125 | resp = db.get_collector_detail(project_id, collector_id) 126 | print json.dumps(resp, indent=1) 127 | elif method == 'get_network_detail': 128 | """ 129 | python __main__.py db get_network_detail project_id network 130 | """ 131 | project_id = sys.argv[3] 132 | network = sys.argv[4] 133 | resp = db.get_network_detail(project_id, network) 134 | print json.dumps(resp, indent=1) 135 | elif method == 'set_collector_detail': 136 | """ 137 | python __main__.py db set_collector_detail 138 | 139 | INPUT FORMATTING 140 | 141 | terms_list = '["your", "array", "of", "terms"]' | none 142 | languages = '["array", "of", "BPR-47 language codes"]' | none 143 | location = '["array", "of", "location", "points"]' | none 144 | 145 | Can be used to both create and update a collector's details 146 | """ 147 | 148 | print '' 149 | print 'To create a collector, please fill in the fields when asked.' 150 | print '' 151 | print 'For the fields "languages", "locations", and "terms" please fill in either a command separated list, or "none":' 152 | print '------' 153 | print 'languages = list, of, codes | none' 154 | print 'Ex. = pr, en' 155 | print '' 156 | print 'locations = list, of, location, points | none' 157 | print 'Ex. = -74, 40, -73, 41' 158 | print '' 159 | print 'terms = list, of, terms | none' 160 | print 'Ex. = social, media' 161 | print '' 162 | print 'If you creating a Facebook collector, please specify the "collection_type", "start_date" and "end_date" fields:' 163 | print '------' 164 | print 'collection_type = realtime | historical' 165 | print '' 166 | print 'start_date = 2015-04-01 | none' 167 | print 'end_date = 2014-04-01 | none' 168 | print '' 169 | 170 | project_name = raw_input('Project Name: ') 171 | password = raw_input('Password: ') 172 | 173 | resp = db.auth(project_name, password) 174 | if resp['status']: 175 | project_id = resp['project_id'] 176 | else: 177 | print 'Invalid Project! Please try again.' 178 | sys.exit(0) 179 | 180 | collector_name = raw_input('Collector Name: ') 181 | network = raw_input('Network: ').lower() 182 | 183 | terms_list = raw_input('Terms: ') 184 | if terms_list == 'none': 185 | terms_list = None 186 | else: 187 | terms_list = terms_list.split(',') 188 | 189 | languages = None 190 | locations = None 191 | api = None 192 | start_date = None 193 | end_date = None 194 | 195 | if network == 'twitter': 196 | languages = raw_input('Languages: ') 197 | if languages == 'none': 198 | languages = None 199 | else: 200 | languages = languages.replace(' ', '') 201 | languages = languages.split(',') 202 | 203 | locations = raw_input('Locations: ') 204 | if locations == 'none': 205 | locations = None 206 | else: 207 | locations = locations.replace(' ', '') 208 | locations = locations.split(',') 209 | 210 | if len(locations) % 4 is not 0: 211 | print 'The number of location coordinates need to be in pairs of four. Please consult the Twitter docs and try again.' 212 | sys.exit(0) 213 | 214 | collection_type = None 215 | 216 | api = raw_input('API: ') 217 | 218 | consumer_key = raw_input('Consumer Key: ') 219 | consumer_secret = raw_input('Consumer Secret: ') 220 | access_token = raw_input('Access Token: ') 221 | access_token_secret = raw_input('Access Token Secret: ') 222 | 223 | api_credentials_dict = { 224 | 'consumer_key' : consumer_key, 225 | 'consumer_secret' : consumer_secret, 226 | 'access_token' : access_token, 227 | 'access_token_secret' : access_token_secret 228 | } 229 | 230 | elif network == 'facebook': 231 | collection_type = raw_input('Collection Type: ') 232 | start_date = raw_input('Start Date: ') 233 | end_date = raw_input('End Date: ') 234 | 235 | # TODO - start and end date reqs for historical 236 | if start_date == 'none': 237 | start_date = None 238 | if end_date == 'none': 239 | end_date = None 240 | 241 | client_id = raw_input('Client ID: ') 242 | client_secret = raw_input('Client Secret: ') 243 | 244 | api_credentials_dict = {'client_id': client_id, 'client_secret': client_secret} 245 | 246 | resp = db.set_collector_detail(project_id, collector_name, network, collection_type, api_credentials_dict, 247 | terms_list, api=api, languages=languages, location=locations, 248 | start_date=start_date, end_date=end_date) 249 | 250 | print json.dumps(resp, indent=1) 251 | 252 | elif method == 'update_collector_detail': 253 | """ 254 | Calls db.update_collector_detail 255 | Can only update a single collector param at a time 256 | 257 | FOR TERMS - must provide term and collection status (1 or 0) 258 | FOR API AUTH CREDS - must provide full list, even if updating one 259 | """ 260 | update_params_list = [ 261 | 'collector_name', 262 | 'api', 263 | 'auth', 264 | 'terms', 265 | 'languages', 266 | 'locations', 267 | 'collection_type', 268 | 'start_date', 269 | 'end_date' 270 | ] 271 | 272 | update_param = sys.argv[3] 273 | if update_param not in update_params_list: 274 | print 'Invalid update paramter. Please try again.' 275 | print 'Valid update params: collector_name, api, auth, terms, \ 276 | languages, locations, collection_type, start_date, \ 277 | end_date.' 278 | sys.exit(1) 279 | 280 | print 'Collector update function called.' 281 | print '' 282 | print 'FOR TERMS - must provide term value and collection status.' 283 | print ' 1 = collect | 0 = do not collect' 284 | print '' 285 | print 'FOR OAUTH CREDS - must provide full list' 286 | print '' 287 | print 'FOR languages and locations - must provide full new list of codes. Update will overwrite.' 288 | print '' 289 | print 'languages = list, of, codes | none' 290 | print 'Ex. = pr, en' 291 | print '' 292 | print 'locations = list, of, location, points | none' 293 | print 'Ex. = -74, 40, -73, 41' 294 | print '' 295 | print 'FOR start & end dates for Facebook, please use the following format:' 296 | print 'YYYY-MM-DD | none' 297 | print '' 298 | print 'FOR collection_type for Facebook: historical | realtime' 299 | print '' 300 | print 'Updating for param: %s' % update_param 301 | print '' 302 | 303 | project_name = raw_input('Project Name: ') 304 | password = raw_input('Password: ') 305 | 306 | resp = db.auth(project_name, password) 307 | if resp['status']: 308 | project_id = resp['project_id'] 309 | else: 310 | print 'Invalid Project! Please try again.' 311 | sys.exit(0) 312 | 313 | collector_id = raw_input('Collector ID: ') 314 | resp = db.get_collector_detail(project_id, collector_id) 315 | resp = resp['collector'] 316 | 317 | params = {} 318 | 319 | # First, do network-wide updates 320 | if update_param == 'collector_name': 321 | params['collector_name'] = raw_input('New Collector Name: ') 322 | elif update_param == 'terms': 323 | # Sets term type value based on collector API 324 | if resp['network'] == 'facebook': 325 | term_type = 'page' 326 | elif resp['api'] == 'follow': 327 | term_type = 'handle' 328 | else: 329 | term_type = 'term' 330 | 331 | # Adds term dict to the params dict based on info provided, will be parsed by update method 332 | cont = True 333 | params['terms_list'] = [] 334 | while cont: 335 | new_term = raw_input('Term: ') 336 | collect_status = int(raw_input('Collect: ')) 337 | 338 | if collect_status not in [1, 0]: 339 | print 'Invalid collect status. Must be 1 or 0.' 340 | sys.exit(0) 341 | 342 | params['terms_list'].append({ 343 | 'term': new_term, 344 | 'collect': collect_status, 345 | 'type': term_type, 346 | 'id': None 347 | }) 348 | 349 | cont_ask = raw_input('Continue? [y/n]: ') 350 | cont_ask = cont_ask.lower() 351 | if cont_ask == 'y': 352 | cont = True 353 | else: 354 | cont = False 355 | 356 | # Next, network specific updates 357 | if resp['network'] == 'twitter': 358 | if update_param == 'api': 359 | params['api'] = raw_input('New API Filter: ') 360 | 361 | elif update_param == 'languages': 362 | languages = raw_input('New Language Codes List: ') 363 | 364 | if languages == 'none': 365 | languages = None 366 | else: 367 | languages = languages.replace(' ', '') 368 | languages = languages.split(',') 369 | 370 | params['languages'] = languages 371 | 372 | elif update_param == 'locations': 373 | locations = raw_input('New Location Codes List: ') 374 | 375 | if locations == 'none': 376 | locations = None 377 | else: 378 | locations = locations.replace(' ', '') 379 | locations = locations.split(',') 380 | 381 | params['location'] = locations 382 | 383 | elif update_param == 'auth': 384 | consumer_key = raw_input('Consumer Key: ') 385 | consumer_secret = raw_input('Consumer Secret: ') 386 | access_token = raw_input('Access Token: ') 387 | access_token_secret = raw_input('Access Token Secret: ') 388 | 389 | api_credentials_dict = { 390 | 'consumer_key' : consumer_key, 391 | 'consumer_secret' : consumer_secret, 392 | 'access_token' : access_token, 393 | 'access_token_secret' : access_token_secret 394 | } 395 | params['api_auth'] = api_credentials_dict 396 | 397 | # Now, Facebook params 398 | elif resp['network'] == 'facebook': 399 | if update_param == 'collection_type': 400 | params['collection_type'] = raw_input('Collection Type: ') 401 | elif update_param == 'start_date': 402 | start_date = raw_input('Start Date: ') 403 | if start_date == 'none': 404 | params['start_date'] = None 405 | else: 406 | params['start_date'] = start_date 407 | 408 | elif update_param == 'end_date': 409 | end_date = raw_input('End Date: ') 410 | if end_date == 'none': 411 | params['end_date'] = None 412 | else: 413 | params['end_date'] = end_date 414 | 415 | elif update_param == 'auth': 416 | client_id = raw_input('Client ID: ') 417 | client_secret = raw_input('Client Secret: ') 418 | 419 | api_credentials_dict = { 420 | 'client_id' : client_id, 421 | 'client_secret': client_secret 422 | } 423 | params['api_auth'] = api_credentials_dict 424 | 425 | resp = db.update_collector_detail(project_id, collector_id, **params) 426 | print json.dumps(resp, indent=1) 427 | 428 | elif wrapper == 'controller' and method in controller_processes: 429 | """ 430 | python __main__.py controller collect|process|insert start|stop|restart project_id {collector_id|network} 431 | 432 | WHERE 433 | 434 | collector_id - optional, only needed for a collection controller 435 | network - optional, needed for processor or inserter controllers 436 | """ 437 | project_id = sys.argv[4] 438 | 439 | if method == 'collect': 440 | collector_id = sys.argv[5] 441 | c = Controller(cmdline=True, project_id=project_id, process=method, collector_id=collector_id) 442 | else: 443 | network = sys.argv[5] 444 | c = Controller(cmdline=True, project_id=project_id, process=method, network=network) 445 | 446 | command = sys.argv[3] 447 | if command in controller_commands: 448 | c.process_command(command) 449 | else: 450 | print 'USAGE: python __main__.py controller collect|process|insert start|stop|restart project_id {collector_id|network}' 451 | 452 | else: 453 | print 'Please try again!' 454 | sys.exit() 455 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from celery import Celery 4 | 5 | # Init and config app 6 | app = Flask(__name__) 7 | app.config.from_object('config') 8 | 9 | # Init and config celery 10 | celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) 11 | celery.conf.update(app.config) 12 | 13 | from app import views 14 | -------------------------------------------------------------------------------- /app/controller.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import os 4 | import signal 5 | import atexit 6 | 7 | from bson.objectid import ObjectId 8 | 9 | from models import DB 10 | from app import app 11 | 12 | # TODO - dynamic import 13 | from twitter import ThreadedCollector, preprocess, mongoBatchInsert 14 | 15 | # wd is the directory used to generate filenames for the Controller / Worker 16 | wd = app.config['BASEDIR'] + '/app' 17 | 18 | 19 | class Controller(object): 20 | """ 21 | Controller - A class for controlling STACK processes. 22 | Calls the Process() class to start and stop STACK processes. 23 | """ 24 | 25 | def __init__(self, process, cmdline=False, home_dir='.', umask=022, verbose=1, **kwargs): 26 | self.db = DB() 27 | self.process = process 28 | self.cmdline = cmdline 29 | self.usage_message = 'controller collect|process|insert start|stop|restart project_id collector_id' 30 | 31 | self.home_dir = home_dir 32 | self.umask = umask 33 | self.verbose = verbose 34 | 35 | if self.cmdline is False: 36 | # Grab information from Flask user object 37 | self.project = kwargs['project'] 38 | self.project_id = self.project['project_id'] 39 | self.project_name = self.project['project_name'] 40 | else: 41 | # Command is coming from the command line, look up info 42 | self.project_id = kwargs['project_id'] 43 | 44 | resp = self.db.get_project_detail(self.project_id) 45 | if resp['status']: 46 | self.project_name = resp['project_name'] 47 | else: 48 | print 'Project w/ ID %s not found!' % self.project_id 49 | print '' 50 | print 'USAGE: python %s %s' % (sys.argv[0], self.usage_message) 51 | sys.exit(1) 52 | 53 | # Project account DB connection 54 | project_info = self.db.get_project_detail(self.project_id) 55 | configdb = project_info['project_config_db'] 56 | project_config_db = self.db.connection[configdb] 57 | self.projectdb = project_config_db.config 58 | 59 | # Loads info for process based on type: collector, processor, inserter 60 | if self.process in ['process', 'insert']: 61 | # Only module type needed for processor / inserter 62 | self.module = kwargs['network'] 63 | self.collector_id = None 64 | # Set name for worker based on gathered info 65 | self.process_name = self.project_name + '-' + self.process + '-' + self.module + '-' + self.project_id 66 | elif process == 'collect': 67 | # For collectors, also grabs: collector_id, api, collector_name 68 | self.collector_id = kwargs['collector_id'] 69 | 70 | resp = self.db.get_collector_detail(self.project_id, self.collector_id) 71 | if resp['status']: 72 | collector = resp['collector'] 73 | self.module = collector['network'] 74 | self.api = collector['api'] 75 | self.collector_name = collector['collector_name'] 76 | else: 77 | print 'Collector (ID: %s) not found!' % self.collector_id 78 | print '' 79 | print 'USAGE: python %s %s' % (sys.argv[0], self.usage_message) 80 | sys.exit(1) 81 | 82 | # Set name for worker based on gathered info 83 | self.process_name = self.project_name + '-' + self.collector_name + '-' + self.process + '-' + self.module + \ 84 | '-' + self.collector_id 85 | 86 | # Sets out directories 87 | self.piddir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/pid' 88 | self.logdir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/logs' 89 | self.stddir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/std' 90 | 91 | # Sets data dirs 92 | # TODO - deprecate w/ Facebook 93 | self.rawdir = app.config[ 94 | 'DATADIR'] + '/' + self.project_name + '-' + self.project_id + '/' + self.module + '/raw' 95 | self.archdir = app.config['DATADIR'] + '/' + self.project_name + '-' + self.project_id + '/' + self.module + \ 96 | '/archive' 97 | self.insertdir = app.config['DATADIR'] + '/' + self.project_name + '-' + self.project_id + '/' + self.module + \ 98 | '/insert_queue' 99 | 100 | # Creates dirs if they don't already exist 101 | if not os.path.exists(self.piddir): os.makedirs(self.piddir) 102 | if not os.path.exists(self.stddir): os.makedirs(self.stddir) 103 | 104 | # These directories only need be created for Twitter 105 | # TODO - deprecate w/ Facebook 106 | if self.module == 'twitter': 107 | if not os.path.exists(self.logdir): os.makedirs(self.logdir) 108 | if not os.path.exists(self.rawdir): os.makedirs(self.rawdir) 109 | if not os.path.exists(self.archdir): os.makedirs(self.archdir) 110 | if not os.path.exists(self.insertdir): os.makedirs(self.insertdir) 111 | 112 | # Sets outfiles 113 | self.pidfile = self.piddir + '/%s.pid' % self.process_name 114 | self.stdout = self.stddir + '/%s-stdout.txt' % self.process_name 115 | self.stderr = self.stddir + '/%s-stderr.txt' % self.process_name 116 | self.stdin = self.stddir + '/%s-stdin.txt' % self.process_name 117 | 118 | # Creates the std files for the daemon 119 | if not os.path.isfile(self.stdout): 120 | create_file = open(self.stdout, 'w') 121 | create_file.close() 122 | if not os.path.isfile(self.stdin): 123 | create_file = open(self.stdin, 'w') 124 | create_file.close() 125 | if not os.path.isfile(self.stderr): 126 | create_file = open(self.stderr, 'w') 127 | create_file.close() 128 | 129 | def process_command(self, cmd): 130 | """ 131 | Prases the passed command (start / stop / restart) and initiates daemonization 132 | """ 133 | # Makes sure the command is relevant 134 | if self.cmdline and cmd not in ['start', 'stop', 'restart']: 135 | print 'Invalid command: %s' % cmd 136 | print '' 137 | print 'USAGE: python %s %s' % (sys.argv[0], self.usage_message) 138 | sys.exit(1) 139 | elif cmd == 'start': 140 | self.start() 141 | elif cmd == 'stop': 142 | self.stop() 143 | elif cmd == 'restart': 144 | self.restart() 145 | else: 146 | print 'USAGE: python %s %s' % (sys.argv[0], self.usage_message) 147 | if self.cmdline: 148 | sys.exit(1) 149 | 150 | def start(self): 151 | """ 152 | Method that starts the daemon process 153 | """ 154 | print 'Initializing the STACK daemon: %s' % self.process_name 155 | 156 | # Sets flags for given process 157 | resp = '' 158 | if self.process == 'collect': 159 | resp = self.db.set_collector_status(self.project_id, self.collector_id, collector_status=1) 160 | elif self.process == 'process': 161 | resp = self.db.set_network_status(self.project_id, self.module, run=1, process=True) 162 | elif self.process == 'insert': 163 | resp = self.db.set_network_status(self.project_id, self.module, run=1, insert=True) 164 | 165 | if 'status' in resp and resp['status']: 166 | print 'Flags set.' 167 | 168 | # Check to see if running based on pidfile 169 | pid = self.get_pid() 170 | if pid: 171 | message = "pidfile %s already exists. Is it already running?\n" 172 | sys.stderr.write(message % self.pidfile) 173 | sys.exit(1) 174 | 175 | # Start the daemon 176 | self.daemonize() 177 | self.run() 178 | else: 179 | print 'Failed to successfully set flags, try again.' 180 | 181 | def stop(self): 182 | """ 183 | Method that sets flags and stops the daemon process 184 | """ 185 | print 'Stop command received.' 186 | print 'Step 1) Setting flags on the STACK process to stop.' 187 | 188 | if self.process == 'collect': 189 | # Set flags for the STACK process to stop 190 | resp = self.db.set_collector_status(self.project_id, self.collector_id, collector_status=0) 191 | 192 | # Grab active flag from collector's Mongo document 193 | collector_conf = self.projectdb.find_one({'_id': ObjectId(self.collector_id)}) 194 | active = collector_conf['active'] 195 | else: 196 | module_conf = self.projectdb.find_one({'module': self.module}) 197 | if self.process == 'process': 198 | resp = self.db.set_network_status(self.project_id, self.module, run=0, process=True) 199 | active = module_conf['processor_active'] 200 | else: 201 | resp = self.db.set_network_status(self.project_id, self.module, run=0, insert=True) 202 | active = module_conf['inserter_active'] 203 | 204 | # TODO - mongo error handling 205 | if resp['status']: 206 | print 'Step 1 complete.' 207 | 208 | # If the daemon has already stopped, then set flags and break 209 | pid = self.get_pid() 210 | if not pid: 211 | print "STACK daemon already terminated." 212 | 213 | # Extra clean up, just in case 214 | if os.path.exists(self.pidfile): 215 | os.remove(self.pidfile) 216 | 217 | if self.process in ['process', 'insert']: 218 | if self.process == 'process': 219 | self.projectdb.update({'module': self.module}, {'$set': {'processor_active': 0}}) 220 | else: 221 | self.projectdb.update({'module': self.module}, {'$set': {'inserter_active': 0}}) 222 | else: 223 | self.projectdb.update({'_id': ObjectId(self.collector_id)}, {'$set': {'active': 0}}) 224 | 225 | return 226 | 227 | # Step 2) Check for task / STACK process completion; loops through 15 times to check 228 | 229 | print 'Step 2) Check for STACK process completion and shutdown the daemon.' 230 | 231 | wait_count = 0 232 | while active == 1: 233 | wait_count += 1 234 | 235 | if self.process in ['process', 'insert']: 236 | module_conf = self.projectdb.find_one({'module': self.module}) 237 | if self.process == 'process': 238 | active = module_conf['processor_active'] 239 | else: 240 | active = module_conf['inserter_active'] 241 | else: 242 | collector_conf = self.projectdb.find_one({'_id': ObjectId(self.collector_id)}) 243 | active = collector_conf['active'] 244 | 245 | print 'Try %d / 15' % wait_count 246 | print 'Active Status: %d' % active 247 | print 'Trying again in 5 seconds.' 248 | print '' 249 | 250 | if wait_count > 15: 251 | break 252 | 253 | time.sleep(5) 254 | 255 | # Get the pid from the pidfile 256 | pid = self.get_pid() 257 | if not pid: 258 | print "Daemon successfully stopped via thread termination." 259 | 260 | # Just to be sure. A ValueError might occur if the PID file is 261 | # empty but does actually exist 262 | if os.path.exists(self.pidfile): 263 | os.remove(self.pidfile) 264 | 265 | return # Not an error in a restart 266 | 267 | # Try killing the daemon process 268 | print 'Daemon still running w/ loose thread. Stopping now...' 269 | 270 | try: 271 | i = 0 272 | while 1: 273 | os.kill(pid, signal.SIGTERM) 274 | time.sleep(0.1) 275 | i = i + 1 276 | if i % 10 == 0: 277 | os.kill(pid, signal.SIGHUP) 278 | except OSError, err: 279 | err = str(err) 280 | if err.find("No such process") > 0: 281 | if os.path.exists(self.pidfile): 282 | os.remove(self.pidfile) 283 | else: 284 | print str(err) 285 | sys.exit(1) 286 | 287 | # Had to kill the daemon, so set the active status flag accordingly. 288 | if self.process in ['process', 'insert']: 289 | if self.process == 'process': 290 | self.projectdb.update({'module': self.module}, {'$set': {'processor_active': 0}}) 291 | else: 292 | self.projectdb.update({'module': self.module}, {'$set': {'inserter_active': 0}}) 293 | else: 294 | self.projectdb.update({'_id': ObjectId(self.collector_id)}, {'$set': {'active': 0}}) 295 | 296 | print 'Stopped.' 297 | 298 | def restart(self): 299 | """ 300 | Simple restart of the daemon 301 | """ 302 | # TODO - restart w/out shutting down daemon as part of extensible processor modules 303 | self.stop() 304 | self.start() 305 | 306 | def run(self): 307 | """ 308 | Calls the process logic scripts and runs 309 | """ 310 | # Backwards compatibility for older Twitter scripts 311 | if self.module == 'twitter': 312 | if self.process == 'collect': 313 | ThreadedCollector.go(self.api, self.project_id, self.collector_id, self.rawdir, self.logdir) 314 | elif self.process == 'process': 315 | preprocess.go(self.project_id, self.rawdir, self.archdir, self.insertdir, self.logdir) 316 | elif self.process == 'insert': 317 | mongoBatchInsert.go(self.project_id, self.rawdir, self.insertdir, self.logdir) 318 | # New approach via extensible collectors 319 | else: 320 | # Dynamically import collect from 321 | os.chdir(app.config['BASEDIR']) 322 | 323 | if self.process == 'collect': 324 | _temp = __import__('app.%s.collect' % self.module, globals(), locals(), ['Collector'], -1) 325 | Collector = _temp.Collector 326 | 327 | c = Collector(self.project_id, self.collector_id, self.process_name) 328 | c.go() 329 | elif self.process == 'process': 330 | _temp = __import__('app.%s.process' % self.module, globals(), locals(), ['Processor'], -1) 331 | Processor = _temp.Processor 332 | 333 | c = Processor(self.project_id, self.process_name, self.module) 334 | c.go() 335 | elif self.process == 'insert': 336 | _temp = __import__('app.%s.insert' % self.module, globals(), locals(), ['Inserter'], -1) 337 | Inserter = _temp.Inserter 338 | 339 | c = Inserter(self.project_id, self.process_name, self.module) 340 | c.go() 341 | 342 | def daemonize(self): 343 | """ 344 | Do the UNIX double-fork magic, see Stevens' "Advanced 345 | Programming in the UNIX Environment" for details (ISBN 0201563177) 346 | http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 347 | """ 348 | try: 349 | pid = os.fork() 350 | if pid > 0: 351 | # Exit first parent 352 | sys.exit(0) 353 | except OSError, e: 354 | sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) 355 | sys.exit(1) 356 | 357 | # Decouple from parent environment 358 | os.chdir(self.home_dir) 359 | os.setsid() 360 | os.umask(self.umask) 361 | 362 | # Do second fork 363 | try: 364 | pid = os.fork() 365 | if pid > 0: 366 | # Exit from second parent 367 | sys.exit(0) 368 | except OSError, e: 369 | sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) 370 | sys.exit(1) 371 | 372 | sys.stdout.flush() 373 | sys.stderr.flush() 374 | si = file(self.stdin, 'r+') 375 | so = file(self.stdout, 'a+') 376 | if self.stderr: 377 | se = file(self.stderr, 'a+', 0) 378 | else: 379 | se = so 380 | 381 | if self.cmdline: 382 | os.dup2(si.fileno(), sys.stdin.fileno()) 383 | os.dup2(so.fileno(), sys.stdout.fileno()) 384 | os.dup2(se.fileno(), sys.stderr.fileno()) 385 | 386 | sys.stderr.flush() 387 | sys.stdout.flush() 388 | 389 | def sigtermhandler(signum, frame): 390 | self.daemon_alive = False 391 | signal.signal(signal.SIGTERM, sigtermhandler) 392 | signal.signal(signal.SIGINT, sigtermhandler) 393 | 394 | if self.verbose >= 1: 395 | print "Started" 396 | 397 | # Write pidfile 398 | atexit.register( 399 | self.delpid) # Make sure pid file is removed if we quit 400 | pid = str(os.getpid()) 401 | file(self.pidfile, 'w+').write("%s\n" % pid) 402 | 403 | def delpid(self): 404 | os.remove(self.pidfile) 405 | 406 | def get_pid(self): 407 | try: 408 | pf = file(self.pidfile, 'r') 409 | pid = int(pf.read().strip()) 410 | pf.close() 411 | except IOError: 412 | pid = None 413 | except SystemExit: 414 | pid = None 415 | return pid -------------------------------------------------------------------------------- /app/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import g, flash, redirect, url_for, request, session 3 | from models import DB 4 | 5 | 6 | # Used to divert users from account-only STACK pages 7 | # Admins are able to access protected pages 8 | def login_required(f): 9 | @wraps(f) 10 | def decorated_function(*args, **kwargs): 11 | if g.project is None: 12 | if g.admin is None: 13 | flash(u'You need to login to view this page!') 14 | return redirect(url_for('index', next=request.path)) 15 | return f(*args, **kwargs) 16 | 17 | return decorated_function 18 | 19 | 20 | def admin_required(f): 21 | @wraps(f) 22 | def decorated_function(*args, **kwargs): 23 | if g.admin is None: 24 | flash(u'You need to be an admin to view this page!') 25 | return redirect(url_for('index', next=request.path)) 26 | return f(*args, **kwargs) 27 | 28 | return decorated_function 29 | 30 | 31 | # Used to load project info into the session if not there 32 | def load_project(f): 33 | @wraps(f) 34 | def decorated_function(*args, **kwargs): 35 | g.project = None 36 | if 'project_id' in session: 37 | db = DB() 38 | resp = db.get_project_detail(session['project_id']) 39 | if resp['status']: 40 | g.project = resp 41 | return f(*args, **kwargs) 42 | 43 | return decorated_function 44 | 45 | 46 | # Used to load admin info into the session 47 | def load_admin(f): 48 | @wraps(f) 49 | def decorated_function(*args, **kwargs): 50 | g.admin = None 51 | if 'admin_project_id' in session: 52 | db = DB() 53 | resp = db.get_project_detail(session['admin_project_id']) 54 | if resp['status']: 55 | g.admin = resp 56 | return f(*args, **kwargs) 57 | 58 | return decorated_function 59 | 60 | -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | from flask.ext.wtf import Form 2 | from wtforms import StringField, PasswordField, TextAreaField, RadioField, SelectField 3 | from wtforms.fields.html5 import DateField 4 | from wtforms.validators import DataRequired, EqualTo, Optional 5 | from wtforms import ValidationError 6 | 7 | 8 | class RequiredIfNetwork(object): 9 | """ 10 | Custom validator to set required fields only for a given network 11 | """ 12 | def __init__(self, network_valid): 13 | self.network_valid = network_valid 14 | self.message = 'Field is required for network: %s' % self.network_valid 15 | 16 | def __call__(self, form, field): 17 | network = form['network'].data 18 | if self.network_valid == network: 19 | if field.data is None or field.data == '': 20 | raise ValidationError(self.message) 21 | 22 | 23 | class TwitterTermsVal(object): 24 | """ 25 | Custom validator for Twitter terms. They are required if an API is selected 26 | """ 27 | def __init__(self): 28 | self.message = 'Terms are required if a Twitter API filter is selected.' 29 | 30 | def __call__(self, form, field): 31 | api_filter = form['api'].data 32 | network = form['network'].data 33 | if network == 'twitter' and api_filter == 'track' or api_filter == 'follow': 34 | if field.data is None or field.data == '': 35 | raise ValidationError(self.message) 36 | 37 | 38 | class LoginForm(Form): 39 | """ 40 | Login form for project accounts. Rendered on /login page 41 | """ 42 | project_name = StringField('Project Name', [DataRequired()]) 43 | password = PasswordField('Password', [DataRequired()]) 44 | 45 | 46 | class CreateForm(Form): 47 | """ 48 | Project account creation form 49 | """ 50 | project_name = StringField('Project Name', [DataRequired()]) 51 | email = StringField('Email', [DataRequired()]) 52 | password = PasswordField('Password', [DataRequired()]) 53 | confirm = PasswordField('Confirm Password', [ 54 | DataRequired(), 55 | EqualTo('password', message='Passwords must match.') 56 | ]) 57 | description = StringField('Account Description', [DataRequired()]) 58 | 59 | 60 | class SetupForm(Form): 61 | """ 62 | Admin account setup form 63 | """ 64 | project_name = StringField('Project Name', [DataRequired()]) 65 | password = PasswordField('Password', [DataRequired()]) 66 | confirm = PasswordField('Confirm Password', [ 67 | DataRequired(), 68 | EqualTo('password', message='Passwords must match.') 69 | ]) 70 | 71 | 72 | class NewCollectorForm(Form): 73 | """ 74 | Collector creation form 75 | """ 76 | # Universal Collector Information 77 | collector_name = StringField('Collector Name', [DataRequired()]) 78 | network = RadioField( 79 | 'Network', 80 | [DataRequired()], 81 | choices=[('twitter', 'Twitter'), ('facebook', 'Facebook')] 82 | ) 83 | 84 | """ Facebook Info """ 85 | # Collection type will become valid for all networks eventually 86 | collection_type = SelectField( 87 | 'Collection Type', 88 | [RequiredIfNetwork('facebook')], 89 | choices=[('realtime', 'Real Time'), ('historical', 'Historical')] 90 | ) 91 | 92 | # Since & Until 93 | start_date = DateField('Start Date (optional)', [Optional()]) 94 | end_date = DateField('End Date (optional)', [Optional()]) 95 | 96 | # Facebook OAuth Info 97 | client_id = StringField('Client ID', [RequiredIfNetwork('facebook')]) 98 | client_secret = StringField('Client Secret', [RequiredIfNetwork('facebook')]) 99 | 100 | # Terms 101 | facebook_terms = TextAreaField('Facebook Terms List', [RequiredIfNetwork('facebook')]) 102 | 103 | """ Twitter Info """ 104 | # Twitter API filter info 105 | api = SelectField( 106 | 'Twitter API Filter', 107 | [RequiredIfNetwork('twitter')], 108 | choices=[('track', 'Track'), ('follow', 'Follow'), ('none', 'None')] 109 | ) 110 | 111 | # OAuth Info 112 | consumer_key = StringField('Consumer Key', [RequiredIfNetwork('twitter')]) 113 | consumer_secret = StringField('Consumer Secret', [RequiredIfNetwork('twitter')]) 114 | access_token = StringField('Access Token', [RequiredIfNetwork('twitter')]) 115 | access_token_secret = StringField('Access Token Secret', [RequiredIfNetwork('twitter')]) 116 | 117 | # Languages & Location 118 | languages = TextAreaField('Languages (optional)', [Optional()]) 119 | locations = TextAreaField('Locations (optional)', [Optional()]) 120 | 121 | # Terms 122 | twitter_terms = TextAreaField('Twitter Terms List', [TwitterTermsVal()]) 123 | 124 | 125 | class UpdateCollectorForm(Form): 126 | """ 127 | Form for updating a collectors details. Terms are handled separately via form prefixes. 128 | """ 129 | collector_name = StringField('Collector Name', [Optional()]) 130 | new_terms = TextAreaField('New Terms', [Optional()]) 131 | 132 | """ Facebook Fields """ 133 | collection_type = SelectField( 134 | 'Collection Type', 135 | [Optional()], 136 | choices=[('realtime', 'Real Time'), ('historical', 'Historical')] 137 | ) 138 | start_date = DateField('Start Date', [Optional()]) 139 | end_date = DateField('End Date', [Optional()]) 140 | client_id = StringField('Client ID', [Optional()]) 141 | client_secret = StringField('Client Secret', [Optional()]) 142 | 143 | """ Twitter Fields """ 144 | api = SelectField( 145 | 'Twitter API Filter', 146 | [Optional()], 147 | choices=[('track', 'Track'), ('follow', 'Follow'), ('none', 'None')] 148 | ) 149 | consumer_key = StringField('Consumer Key', [Optional()]) 150 | consumer_secret = StringField('Consumer Secret', [Optional()]) 151 | access_token = StringField('Access Token', [Optional()]) 152 | access_token_secret = StringField('Access Token Secret', [Optional()]) 153 | languages = TextAreaField('Languages', [Optional()]) 154 | locations = TextAreaField('Locations', [Optional()]) 155 | 156 | 157 | class UpdateCollectorTermsForm(Form): 158 | """ 159 | For for updating a collector term details. Rendered multiple times w/ prefixes, along with the UpdateCollectorForm() 160 | """ 161 | term = StringField('Term', [Optional()]) 162 | collect = SelectField( 163 | 'Collect', 164 | [Optional()], 165 | choices=[(0, 'No'), (1, 'Yes')] 166 | ) 167 | 168 | 169 | class ProcessControlForm(Form): 170 | """ 171 | A base class for collector start/stop/restart buttons 172 | """ 173 | pass 174 | -------------------------------------------------------------------------------- /app/processes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import time 4 | import json 5 | import threading 6 | 7 | from bson.objectid import ObjectId 8 | 9 | from models import DB 10 | from app import app 11 | 12 | 13 | class BaseCollector(object): 14 | """ 15 | Extensible base class for all STACK collectors 16 | """ 17 | 18 | def __init__(self, project_id, collector_id, process_name): 19 | self.project_id = project_id 20 | self.collector_id = collector_id 21 | self.process_name = process_name 22 | self.collecting_data = False 23 | 24 | # Sets up connection w/ project config DB & loads in collector info 25 | self.db = DB() 26 | 27 | project = self.db.get_project_detail(self.project_id) 28 | if project['status']: 29 | self.project_name = project['project_name'] 30 | 31 | configdb = project['project_config_db'] 32 | project_db = self.db.connection[configdb] 33 | self.project_db = project_db.config 34 | 35 | resp = self.db.get_collector_detail(self.project_id, self.collector_id) 36 | if resp['status']: 37 | collector_info = resp['collector'] 38 | 39 | # Load in collector info 40 | self.collector_name = collector_info['collector_name'] 41 | self.network = collector_info['network'] 42 | self.api = collector_info['api'] 43 | self.collection_type = collector_info['collection_type'] 44 | self.params = collector_info['params'] 45 | self.terms_list = collector_info['terms_list'] 46 | self.languages = collector_info['languages'] 47 | self.locations = collector_info['location'] 48 | self.auth = collector_info['api_auth'] 49 | # TODO - file format to Mongo 50 | # TODO - less then hour = warning 51 | self.file_format = '%Y%m%d-%H' 52 | 53 | # If this is a streaming collector 54 | if self.collection_type == 'realtime': 55 | self.project_db.update({'_id': ObjectId(self.collector_id)}, {'$set': {'stream_limits': []}}) 56 | 57 | # Sets up logdir and logging 58 | logdir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/logs' 59 | if not os.path.exists(logdir): 60 | os.makedirs(logdir) 61 | 62 | # Sets logger w/ name collector_name and level INFO 63 | self.logger = logging.getLogger(self.collector_name) 64 | self.logger.setLevel(logging.INFO) 65 | 66 | # Sets up logging file handler 67 | logfile = logdir + '/%s.log' % self.process_name 68 | # TODO - logging params 69 | # TODO - port logging rotation params to Mongo for user control later / these default values good 70 | handler = logging.handlers.TimedRotatingFileHandler(logfile, when='D', backupCount=30) 71 | handler.setLevel(logging.INFO) 72 | # Formats 73 | format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' 74 | dateformat = '%m-%d %H:%M' 75 | formatter = logging.Formatter(format, dateformat) 76 | handler.setFormatter(formatter) 77 | # Adds handler to logger to finish 78 | self.logger.addHandler(handler) 79 | 80 | self.log('STACK collector %s initiated.' % self.collector_name) 81 | 82 | # Sets up rawdir 83 | self.rawdir = app.config['DATADIR'] + '/' + self.project_name + '-' + self.project_id + '/' + self.network + '/raw' 84 | if not os.path.exists(self.rawdir): 85 | os.makedirs(self.rawdir) 86 | 87 | self.log('All raw files and directories set. Now starting collector...') 88 | 89 | def go(self): 90 | """ 91 | Starts and maintains the loop that monitors the collection thread. 92 | Threads are maintained in the extended versions of the class 93 | """ 94 | # Checks if we're supposed to be running 95 | self.run_flag = self.check_flags()['run'] 96 | self.collect_flag = 0 97 | self.update_flag = 0 98 | 99 | if self.run_flag: 100 | self.log('Starting Facebook collector %s with signal %d' % (self.process_name, self.run_flag)) 101 | self.set_active(1) 102 | 103 | # If run_flag is set - begin the loop 104 | while self.run_flag: 105 | try: 106 | flags = self.check_flags() 107 | self.run_flag = flags['run'] 108 | self.collect_flag = flags['collect'] 109 | self.update_flag = flags['update'] 110 | except Exception as e: 111 | self.log('Mongo connection refused with exception: %s' % e, level='warn') 112 | 113 | # If we've been flagged to stop or update and we're collecting - shut it down 114 | if self.collecting_data and (self.update_flag or not self.collect_flag or not self.run_flag): 115 | self.stop_thread() 116 | 117 | # If we've been flagged to start and we're not collecting - start it up 118 | if self.collect_flag and threading.activeCount() == 1: 119 | self.start_thread() 120 | 121 | time.sleep(2) 122 | 123 | self.log('Exiting Facebook collection.') 124 | self.set_active(0) 125 | 126 | def write(self, data): 127 | """ 128 | Called to write raw data to raw file - handles rotation 129 | """ 130 | timestr = time.strftime(self.file_format) 131 | filename = self.rawdir + '/' + timestr + '-' + self.collector_name + '-' + self.collector_id + '-out.json' 132 | if not os.path.isfile(filename): 133 | self.log('Creating new raw file: %s' % filename) 134 | 135 | with open(filename, 'a') as rawfile: 136 | rawfile.write(json.dumps(data).encode('utf-8')) 137 | rawfile.write('\n') 138 | 139 | def log(self, message, level='info', thread='MAIN:'): 140 | """ 141 | Logs messages to process logfile 142 | """ 143 | message = str(message) 144 | if level == 'warn': 145 | self.logger.warning(thread + ' ' + message) 146 | elif level == 'error': 147 | self.logger.error(thread + ' ' + message) 148 | else: 149 | self.logger.info(thread + ' ' + message) 150 | 151 | def check_flags(self): 152 | """ 153 | Quick method to grab and return all Mongo flags for given Collector instance 154 | """ 155 | 156 | resp = self.db.get_collector_detail(self.project_id, self.collector_id) 157 | collector = resp['collector'] 158 | 159 | return { 160 | 'run': collector['collector']['run'], 161 | 'collect': collector['collector']['collect'], 162 | 'update': collector['collector']['update'], 163 | 'active': collector['active'] 164 | } 165 | 166 | def set_active(self, active): 167 | """ 168 | Quick method to set the active flag to 1 or 0 169 | """ 170 | self.project_db.update({'_id': ObjectId(self.collector_id)}, {'$set': {'active': active}}) 171 | 172 | def start_thread(self): 173 | """ 174 | Modify this method when extending the class to manage the actual collection thread 175 | """ 176 | 177 | def stop_thread(self): 178 | """ 179 | Modify this method when extending the class to stop the collection thread 180 | """ 181 | 182 | 183 | class BaseProcessor(object): 184 | """ 185 | Extensible base class for all STACK processors 186 | 187 | NOTE - when extending, must initiate connections to network specific data directories! 188 | """ 189 | 190 | def __init__(self, project_id, process_name, network): 191 | self.project_id = project_id 192 | self.process_name = process_name 193 | self.network = network 194 | 195 | # Sets up connection w/ project config DB & loads in collector info 196 | self.db = DB() 197 | 198 | project = self.db.get_project_detail(self.project_id) 199 | self.project_name = project['project_name'] 200 | 201 | configdb = project['project_config_db'] 202 | project_db = self.db.connection[configdb] 203 | self.project_db = project_db.config 204 | 205 | # Sets up logdir and logging 206 | logdir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/logs' 207 | if not os.path.exists(logdir): 208 | os.makedirs(logdir) 209 | 210 | # Sets logger w/ name collector_name and level INFO 211 | self.logger = logging.getLogger('Processor') 212 | self.logger.setLevel(logging.INFO) 213 | 214 | # Sets up logging file handler 215 | logfile = logdir + '/%s.log' % self.process_name 216 | # TODO - port logging rotation params to Mongo for user control later / these default values good 217 | handler = logging.handlers.TimedRotatingFileHandler(logfile, when='D', backupCount=30) 218 | handler.setLevel(logging.INFO) 219 | # Formats 220 | format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' 221 | dateformat = '%m-%d %H:%M' 222 | formatter = logging.Formatter(format, dateformat) 223 | handler.setFormatter(formatter) 224 | # Adds handler to logger to finish 225 | self.logger.addHandler(handler) 226 | 227 | self.log('STACK processor for project %s initiated.' % self.project_name) 228 | 229 | # Sets up data directory 230 | self.datadir = app.config['DATADIR'] + '/' + self.project_name + '-' + self.project_id 231 | 232 | # Establish connections to data directories 233 | self.raw = self.datadir + '/' + self.network + '/raw' 234 | self.archive = self.datadir + '/' + self.network + '/archive' 235 | self.queue = self.datadir + '/' + self.network + '/queue' 236 | self.error = self.datadir + '/' + self.network + '/error' 237 | 238 | if not os.path.exists(self.raw): 239 | os.makedirs(self.raw) 240 | if not os.path.exists(self.archive): 241 | os.makedirs(self.archive) 242 | if not os.path.exists(self.queue): 243 | os.makedirs(self.queue) 244 | if not os.path.exists(self.error): 245 | os.makedirs(self.error) 246 | 247 | self.log('STACK processor setup completed. Now starting...') 248 | 249 | def go(self): 250 | """ 251 | Runs the processor 252 | """ 253 | self.run_flag = self.check_flags()['run'] 254 | self.restart_flag = 0 255 | 256 | if self.run_flag: 257 | self.log('Starting processor %s with signal %d' % (self.process_name, self.run_flag)) 258 | self.set_active(1) 259 | 260 | while self.run_flag: 261 | # Call function to process files 262 | self.process() 263 | 264 | # Lastly, see if the run status has changed 265 | try: 266 | flags = self.check_flags() 267 | self.run_flag = flags['run'] 268 | self.restart_flag = flags['restart'] 269 | except Exception as e: 270 | self.log('Mongo connection refused with exception when attempting to check flags: %s' % e, level='warn') 271 | self.log('Will keep running the processing until reconnect is established.', level='warn') 272 | 273 | # Clean up upon run loop conclude 274 | self.log('Exiting processor.') 275 | self.set_active(0) 276 | 277 | def log(self, message, level='info', thread='MAIN:'): 278 | """ 279 | Logs messages to process logfile 280 | """ 281 | message = str(message) 282 | if level == 'warn': 283 | self.logger.warning(thread + ' ' + message) 284 | elif level == 'error': 285 | self.logger.error(thread + ' ' + message) 286 | else: 287 | self.logger.info(thread + ' ' + message) 288 | 289 | def check_flags(self): 290 | """ 291 | Quick method to grab and return all Mongo flags for given Collector instance 292 | """ 293 | resp = self.project_db.find_one({'module': self.network}) 294 | 295 | return { 296 | 'run': resp['processor']['run'], 297 | 'restart': resp['processor']['restart'] 298 | } 299 | 300 | def set_active(self, active): 301 | """ 302 | Quick method to set the active flag to 1 or 0 303 | """ 304 | self.project_db.update({'module': self.network}, {'$set': {'processor_active': active}}) 305 | 306 | def process(self): 307 | """ 308 | Extend this function to implement your custom processing schemes 309 | """ 310 | 311 | class BaseInserter(object): 312 | """ 313 | Extensible base class for all STACK processors 314 | 315 | NOTE - when extending, must initiate connections to network specific data directories! 316 | """ 317 | 318 | def __init__(self, project_id, process_name, network): 319 | self.project_id = project_id 320 | self.process_name = process_name 321 | self.network = network 322 | 323 | # Sets up connection w/ project config DB & loads in collector info 324 | self.db = DB() 325 | 326 | project = self.db.get_project_detail(self.project_id) 327 | self.project_name = project['project_name'] 328 | 329 | # Grabs connection to project config DB 330 | configdb = project['project_config_db'] 331 | project_db = self.db.connection[configdb] 332 | self.project_db = project_db.config 333 | 334 | # Grabs connection to insertion DB 335 | # NOTE - on init, need to connect to appropriate network collection 336 | db_name = self.project_name + '_' + self.project_id 337 | self.insert_db = self.db.connection[db_name] 338 | 339 | # Sets up logdir and logging 340 | logdir = app.config['LOGDIR'] + '/' + self.project_name + '-' + self.project_id + '/logs' 341 | if not os.path.exists(logdir): 342 | os.makedirs(logdir) 343 | 344 | # Sets logger w/ name collector_name and level INFO 345 | self.logger = logging.getLogger('Inserter') 346 | self.logger.setLevel(logging.INFO) 347 | 348 | # Sets up logging file handler 349 | logfile = logdir + '/%s.log' % self.process_name 350 | # TODO - port logging rotation params to Mongo for user control later / these default values good 351 | handler = logging.handlers.TimedRotatingFileHandler(logfile, when='D', backupCount=30) 352 | handler.setLevel(logging.INFO) 353 | # Formats 354 | format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' 355 | dateformat = '%m-%d %H:%M' 356 | formatter = logging.Formatter(format, dateformat) 357 | handler.setFormatter(formatter) 358 | # Adds handler to logger to finish 359 | self.logger.addHandler(handler) 360 | 361 | self.log('STACK inserter for project %s initiated.' % self.project_name) 362 | 363 | # Sets up data directory 364 | self.datadir = app.config['DATADIR'] + '/' + self.project_name + '-' + self.project_id 365 | 366 | # Establish connections to data directories 367 | self.raw = self.datadir + '/' + self.network + '/raw' 368 | self.archive = self.datadir + '/' + self.network + '/archive' 369 | self.queue = self.datadir + '/' + self.network + '/queue' 370 | self.error = self.datadir + '/' + self.network + '/error' 371 | 372 | if not os.path.exists(self.raw): 373 | os.makedirs(self.raw) 374 | if not os.path.exists(self.archive): 375 | os.makedirs(self.archive) 376 | if not os.path.exists(self.queue): 377 | os.makedirs(self.queue) 378 | if not os.path.exists(self.error): 379 | os.makedirs(self.error) 380 | 381 | self.log('STACK processor setup completed. Now starting...') 382 | 383 | def go(self): 384 | """ 385 | Runs the processor 386 | """ 387 | self.run_flag = self.check_flags()['run'] 388 | self.restart_flag = 0 389 | 390 | if self.run_flag: 391 | self.log('Starting inserter %s with signal %d' % (self.process_name, self.run_flag)) 392 | self.set_active(1) 393 | 394 | while self.run_flag: 395 | # Call function to process files 396 | self.insert() 397 | 398 | # Lastly, see if the run status has changed 399 | try: 400 | flags = self.check_flags() 401 | self.run_flag = flags['run'] 402 | self.restart_flag = flags['restart'] 403 | except Exception as e: 404 | self.log('Mongo connection refused with exception when attempting to check flags: %s' % e, level='warn') 405 | self.log('Will keep running the processing until reconnect is established.', level='warn') 406 | 407 | # Clean up upon run loop conclude 408 | self.log('Exiting inserter.') 409 | self.set_active(0) 410 | 411 | def log(self, message, level='info', thread='MAIN:'): 412 | """ 413 | Logs messages to process logfile 414 | """ 415 | message = str(message) 416 | if level == 'warn': 417 | self.logger.warning(thread + ' ' + message) 418 | elif level == 'error': 419 | self.logger.error(thread + ' ' + message) 420 | else: 421 | self.logger.info(thread + ' ' + message) 422 | 423 | def check_flags(self): 424 | """ 425 | Quick method to grab and return all Mongo flags for given Collector instance 426 | """ 427 | resp = self.project_db.find_one({'module': self.network}) 428 | 429 | return { 430 | 'run': resp['inserter']['run'], 431 | 'restart': resp['inserter']['restart'] 432 | } 433 | 434 | def set_active(self, active): 435 | """ 436 | Quick method to set the active flag to 1 or 0 437 | """ 438 | self.project_db.update({'module': self.network}, {'$set': {'inserter_active': active}}) 439 | 440 | def insert(self): 441 | """ 442 | Extend this function to implement your custom processing schemes 443 | """ 444 | -------------------------------------------------------------------------------- /app/static/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn-default .badge, 33 | .btn-primary .badge, 34 | .btn-success .badge, 35 | .btn-info .badge, 36 | .btn-warning .badge, 37 | .btn-danger .badge { 38 | text-shadow: none; 39 | } 40 | .btn:active, 41 | .btn.active { 42 | background-image: none; 43 | } 44 | .btn-default { 45 | text-shadow: 0 1px 0 #fff; 46 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 47 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 48 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 49 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 50 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 51 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 52 | background-repeat: repeat-x; 53 | border-color: #dbdbdb; 54 | border-color: #ccc; 55 | } 56 | .btn-default:hover, 57 | .btn-default:focus { 58 | background-color: #e0e0e0; 59 | background-position: 0 -15px; 60 | } 61 | .btn-default:active, 62 | .btn-default.active { 63 | background-color: #e0e0e0; 64 | border-color: #dbdbdb; 65 | } 66 | .btn-default.disabled, 67 | .btn-default:disabled, 68 | .btn-default[disabled] { 69 | background-color: #e0e0e0; 70 | background-image: none; 71 | } 72 | .btn-primary { 73 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 74 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 75 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 76 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #245580; 81 | } 82 | .btn-primary:hover, 83 | .btn-primary:focus { 84 | background-color: #265a88; 85 | background-position: 0 -15px; 86 | } 87 | .btn-primary:active, 88 | .btn-primary.active { 89 | background-color: #265a88; 90 | border-color: #245580; 91 | } 92 | .btn-primary.disabled, 93 | .btn-primary:disabled, 94 | .btn-primary[disabled] { 95 | background-color: #265a88; 96 | background-image: none; 97 | } 98 | .btn-success { 99 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 100 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 101 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 102 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 103 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 104 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 105 | background-repeat: repeat-x; 106 | border-color: #3e8f3e; 107 | } 108 | .btn-success:hover, 109 | .btn-success:focus { 110 | background-color: #419641; 111 | background-position: 0 -15px; 112 | } 113 | .btn-success:active, 114 | .btn-success.active { 115 | background-color: #419641; 116 | border-color: #3e8f3e; 117 | } 118 | .btn-success.disabled, 119 | .btn-success:disabled, 120 | .btn-success[disabled] { 121 | background-color: #419641; 122 | background-image: none; 123 | } 124 | .btn-info { 125 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 126 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 127 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 128 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 129 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 130 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 131 | background-repeat: repeat-x; 132 | border-color: #28a4c9; 133 | } 134 | .btn-info:hover, 135 | .btn-info:focus { 136 | background-color: #2aabd2; 137 | background-position: 0 -15px; 138 | } 139 | .btn-info:active, 140 | .btn-info.active { 141 | background-color: #2aabd2; 142 | border-color: #28a4c9; 143 | } 144 | .btn-info.disabled, 145 | .btn-info:disabled, 146 | .btn-info[disabled] { 147 | background-color: #2aabd2; 148 | background-image: none; 149 | } 150 | .btn-warning { 151 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 152 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 153 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 154 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 155 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 156 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 157 | background-repeat: repeat-x; 158 | border-color: #e38d13; 159 | } 160 | .btn-warning:hover, 161 | .btn-warning:focus { 162 | background-color: #eb9316; 163 | background-position: 0 -15px; 164 | } 165 | .btn-warning:active, 166 | .btn-warning.active { 167 | background-color: #eb9316; 168 | border-color: #e38d13; 169 | } 170 | .btn-warning.disabled, 171 | .btn-warning:disabled, 172 | .btn-warning[disabled] { 173 | background-color: #eb9316; 174 | background-image: none; 175 | } 176 | .btn-danger { 177 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 178 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 179 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 180 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 182 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 183 | background-repeat: repeat-x; 184 | border-color: #b92c28; 185 | } 186 | .btn-danger:hover, 187 | .btn-danger:focus { 188 | background-color: #c12e2a; 189 | background-position: 0 -15px; 190 | } 191 | .btn-danger:active, 192 | .btn-danger.active { 193 | background-color: #c12e2a; 194 | border-color: #b92c28; 195 | } 196 | .btn-danger.disabled, 197 | .btn-danger:disabled, 198 | .btn-danger[disabled] { 199 | background-color: #c12e2a; 200 | background-image: none; 201 | } 202 | .thumbnail, 203 | .img-thumbnail { 204 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 205 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 206 | } 207 | .dropdown-menu > li > a:hover, 208 | .dropdown-menu > li > a:focus { 209 | background-color: #e8e8e8; 210 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 211 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 212 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 213 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 214 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 215 | background-repeat: repeat-x; 216 | } 217 | .dropdown-menu > .active > a, 218 | .dropdown-menu > .active > a:hover, 219 | .dropdown-menu > .active > a:focus { 220 | background-color: #2e6da4; 221 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 222 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 223 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 224 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 226 | background-repeat: repeat-x; 227 | } 228 | .navbar-default { 229 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 230 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 231 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 232 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 233 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 234 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 235 | background-repeat: repeat-x; 236 | border-radius: 4px; 237 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 238 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 239 | } 240 | .navbar-default .navbar-nav > .open > a, 241 | .navbar-default .navbar-nav > .active > a { 242 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 243 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 244 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 245 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 246 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 247 | background-repeat: repeat-x; 248 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 249 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 250 | } 251 | .navbar-brand, 252 | .navbar-nav > li > a { 253 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 254 | } 255 | .navbar-inverse { 256 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 257 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 258 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 259 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 260 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 261 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 262 | background-repeat: repeat-x; 263 | } 264 | .navbar-inverse .navbar-nav > .open > a, 265 | .navbar-inverse .navbar-nav > .active > a { 266 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 267 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 268 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 269 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 270 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 271 | background-repeat: repeat-x; 272 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 273 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 274 | } 275 | .navbar-inverse .navbar-brand, 276 | .navbar-inverse .navbar-nav > li > a { 277 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 278 | } 279 | .navbar-static-top, 280 | .navbar-fixed-top, 281 | .navbar-fixed-bottom { 282 | border-radius: 0; 283 | } 284 | @media (max-width: 767px) { 285 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 286 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 287 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 288 | color: #fff; 289 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 290 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 291 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 292 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 294 | background-repeat: repeat-x; 295 | } 296 | } 297 | .alert { 298 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 299 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .alert-success { 303 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 304 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 305 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 306 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 307 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 308 | background-repeat: repeat-x; 309 | border-color: #b2dba1; 310 | } 311 | .alert-info { 312 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 313 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 314 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 315 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 316 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 317 | background-repeat: repeat-x; 318 | border-color: #9acfea; 319 | } 320 | .alert-warning { 321 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 322 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 323 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 324 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 325 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 326 | background-repeat: repeat-x; 327 | border-color: #f5e79e; 328 | } 329 | .alert-danger { 330 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 331 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 332 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 333 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 334 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 335 | background-repeat: repeat-x; 336 | border-color: #dca7a7; 337 | } 338 | .progress { 339 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 340 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 342 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 344 | background-repeat: repeat-x; 345 | } 346 | .progress-bar { 347 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 348 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 349 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 350 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 351 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 352 | background-repeat: repeat-x; 353 | } 354 | .progress-bar-success { 355 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 356 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 357 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 358 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 359 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 360 | background-repeat: repeat-x; 361 | } 362 | .progress-bar-info { 363 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 364 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 365 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 366 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 367 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 368 | background-repeat: repeat-x; 369 | } 370 | .progress-bar-warning { 371 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 372 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 373 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 374 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 375 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 376 | background-repeat: repeat-x; 377 | } 378 | .progress-bar-danger { 379 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 380 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 381 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 382 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 383 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 384 | background-repeat: repeat-x; 385 | } 386 | .progress-bar-striped { 387 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 388 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 389 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 390 | } 391 | .list-group { 392 | border-radius: 4px; 393 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 394 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 395 | } 396 | .list-group-item.active, 397 | .list-group-item.active:hover, 398 | .list-group-item.active:focus { 399 | text-shadow: 0 -1px 0 #286090; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 405 | background-repeat: repeat-x; 406 | border-color: #2b669a; 407 | } 408 | .list-group-item.active .badge, 409 | .list-group-item.active:hover .badge, 410 | .list-group-item.active:focus .badge { 411 | text-shadow: none; 412 | } 413 | .panel { 414 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 415 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 416 | } 417 | .panel-default > .panel-heading { 418 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 419 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 420 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 421 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 422 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 423 | background-repeat: repeat-x; 424 | } 425 | .panel-primary > .panel-heading { 426 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 427 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 428 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 429 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 430 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 431 | background-repeat: repeat-x; 432 | } 433 | .panel-success > .panel-heading { 434 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 435 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 436 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 437 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 438 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 439 | background-repeat: repeat-x; 440 | } 441 | .panel-info > .panel-heading { 442 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 443 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 444 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 445 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 446 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 447 | background-repeat: repeat-x; 448 | } 449 | .panel-warning > .panel-heading { 450 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 451 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 453 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .panel-danger > .panel-heading { 458 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 459 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 461 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .well { 466 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 467 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 469 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 471 | background-repeat: repeat-x; 472 | border-color: #dcdcdc; 473 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 474 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 475 | } 476 | /*# sourceMappingURL=bootstrap-theme.css.map */ 477 | -------------------------------------------------------------------------------- /app/static/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitslabsyr/stack/5cc686b60717b5a7674d2dbb9f9e332964a8916f/app/static/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/static/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitslabsyr/stack/5cc686b60717b5a7674d2dbb9f9e332964a8916f/app/static/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/static/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitslabsyr/stack/5cc686b60717b5a7674d2dbb9f9e332964a8916f/app/static/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/static/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitslabsyr/stack/5cc686b60717b5a7674d2dbb9f9e332964a8916f/app/static/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/static/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /app/static/style.css: -------------------------------------------------------------------------------- 1 | #messages-wrap { 2 | padding: 0px 20px; 3 | } 4 | 5 | main { 6 | padding: 0px 20px; 7 | } 8 | 9 | /* Project Account Home Panel */ 10 | .project-home .network-buttons { 11 | padding-top: 10px; 12 | } 13 | 14 | .project-home .network-buttons > div { 15 | text-align: center; 16 | } -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import threading 5 | 6 | from app import celery, app 7 | from controller import Controller 8 | 9 | 10 | @celery.task() 11 | def start_daemon(process, project, collector_id=None, network=None): 12 | """ 13 | Calls a Controller to daemonize and start a STACK process 14 | """ 15 | if process == 'collect': 16 | c = Controller( 17 | process=process, 18 | project=project, 19 | collector_id=collector_id 20 | ) 21 | else: 22 | c = Controller( 23 | process=process, 24 | project=project, 25 | network=network 26 | ) 27 | 28 | t = threading.Thread(name='test-thread', target=c.process_command, args=('start',)) 29 | t.start() 30 | 31 | # c.process_command('start') 32 | 33 | 34 | @celery.task() 35 | def stop_daemon(process, project, collector_id=None, network=None): 36 | """ 37 | Calls a Controller to stop a daemonized STACK process 38 | """ 39 | if process == 'collect': 40 | c = Controller( 41 | process=process, 42 | project=project, 43 | collector_id=collector_id 44 | ) 45 | else: 46 | c = Controller( 47 | process=process, 48 | project=project, 49 | network=network 50 | ) 51 | 52 | t = threading.Thread(name='test-thread', target=c.process_command, args=('stop',)) 53 | t.start() 54 | t.join() 55 | 56 | # c.process_command('stop') 57 | 58 | 59 | @celery.task() 60 | def restart_daemon(process, project, collector_id=None, network=None): 61 | """ 62 | Calls a Controller to restart a daemonized STACK process 63 | """ 64 | if process == 'collect': 65 | c = Controller( 66 | process=process, 67 | project=project, 68 | collector_id=collector_id 69 | ) 70 | else: 71 | c = Controller( 72 | process=process, 73 | project=project, 74 | network=network 75 | ) 76 | 77 | t = threading.Thread(name='test-thread', target=c.process_command, args=('restart',)) 78 | t.start() 79 | 80 | # c.process_command('restart') 81 | 82 | 83 | def start_workers(): 84 | """ 85 | Starts two Celery workers on app spin up 86 | 87 | -- 1) Handles starting of all STACK processes 88 | -- 2) Handles stoping of all STACK processes 89 | """ 90 | base_command = 'celery multi start ' 91 | os.chdir(app.config['BASEDIR']) 92 | 93 | # Worker names 94 | start_worker = 'stack-start' 95 | stop_worker = 'stack-stop' 96 | 97 | # Directories for log and pid information 98 | outdir = app.config['LOGDIR'] + '/app' 99 | piddir = outdir + '/pid' 100 | logdir = outdir + '/log' 101 | 102 | # Filenames 103 | start_logfile = logdir + '/%s.log' % start_worker 104 | stop_logfile = logdir + '/%s.log' % stop_worker 105 | start_pidfile = piddir + '/%s.pid' % start_worker 106 | stop_pidfile = piddir + '/%s.pid' % stop_worker 107 | 108 | # Creates directories if they don't exist 109 | if not os.path.exists(piddir): 110 | os.makedirs(piddir) 111 | if not os.path.exists(logdir): 112 | os.makedirs(logdir) 113 | 114 | start_pid = get_pid(start_pidfile) 115 | stop_pid = get_pid(stop_pidfile) 116 | 117 | # Completes the command syntax to spin up the workers 118 | if not start_pid: 119 | start_worker_cmd = base_command + '%s-worker -A app.celery -l info -Q %s --logfile=%s --pidfile=%s' % \ 120 | (start_worker, start_worker, start_logfile, start_pidfile) 121 | subprocess.call(start_worker_cmd.split(' ')) 122 | 123 | if not stop_pid: 124 | stop_worker_cmd = base_command + '%s-worker -A app.celery -l info -Q %s --logfile=%s --pidfile=%s' % \ 125 | (stop_worker, stop_worker, stop_logfile, stop_pidfile) 126 | subprocess.call(stop_worker_cmd.split(' ')) 127 | 128 | 129 | def get_pid(pidfile): 130 | try: 131 | pf = file(pidfile, 'r') 132 | pid = int(pf.read().strip()) 133 | pf.close() 134 | except IOError: 135 | pid = None 136 | except SystemExit: 137 | pid = None 138 | return pid -------------------------------------------------------------------------------- /app/templates/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro terms_field(field) %} 2 | 3 | {% endmacro %} 4 | 5 | {% macro form_field(field) %} 6 | {% set inner_text = kwargs.pop('inner_text', '') %} 7 | {% set group_class = kwargs.pop('group_class', '') %} 8 | {% set label_class = kwargs.pop('label_class', '') %} 9 | {% set input_class = kwargs.pop('input_class', '') %} 10 | 11 | {% if field.flags.required %} 12 | {% set input_class = input_class + ' required' %} 13 | {% endif %} 14 | 15 |
{{ field.description|safe }}
50 | {% endif %} 51 |As an admin you can create new project accounts or view existing ones.
11 | Create New Project Account 12 | 13 |Project Name | 20 |# Collectors | 21 |# Active Collectors | 22 |
---|---|---|
{{ project['project_name'] }} | 29 |{{ project['num_collectors'] }} | 30 |{{ project['active_collectors'] }} | 31 |
Term | 127 |Type | 128 |Collecting | 129 |Start / Stop Dates | 130 |Term ID | 131 |
---|---|---|---|---|
{{ term['term'] }} | 137 |{{ term['type'] }} | 138 |139 | {% if term['collect'] == 1 %} 140 | Yes 141 | {% else %} 142 | No 143 | {% endif %} 144 | | 145 |
146 | {% for range in term['history'] %}
147 | {{ range['start_date'] }} -- {{ range['end_date'] }} 148 | {% endfor %} 149 | |
150 | 151 | {% if term['id'] %} 152 | {{ term['id'] }} 153 | {% else %} 154 | None 155 | {% endif %} 156 | | 157 |
{{ project_detail['project_description'] }}
9 | 10 |Collector Name | 36 |Network | 37 |Active | 38 |# Terms | 39 |
---|---|---|---|
47 | {{ collector['collector_name'] }} 48 | | 49 |{{ collector['network'] }} | 50 |51 | {% if collector['active'] == 0 %} 52 | No 53 | {% else %} 54 | Yes 55 | {% endif %} 56 | | 57 |{{ collector['num_terms'] }} | 58 |
No collectors exist for this account yet!
66 | {% endif %} 67 | 68 |STACKS is a social data collection and analysis geared towards researcher. With STACKS you can collect data 10 | from public social networks, all for free. STACKS is built an maintained at the Syracuse University 11 | iSchool. Follow STACKS on Github 12 | for regular development updates.
13 |19 | {% if processor_active_status == 'active' %} 20 | Active 21 | {% else %} 22 | Inactive 23 | {% endif %} 24 |
25 | 26 | 32 | 33 |35 | {% if inserter_active_status == 'active' %} 36 | Active 37 | {% else %} 38 | Inactive 39 | {% endif %} 40 |
41 | 42 | 48 |Collector Name | 59 |Network | 60 |Active | 61 |# Terms | 62 |
---|---|---|---|
70 | {{ collector['collector_name'] }} 71 | | 72 |{{ collector['network'] }} | 73 |74 | {% if collector['active'] == 0 %} 75 | No 76 | {% else %} 77 | Yes 78 | {% endif %} 79 | | 80 |{{ collector['num_terms'] }} | 81 |
No collectors exist for this account yet!
89 | {% endif %} 90 |Welcome to STACKS. Before you begin, please make an administrator account. With an admin account you'll be able 9 | to create new projects and manage collections.
10 | 11 |