├── .gitignore ├── CONFIG.json ├── README.md ├── SETUP.py ├── analytics ├── SqlToCsv.ipynb └── pair_graph_analysis │ └── src │ └── main │ └── java │ └── edu │ └── umd │ └── hcil │ └── collabortweet │ └── graph │ ├── EdgeTuple.java │ ├── RemoveCyclesDFS.java │ └── TestGraph.java ├── bin ├── SqlToCsv.py ├── add_tweets_to_task.py ├── add_users.py ├── assignTask.py ├── deleteTask.py ├── embed_tweets_2_sql.py ├── partition_data.py ├── retrieve_partitioned_task.py ├── tweets2sql.py └── utils.py ├── compTaskDesc.json ├── createSchema.sql ├── database.sqlite3 ├── labelTaskDesc.json ├── package.json ├── public ├── .DS_Store ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ └── jumbotron.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── imgs │ └── spinner.gif └── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── ie10-viewport-bug-workaround.js │ ├── labelview.js │ ├── npm.js │ ├── pairview.js │ ├── rangeview.js │ └── taskStats-detail.js ├── rangeTaskDesc.json ├── server.js ├── tweetSample.json ├── users ├── index.js └── users.js ├── views ├── .DS_Store ├── export.pug ├── includes │ ├── head.pug │ ├── jsFooter.pug │ └── nav.pug ├── index.pug ├── labelView.pug ├── pairView.pug ├── rangeView.pug ├── taskStats-detail.pug ├── taskStats.pug └── taskView.pug └── vignette.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | testdata/ 3 | bin/__pycache__/ 4 | package-lock.json 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /CONFIG.json: -------------------------------------------------------------------------------- 1 | {"port": 3000, "db_path": "database.sqlite3"} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CollaborTweet 2 | 3 | This code is a framework for assessing text documents using a collaborative, online framework. It's written in Node.js and makes heavy use of third-party packages, like Express, Sqlite, etc. 4 | 5 | This framework was initially developed to impose an ordering over a textual data set using pairwise comparisons (not unlike [SentimentIt](https://www.sentimentit.com)). The original task was to evaluate emotional intensity of Twitter data by providing users with a pair of tweets and asking them to assess which one exhibits a more negative feeling. This framework can support any such task using textual data and is not restricted to Twitter data, though I suggest short-form text rather than long documents. 6 | 7 | In addition, the system has been updated to support labeling text as well. If you want to do a simple relevance classification task on text, this platform will work for you too. 8 | 9 | The framework is also designed to support multiple users and multiple tasks, but these settings are only vaguely implemented. The system currently supports a single task, and users are specified by a file (users.js) in the users directory. One or both of these features should be implemented soon. 10 | 11 | ## Setup 12 | 13 | You should be able to use `npm install` to set up the server and download any dependencies. Then, `npm start` will run the server, which you can access (currently) at (http://localhost:3000). 14 | 15 | Before starting the server, you need to set up the database. This can be done using the createSchema.sql file (e.g., `sqlite3 database.sqlite3 < createSchema.sql`). Then, use the `tweets2sql.py` python script to populate the database. 16 | 17 | ### Creating Tasks 18 | 19 | The `tweets2sql.py` script expects three arguments: a JSON file describing the task, the path to the SQLite file you created before, and the path the JSON file containing tweets (either in Twitter's format or Gnip's activity format). 20 | 21 | The JSON task description file contains the name of the task, the question you want to ask the user, and the type of task (1 for pairwise comparison, 2 for labeling tasks, 3 for range-based tasks). For pairwise comparisons, you don't need more information. For the labeling task, you also need to include a list of labels. 22 | 23 | Examples are below: 24 | 25 | #### Comparison Task Description JSON 26 | 27 | { 28 | "name": "Election Content - Negativity Comparisons", 29 | "question": "Which of these tweets seems more emotionally negative?", 30 | "type": 1 31 | } 32 | 33 | #### Labeling Task Description JSON 34 | 35 | { 36 | "name": "Election Content - Relevance Labels", 37 | "question": "Is this tweet relevant to the current election?", 38 | "type": 2, 39 | "labels": [ 40 | {"Relevant": [ 41 | "Pro-Government", 42 | {"Pro-Opposition": [ 43 | "Pro Opp Candidate 1", 44 | "Pro Opp Candidate 2", 45 | ]} 46 | ]}, 47 | "Not Relevant", 48 | "Not English", 49 | "Can't Decide" 50 | ] 51 | } 52 | 53 | #### Range-based question tasks 54 | 55 | The platform also supports labeling of range-based questions, sets of questions with associated scale values as the possible label options 56 | The task configuration file looks like this for range-based questions: 57 | 58 | { 59 | "name": "Nigeria 2014 - Emotional Ranges", 60 | "question": "Answer several questions about the emotional range of a social media message.", 61 | "questions": [ 62 | { 63 | "question": "Rate the emotional negativity of this tweet", 64 | "scale": [ 65 | "1 - Not At All", 66 | "2 - Slightly", 67 | "3 - Moderate", 68 | "4 - Very", 69 | "5 - Incredibly" 70 | ] 71 | }, 72 | { 73 | "question": "Rate the emotional positivity of this tweet", 74 | "scale": [ 75 | "1 - Not At All", 76 | "2 - Slightly", 77 | "3 - Moderate", 78 | "4 - Very", 79 | "5 - Incredibly" 80 | ] 81 | } 82 | ], 83 | "type": 3 84 | } 85 | 86 | NOTE: The order of the values in each scale is preserved in the database. 87 | 88 | ### Creating Users 89 | 90 | Once your database is populated, you need to add users to the system. Users aren't for authentication so much as ensuring we don't show the same pair to the same user multiple times. Currently, the system uses the __users__ table in the sqlite file, so add users there. Using sqlite, you can do it easily: 91 | 92 | sqlite3 database.sqlite3 'INSERT INTO users (userId, screenname, password, fname, lname) VALUES (1, "cbuntain", "cb123", "Cody", "Buntain")' 93 | 94 | ### Assigning User Tasks 95 | 96 | Assign tasks to individual users, and restrict their ability to see certain tasks. 97 | 98 | Run the bin/assignTask.py file with the following arguments: 99 | 100 | --database ../DATABASE/PATH 101 | 102 | --user SCREENNAME 103 | 104 | --task_id TASKIDNUMBER 105 | 106 | Example: python bin/assignTask.py --database database.sqlite3 --user screenname --task_id 1 107 | -------------------------------------------------------------------------------- /SETUP.py: -------------------------------------------------------------------------------- 1 | '''This script runs some basic setup steps for collabortweet 2 | Author: Fridolin Linder 3 | 4 | Usage: 5 | $> python setup.py 6 | ''' 7 | import subprocess 8 | import json 9 | import sys 10 | 11 | with open('CONFIG.json') as infile: 12 | config = json.load(infile) 13 | 14 | # Install dependencies 15 | subprocess.call('npm install', shell=True) 16 | 17 | # Set up the database 18 | subprocess.call( 19 | 'sqlite3 {} < createSchema.sql'.format(config['db_path']), 20 | shell=True 21 | ) 22 | 23 | # Print out info 24 | print('\n\n') 25 | print('+'*80) 26 | print('You can now start the collabortweet application with the command `npm start`') 27 | print('Your application will be available at http://178.128.157.54:{}'.format(config['port'])) 28 | print('+'*80) 29 | print('\n\n') 30 | 31 | -------------------------------------------------------------------------------- /analytics/SqlToCsv.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import sqlite3\n", 12 | "import sklearn.metrics\n", 13 | "import statsmodels.stats.inter_rater\n", 14 | "import pandas as pd" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "# Connect to SQLite DB" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": { 28 | "collapsed": false 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "conn = sqlite3.connect('database.sqlite3')\n", 33 | "c = conn.cursor()" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "# Get Labelers" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": { 47 | "collapsed": false 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "userMap = {}\n", 52 | "for userRow in c.execute(\"SELECT userId, fname, lname FROM users\"):\n", 53 | " userName = \"%s %s\" % (userRow[1], userRow[2])\n", 54 | " userMap[userRow[0]] = userName\n", 55 | " print(userRow[0], userName)\n", 56 | "#print userMap" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "# Get Tasks" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "collapsed": false 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "for taskRow in c.execute(\"SELECT taskId, taskName, taskType FROM tasks\"):\n", 75 | " print(\"Task ID:\", taskRow[0], \"Type:\", taskRow[2], \"Name:\", taskRow[1])" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "# Convert SQL Labels to CSV File\n", 83 | "\n", 84 | "Each task will be dumped to a CSV file. Each CSV file will contain rows, one row for each user-label pair for each labeled element in that task." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "metadata": { 91 | "collapsed": false, 92 | "scrolled": false 93 | }, 94 | "outputs": [], 95 | "source": [ 96 | "taskRows = c.execute(\"SELECT taskId, taskName FROM tasks WHERE taskType == 2\").fetchall()\n", 97 | "\n", 98 | "for r in taskRows:\n", 99 | " taskName = r[1]\n", 100 | " taskId = r[0]\n", 101 | " \n", 102 | " # Task name\n", 103 | " print(\"Task Name:\", taskName)\n", 104 | " \n", 105 | " # Get the number of elements you want to label\n", 106 | " eCount = c.execute(\"SELECT COUNT(*) FROM elements WHERE taskId = ?\", (taskId, )).fetchone()\n", 107 | " print(\"Number of Elements to Label:\", eCount[0])\n", 108 | " \n", 109 | " # How many labels do we actually have?\n", 110 | " # Each user contributes a label, so this number could be at max:\n", 111 | " # number of labelers * number of elements to label\n", 112 | " lCount = c.execute(\"SELECT COUNT(*) \" + \\\n", 113 | " \"FROM elementLabels el JOIN elements e \" + \\\n", 114 | " \"ON e.elementId = el.elementId \" + \\\n", 115 | " \"WHERE e.taskId = ?\", \n", 116 | " (taskId, )).fetchone()\n", 117 | " print(\"Number of Labels:\", lCount[0])\n", 118 | " \n", 119 | " # Print the users who participated in this task\n", 120 | " userLabelCounts = {}\n", 121 | " print(\"Users who Labeled:\")\n", 122 | " labelerList = c.execute(\"SELECT DISTINCT userId \" + \\\n", 123 | " \"FROM elementLabels el JOIN elements e \" + \\\n", 124 | " \"ON e.elementId = el.elementId \" + \\\n", 125 | " \"WHERE e.taskId = ?\", \n", 126 | " (taskId, )).fetchall()\n", 127 | " labelerList = list(map(lambda x: x[0], labelerList))\n", 128 | " for labelerId in labelerList:\n", 129 | " name = userMap[labelerId]\n", 130 | " \n", 131 | " userCount = c.execute(\"SELECT COUNT(*) \" + \\\n", 132 | " \"FROM elementLabels el JOIN elements e \" + \\\n", 133 | " \"ON e.elementId = el.elementId \" + \\\n", 134 | " \"WHERE e.taskId = ? AND el.userId = ?\", \n", 135 | " (taskId, labelerId)).fetchone()\n", 136 | " userLabelCounts[labelerId] = userCount[0]\n", 137 | " print(\"\\t\", name, \" - \", userCount[0], \"Labels\")\n", 138 | " \n", 139 | " \n", 140 | " # Get the tweet IDs for each label\n", 141 | " dataSamples = []\n", 142 | " elementAndLabelList = c.execute(\"SELECT el.elementId, el.labelId, el.userId, \" + \\\n", 143 | " \"e.externalId, l.labelText, l.taskId, e.elementText, \" + \\\n", 144 | " \"u.screenname \" + \\\n", 145 | " \"FROM elementLabels el JOIN elements e \" + \\\n", 146 | " \"ON e.elementId = el.elementId \" + \\\n", 147 | " \"JOIN labels l ON el.labelId = l.labelId \" + \\\n", 148 | " \"JOIN users u ON el.userId = u.userId \" + \\\n", 149 | " \"WHERE e.taskId = ?\", (taskId, )).fetchall()\n", 150 | " # For each element in the row, convert it to a map and add it to our \n", 151 | " # list of samples. NOTE: If you want to add text here\n", 152 | " for row in elementAndLabelList:\n", 153 | " dataSample = {\n", 154 | " \"external_id\": row[3],\n", 155 | " \"label\": row[4],\n", 156 | " \"user_id\": row[2],\n", 157 | " \"username\": row[7],\n", 158 | " \"text\": row[6],\n", 159 | " }\n", 160 | " dataSamples.append(dataSample)\n", 161 | "\n", 162 | " \n", 163 | " labelDf = pd.DataFrame(dataSamples)\n", 164 | " \n", 165 | " # Add \"text\" to the list below to add text to the resulting CSV\n", 166 | " labelDf[[\"external_id\", \"user_id\", \"username\", \"label\"]].\\\n", 167 | " sort_values(by=\"external_id\").\\\n", 168 | " to_csv(\"task_id_%03d.csv\" % taskId, index=False)\n", 169 | " " 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": null, 175 | "metadata": { 176 | "collapsed": false 177 | }, 178 | "outputs": [], 179 | "source": [ 180 | "labelerList" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": { 187 | "collapsed": true 188 | }, 189 | "outputs": [], 190 | "source": [] 191 | } 192 | ], 193 | "metadata": { 194 | "kernelspec": { 195 | "display_name": "Python 3", 196 | "language": "python", 197 | "name": "python3" 198 | }, 199 | "language_info": { 200 | "codemirror_mode": { 201 | "name": "ipython", 202 | "version": 3 203 | }, 204 | "file_extension": ".py", 205 | "mimetype": "text/x-python", 206 | "name": "python", 207 | "nbconvert_exporter": "python", 208 | "pygments_lexer": "ipython3", 209 | "version": "3.6.0" 210 | } 211 | }, 212 | "nbformat": 4, 213 | "nbformat_minor": 0 214 | } 215 | -------------------------------------------------------------------------------- /analytics/pair_graph_analysis/src/main/java/edu/umd/hcil/collabortweet/graph/EdgeTuple.java: -------------------------------------------------------------------------------- 1 | package edu.umd.hcil.collabortweet.graph; 2 | 3 | import org.graphstream.graph.Edge; 4 | 5 | /** 6 | * Created by cbuntain on 5/6/17. 7 | */ 8 | public class EdgeTuple implements Comparable { 9 | private String left; 10 | private String right; 11 | 12 | public EdgeTuple(String l, String r) { 13 | left = l; 14 | right = r; 15 | } 16 | 17 | public EdgeTuple(Edge e) { 18 | left = e.getSourceNode().getId(); 19 | right = e.getTargetNode().getId(); 20 | } 21 | 22 | public String getId() { 23 | return String.format("%s->%s", left, right); 24 | } 25 | 26 | public String getLeftId() { 27 | return left; 28 | } 29 | 30 | public String getRightId() { 31 | return right; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return this.getId(); 37 | } 38 | 39 | @Override 40 | public int compareTo(EdgeTuple o) { 41 | return this.getId().compareTo(o.getId()); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return this.getId().hashCode(); 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | return (o != null && o instanceof EdgeTuple) ? o.hashCode() == this.hashCode() : false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /analytics/pair_graph_analysis/src/main/java/edu/umd/hcil/collabortweet/graph/RemoveCyclesDFS.java: -------------------------------------------------------------------------------- 1 | package edu.umd.hcil.collabortweet.graph; 2 | 3 | import org.graphstream.graph.Edge; 4 | import org.graphstream.graph.Graph; 5 | import org.graphstream.graph.Node; 6 | import org.graphstream.graph.implementations.Graphs; 7 | import org.graphstream.graph.implementations.SingleGraph; 8 | import org.graphstream.stream.file.FileSink; 9 | import org.graphstream.stream.file.FileSinkGEXF2; 10 | import org.graphstream.stream.file.FileSource; 11 | import org.graphstream.stream.file.FileSourceGEXF; 12 | 13 | import java.io.IOException; 14 | import java.util.*; 15 | import java.util.stream.Collectors; 16 | 17 | 18 | /** 19 | * Created by cbuntain on 5/4/17. 20 | */ 21 | public class RemoveCyclesDFS { 22 | 23 | public static void main(String[] args) { 24 | 25 | Graph g = new SingleGraph("PairGraph"); 26 | FileSource fs = new FileSourceGEXF(); 27 | 28 | fs.addSink(g); 29 | 30 | try { 31 | String graphPath = args[0]; 32 | 33 | System.out.println(String.format("Reading File: %s", graphPath)); 34 | 35 | fs.readAll(graphPath); 36 | 37 | System.out.println("Read successful."); 38 | } catch( IOException e) { 39 | e.printStackTrace(); 40 | } finally { 41 | fs.removeSink(g); 42 | } 43 | 44 | // g.display(true); 45 | 46 | // EdgeTuple x = new EdgeTuple("123", "456"); 47 | // System.out.println(x); 48 | // System.out.println(x.getId()); 49 | // System.out.println(x.hashCode()); 50 | // EdgeTuple y = new EdgeTuple("123", "456"); 51 | // System.out.println(y); 52 | // System.out.println(y.getId()); 53 | // System.out.println(y.hashCode()); 54 | // 55 | // System.out.println("Equals: " + x.equals(y)); 56 | // 57 | // Set m = new HashSet<>(); 58 | // System.out.println("After INIT:"); 59 | // m.forEach(et -> System.out.println(et)); 60 | // m.add(x); 61 | // System.out.println("After X:"); 62 | // m.forEach(et -> System.out.println(et)); 63 | // m.add(y); 64 | // System.out.println("After Y:"); 65 | // m.forEach(et -> System.out.println(et)); 66 | // System.exit(1); 67 | 68 | System.out.println(String.format("Node Count: %d", g.getNodeCount())); 69 | System.out.println(String.format("Edge Count: %d", g.getEdgeCount())); 70 | 71 | // How many edges did we originally have 72 | int origEdgeCount = g.getEdgeCount(); 73 | 74 | Graph acyclic = createAcyclicGraph(g); 75 | 76 | System.out.println(String.format("Node Count: %d", acyclic.getNodeCount())); 77 | System.out.println(String.format("Edge Count: %d", acyclic.getEdgeCount())); 78 | 79 | int thisEdgeCount = acyclic.getEdgeCount(); 80 | 81 | int thisFASSize = origEdgeCount - thisEdgeCount; 82 | 83 | System.out.println("Feedback Arc Set Size: " + thisFASSize); 84 | 85 | System.out.println("Minimum FAS Size: " + thisFASSize); 86 | 87 | FileSink outSink = new FileSinkGEXF2(); 88 | 89 | try { 90 | String outPath = args[1]; 91 | 92 | System.out.println(String.format("Writing to File: %s", outPath)); 93 | 94 | outSink.writeAll(acyclic, outPath); 95 | 96 | System.out.println("Write successful."); 97 | } catch( IOException e) { 98 | e.printStackTrace(); 99 | } finally { 100 | fs.removeSink(g); 101 | } 102 | } 103 | 104 | public static Graph createAcyclicGraph(Graph g) { 105 | 106 | // Find a maximal acyclic arc set from each node 107 | // List> arcSets = g.getNodeSet().stream().map(node -> { 108 | // Set originSet = new HashSet(); 109 | // originSet.add(node.getId()); 110 | // 111 | // return recursiveDfs(node, originSet, 0); 112 | // }).collect(Collectors.toList()); 113 | List> arcSets = new ArrayList<>(); 114 | for ( Node node : g.getNodeSet() ) { 115 | Set originSet = new HashSet<>(); 116 | originSet.add(node.getId()); 117 | 118 | Set badEdges = recursiveDfs(node, originSet, 0); 119 | arcSets.add(badEdges); 120 | 121 | System.out.println("Does node [" + node + "] Have cycles: " + badEdges.size()); 122 | badEdges.forEach(et -> System.out.println(et)); 123 | } 124 | 125 | // Find the min set size 126 | int minSetSize = Integer.MAX_VALUE; 127 | Set minArcSet = null; 128 | for ( Set acyclicArcSet : arcSets ) { 129 | int setSize = acyclicArcSet.size(); 130 | if ( setSize < minSetSize ) { 131 | minSetSize = setSize; 132 | minArcSet = acyclicArcSet; 133 | } 134 | } 135 | 136 | System.out.println("Minimum Feedback Arc Set Size: " + minSetSize); 137 | 138 | // Create a new graph with no error checking and activated auto-node-creation 139 | Graph acyclicGraph = new SingleGraph("Acyclic", false, true); 140 | // for ( EdgeTuple e : maxArcSet ) { 141 | // String sourceId = e.getLeftId(); 142 | // String destId = e.getRightId(); 143 | // String edgeId = String.format("%s->%s", sourceId, destId); 144 | // 145 | // acyclicGraph.addEdge(edgeId, sourceId, destId, true); 146 | // } 147 | 148 | return acyclicGraph; 149 | } 150 | 151 | private static Set recursiveDfs(Node origin, Set visited, int depth) { 152 | 153 | // for ( int i=0; i outgoingEdges = origin.getLeavingEdgeSet().stream(). 159 | filter(e -> !visited.contains(e.getTargetNode().getId())).collect(Collectors.toList()); 160 | 161 | if ( outgoingEdges.size() == 0 ) { 162 | return new HashSet<>(); 163 | } 164 | 165 | List> badEdgeList = new ArrayList<>(); 166 | for ( Edge e : origin.getLeavingEdgeSet() ) { 167 | Set badEdges = new HashSet<>(); 168 | 169 | Node target = e.getTargetNode(); 170 | 171 | // If we have not visited this node before, keep this edge, and 172 | // traverse this node 173 | if ( !visited.contains(target.getId()) ) { 174 | 175 | Set localVisited = new HashSet<>(visited); 176 | localVisited.add(target.getId()); 177 | 178 | // Add all further bad edges 179 | badEdges.addAll(recursiveDfs(target, localVisited, depth + 1)); 180 | } else { 181 | // System.out.println("Edge Would Have Induced Cycle: " + e.toString()); 182 | badEdges.add(new EdgeTuple(e)); 183 | } 184 | 185 | badEdgeList.add(badEdges); 186 | } 187 | 188 | int minBadEdgeCount = Integer.MAX_VALUE; 189 | Set minBadEdges = null; 190 | for ( Set cycleEdges : badEdgeList ) { 191 | int localDelCount = cycleEdges.size(); 192 | 193 | if ( localDelCount == 0 ) { 194 | continue; 195 | } 196 | 197 | if ( localDelCount < minBadEdgeCount ) { 198 | minBadEdgeCount = localDelCount; 199 | minBadEdges = cycleEdges; 200 | } 201 | } 202 | 203 | return minBadEdges; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /analytics/pair_graph_analysis/src/main/java/edu/umd/hcil/collabortweet/graph/TestGraph.java: -------------------------------------------------------------------------------- 1 | package edu.umd.hcil.collabortweet.graph; 2 | 3 | import org.graphstream.graph.Edge; 4 | import org.graphstream.graph.Graph; 5 | import org.graphstream.graph.Node; 6 | import org.graphstream.graph.implementations.SingleGraph; 7 | 8 | import java.util.Collection; 9 | 10 | /** 11 | * Created by cbuntain on 5/4/17. 12 | */ 13 | public class TestGraph { 14 | 15 | public static void main(String[] args) { 16 | Graph graph = new SingleGraph("Tutorial 1"); 17 | 18 | graph.addNode("A" ); 19 | graph.addNode("B" ); 20 | graph.addNode("C" ); 21 | graph.addEdge("AB", "A", "B", true); 22 | graph.addEdge("BC", "B", "C", true); 23 | graph.addEdge("CA", "C", "A", true); 24 | 25 | // graph.display(); 26 | 27 | Collection nodeList = graph.getNodeSet(); 28 | Node aNode = graph.getNode("A"); 29 | System.out.println("Edges to A:"); 30 | for ( Edge e : aNode.getLeavingEdgeSet() ) { 31 | System.out.println("\t" + e); 32 | } 33 | 34 | graph.removeEdge("A", "B"); 35 | 36 | System.out.println("Edges to A:"); 37 | for ( Edge e : aNode.getLeavingEdgeSet() ) { 38 | System.out.println("\t" + e); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /bin/SqlToCsv.py: -------------------------------------------------------------------------------- 1 | 2 | # coding: utf-8 3 | 4 | # In[ ]: 5 | import sys 6 | import argparse 7 | import sqlite3 8 | import sklearn.metrics 9 | import statsmodels.stats.inter_rater 10 | import pandas as pd 11 | 12 | 13 | # # Connect to SQLite DB 14 | 15 | # In[ ]: 16 | 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument('--database', default='database.sqlite3') 19 | args = parser.parse_args() 20 | 21 | database_path = args.database 22 | 23 | conn = sqlite3.connect(database_path) 24 | c = conn.cursor() 25 | 26 | 27 | # # Get Labelers 28 | 29 | # In[ ]: 30 | 31 | userMap = {} 32 | for userRow in c.execute("SELECT userId, fname, lname FROM users"): 33 | userName = "%s %s" % (userRow[1], userRow[2]) 34 | userMap[userRow[0]] = userName 35 | print(userRow[0], userName) 36 | #print userMap 37 | 38 | 39 | # # Get Tasks 40 | 41 | # In[ ]: 42 | 43 | for taskRow in c.execute("SELECT taskId, taskName, taskType FROM tasks"): 44 | print("Task ID:", taskRow[0], "Type:", taskRow[2], "Name:", taskRow[1]) 45 | 46 | 47 | # # Convert SQL Labels to CSV File 48 | # 49 | # Each task will be dumped to a CSV file. Each CSV file will contain rows, one row for each user-label pair for each labeled element in that task. 50 | 51 | # In[ ]: 52 | 53 | taskRows = c.execute("SELECT taskId, taskName FROM tasks WHERE taskType == 2").fetchall() 54 | 55 | for r in taskRows: 56 | taskName = r[1] 57 | taskId = r[0] 58 | 59 | # Task name 60 | print("Task Name:", taskName) 61 | 62 | # Get the number of elements you want to label 63 | eCount = c.execute("SELECT COUNT(*) FROM elements WHERE taskId = ?", (taskId, )).fetchone() 64 | print("Number of Elements to Label:", eCount[0]) 65 | 66 | # How many labels do we actually have? 67 | # Each user contributes a label, so this number could be at max: 68 | # number of labelers * number of elements to label 69 | lCount = c.execute("SELECT COUNT(*) " + \ 70 | "FROM elementLabels el JOIN elements e " + \ 71 | "ON e.elementId = el.elementId " + \ 72 | "WHERE e.taskId = ?", 73 | (taskId, )).fetchone() 74 | print("Number of Labels:", lCount[0]) 75 | 76 | # Print the users who participated in this task 77 | userLabelCounts = {} 78 | print("Users who Labeled:") 79 | labelerList = c.execute("SELECT DISTINCT userId " + \ 80 | "FROM elementLabels el JOIN elements e " + \ 81 | "ON e.elementId = el.elementId " + \ 82 | "WHERE e.taskId = ?", 83 | (taskId, )).fetchall() 84 | labelerList = list(map(lambda x: x[0], labelerList)) 85 | for labelerId in labelerList: 86 | name = userMap[labelerId] 87 | 88 | userCount = c.execute("SELECT COUNT(*) " + \ 89 | "FROM elementLabels el JOIN elements e " + \ 90 | "ON e.elementId = el.elementId " + \ 91 | "WHERE e.taskId = ? AND el.userId = ?", 92 | (taskId, labelerId)).fetchone() 93 | userLabelCounts[labelerId] = userCount[0] 94 | print("\t", name, " - ", userCount[0], "Labels") 95 | 96 | 97 | # Get the tweet IDs for each label 98 | dataSamples = [] 99 | elementAndLabelList = c.execute("SELECT el.elementId, el.labelId, el.userId, " + \ 100 | "e.externalId, l.labelText, l.taskId, e.elementText, " + \ 101 | "u.screenname, l.parentLabel, pl.labelText " + \ 102 | "FROM elementLabels el JOIN elements e " + \ 103 | "ON e.elementId = el.elementId " + \ 104 | "JOIN labels l ON el.labelId = l.labelId " + \ 105 | "JOIN users u ON el.userId = u.userId " + \ 106 | "LEFT OUTER JOIN labels pl ON l.parentLabel = pl.labelId " + \ 107 | "WHERE e.taskId = ?", (taskId, )).fetchall() 108 | # For each element in the row, convert it to a map and add it to our 109 | # list of samples. NOTE: If you want to add text here 110 | for row in elementAndLabelList: 111 | dataSample = { 112 | "external_id": row[3], 113 | "label": row[4], 114 | "parentLabel": row[9], 115 | "user_id": row[2], 116 | "username": row[7], 117 | "text": row[6], 118 | } 119 | dataSamples.append(dataSample) 120 | 121 | 122 | labelDf = pd.DataFrame(dataSamples) 123 | 124 | # Add "text" to the list below to add text to the resulting CSV 125 | if labelDf.shape[0] > 0: 126 | labelDf[["external_id", "user_id", "username", "label", "parentLabel"]].sort_values(by="external_id").to_csv("task_id_%03d.csv" % taskId, index=False) 127 | else: 128 | print("Nothing to export") 129 | 130 | 131 | 132 | # In[ ]: 133 | 134 | labelerList 135 | 136 | 137 | # In[ ]: 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /bin/add_tweets_to_task.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script imports tweets into an exsisting collabortweet task 3 | 4 | If the tweet is 5 | still available on Twitter, the html for the full rendered tweet (as it would 6 | appear on Twitter) is extracted from the Twitter oEmbed API (see 7 | https://developer.twitter.com/en/docs/twitter-for-websites/embedded-tweets/overview.html) 8 | 9 | Author: Fridolin Linder 10 | 11 | Usage: 12 | $> python add_tweets_to_task.py --sqlite_path [sql_path] 13 | --data_path [tweet_file_path] --task_id [task_id] 14 | 15 | Arguments: 16 | sqlite_path: sqlite database file (default `database.sqlite3`) 17 | data_path: json file containing one tweet per line in twitter format 18 | task_id: id of the task the tweets shoud be added to 19 | ''' 20 | import json 21 | import argparse 22 | import sqlite3 23 | 24 | from utils import read_tweet 25 | 26 | if __name__ == '__main__': 27 | parser = argparse.ArgumentParser( 28 | description='imports tweets into an exsisting collabortweet task' 29 | ) 30 | parser.add_argument('--sqlite_path') 31 | parser.add_argument('--data_path') 32 | parser.add_argument('--task_id') 33 | args = parser.parse_args() 34 | 35 | # Store the commandline arguments passed to the script 36 | SQLITE_PATH = args.sqlite_path 37 | TWEET_PATH = args.data_path 38 | TASK_ID = int(args.task_id) 39 | 40 | conn = sqlite3.connect(SQLITE_PATH) 41 | c = conn.cursor() 42 | 43 | # For every tweet in the input json, generate extract the html and id 44 | tweet_list = [] 45 | with open(TWEET_PATH, "r") as infile: 46 | for i, line in enumerate(infile): 47 | try: 48 | tweet = json.loads(line) 49 | except json.JSONDecodeError: 50 | print('Json decode error in line {}. Skipped'.format(i)) 51 | continue 52 | 53 | 54 | # CB 20200326: Adding this check to remove instances 55 | # where we have a retweeted_status and quoted_status 56 | # field but they are NONE. This can happen if the data 57 | # source of the tweets embed these fields because some 58 | # tweets have them. E.g., Pandas does this when you read 59 | # in tweets to a DataFrame and then export them to JSON 60 | if "retweeted_status" in tweet and tweet["retweeted_status"] is None: 61 | tweet.pop("retweeted_status") 62 | if "quoted_status" in tweet and tweet["quoted_status"] is None: 63 | tweet.pop("quoted_status") 64 | 65 | # Now process the tweet as normal 66 | tweet_html, tweet_id = read_tweet(tweet) 67 | 68 | if tweet_html is None: 69 | print("Skipping:", line) 70 | continue 71 | 72 | tweet_list.append((tweet_html, tweet_id)) 73 | 74 | element_list = [(TASK_ID, x[0], x[1]) for x in tweet_list] 75 | element_ids = [] 76 | for el_tup in element_list: 77 | c.execute('INSERT INTO elements (taskId, elementText, externalId) ' 78 | 'VALUES (?,?,?)', el_tup) 79 | el_id = c.lastrowid 80 | element_ids.append(el_id) 81 | 82 | print("Element Count:", len(element_ids)) 83 | 84 | conn.commit() 85 | conn.close() 86 | -------------------------------------------------------------------------------- /bin/add_users.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Import a single user or a list of users to the application 3 | Author: Fridolin Linder 4 | ''' 5 | import argparse 6 | import sqlite3 7 | import pandas as pd 8 | 9 | if __name__ == '__main__': 10 | 11 | # Parse commandline arguments 12 | parser = argparse.ArgumentParser( 13 | description='Ad a single user or multiple users to the application' 14 | ) 15 | parser.add_argument('--sqlite_path') 16 | parser.add_argument('--users_file') 17 | parser.add_argument('--screenname') 18 | parser.add_argument('--password') 19 | parser.add_argument('--first_name') 20 | parser.add_argument('--last_name') 21 | parser.add_argument('--admin') 22 | args = parser.parse_args() 23 | 24 | # Establish database connection 25 | conn = sqlite3.connect(args.sqlite_path) 26 | c = conn.cursor() 27 | 28 | # Add many users from a csv file 29 | if args.users_file is not None: 30 | user_data = [] 31 | users = pd.read_csv(args.users_file) 32 | for index, user in users.iterrows(): 33 | user_data.append([x for x in user]) 34 | c.executemany( 35 | ('INSERT INTO users (screenname, password, fname, lname) VALUES ' 36 | '(?, ?, ?, ?)'), 37 | user_data 38 | ) 39 | # Add a single user from cli 40 | else: 41 | user_data = (args.screenname, args.password, args.first_name, 42 | args.last_name, args.admin) 43 | c.execute( 44 | ('INSERT INTO users (screenname, password, fname, lname, isadmin) VALUES (?,' 45 | '?, ?, ?, ?)'), 46 | user_data 47 | ) 48 | 49 | conn.commit() 50 | conn.close() 51 | -------------------------------------------------------------------------------- /bin/assignTask.py: -------------------------------------------------------------------------------- 1 | '''CLI script to assign task to user from collabortweet 2 | 3 | 4 | Allows admin to assign tasks to non-admin users, 5 | for the purpose of restricting views 6 | 7 | Arguments: 8 | database: path to `.sqlite3` database file to delet task from. 9 | task_id: id of the task in `database`. 10 | 11 | Authors: Cody Buntain, Fridolin Linder, Ian Rosenberg 12 | ''' 13 | 14 | import sqlite3 15 | import argparse 16 | import sys 17 | 18 | if __name__ == '__main__': 19 | 20 | # ========================================================================== 21 | # Parse commandline arguments 22 | # ========================================================================== 23 | 24 | parser = argparse.ArgumentParser() 25 | parser.add_argument('--database') 26 | parser.add_argument('--user') 27 | parser.add_argument('--task_id') 28 | args = parser.parse_args() 29 | 30 | sqlitePath = args.database 31 | taskId = int(args.task_id) 32 | username = args.user 33 | 34 | # Open the sqlite3 file 35 | conn = sqlite3.connect(sqlitePath) 36 | c = conn.cursor() 37 | 38 | param = (username,) 39 | 40 | c.execute("SELECT * FROM users WHERE screenname = ?", param) 41 | 42 | row = c.fetchall() 43 | 44 | # Are there any users that match the above screenname? 45 | if ( len(row) == 0 ): 46 | print("No user with screen name:", username) 47 | sys.exit(-1) 48 | 49 | 50 | print("Restricting task views for %s Task ID: [%d] from: %s" % (username, taskId, sqlitePath)) 51 | 52 | userID = row[0][0] 53 | 54 | fields = (taskId,userID,taskId,userID,) 55 | 56 | # Assign user a task ID 57 | c.execute("INSERT INTO assignedTasks(assignedTaskId,userId) SELECT ?, ?" 58 | "WHERE NOT EXISTS(SELECT 1 FROM assignedTasks WHERE assignedTaskId = ? AND userId = ?)", fields) 59 | 60 | # Commit 61 | conn.commit() 62 | conn.close() 63 | 64 | print("Task ", taskId, " assigned to user ", username) 65 | -------------------------------------------------------------------------------- /bin/deleteTask.py: -------------------------------------------------------------------------------- 1 | '''CLI script to delete task from collabortweet 2 | 3 | Deletes the task + all records and exsisting labels (!) 4 | 5 | Arguments: 6 | database: path to `.sqlite3` database file to delet task from. 7 | task_id: id of the task in `database`. 8 | 9 | Authors: Cody Buntain, Fridolin Linder 10 | ''' 11 | 12 | import sqlite3 13 | import argparse 14 | 15 | if __name__ == '__main__': 16 | 17 | # ========================================================================== 18 | # Parse commandline arguments 19 | # ========================================================================== 20 | 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument('--database') 23 | parser.add_argument('--task_id') 24 | args = parser.parse_args() 25 | 26 | sqlitePath = args.database 27 | taskId = int(args.task_id) 28 | 29 | print("Deleting Task [%d] from: %s" % (taskId, sqlitePath)) 30 | 31 | # Open the sqlite3 file 32 | conn = sqlite3.connect(sqlitePath) 33 | c = conn.cursor() 34 | 35 | # Delete all element labels 36 | c.execute("DELETE FROM elementLabels WHERE labelId IN" 37 | " (SELECT l.labelId FROM labels l WHERE l.taskId = :taskId)", 38 | {"taskId": taskId}) 39 | 40 | # Delete all elements 41 | c.execute("DELETE FROM elements WHERE taskId = :taskId", 42 | {"taskId": taskId}) 43 | 44 | # Delete all labels 45 | c.execute("DELETE FROM labels WHERE taskId = :taskId", 46 | {"taskId": taskId}) 47 | 48 | # Delete task 49 | c.execute("DELETE FROM tasks WHERE taskId = :taskId", 50 | {"taskId": taskId}) 51 | 52 | # Commit 53 | conn.commit() 54 | conn.close() 55 | 56 | print("Task Deleted.") 57 | -------------------------------------------------------------------------------- /bin/embed_tweets_2_sql.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script imports tweets into the collabortweet platform. 3 | 4 | If the tweet is 5 | still available on Twitter, the html for the full rendered tweet (as it would 6 | appear on Twitter) is extracted from the Twitter oEmbed API (see 7 | https://developer.twitter.com/en/docs/twitter-for-websites/embedded-tweets/overview.html) 8 | 9 | Author: Cody Buntain (creator), Fridolin Linder (modifications) 10 | 11 | Arguments: 12 | task_path: json file with task data (see collabortweet 13 | documentation) 14 | sqlite_path: sqlite database file (default `database.sqlite3`) 15 | data_path: json file containing one tweet per line in either twitter 16 | API format or GNIP format 17 | 18 | Usage: 19 | $> python embed_tweets_2_sql.py --task_path [task_description_path] 20 | --sqlite_path [sqlite_file_path] --data_path [tweet_json_path] 21 | ''' 22 | import sqlite3 23 | import json 24 | import itertools 25 | import random 26 | import argparse 27 | 28 | from utils import read_tweet, insert_labels, insert_ranges 29 | 30 | if __name__ == '__main__': 31 | # Store the commandline arguments passed to the script 32 | parser = argparse.ArgumentParser( 33 | description='Create new task and import tweets.' 34 | ) 35 | parser.add_argument('--task_path') 36 | parser.add_argument('--sqlite_path') 37 | parser.add_argument('--data_path') 38 | parser.add_argument('--pair_count', default=None) 39 | args = parser.parse_args() 40 | 41 | TASK_DESC_PATH = args.task_path 42 | SQLITE_PATH = args.sqlite_path 43 | TWEET_PATH = args.data_path 44 | 45 | # If pairwise task get pair count 46 | pair_count = args.pair_count 47 | if pair_count is not None: 48 | pair_count = int(pair_count) 49 | 50 | # Load the task metadata from taskdescription 51 | with open(TASK_DESC_PATH, "r") as infile: 52 | task_desc = json.load(infile) 53 | print(task_desc) 54 | 55 | # For every tweet in the input json, generate extract the html and id 56 | tweetList = [] 57 | with open(TWEET_PATH, "r") as infile: 58 | for i, line in enumerate(infile): 59 | try: 60 | tweet = json.loads(line) 61 | except json.JSONDecodeError: 62 | print('Json decode error in line {}. Skipped'.format(i)) 63 | continue 64 | 65 | # CB 20200326: Adding this check to remove instances 66 | # where we have a retweeted_status and quoted_status 67 | # field but they are NONE. This can happen if the data 68 | # source of the tweets embed these fields because some 69 | # tweets have them. E.g., Pandas does this when you read 70 | # in tweets to a DataFrame and then export them to JSON 71 | if "retweeted_status" in tweet and tweet["retweeted_status"] is None: 72 | tweet.pop("retweeted_status") 73 | if "quoted_status" in tweet and tweet["quoted_status"] is None: 74 | tweet.pop("quoted_status") 75 | 76 | # Now process the tweet as normal 77 | tweet_html, tweet_id = read_tweet(tweet) 78 | 79 | if tweet_html is None: 80 | print("Skipping:", line) 81 | continue 82 | 83 | tweetList.append((tweet_html, tweet_id)) 84 | 85 | # Insert the data into the database 86 | conn = sqlite3.connect(SQLITE_PATH) 87 | c = conn.cursor() 88 | 89 | c.execute('INSERT INTO tasks (taskName, question, taskType) VALUES (:name,' 90 | ':question, :type)', task_desc) 91 | task_id = c.lastrowid 92 | print("Task ID:", task_id) 93 | 94 | element_list = [(task_id, x[0], x[1]) for x in tweetList] 95 | element_ids = [] 96 | for el_tup in element_list: 97 | c.execute('INSERT INTO elements (taskId, elementText, externalId) ' 98 | 'VALUES (?,?,?)', el_tup) 99 | el_id = c.lastrowid 100 | element_ids.append(el_id) 101 | 102 | print("Element Count:", len(element_ids)) 103 | 104 | # Only create pairs if the task type == 1 (i.e. pairwise comparison task) 105 | if task_desc["type"] == 1: 106 | # Create the pairs 107 | pair_list = None 108 | 109 | # If we didn't specify a number of pairs, find all 110 | if pair_count is None: 111 | pair_list = itertools.combinations(element_ids, 2) 112 | else: # Otherwise, randomly select k pairs 113 | pair_accum = set() 114 | for e_index, e_id in enumerate(element_ids): 115 | start_index = max(0, e_index-1) 116 | others = element_ids[:start_index] + element_ids[e_index+1:] 117 | 118 | # Put the pair in canonical order to avoid duplicates 119 | new_pairs = set(map(lambda x: (min(e_id, x), max(e_id, x)), 120 | random.sample(others, pair_count))) 121 | 122 | pair_accum = pair_accum.union(new_pairs) 123 | 124 | pair_list = list(pair_accum) 125 | 126 | pair_list = [(task_id, x[0], x[1]) for x in pair_list] 127 | print("Pair Count:", len(pair_list)) 128 | 129 | c.executemany('INSERT INTO pairs (taskId, leftElement, rightElement) ' 130 | 'VALUES (?,?,?)', pair_list) 131 | 132 | # If we are dealing with a labeling task (type == 2), insert the labels 133 | elif task_desc["type"] == 2: 134 | 135 | print("Calling label insertion...") 136 | insert_labels(c, task_desc["labels"], task_id) 137 | 138 | elif task_desc["type"] == 3: 139 | print("Calling range insertion...") 140 | insert_ranges(c, task_id, task_desc["name"], task_desc["questions"]) 141 | 142 | # Otherwise, we have an invalid task type 143 | else: 144 | conn.close() 145 | raise ValueError( 146 | "ERROR! Task type '{}' is not valid!".format(task_desc["type"])) 147 | 148 | conn.commit() 149 | conn.close() 150 | -------------------------------------------------------------------------------- /bin/partition_data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script takes a sample of tweets in json format and splits them up for 3 | labeling by multiple labelers. 4 | 5 | Given the inputs, the data is split up into equal sized task datasets for each 6 | of the [n_labelers] labeler. Each task dataset also contains a overlapping set 7 | of evaluation tweets of size [n_evaluation_tweets] that will also be stored 8 | separately. 9 | 10 | Arguments: 11 | input_data: json file with one tweet per line. 12 | n_partitions: The number of 'workers' for the task. 13 | n_eval: The number of overlapping tweets between labelers for 14 | evaluation. 15 | ouput_prefix: Prefix for the filenames for the output files (see below). 16 | seed: optional, seed to use to split up the files randomly if not given 17 | default value 88332 is used for replicability. 18 | Returns: 19 | - [output_prefix]_eval.json 20 | - [output_prefix]_partition_[x].json for x in n_labelers 21 | 22 | Usage: 23 | python partition_data.py --input_data [input_file] 24 | --n_partitions [n] --n_eval [n] --output_prefix 25 | [prefix] 26 | 27 | Author: Fridolin Linder 28 | ''' 29 | import argparse 30 | import json 31 | 32 | import numpy as np 33 | 34 | def random_partition(lst, n): 35 | '''Partition a list into n (near) equal length partitions 36 | adapted from: https://stackoverflow.com/questions/2659900/ 37 | python-slicing-a-list-into-n-nearly-equal-length-partitions 38 | ''' 39 | np.random.shuffle(lst) 40 | division = len(lst) / n 41 | return [lst[round(division*i):round(division*(i+1))] for i in range(n)] 42 | 43 | 44 | if __name__ == "__main__": 45 | 46 | # ========================================================================== 47 | # Parse commandline arguments 48 | # ========================================================================== 49 | 50 | parser = argparse.ArgumentParser( 51 | description='Draw random sample from collection' 52 | ) 53 | parser.add_argument('--input_data') 54 | parser.add_argument('--n_partitions') 55 | parser.add_argument('--n_eval') 56 | parser.add_argument('--output_prefix') 57 | parser.add_argument('--seed', default=88332) 58 | args = parser.parse_args() 59 | # ========================================================================== 60 | 61 | np.random.seed(args.seed) 62 | 63 | n_eval = int(args.n_eval) 64 | n_partitions = int(args.n_partitions) 65 | input_data = args.input_data 66 | output_prefix = args.output_prefix 67 | 68 | # Count the number of lines in the input file (and check if valid) 69 | print('Checking input file...') 70 | with open(input_data) as infile: 71 | for i, line in enumerate(infile): 72 | tweet = json.loads(line) 73 | 74 | n_tweets = i + 1 75 | all_idxs = set(range(0, n_tweets)) 76 | print(f'Number of tweets: {n_tweets}') 77 | 78 | # Assign eval indices: 79 | eval_idxs = set(np.random.choice(list(all_idxs), n_eval, replace=False)) 80 | 81 | # Assign what remains as to be split up between workers 82 | rem = all_idxs - eval_idxs 83 | partitions = random_partition(list(rem), n_partitions) 84 | 85 | # Add the eval tweets to each partition 86 | pl = [str(len(x)) + f'(+{n_eval})' for x in partitions] 87 | partitions = [p + list(eval_idxs) for p in partitions] 88 | print(f'Generating {len(partitions)} partitions of sizes(+eval size): {pl}') 89 | 90 | # Create a mapping of index --> partition file connection to write output 91 | 92 | ## Open file connections for all parts 93 | part_conns = [open(output_prefix + f'_partition_{p}.json', 'w') 94 | for p in range(n_partitions)] 95 | eval_conn = open(output_prefix + '_eval.json', 'w') 96 | 97 | ## Map indices to connections according to partitioning 98 | index = {} 99 | for p, partition in enumerate(partitions): 100 | for idx in partition: 101 | if idx not in index: 102 | index[idx] = [part_conns[p]] 103 | else: 104 | index[idx].append(part_conns[p]) 105 | 106 | for idx in eval_idxs: 107 | index[idx].append(eval_conn) 108 | 109 | # Go through input and write to connections 110 | with open(input_data) as infile: 111 | for idx, line in enumerate(infile): 112 | for conn in index[idx]: 113 | conn.write(line) 114 | 115 | for p_con in part_conns: 116 | p_con.close() 117 | eval_conn.close() 118 | -------------------------------------------------------------------------------- /bin/retrieve_partitioned_task.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script extracts labels from the database and evaluates coder quality 3 | using all labeled elements that overlap between all coders. 4 | 5 | Author: Fridolin Linder 6 | 7 | Arguments: 8 | db_path: str, path of the sqlite databasefile 9 | task_ids: iterable of integers, Collabortweet task ID(s) 10 | gold_standard_coder: str, Collabortweet screenname for the gold standard coder. If omitted, 11 | no evaluation is done and all labeled elements are stored in `output`. 12 | output: str, path where to store the output csv file 13 | time_cutoff: int, optional, seconds to use as cutoff for the labeling time for each element 14 | (default is 60*3) 15 | 16 | Returns: 17 | Prints evaluation metrics and summary stats for every coder. If gold standard coder is provided 18 | (`--gold_standard_coder`) accuracy in recovering this coder's labels is reported as well. 19 | The curated data is returned in csv format with columns: 20 | - 'externalId': Id of the element 21 | - 'label': Label given by coder (if multiple coders labled the element the label according 22 | to plurality vote is given) 23 | - 'entropy': The shannon entropy of the labels for elements with multiple coders, NA for 24 | elements with single coder 25 | ''' 26 | 27 | import itertools 28 | import copy 29 | import argparse 30 | import sqlite3 31 | import numpy as np 32 | import pandas as pd 33 | from statsmodels.stats.inter_rater import cohens_kappa 34 | from sklearn.metrics import confusion_matrix, accuracy_score 35 | from scipy.stats import entropy 36 | 37 | def print_summary(title, data): 38 | print('+'*80) 39 | print(f'{title}:') 40 | print('+'*80) 41 | print(data) 42 | print('+'*80) 43 | print('\n') 44 | 45 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 46 | # Parse input arguments 47 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 48 | parser = argparse.ArgumentParser() 49 | parser.add_argument('--db_path', type=str) 50 | parser.add_argument('--task_ids', nargs='+', type=int) 51 | parser.add_argument('--gold_standard_coder', type=str, default=None) 52 | parser.add_argument('--output', type=str) 53 | parser.add_argument('--time_cutoff', type=int, default=180) 54 | args = parser.parse_args() 55 | 56 | DB_PATH = args.db_path 57 | CUTOFF = args.time_cutoff 58 | TASK_IDS = args.task_ids 59 | EVAL_USER = args.gold_standard_coder 60 | OUTPATH = args.output 61 | 62 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 63 | 64 | # Don't truncate pandas dataframes when printing coder stats 65 | pd.set_option('display.max_colwidth', -1) 66 | pd.set_option('display.max_columns', None) 67 | 68 | # Get the raw labels from the database 69 | db_connection = sqlite3.connect(DB_PATH) 70 | query = ''' 71 | SELECT elements.elementId, externalId, 72 | elements.taskId, 73 | elementLabels.userId, 74 | elementLabels.labelId, 75 | elementText, 76 | screenname, 77 | labelText, 78 | elementLabels.time 79 | FROM elementLabels 80 | LEFT JOIN elements ON elementLabels.elementId = elements.elementId 81 | LEFT JOIN users ON elementLabels.userId = users.userId 82 | LEFT JOIN labels ON elementLabels.labelId = labels.labelId 83 | WHERE elements.taskId IN ({task_ids}); 84 | '''.format(task_ids=','.join([str(x) for x in TASK_IDS])) 85 | labels = pd.read_sql_query(query, db_connection) 86 | 87 | # Remove duplicated element/coder pairs (collabortweet artifacts) 88 | labels = labels[~labels.duplicated(subset=['externalId', 'screenname'])] 89 | coders = labels['screenname'].unique() 90 | 91 | # Number of labels per coder and overlap with every other coder 92 | ccols = labels[['externalId', 'screenname', 'labelText']].pivot( 93 | index='externalId', columns='screenname', values='labelText' 94 | ) 95 | ccols = ccols.notna() 96 | out = [] 97 | for coder in coders: 98 | tempdf = ccols[ccols[coder]] 99 | o = tempdf.sum(axis=0) 100 | o['screenname'] = coder 101 | out.append(o) 102 | sumdf = pd.DataFrame(out) 103 | sumdf = sumdf[['screenname'] + list(coders)] 104 | sumdf.set_index('screenname', inplace=True, drop=True) 105 | for i in range(0, sumdf.shape[0]): 106 | for j in range(0, sumdf.shape[1]): 107 | if i > j: 108 | sumdf.iloc[i, j] = np.nan 109 | sumdf['total'] = sumdf.max(axis=0) 110 | print_summary('Number of coded elements per coder and overlap with other coders', sumdf) 111 | 112 | # Calculate average time per tweet for each labeler 113 | labels['time_delta'] = pd.to_datetime(labels['time']).diff() 114 | 115 | # Remove time deltas larger than CUTOFF 116 | CUTOFF = 60*3 117 | time_df = copy.copy(labels.dropna()) 118 | time_df['time_delta'] = [x.seconds for x in time_df['time_delta']] 119 | time_df = time_df[time_df['time_delta'] <= CUTOFF] 120 | time_df = time_df[['screenname', 'time_delta']].groupby('screenname')\ 121 | .agg(['median', 'mean']) 122 | 123 | time_df.reset_index(inplace=True, drop=False) 124 | time_df.columns = ['coder', 'median_time', 'mean_time'] 125 | time_df.set_index('coder', drop=True, inplace=True) 126 | print_summary('Time spent on coding task', time_df) 127 | 128 | # Calculate cohens cappa for all coder pairs 129 | kappas = [] 130 | for coder_pair in itertools.combinations(coders, 2): 131 | tempdf = labels[labels['screenname'].isin(coder_pair)] 132 | label_counts = tempdf[['externalId', 'screenname']].groupby('externalId').count() 133 | overlapping_ids = label_counts[label_counts['screenname'] == 2].index 134 | if len(overlapping_ids) == 0: 135 | kappa = np.nan 136 | else: 137 | eval_df = tempdf.pivot(index='externalId', columns='screenname', values='labelText') 138 | eval_df = eval_df.loc[overlapping_ids] 139 | cm = confusion_matrix(eval_df[[coder_pair[0]]], eval_df[[coder_pair[1]]]) 140 | kappa = cohens_kappa(cm) 141 | kappas.append((*coder_pair, kappa['kappa'], len(overlapping_ids))) 142 | kappas = pd.DataFrame(kappas, columns=['coder_1', 'coder_2', 'kappa', 'overlapping_elements']) 143 | kappas.set_index(['coder_1', 'coder_2'], drop=True, inplace=True) 144 | print_summary("Cohen's Kappa for all coder pairs", kappas) 145 | 146 | avg_kappas = [] 147 | kappas.reset_index(drop=False, inplace=True) 148 | for coder in coders: 149 | val = kappas[(kappas['coder_1'] == coder) | 150 | (kappas['coder_2'] == coder)]['kappa'].mean() 151 | avg_kappas.append((coder, val)) 152 | avg_kappas = pd.DataFrame(avg_kappas, columns=['coder', 'avg_kappa']) 153 | avg_kappas.set_index('coder', inplace=True, verify_integrity=True) 154 | print_summary("Average Cohen's Kappa by coder", avg_kappas) 155 | 156 | if EVAL_USER is not None: 157 | # Give overview of how many tweets were labeled by each coder + overlap with Gold standard 158 | gs_ids = set(labels[labels['screenname'] == EVAL_USER]['externalId']) 159 | out = [] 160 | for coder in coders: 161 | if coder == EVAL_USER: 162 | continue 163 | # Get the number of labels provided by this coder 164 | coder_ids = set(labels[labels['screenname'] == coder]['externalId']) 165 | 166 | # Get the ids that overlap with gold standard (if any) 167 | overlapping_ids = coder_ids.intersection(gs_ids) 168 | 169 | if len(overlapping_ids) > 0: 170 | # Calculate the % agreement on the overlapping labels 171 | tempdf = labels[labels['screenname'].isin([coder, EVAL_USER])] 172 | tempdf = tempdf[tempdf['externalId'].isin(overlapping_ids)] 173 | tempdf = tempdf.pivot(index='externalId', columns='screenname', values='labelText') 174 | acc = accuracy_score(tempdf[EVAL_USER], tempdf[coder]) 175 | else: 176 | acc = np.nan 177 | 178 | # Get the stats on overlapping and total coded elements 179 | out.append({'coder': coder, 180 | 'gold_standard_accuracy': acc, 181 | 'total': len(coder_ids), 182 | 'unique': len(coder_ids.difference(gs_ids)), 183 | 'overlap_gold_standard': len(overlapping_ids),}) 184 | 185 | sumdf = pd.DataFrame(out) 186 | # Change col order and row index for nicer display 187 | #sumdf = sumdf[['coder', 'total', 'unique', 'overlap_gold_standard']] 188 | sumdf.set_index('coder', drop=True, inplace=True) 189 | print_summary(f'Comparison to gold standard coder ({EVAL_USER})', sumdf) 190 | 191 | label_count = labels[['externalId', 'labelText']].groupby('externalId').count() 192 | multi_labeled_ids = label_count[label_count['labelText'] > 1].index 193 | multi_labeled = labels[labels['externalId'].isin(multi_labeled_ids)] 194 | 195 | multi_labeled = multi_labeled.pivot(index='externalId', columns='screenname', values='labelText') 196 | 197 | # Create the labels output 198 | 199 | ## Get the plurality vote for the overlapping elements 200 | eval_label_counts = multi_labeled.apply(lambda row: row.value_counts(), axis=1) 201 | 202 | eval_label_counts.fillna(0, inplace=True) 203 | eval_labels = pd.DataFrame(eval_label_counts.idxmax(axis=1), columns=['label']) 204 | eval_labels['entropy'] = eval_label_counts.apply( 205 | lambda row: entropy(row/len(coders)), axis=1 206 | ) 207 | eval_labels.columns = ['label', 'entropy'] 208 | 209 | ## Extract the labels for the rest of the data 210 | final_labels = labels[~labels['externalId'].isin(multi_labeled_ids)][['externalId', 'labelText']] 211 | final_labels['entropy'] = np.nan 212 | final_labels.set_index('externalId', inplace=True) 213 | final_labels.columns = ['label', 'entropy'] 214 | final_labels = pd.concat([eval_labels, final_labels]) 215 | 216 | print(f'Storing output data in {OUTPATH}') 217 | final_labels.to_csv(OUTPATH) 218 | -------------------------------------------------------------------------------- /bin/tweets2sql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sqlite3 4 | import sys 5 | import json 6 | import html 7 | import codecs 8 | import itertools 9 | import random 10 | 11 | from utils import insert_labels 12 | from utils import insert_ranges 13 | 14 | taskDescPath = sys.argv[1] 15 | sqlitePath = sys.argv[2] 16 | tweetPath = sys.argv[3] 17 | 18 | pairCount = None 19 | if ( len(sys.argv) > 4 ): 20 | pairCount = int(sys.argv[4]) 21 | 22 | taskDesc = {} 23 | with codecs.open(taskDescPath, "r", "utf8") as inFile: 24 | taskDesc = json.load(inFile) 25 | 26 | print(taskDesc) 27 | 28 | tweetList = [] 29 | 30 | # Get the contents of this tweet 31 | def readTweet(tweetObj): 32 | tweetText = None 33 | tweetId = None 34 | 35 | if ( "text" in tweetObj ): # Twitter format 36 | tweetText = "%s - %s" % (tweetObj["user"]["screen_name"], tweetObj["text"]) 37 | tweetId = tweetObj["id"] 38 | elif ( "body" in tweetObj ): # Gnip Format 39 | tweetText = "%s - %s" % (tweetObj["actor"]["preferredUsername"], tweetObj["body"]) 40 | idstr = tweetObj["id"] 41 | tweetId = idstr[idstr.rfind(":")+1:] 42 | 43 | htmlText = "
" + html.escape(tweetText) + "
" 44 | 45 | return (tweetText, tweetId) 46 | 47 | with codecs.open(tweetPath, "r", "utf8") as inFile: 48 | line_count = 0 49 | for line in inFile: 50 | 51 | # Error catching for when the input file has an issue 52 | tweet = None 53 | try: 54 | tweet = json.loads(line) 55 | except Exception as e: 56 | print("Error on line: %d" % line_count) 57 | print("Line:\n%s\n" % line) 58 | raise e 59 | 60 | # Track the line we're reading for more informative errors 61 | line_count += 1 62 | 63 | # CB 20200326: Adding this check to remove instances 64 | # where we have a retweeted_status and quoted_status 65 | # field but they are NONE. This can happen if the data 66 | # source of the tweets embed these fields because some 67 | # tweets have them. E.g., Pandas does this when you read 68 | # in tweets to a DataFrame and then export them to JSON 69 | if "retweeted_status" in tweet and tweet["retweeted_status"] is None: 70 | tweet.pop("retweeted_status") 71 | if "quoted_status" in tweet and tweet["quoted_status"] is None: 72 | tweet.pop("quoted_status") 73 | 74 | # Now process the tweet as normal 75 | (tweetText, tweetId) = readTweet(tweet) 76 | 77 | if ( tweetText == None ): 78 | print("Skipping:", line) 79 | continue 80 | 81 | tweetList.append((tweetText, tweetId)) 82 | 83 | 84 | # Open the sqlite3 file 85 | conn = sqlite3.connect(sqlitePath) 86 | c = conn.cursor() 87 | 88 | if taskDesc["type"] != 3: 89 | c.execute('INSERT INTO tasks (taskName, question, taskType) VALUES (:name,:question,:type)', 90 | taskDesc) 91 | taskId = c.lastrowid 92 | print("Task ID:", taskId) 93 | elif taskDesc["type"] == 3: 94 | rangeDesc = [taskDesc["name"], "Range-Based Question", 3] 95 | 96 | c.execute('INSERT INTO tasks (taskName, question, taskType) VALUES (?,?,?)', 97 | rangeDesc) 98 | taskId = c.lastrowid 99 | 100 | elementList = [(taskId, x[0], x[1]) for x in tweetList] 101 | elementIds = [] 102 | for elTup in elementList: 103 | c.execute('INSERT INTO elements (taskId, elementText, externalId) VALUES (?,?,?)', 104 | elTup) 105 | elId = c.lastrowid 106 | elementIds.append(elId) 107 | 108 | print( "Element Count:", len(elementIds)) 109 | 110 | 111 | # Only create pairs if the task type == 1 112 | if ( taskDesc["type"] == 1 ): 113 | # Create the pairs 114 | pairList = None 115 | 116 | # If we didn't specify a number of pairs, find all 117 | if ( pairCount == None ): 118 | pairList = itertools.combinations(elementIds, 2) 119 | 120 | else: # Otherwise, randomly select k pairs 121 | pairAccum = set() 122 | 123 | for eIndex in range(len(elementIds)): 124 | eId = elementIds[eIndex] 125 | startIndex = max(0, eIndex-1) 126 | others = elementIds[:startIndex] + elementIds[eIndex+1:] 127 | 128 | # Put the pair in canonical order to avoid duplicates 129 | newPairs = set(map(lambda x: (min(eId, x), max(eId, x)), 130 | random.sample(others, pairCount))) 131 | 132 | pairAccum = pairAccum.union(newPairs) 133 | 134 | pairList = list(pairAccum) 135 | 136 | pairList = [(taskId, x[0], x[1]) for x in pairList] 137 | print ("Pair Count:", len(pairList)) 138 | 139 | c.executemany('INSERT INTO pairs (taskId, leftElement, rightElement) VALUES (?,?,?)', 140 | pairList) 141 | 142 | # If we are dealing with a labeling task (type == 2), insert the labels 143 | elif ( taskDesc["type"] == 2 ): 144 | 145 | print ("Insert labels...") 146 | insert_labels(c, taskDesc["labels"], taskId) 147 | 148 | # If we are dealing with a range task (type == 3), provide the ranges 149 | elif(taskDesc["type"] == 3): 150 | print("Insert range questions, scales and values...") 151 | insert_ranges(c, taskId, taskDesc["name"], taskDesc["questions"]) 152 | 153 | # If we are dealing with a multiple choice task (type == 4), provide the choices 154 | elif ( taskDesc["type"] == 4 ): 155 | 156 | print ("Insert choices...") 157 | insert_labels(c, taskDesc["labels"], taskId) 158 | 159 | 160 | # Otherwise, we have an invalid task type 161 | else: 162 | print ("ERROR! Task type [" + taskDesc["type"] + "] is not valid!") 163 | 164 | conn.commit() 165 | conn.close() 166 | -------------------------------------------------------------------------------- /bin/utils.py: -------------------------------------------------------------------------------- 1 | '''Utilities to process tweets for import into the database 2 | 3 | Authors: Fridolin Linder, Cody Buntain 4 | ''' 5 | import sqlite3 6 | import json 7 | import html 8 | import re 9 | import requests 10 | 11 | def get_embed(username, tweet_id): 12 | '''Use Twitter's embed API endpoint to get the HTML for a tweet 13 | 14 | Arguments: 15 | ---------- 16 | username: str, user's twitter screen name 17 | tweet_id: int, Twitter ID for the Tweet to be embedded 18 | default: str, default return value for tweets that can't be embedded 19 | 20 | Returns: 21 | --------- 22 | If tweet is available through API: str, html of embedded tweet 23 | If tweet is not available: default 24 | ''' 25 | payload = { 26 | "url": html.escape("https://twitter.com/{}/status/{}".format(username, 27 | tweet_id)) 28 | } 29 | req = requests.get('https://publish.twitter.com/oembed', params=payload) 30 | 31 | rendered_html = None 32 | 33 | # Try to get the HTML from Twitter's oEmbed API. 34 | #. we check if we get 200 Status OK code and if the "HTML" key is 35 | #. in the response before extracting it. Deleted tweets return 404, 36 | #. and some tweets return 403, which I assume means tweet is 37 | #. protected. 38 | try: 39 | if req.status_code == 200: 40 | resp = req.json() 41 | if "html" in resp: 42 | rendered_html = resp["html"] # replace default HTML 43 | else: 44 | print("Wrong Code:", req.status_code) 45 | except json.decoder.JSONDecodeError: 46 | print("Error on getting tweet:", tweet_id) 47 | print("Response Code:", req.status_code) 48 | print("Response:", req.text) 49 | 50 | return rendered_html 51 | 52 | def get_field(obj, path, default=None): 53 | '''Get a field from a nested json via a path 54 | 55 | Arguments: 56 | ---------- 57 | obj: dict, object to extract data from 58 | path: str, path of keys to target field in format 'key1/key2/...' 59 | default: value to return if path is not available 60 | ''' 61 | components = path.split('/') 62 | try: 63 | if len(components) == 1: 64 | return obj[components[0]] 65 | elif obj[components[0]] is None: 66 | return default 67 | else: 68 | return get_field(obj=obj[components[0]], 69 | path='/'.join(components[1:]), 70 | default=default) 71 | except KeyError: 72 | return default 73 | 74 | def linkify(text): 75 | '''Takes text and transforms urls to html links''' 76 | regex = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%' 77 | r'[0-9a-fA-F][0-9a-fA-F]))+)') 78 | return regex.sub(r'\1', text) 79 | 80 | def get_tweet_content(tweet): 81 | '''Get the relevant content of a tweet to be displayed to the labeler. 82 | 83 | The content returned depends on the tweet type (see Returns section). 84 | 85 | Arguments: 86 | ---------- 87 | tweet, dict Tweet object (for now in Twitter format only. TODO: make work 88 | with GNIP) 89 | 90 | Returns: 91 | ---------- 92 | dict: 93 | {'type': ['tweet', 'retweet', 'quotetweet', 'retweet_of_quotetweet'] 94 | 'id' 95 | 'author' 96 | 'retweeted_author' 97 | 'quoted_author' 98 | 'replied_author' 99 | 'text', 100 | 'retweeted_text', 101 | 'quoted_text'} 102 | If the field doesn't apply to the tweet the value is None. 103 | ''' 104 | 105 | # Extract data or fill None if not available (see `get_field`) 106 | out = { 107 | 'retweeted_author': get_field( 108 | tweet, 'retweeted_status/user/screen_name'), 109 | 'quoted_author': get_field(tweet, 'quoted_status/user/screen_name'), 110 | 'text': get_field(tweet, 'text'), 111 | 'retweeted_text': get_field(tweet, 'retweeted_status/text'), 112 | 'quoted_text': get_field(tweet, 'quoted_status/text'), 113 | 'author': get_field(tweet, 'user/screen_name'), 114 | 'id': get_field(tweet, 'id'), 115 | 'replied_author': get_field(tweet, 'in_reply_to_screen_name') 116 | } 117 | 118 | # linkify text fields: 119 | for key in out: 120 | if 'text' in key and out[key] is not None: 121 | out[key] = linkify(out[key]) 122 | 123 | # Determine tweet type 124 | rt = 'retweeted_status' in tweet 125 | qt = 'quoted_status' in tweet 126 | if not rt and not qt: 127 | out['type'] = 'tweet' 128 | elif rt and not qt: 129 | if 'quoted_status' in tweet['retweeted_status']: 130 | out['type'] = 'retweet_of_quotetweet' 131 | else: 132 | out['type'] = 'retweet' 133 | elif qt and not rt: 134 | out['type'] = 'quotetweet' 135 | # Weird case where it has both 'retweeted_status' and 'quoted_status' 136 | # I don't completely understand this case (TODO). For now treated as 137 | # retweet 138 | else: 139 | out['type'] = 'retweet_of_quotetweet' 140 | 141 | return out 142 | 143 | def read_tweet(tweet): 144 | '''Extract relevant content from tweet object 145 | 146 | Arguments: 147 | --------- 148 | tweet: dict, tweet in either native Twitter json format or GNIP format 149 | 150 | Returns: 151 | --------- 152 | html for the tweet (either as received from oembed endpoint or if tweet not 153 | available on twitter the extracted text with minimal html embedding.) 154 | ''' 155 | html_templates = { 156 | 'tweet': ('
Tweet type: {type}
' 157 | 'Author: {author}
' 158 | 'Text: {text}
'), 159 | 'retweet': ('
Tweet type: {type}
' 160 | 'Author: {author}
' 161 | 'Retweeted Author: {retweeted_author}
' 162 | 'Retweeted text: {retweeted_text}
'), 163 | 'quotetweet': ('
Tweet type: {type}
' 164 | 'Author: {author}
' 165 | 'Text: {text}
' 166 | 'Quoted author: {quoted_author}
' 167 | 'Quoted text: {quoted_text}
'), 168 | 'retweet_of_quotetweet': ('
Tweet type: {type}
' 169 | 'Author: {author}
' 170 | 'Tweet text: {text}
' 171 | 'Retweeted author: {retweeted_author}
' 172 | 'Retweeted text: {retweeted_text}
' 173 | 'Quoted author: {quoted_author}
' 174 | 'Quoted text: {quoted_text}
'), 175 | 'embedded': ('
Tweet type: {type}
' 176 | 'Author: {author}

{embedded}') 177 | } 178 | 179 | # Try to extract all info with consideration of tweet type (see 180 | # `get_tweet_content()`) this only works for standard twitter json format 181 | # (not GNIP) so if that fails, resort to the old way 182 | try: 183 | contents = get_tweet_content(tweet) 184 | tweet_id = contents['id'] 185 | tweet_user = contents['author'] 186 | ttype = contents['type'] 187 | default_html = html_templates[ttype].format(**contents) 188 | except KeyError as e: 189 | if 'body' in tweet: 190 | idstr = tweet["id"] 191 | tweet_id = int(idstr[idstr.rfind(":")+1:]) 192 | tweet_user = tweet["actor"]["preferredUsername"] 193 | default_html = html_templates['tweet'].format(type='', 194 | author=tweet_user, 195 | text=tweet['body']) 196 | else: 197 | raise e 198 | 199 | # Try to get html from oembed endpoint 200 | rendered_html = get_embed(tweet_user, tweet_id) 201 | 202 | if rendered_html is not None: 203 | out_html = html_templates['embedded'].format(author=tweet_user, 204 | type=ttype, 205 | embedded=rendered_html) 206 | else: 207 | out_html = default_html 208 | 209 | return out_html, tweet_id 210 | 211 | def insert_labels(cursor, label_list, task_id, parent_label=-1): 212 | 213 | print("Inserting labels...") 214 | if parent_label > 0: 215 | print("\tSublabel of:", parent_label) 216 | 217 | for label in label_list: 218 | 219 | if isinstance(label, str): 220 | cursor.execute( 221 | 'INSERT INTO labels (taskId, labelText, parentLabel) VALUES (:taskId, :labelText, :parentLabel)', 222 | {"taskId": task_id, "labelText": label, "parentLabel": parent_label} 223 | ) 224 | 225 | elif isinstance(label, dict): 226 | for label_text, sublabels in label.items(): 227 | cursor.execute( 228 | 'INSERT INTO labels (taskId, labelText, parentLabel) VALUES (:taskId, :labelText, :parentLabel)', 229 | {"taskId": task_id, "labelText": label_text, "parentLabel": parent_label} 230 | ) 231 | this_parent_label = cursor.lastrowid 232 | 233 | insert_labels(cursor, sublabels, task_id, this_parent_label) 234 | 235 | def insert_ranges(cursor, task_id, name, questions): 236 | if len(questions) > 0: 237 | for question in questions: 238 | scaleSep = "," 239 | scale = scaleSep.join(str(question["scale"])) 240 | 241 | print(scale) 242 | 243 | data = [question["question"], task_id] 244 | 245 | cursor.execute('INSERT INTO rangeQuestions (rangeQuestion, taskId) VALUES (?,?)', data) 246 | tempId = cursor.lastrowid 247 | 248 | i = 1 249 | for val in question["scale"]: 250 | rangeVals = [tempId, val, i] 251 | cursor.execute('INSERT INTO rangeScales (rangeQuestionId, rangeValue, rangeOrder) VALUES (?,?,?)', rangeVals) 252 | i += 1 253 | print("Done inserting range task") 254 | -------------------------------------------------------------------------------- /compTaskDesc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nigeria 2014 - Negativity Comparisons", 3 | "question": "Which of these tweets seems more emotionally negative?", 4 | "type": 1 5 | } 6 | -------------------------------------------------------------------------------- /createSchema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS assignedTasks ( 2 | id INTEGER PRIMARY KEY, 3 | userId INTEGER NOT NULL, 4 | assignedTaskId INTEGER NOT NULL, 5 | CONSTRAINT userId_fkey FOREIGN KEY (userId) REFERENCES users (userId), 6 | CONSTRAINT taskId_fkey FOREIGN KEY (assignedTaskId) REFERENCES tasks (taskId) 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS tasks ( 10 | taskId INTEGER PRIMARY KEY, 11 | taskName TEXT, 12 | question TEXT, 13 | taskType INTEGER 14 | ); 15 | 16 | CREATE TABLE IF NOT EXISTS users ( 17 | userId INTEGER PRIMARY KEY, 18 | screenname TEXT UNIQUE NOT NULL, 19 | password TEXT NOT NULL, 20 | fname TEXT, 21 | lname TEXT, 22 | isadmin BIT 23 | ); 24 | 25 | CREATE TABLE IF NOT EXISTS elements ( 26 | elementId INTEGER PRIMARY KEY, 27 | elementText TEXT NOT NULL, 28 | taskId INTEGER NOT NULL, 29 | externalId TEXT 30 | ); 31 | 32 | CREATE TABLE IF NOT EXISTS labels ( 33 | labelId INTEGER PRIMARY KEY, 34 | labelText TEXT NOT NULL, 35 | taskId INTEGER NOT NULL, 36 | parentLabel INTEGER NOT NULL 37 | ); 38 | 39 | CREATE TABLE IF NOT EXISTS elementLabels ( 40 | elementLabelId INTEGER PRIMARY KEY, 41 | elementId INTEGER NOT NULL, 42 | labelId INTEGER NOT NULL, 43 | userId INTEGER NOT NULL, 44 | time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL 45 | ); 46 | 47 | CREATE TABLE IF NOT EXISTS pairs ( 48 | pairId INTEGER PRIMARY KEY, 49 | taskId INTEGER NOT NULL, 50 | leftElement INTEGER NOT NULL, 51 | rightElement INTEGER NOT NULL 52 | ); 53 | 54 | CREATE TABLE IF NOT EXISTS comparisons ( 55 | compareId INTEGER PRIMARY KEY, 56 | pairId INTEGER NOT NULL, 57 | userId INTEGER NOT NULL, 58 | decision INTEGER NOT NULL 59 | ); 60 | 61 | CREATE TABLE IF NOT EXISTS rangeQuestions( 62 | rangeQuestionId INTEGER PRIMARY KEY, 63 | rangeQuestion TEXT NOT NULL, 64 | taskId INTEGER NOT NULL 65 | ); 66 | 67 | CREATE TABLE IF NOT EXISTS rangeScales( 68 | rangeScaleId INTEGER PRIMARY KEY, 69 | rangeQuestionId INTEGER NOT NULL, 70 | rangeValue TEXT NOT NULL, 71 | rangeOrder INTEGER NOT NULL 72 | ); 73 | 74 | CREATE TABLE IF NOT EXISTS rangeDecisions( 75 | rangeDecisionId INTEGER PRIMARY KEY, 76 | elementId INTEGER NOT NULL, 77 | rangeQuestionId INTEGER NOT NULL, 78 | rangeScaleId INTEGER NOT NULL, 79 | userId INTEGER NOT NULL, 80 | time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL 81 | ); 82 | 83 | -------------------------------------------------------------------------------- /database.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/database.sqlite3 -------------------------------------------------------------------------------- /labelTaskDesc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Election Content - Relevance Labels", 3 | "question": "Is this tweet relevant to the current election?", 4 | "type": 2, 5 | "labels": [ 6 | { 7 | "Relevant": [ 8 | "Pro-Government", 9 | { 10 | "Pro-Opposition": [ 11 | "Pro Opp Candidate 1", 12 | "Pro Opp Candidate 2" 13 | ] 14 | } 15 | ] 16 | }, 17 | "Not Relevant", 18 | "Not English", 19 | "Can't Decide" 20 | ] 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pairwise-comparator", 3 | "version": "1.1.0", 4 | "description": "Annotation and pairwise comparison framework", 5 | "main": "server.js", 6 | "dependencies": { 7 | "bluebird": "^3.4.7", 8 | "body-parser": "^1.15.2", 9 | "connect-flash": "^0.1.1", 10 | "cookie-parser": "^1.4.3", 11 | "csv": "^5.3.2", 12 | "export-to-csv": "^0.2.1", 13 | "express": "^4.14.0", 14 | "express-session": "^1.14.2", 15 | "fs-promise": "^1.0.0", 16 | "html": "^1.0.0", 17 | "pandas": "0.0.3", 18 | "passport": "^0.3.2", 19 | "passport-local": "^1.0.0", 20 | "pug": "^3.0.0", 21 | "sqlite": "^2.2.4", 22 | "sqlite3": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.18.0" 26 | }, 27 | "scripts": { 28 | "test": "echo \"Error: no test specified\" && exit 1", 29 | "start": "node server.js" 30 | }, 31 | "author": "cody@bunta.in", 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/.DS_Store -------------------------------------------------------------------------------- /public/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | 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); 499 | 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); 500 | 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); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /public/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{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);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);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)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /public/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA","sourcesContent":["/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} -------------------------------------------------------------------------------- /public/css/jumbotron.css: -------------------------------------------------------------------------------- 1 | /* Move down content because we have a fixed navbar that is 50px tall */ 2 | body { 3 | padding-top: 50px; 4 | padding-bottom: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/imgs/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/public/imgs/spinner.gif -------------------------------------------------------------------------------- /public/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | // See the Getting Started docs for more information: 8 | // http://getbootstrap.com/getting-started/#support-ie10-width 9 | 10 | (function () { 11 | 'use strict'; 12 | 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /public/js/labelview.js: -------------------------------------------------------------------------------- 1 | // Call when the document is ready 2 | $( document ).ready(function() { 3 | loadDataElements(); 4 | }); 5 | 6 | var sendSelectedElement = function(elementId, selectedLabelId) { 7 | result = { 8 | element: elementId, 9 | selected: selectedLabelId, 10 | } 11 | 12 | $.post("/item", result, function(data) { 13 | console.log("Successfully sent selection..."); 14 | loadDataElements(); 15 | }) 16 | } 17 | 18 | var handleButtonClick = function(thisButton) { 19 | var labelIndex = thisButton.attr('labelindex'); 20 | var labelId = thisButton.attr('labelid'); 21 | var parentId = thisButton.attr('parentid'); 22 | var childIds = thisButton.data('childids'); 23 | 24 | if (childIds.length == 0) { 25 | 26 | // Reset the selectable buttons... 27 | $('.ct-label').each(function() { 28 | var localParentId = $(this).attr('parentid'); 29 | 30 | if ( localParentId < 1 ) { 31 | $(this).addClass('selected-label'); 32 | $(this).removeClass('hidden-label'); 33 | } else { 34 | $(this).removeClass('selected-label'); 35 | $(this).addClass('hidden-label'); 36 | } 37 | 38 | }); 39 | 40 | sendSelectedElement(dataElement.elementId, labelId); 41 | } else { 42 | 43 | // Turn off all the selectable buttons... 44 | $('.selected-label').each(function() { 45 | $(this).removeClass('selected-label'); 46 | $(this).addClass('hidden-label'); 47 | 48 | }); 49 | $('.hidden-label').each(function() { 50 | var localParentId = $(this).attr('parentid'); 51 | 52 | // Add selected-label class to this index... 53 | if ( localParentId == labelId ) { 54 | $(this).removeClass('hidden-label'); 55 | $(this).addClass('selected-label'); 56 | } 57 | }); 58 | 59 | // Rebuild the buttons display 60 | regenDisplayableButtons(); 61 | } 62 | } 63 | 64 | var regenDisplayableButtons = function() { 65 | console.log("Called regen..."); 66 | 67 | // turn off the keypress function 68 | $(document).off("keypress"); 69 | 70 | $('.hidden-label').each(function() { 71 | $(this).off("click"); 72 | $(this).hide(); 73 | }); 74 | 75 | $('.selected-label').each(function() { 76 | var labelIndex = $(this).attr('labelindex'); 77 | 78 | $(this).show(); 79 | 80 | // Set the click function for this label ID 81 | $(this).off("click").click(function() { 82 | handleButtonClick($(this)); 83 | }); 84 | 85 | // Set the keypress for this label 86 | var thisButton = $(this); 87 | $(document).keypress(function(e) { 88 | if ( e.which-48 == labelIndex ) { 89 | handleButtonClick(thisButton); 90 | } 91 | }); 92 | 93 | }); 94 | } 95 | 96 | var loadDataElements = function() { 97 | 98 | console.log("loadDataElements() called."); 99 | $("#loadingDialog").modal('show'); 100 | 101 | $.get("/item", function(json) { 102 | dataElement = json; 103 | 104 | if ( "empty" in dataElement ) { 105 | $("#loadingDialog").modal('hide'); 106 | 107 | alert("You have no more elements in this task!"); 108 | console.log("You have no more elements in this task!"); 109 | 110 | // turn off the keypress function 111 | $(document).off("keypress"); 112 | 113 | $('.hidden-label').each(function() { 114 | $(this).off("click"); 115 | $(this).hide(); 116 | }); 117 | 118 | $('.selected-label').each(function() { 119 | $(this).off("click"); 120 | $(this).hide(); 121 | }); 122 | 123 | } else { 124 | console.log("Acquired element..."); 125 | 126 | $("#element-content-panel").html(dataElement.elementText); 127 | 128 | // Set up the buttons... 129 | regenDisplayableButtons(); 130 | 131 | $("#loadingDialog").modal('hide'); 132 | console.log("Loaded element..."); 133 | } 134 | }) 135 | 136 | } -------------------------------------------------------------------------------- /public/js/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') -------------------------------------------------------------------------------- /public/js/pairview.js: -------------------------------------------------------------------------------- 1 | // Call when the document is ready 2 | $( document ).ready(function() { 3 | console.log( "ready!" ); 4 | 5 | loadTweets(); 6 | }); 7 | 8 | var sendSelectedTweet = function(pairId, selectedId) { 9 | result = { 10 | pair: pairId, 11 | selected: selectedId, 12 | } 13 | 14 | $.post("/pair", result, function(data) { 15 | console.log("Successfully sent selection..."); 16 | loadTweets(); 17 | }) 18 | } 19 | 20 | var loadTweets = function() { 21 | 22 | console.log("loadTweets() called."); 23 | $("#loadingDialog").modal('show'); 24 | 25 | $.get("/pair", function(json) { 26 | tweetPair = json; 27 | 28 | if ( "empty" in tweetPair ) { 29 | $("#loadingDialog").modal('hide'); 30 | 31 | alert("You have no more elements in this task!"); 32 | console.log("You have no more elements in this task!"); 33 | 34 | // turn off the keypress function 35 | $(document).off("keypress"); 36 | 37 | // Turn off the click handler 38 | $(this).off("click"); 39 | } else { 40 | pairId = tweetPair.id; 41 | 42 | $("#left-tweet-panel").text(tweetPair.left.tweet.text); 43 | $("#right-tweet-panel").text(tweetPair.right.tweet.text); 44 | 45 | 46 | $(document).off("keypress").keypress(function(e) { 47 | 48 | switch(e.which) { 49 | case 65: 50 | case 97: 51 | sendSelectedTweet(pairId, tweetPair.left.tweet.id); 52 | break; 53 | 54 | case 66: 55 | case 98: 56 | sendSelectedTweet(pairId, tweetPair.right.tweet.id); 57 | break; 58 | 59 | case 67: 60 | case 99: 61 | sendSelectedTweet(pairId, -1); 62 | break; 63 | } 64 | }); 65 | 66 | $("#left-tweet-button").off("click").click(function() { 67 | sendSelectedTweet(pairId, tweetPair.left.tweet.id); 68 | }); 69 | $("#right-tweet-button").off("click").click(function() { 70 | sendSelectedTweet(pairId, tweetPair.right.tweet.id); 71 | }); 72 | $("#undecided-button").off("click").click(function() { 73 | sendSelectedTweet(pairId, -1); 74 | }); 75 | 76 | $("#loadingDialog").modal('hide'); 77 | console.log("Loaded pair..."); 78 | } 79 | }) 80 | 81 | } -------------------------------------------------------------------------------- /public/js/rangeview.js: -------------------------------------------------------------------------------- 1 | var numQuestions = 0; 2 | var answers = []; 3 | var currentlySelected; 4 | var submitAdded = false; 5 | var questionIndex = 0; 6 | 7 | // Call when the document is ready 8 | $(document).ready(function () { 9 | var i; 10 | 11 | numQuestions = $(".rangeQuestionContainer .row").length; 12 | 13 | loadDataElements(); 14 | 15 | questionIndex = numQuestions; 16 | 17 | $("input:radio").change(function () { 18 | currentlySelected = $(this); 19 | 20 | $('input:radio').each((index, element) => { 21 | if ($(element).id != currentlySelected.id) { 22 | $(element).prop("checked", false); 23 | } 24 | }); 25 | 26 | if ($(":checked").length == numQuestions) { 27 | $(":checked").each((index, element) => { 28 | var rangeId = $(element).prop("id").split("-")[1]; 29 | 30 | answers.push([$(element).prop("name"), rangeId, dataElement.elementId]) 31 | }); 32 | 33 | if (!submitAdded) { 34 | $(".btn").removeClass("collapse"); 35 | 36 | submitAdded = true; 37 | } 38 | } 39 | }); 40 | }); 41 | 42 | var readyHTMLElements = function () { 43 | questionIndex += numQuestions; 44 | 45 | console.log("Question index: " + questionIndex); 46 | 47 | $("input:radio").change(function () { 48 | currentlySelected = $(this); 49 | 50 | $('input:radio').each((index, element) => { 51 | if ($(element).id != currentlySelected.id) { 52 | $(element).prop("checked", false); 53 | } 54 | }); 55 | 56 | if ($(":checked").length == numQuestions) { 57 | $(":checked").each((index, element) => { 58 | var rangeId = $(element).prop("id").split("-")[1]; 59 | 60 | answers.push([$(element).prop("name"), rangeId, dataElement.elementId]) 61 | }); 62 | 63 | if (!submitAdded) { 64 | $(".btn").removeClass("collapse"); 65 | 66 | submitAdded = true; 67 | } 68 | } 69 | }); 70 | } 71 | 72 | $(".container > .btn").click(function() { 73 | var k; 74 | for (k = questionIndex - numQuestions; k < answers.length; k++) { 75 | console.log("k " + k); 76 | 77 | console.log("Sending " + answers[k][0] + " " + answers[k][1] + " " + answers[k][2]); 78 | 79 | sendSelectedElement(answers[k][0], answers[k][1], answers[k][2]); 80 | } 81 | 82 | $(":checked").each((index, element) => { 83 | $(element).prop("checked", false); 84 | }); 85 | 86 | $(".btn").addClass("collapse"); 87 | 88 | var i; 89 | for (i = 0; i < answers.length; i++) { 90 | answers[i].pop(); 91 | } 92 | 93 | while (answers.length) { 94 | answers.pop(); 95 | } 96 | 97 | currentlySelected = null; 98 | submitAdded = false; 99 | 100 | loadDataElements(); 101 | 102 | readyHTMLElements(); 103 | 104 | console.log({ answers }); 105 | 106 | }); 107 | 108 | var sendSelectedElement = function(questionId, decisionId, elementId) { 109 | result = { 110 | element: elementId, 111 | question: questionId, 112 | selected: decisionId, 113 | } 114 | 115 | $.post("/range", result, function(data) { 116 | console.log("Successfully sent selection..."); 117 | }) 118 | 119 | } 120 | 121 | var loadDataElements = function() { 122 | 123 | console.log("loadDataElements() called."); 124 | $("#loadingDialog").modal('show'); 125 | 126 | $.get("/range", function(json) { 127 | dataElement = json; 128 | 129 | if ( "empty" in dataElement ) { 130 | $("#loadingDialog").modal('hide'); 131 | 132 | alert("You have no more elements in this task!"); 133 | console.log("You have no more elements in this task!"); 134 | 135 | // turn off the keypress function 136 | $(document).off("keypress"); 137 | 138 | $('.hidden-label').each(function() { 139 | $(this).off("click"); 140 | $(this).hide(); 141 | }); 142 | 143 | $('.selected-label').each(function() { 144 | $(this).off("click"); 145 | $(this).hide(); 146 | }); 147 | 148 | } else { 149 | console.log("Acquired element..."); 150 | 151 | $("#element-content-panel").html(dataElement.elementText); 152 | 153 | $("#loadingDialog").modal('hide'); 154 | console.log("Loaded element..."); 155 | } 156 | }) 157 | 158 | } -------------------------------------------------------------------------------- /public/js/taskStats-detail.js: -------------------------------------------------------------------------------- 1 | // Call when the document is ready 2 | $( document ).ready(function() { 3 | loadDataElements(); 4 | }); 5 | 6 | var result = {}; 7 | 8 | var updateSelectedElement = function(elementLabelId, labelId) { 9 | result = { 10 | elementLabelId: elementLabelId, 11 | newLabelId: labelId, 12 | } 13 | 14 | $.post("/updateLabel", result, function(data) { 15 | console.log("Successfully sent update..."); 16 | }) 17 | } 18 | 19 | var updateSelectedRangeElement = function(oldDecisionId, newScaleId){ 20 | result = { 21 | previousDecisionId: oldDecisionId, 22 | newScaleId: newScaleId, 23 | } 24 | 25 | console.log(result); 26 | 27 | $.post("/updateRange", result, function(data) { 28 | console.log("Successfully sent update..."); 29 | }) 30 | } 31 | 32 | var handleNavClick = function(pageSize, pageCursor) { 33 | 34 | var rowCount = $('.reviewable-row').length; 35 | 36 | $('.reviewable-row').each(function() { 37 | var thisRowId = $(this).attr("rowindex"); 38 | 39 | if ( thisRowId >= ((pageCursor - 1) * pageSize) && thisRowId < (pageCursor * pageSize) ) { 40 | $(this).show(); 41 | } else { 42 | $(this).hide(); 43 | } 44 | }); 45 | 46 | if ( pageCursor <= 1 ) { 47 | $('#pre-button').addClass("disabled"); 48 | } else { 49 | $('#pre-button').removeClass("disabled"); 50 | $('#pre-button').off("click").click(function() { 51 | handleNavClick(pageSize, pageCursor - 1); 52 | }); 53 | } 54 | 55 | if ( pageCursor * pageSize >= rowCount ) { 56 | $('#next-button').addClass("disabled"); 57 | } else { 58 | $('#next-button').removeClass("disabled"); 59 | $('#next-button').off("click").click(function() { 60 | handleNavClick(pageSize, pageCursor + 1); 61 | }); 62 | } 63 | } 64 | 65 | var loadDataElements = function() { 66 | 67 | var pageSize = 10; 68 | var pageCursor = 1; 69 | 70 | handleNavClick(pageSize, pageCursor); 71 | 72 | $('#next-button').off("click").click(function() { 73 | handleNavClick(pageSize, pageCursor + 1); 74 | }); 75 | 76 | $('.update-label').each(function() { 77 | // The element-label ID pair is part of the form... 78 | var elementLabelId = $(this).attr('elementlabelid'); 79 | 80 | // We need the button in this form. 81 | var form = $(this); 82 | var button = $(this).children("input"); 83 | 84 | // Set the click function for this form's button to 85 | //. update the element-label pair with the selected option 86 | button.off("click").click(function() { 87 | 88 | var selectedLabelId = form.children("select").children("option:selected").val(); 89 | updateSelectedElement(elementLabelId, selectedLabelId); 90 | 91 | }); 92 | }); 93 | 94 | 95 | // Set the click function for this form's button to 96 | // update the elementrange decision with the selected option 97 | // Need to send the rsId (decision id) and radioAnswer (new scale value) 98 | $('input:radio').change(function () { 99 | var oldDecisionId = $(this).parent().parent('form').attr('elementlabelid'); 100 | var newRadioAnswer = $(this).val(); 101 | var thisButton = $(this).parent().parent('form').children('input:button'); 102 | 103 | 104 | thisButton.off("click").click(function() { 105 | updateSelectedRangeElement(oldDecisionId, newRadioAnswer); 106 | }); 107 | }); 108 | } -------------------------------------------------------------------------------- /rangeTaskDesc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nigeria 2014 - Emotional Ranges", 3 | "question": "Answer several questions about the emotional range of a social media message.", 4 | "questions": [ 5 | { 6 | "question": "Rate the emotional negativity of this tweet", 7 | "scale": [ 8 | "1 - Not At All", 9 | "2 - Slightly", 10 | "3 - Moderate", 11 | "4 - Very", 12 | "5 - Incredibly" 13 | ] 14 | }, 15 | { 16 | "question": "Rate the emotional positivity of this tweet", 17 | "scale": [ 18 | "1 - Not At All", 19 | "2 - Slightly", 20 | "3 - Moderate", 21 | "4 - Very", 22 | "5 - Incredibly" 23 | ] 24 | } 25 | ], 26 | "type": 3 27 | } 28 | -------------------------------------------------------------------------------- /users/index.js: -------------------------------------------------------------------------------- 1 | exports.users = require('./users'); -------------------------------------------------------------------------------- /users/users.js: -------------------------------------------------------------------------------- 1 | exports.getUsers = function(db, cb) { 2 | db.all("SELECT userId, screenname, fname, lname, isadmin FROM users") 3 | .then(function(userData) { 4 | cb(userData); 5 | }); 6 | } 7 | 8 | exports.findById = function(db, id, cb) { 9 | 10 | db.get("SELECT userId, screenname, password, fname, lname, isadmin FROM users WHERE userId = ?", id) 11 | .then(function(userData) { 12 | cb(null, userData); 13 | }); 14 | } 15 | 16 | 17 | exports.findByScreenname = function(db, sn, cb) { 18 | 19 | db.get("SELECT userId, screenname, password, fname, lname, isadmin FROM users WHERE screenname = ?", sn) 20 | .then(function(userData) { 21 | cb(null, userData); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbuntain/collabortweet/80bcdaa4cef04cb2b0a25830ea3b0269d9f0d818/views/.DS_Store -------------------------------------------------------------------------------- /views/export.pug: -------------------------------------------------------------------------------- 1 | //- index.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 Select the task to export. 13 | 14 | //- Display options 15 | div(class="container") 16 | if dataMap.pairTasks.length > 0 17 | h2 Available Comparison Tasks: 18 | each v in dataMap.pairTasks 19 | div(class="row") 20 | div(class="panel panel-default") 21 | 22 | div(class="panel-heading") 23 | h4 24 | Task #{v.taskId}: #{v.taskName} 25 | ul.list-unstyled 26 | li 27 | a.text-white(href="/json/" + v.taskId) JSON 28 | li 29 | a.text-white(href="/csv/" + v.taskId) CSV 30 | 31 | div(class="panel-body") 32 | p Comparisons Answered: #{v.counter} 33 | p #{v.question} 34 | 35 | if dataMap.labelTasks.length > 0 36 | h2 Available Labeling Tasks: 37 | each v in dataMap.labelTasks 38 | div(class="row") 39 | div(class="panel panel-default") 40 | 41 | div(class="panel-heading") 42 | h4 43 | Task #{v.taskId}: #{v.taskName} 44 | ul.list-unstyled 45 | li 46 | a.text-white(href="/json/" + v.taskId) JSON 47 | li 48 | a.text-white(href="/csv/" + v.taskId) CSV 49 | 50 | div(class="panel-body") 51 | p Elements to Label: #{v.eCount} 52 | p Labels Provided: #{v.labelCount} 53 | p #{v.question} 54 | 55 | if dataMap.rangeTasks.length > 0 56 | h2 Available Range-Based Tasks: 57 | each v in dataMap.rangeTasks 58 | div(class="row") 59 | div(class="panel panel-default") 60 | 61 | div(class="panel-heading") 62 | h4 63 | Task #{v.taskId}: #{v.taskName} 64 | ul.list-unstyled 65 | li 66 | a.text-white(href="/json/" + v.taskId) JSON 67 | li 68 | a.text-white(href="/csv/" + v.taskId) CSV 69 | 70 | div(class="panel-body") 71 | p Ranges to Label: #{v.eCount} 72 | p Range Decisions Provided: #{v.labelCount} 73 | p #{v.question} 74 | 75 | 76 | include includes/jsFooter.pug -------------------------------------------------------------------------------- /views/includes/head.pug: -------------------------------------------------------------------------------- 1 | meta(charset="utf-8") 2 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 3 | meta(name="viewport", content="width=device-width, initial-scale=1") 4 | 5 | title= pageTitle 6 | 7 | //- 8 | link(href="/static/css/bootstrap.min.css", rel="stylesheet") 9 | 10 | //- 11 | //- 12 | 16 | 17 | //- Custom styles for this template 18 | link(href="/static/css/jumbotron.css", rel="stylesheet") -------------------------------------------------------------------------------- /views/includes/jsFooter.pug: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /views/includes/nav.pug: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | //- index.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 Welcome. Enter your username and password. 13 | 14 | //- Display options 15 | //- div(class="container") 16 | //- each v in dataMap.userList 17 | //- div(class="row") 18 | //- div(class="panel panel-default") 19 | //- div(class="panel-body") 20 | //- h4 21 | //- a(href="/login?userId=" + v.userId) #{v.userId}. #{v.fname} #{v.lname} 22 | div(class = "container") 23 | div(class = "row") 24 | div(class = "panel panel-default") 25 | div(class = "panel-body") 26 | form(action = '/login', method='GET') 27 | 28 | p 29 | | username: 30 | input#username(type='text', name='username', value='') 31 | 32 | p 33 | | password: 34 | input#password(type = 'password', name = 'password', value = '') 35 | 36 | p 37 | input(type='submit', value='Log In') 38 | 39 | //- block content 40 | //- div.text-center 41 | //- form(class="form-signin" method="POST" action="/users/login") 42 | //- #error 43 | //- if error 44 | //- p.text-danger Error!!! 45 | //- - var h1Classes = ['h3', 'mb-3', 'font-weight-normal'] 46 | //- h1(class=h1Classes) Please sign in 47 | //- //-input email 48 | 49 | //- label( for="inputEmail" class="sr-only") Email address 50 | //- input(type="username" name="username" id="inputUsername" class="form-control" placeholder="Username" required autofocus) 51 | //- //-input password 52 | //- label(for="inputPassword" class="sr-only") Password 53 | //- input(type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required) 54 | 55 | //- //-signIn button 56 | //- - var buttonClass=['btn', 'btn-lg', 'btn-primary', 'btn-block']; 57 | //- button(class=buttonClass type="submit") Sign in 58 | 59 | 60 | include includes/jsFooter.pug 61 | -------------------------------------------------------------------------------- /views/labelView.pug: -------------------------------------------------------------------------------- 1 | //- pairView.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 #{taskName} Question: 13 | p #{question} 14 | 15 | //- Display options 16 | div(class="container") 17 | 18 | div(class="row") 19 | div(class="panel panel-default") 20 | div(class="panel-body") 21 | div(id="element-content-panel") 22 | 23 | - var labelCount = labels.length 24 | - var labelIndex = 0 25 | while labelIndex < labelCount 26 | - var labelItem = labels[labelIndex] 27 | - labelIndex++ 28 | 29 | if labelItem.parentLabel < 1 30 | div(class="row text-center") 31 | h4(class="btn btn-primary btn-lg ct-label selected-label", labelindex=labelItem.buttonIndex, labelid=labelItem.labelId, parentid=labelItem.parentLabel, data-childids=labelItem.childrenIds) 32 | u= labelItem.buttonIndex 33 | span . #{labelItem.labelText} 34 | else 35 | div(class="row text-center") 36 | h4(class="btn btn-primary btn-lg ct-label hidden-label", labelindex=labelItem.buttonIndex, labelid=labelItem.labelId, parentid=labelItem.parentLabel, data-childids=labelItem.childrenIds) 37 | u= labelItem.buttonIndex 38 | span . #{labelItem.labelText} 39 | 40 | 41 | //- Footer navigation 42 | div(class="row navbar-fixed-bottom") 43 | div(class=".col-md-6") 44 | ol(class="breadcrumb") 45 | li(id="prevCrumb") 46 | li(class="active") Current 47 | li(id="prevCrumb") 48 | 49 | div(class="modal fade", id="loadingDialog", tabindex="-1" role="dialog") 50 | div(class="modal-dialog", role="document") 51 | div(class="modal-content") 52 | div(class="modal-header") 53 | h4(class="modal-title") Please Wait... 54 | div(class="modal-body text-center") 55 | img(src="/static/imgs/spinner.gif") 56 | 57 | include includes/jsFooter.pug 58 | 59 | 60 | script(src="/static/js/labelview.js") -------------------------------------------------------------------------------- /views/pairView.pug: -------------------------------------------------------------------------------- 1 | //- pairView.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 #{taskName} Question: 13 | p #{question} 14 | 15 | //- Display options 16 | div(class="container") 17 | div(class="row") 18 | div(class="col-md-6") 19 | div(class="panel panel-default") 20 | div(class="panel-heading text-center") 21 | h4(class="btn btn-primary btn-lg", id="left-tweet-button") Option 22 | u A 23 | div(class="panel-body") 24 | p(id="left-tweet-panel") 25 | div(class="col-md-6") 26 | div(class="panel panel-default") 27 | div(class="panel-heading text-center") 28 | h4(class="btn btn-primary btn-lg", id="right-tweet-button") Option 29 | u B 30 | div(class="panel-body") 31 | p(id="right-tweet-panel") 32 | div(class="row text-center") 33 | div(class="btn btn-warning btn-lg", id="undecided-button") 34 | u C 35 | span an't Decide 36 | 37 | //- Footer navigation 38 | div(class="row navbar-fixed-bottom") 39 | div(class=".col-md-6") 40 | ol(class="breadcrumb") 41 | li(id="prevCrumb") 42 | li(class="active") Current 43 | li(id="prevCrumb") 44 | 45 | div(class="modal fade", id="loadingDialog", tabindex="-1" role="dialog") 46 | div(class="modal-dialog", role="document") 47 | div(class="modal-content") 48 | div(class="modal-header") 49 | h4(class="modal-title") Please Wait... 50 | div(class="modal-body text-center") 51 | img(src="/static/imgs/spinner.gif") 52 | 53 | include includes/jsFooter.pug 54 | 55 | 56 | script(src="/static/js/pairview.js") -------------------------------------------------------------------------------- /views/rangeView.pug: -------------------------------------------------------------------------------- 1 | //-rangeView.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 #{taskName} Question: 13 | p #{question} 14 | 15 | //- Display options 16 | div(class="container") 17 | 18 | div(class="row") 19 | div(class="panel panel-default") 20 | div(class="panel-body") 21 | div(id="element-content-panel") 22 | 23 | each rangeQuestionObject, rangeQuestionId in ranges 24 | - var rangeQuestionObject = ranges[rangeQuestionId] 25 | - var rangeQuestion = rangeQuestionObject["rangeQuestion"] 26 | - var scaleIndex = 1 27 | 28 | div(class="text-left rangeQuestionContainer") 29 | h4 #{rangeQuestion} : 30 | h5 Pick One 31 | 32 | span(class="row" id="rangeQuestion" + rangeQuestionId) 33 | each scale in rangeQuestionObject["scale"] 34 | span(style="width: 10%; display: inline-block;" class="text-left") 35 | p #{scale["rangeScaleValue"]} 36 | input( 37 | type="radio" 38 | style="display:block; text-align:center;" 39 | id=rangeQuestionId + "-" + scale["rangeScaleId"] 40 | name=rangeQuestionId 41 | value=scale["rangeScaleValue"] 42 | ) 43 | - scaleIndex++ 44 | 45 | button.btn.btn-primary.collapse#submit Submit Answers! 46 | 47 | 48 | //- Footer navigation 49 | div(class="row navbar-fixed-bottom") 50 | div(class=".col-md-6") 51 | ol(class="breadcrumb") 52 | li(id="prevCrumb") 53 | li(class="active") Current 54 | li(id="prevCrumb") 55 | div(class="modal fade", id="loadingDialog", tabindex="-1" role="dialog") 56 | div(class="modal-dialog", role="document") 57 | div(class="modal-content") 58 | div(class="modal-header") 59 | h4(class="modal-title") Please Wait... 60 | div(class="modal-body text-center") 61 | img(src="/static/imgs/spinner.gif") 62 | 63 | include includes/jsFooter.pug 64 | 65 | 66 | script(src="/static/js/rangeview.js") -------------------------------------------------------------------------------- /views/taskStats-detail.pug: -------------------------------------------------------------------------------- 1 | //- index.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(style="display:none:" class="TaskType" id=taskInfo.taskType) 11 | 12 | 13 | div(class="jumbotron") 14 | div(class="container") 15 | h2 #{taskInfo.taskName} Question: 16 | p #{taskInfo.question} 17 | 18 | //- Display options 19 | div(class="container") 20 | h2 User Label Counts: 21 | div(class="row") 22 | div(class="panel panel-default") 23 | 24 | div(class="panel-body") 25 | each ud in userDetails 26 | h4 #{ud.fname} #{ud.lname}: #{ud.count} 27 | 28 | if agreement.user1 != null 29 | //- Display options 30 | div(class="container") 31 | h2 Agreement Statistics: 32 | div(class="row") 33 | div(class="panel panel-default") 34 | 35 | div(class="panel-body") 36 | h4 Top Agreed users: 37 | h5 #{agreement.user1.fname} #{agreement.user1.lname} 38 | h5 #{agreement.user2.fname} #{agreement.user2.lname} 39 | h4 Overlapped Elements: #{agreement.agreeCount} 40 | h4 Cohen's Kappa: #{agreement.agreement} 41 | 42 | //- Display options 43 | div(class="container") 44 | if taskInfo.taskType == 1 45 | div(class="taskType" style="display:none" taskType=taskInfo.taskType) 46 | h2 Comparisons: 47 | - var rowIndex = 0 48 | each v in detailList 49 | div(class="row reviewable-row", rowindex=rowIndex) 50 | - rowIndex++ 51 | div(class="panel panel-default") 52 | 53 | div(class="panel-heading") 54 | h4 Selected #{v.decision} 55 | 56 | div(class="panel-body") 57 | h4 Item ID: #{v.lId} 58 | p #{v.lText} 59 | h4 Item ID: #{v.rId} 60 | p #{v.rText} 61 | 62 | else if taskInfo.taskType == 2 63 | div(class="taskType" style="display:none" taskType=taskInfo.taskType) 64 | h2 Labels: 65 | 66 | div(class="row") 67 | nav(aria-label="Page Navigation") 68 | ul(class="pagination") 69 | li(class="page-item", id="pre-button") 70 | a(class="page-link", href="#") Previous 71 | li(class="page-item", id="next-button") 72 | a(class="page-link", href="#") Next 73 | 74 | - var rowIndex = 0 75 | each v in detailList 76 | div(class="row reviewable-row", rowindex=rowIndex) 77 | - rowIndex++ 78 | div(class="panel panel-default") 79 | 80 | div(class="panel-heading") 81 | h4 Entry: #{rowIndex} 82 | div !{v.eText} 83 | 84 | div(class="panel-body") 85 | h4 User: #{v.screenname} 86 | form(id="update-label-"+v.elId, class="update-label", elementlabelid=v.elId) 87 | select 88 | each opt in labelDetails 89 | if v.lId == opt.lId 90 | option(value=opt.lId, selected="true") #{opt.lText} 91 | else 92 | option(value=opt.lId) #{opt.lText} 93 | br 94 | input(class="btn btn-primary btn-sm", type="button", value="Update") 95 | 96 | 97 | else if taskInfo.taskType == 3 98 | div(class="taskType" style="display:none" taskType=taskInfo.taskType) 99 | h2 Labels: 100 | 101 | div(class="row") 102 | nav(aria-label="Page Navigation") 103 | ul(class="pagination") 104 | li(class="page-item", id="pre-button") 105 | a(class="page-link", href="#") Previous 106 | li(class="page-item", id="next-button") 107 | a(class="page-link", href="#") Next 108 | 109 | - var prevIndex = -1 110 | - var rowIndex = 0 111 | 112 | each thisUserElementKey of detailList.keys() 113 | - thisElementLabels = detailList.get(thisUserElementKey) 114 | div(class="row reviewable-row", rowindex=rowIndex) 115 | - rowIndex++ 116 | div(class="panel panel-default") 117 | 118 | div(class="panel-heading") 119 | h4 Entry: #{rowIndex} 120 | div !{thisElementLabels.eText} 121 | 122 | 123 | div(class="panel-body") 124 | h4 User: #{thisElementLabels.screenname} 125 | 126 | each rqId of labelDetails.keys() 127 | - thisQuestion = labelDetails.get(rqId) 128 | - rq = thisQuestion["question"] 129 | - rqAnswer = thisElementLabels["labels"].get(rqId) 130 | - rqOptions = thisQuestion["options"] 131 | - groupName = `${thisUserElementKey}-${thisUserElementKey}` 132 | 133 | p(class="text-center") 134 | p #{rq} 135 | 136 | form(id="update-range", class="update-label", elementlabelid=rqAnswer.rdId) 137 | each rqScaleVal in rqOptions 138 | span(style="width: 10%; display: inline-block;" class="text-left") 139 | p #{rqScaleVal.rsValue} 140 | if (rqScaleVal.rsId == rqAnswer.rsId) 141 | input( 142 | type="radio" 143 | class="text-center" 144 | style="display:block;" 145 | value=rqScaleVal.rsId 146 | decisionId=rqAnswer.rdId 147 | name=groupName 148 | checked="checked" 149 | ) 150 | else 151 | input( 152 | type="radio" 153 | class="text-center" 154 | style="display:block;" 155 | value=rqScaleVal.rsId 156 | decisionId=rqAnswer.rdId 157 | name=groupName 158 | ) 159 | input(id=rqAnswer.rdId, class="btn btn-primary btn-sm text-center", type="button", value="Update") 160 | 161 | br 162 | br 163 | 164 | 165 | include includes/jsFooter.pug 166 | 167 | 168 | script(src="/static/js/taskStats-detail.js") -------------------------------------------------------------------------------- /views/taskStats.pug: -------------------------------------------------------------------------------- 1 | //- index.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 Welcome. Select your task. 13 | 14 | //- Display options 15 | div(class="container") 16 | if dataMap.pairTasks.length > 0 17 | h2 Available Comparison Tasks: 18 | each v in dataMap.pairTasks 19 | div(class="row") 20 | div(class="panel panel-default") 21 | 22 | div(class="panel-heading") 23 | h4 24 | a(href="/taskStats/" + v.taskId) Task #{v.taskId}: #{v.taskName} 25 | 26 | div(class="panel-body") 27 | p Comparisons Answered: #{v.counter} 28 | p #{v.question} 29 | 30 | if dataMap.labelTasks.length > 0 31 | h2 Available Labeling Tasks: 32 | each v in dataMap.labelTasks 33 | div(class="row") 34 | div(class="panel panel-default") 35 | 36 | div(class="panel-heading") 37 | h4 38 | a(href="/taskStats/" + v.taskId) Task #{v.taskId}: #{v.taskName} 39 | 40 | div(class="panel-body") 41 | p Elements to Label: #{v.eCount} 42 | p Labels Provided: #{v.labelCount} 43 | p #{v.question} 44 | 45 | if dataMap.rangeTasks.length > 0 46 | h2 Available Range Tasks: 47 | each v in dataMap.rangeTasks 48 | div(class="row") 49 | div(class="panel panel-default") 50 | 51 | div(class="panel-heading") 52 | h4 53 | a(href="/taskStats/" + v.taskId) Task #{v.taskId}: #{v.taskName} 54 | 55 | div(class="panel-body") 56 | p Elements to Label: #{v.eCount} 57 | p Labels Provided: #{v.rdCount} 58 | p #{v.question} 59 | 60 | 61 | include includes/jsFooter.pug -------------------------------------------------------------------------------- /views/taskView.pug: -------------------------------------------------------------------------------- 1 | //- index.pug 2 | doctype html 3 | html 4 | head 5 | include includes/head.pug 6 | body 7 | include includes/nav.pug 8 | 9 | 10 | div(class="jumbotron") 11 | div(class="container") 12 | h2 Welcome. Select your task. 13 | 14 | //- Display options 15 | div(class="container") 16 | h2 Available Tasks: 17 | each v in dataMap.tasks 18 | div(class="row") 19 | div(class="panel panel-default") 20 | 21 | div(class="panel-heading") 22 | h4 23 | if v.taskType == 1 24 | a(href="/pairView/" + v.taskId) Task #{v.taskId}: #{v.taskName} 25 | else if v.taskType == 2 26 | a(href="/labelerView/" + v.taskId) Task #{v.taskId}: #{v.taskName} 27 | else if v.taskType == 3 28 | a(href="/rangeView/" + v.taskId) Task #{v.taskId}: #{v.taskName} 29 | else 30 | p Unsupported Task #{v.taskId}: #{v.taskName} 31 | 32 | div(class="panel-body") 33 | p #{v.question} 34 | 35 | 36 | include includes/jsFooter.pug -------------------------------------------------------------------------------- /vignette.md: -------------------------------------------------------------------------------- 1 | # HOWTO use collabortweet for SMaPP members and collaborators 2 | 3 | 4 | ## Log on to the Collabortweet digital ocean server 5 | 6 | In principle you can run host collabortweet on any machine if you want to. Here we only show you how to use the dedicated server we have for this purpose at SMaPP. Running it on a different Linux machine should be straight forward. 7 | 8 | Contact Megan [](meganbrown@nyu.edu) or anybody else with access to the Collabortweet server and give them your ssh public key. 9 | 10 | In your terminal of choice, use ssh to log on to the server: 11 | 12 | ```{bash} 13 | ssh root@178.128.157.54 -i path/to/privat/key 14 | ``` 15 | 16 | ## Create a directory for your project 17 | 18 | All labeling project directories should be located in `/home`: 19 | 20 | After logging in navigate to the `/home` and create your project directory there: 21 | ``` 22 | cd /home 23 | mkdir my_project 24 | ``` 25 | 26 | ## Upload the data you want to label to the server 27 | 28 | On your local computer (or the computer you want to transfer the data from, e.g. the prince cluster) run the following command to upload your data: 29 | ``` 30 | scp -i path/to/private/key path/to/data/file.json root@178.128.157.54:/home/my_project 31 | ``` 32 | 33 | ## Install collabortweet 34 | 35 | Now we download the collabortweet platform from the GitHub repo: 36 | 37 | ``` 38 | cd /home/my_project 39 | git clone https://github.com/smappnyu/collabortweet 40 | ``` 41 | 42 | ## Set up the project 43 | 44 | First we need to configure what port to run the server on and where to store all data. To figure out if a port is in use by another project, choose a port number between 3000 and 7000. Then check if another application is running by navigating in a web browser of your choice to `178.128.157.54:XXXX`, where `XXXX` refers to the port number. 45 | 46 | Open `/home/my_project/collabortweet/CONFIG.json` and edit the `port` entry to `XXXX`. You can also change the `db_path` field. `db_path` tells the application where to store all the data. It might also be a good idea to set up some form of backup for this file for larger projects to make sure labels are not lost accidentally. 47 | 48 | Now run the setup with: 49 | 50 | ``` 51 | python SETUP.py 52 | ``` 53 | 54 | This will install all dependencies for the webapp and create the empty database schema that holds the data. 55 | 56 | ## Adding Users 57 | 58 | Now that the database is set up we can add user or coders, the people that will use the application: 59 | 60 | You can either add many users at once or single users at a time. 61 | 62 | ### Adding many users 63 | 64 | Create a `.csv` file containing all users that you want to add, in the following format (one user per row): 65 | 66 | Run 67 | ``` 68 | screenname,password,first_name,last_name 69 | exampleuser,12345,example,user 70 | ...,...,...,... 71 | ...,...,...,... 72 | ``` 73 | 74 | Then import the users with the import script by running the following command (from the `collabortweet` directory): 75 | 76 | ``` 77 | python bin/add_users.py --sqlite_path [database_file_path] --users_file [user_csv_file] 78 | ``` 79 | 80 | The `[database_file_path]` is the path that's specified as `db_path` in `CONFIG.json` (drop the brackets). 81 | 82 | ### Adding a single user 83 | 84 | You can add a single user at a time via the command line interface of the user import script: 85 | 86 | ``` 87 | python bin/add_users.py --sqlite_path [database_file_path] --screenname exampleuser --password 12345 --first_name example --last_name user 88 | ``` 89 | 90 | ## Creating Tasks 91 | 92 | ### The task description file 93 | 94 | The platform can run multiple tasks simultaneously. A task is a set of elements to be labeled that can be accessed by all users that have been added in the previous step. 95 | In order to create a task, you need to write a task configuration file. The collabortweet repo contains an example of such a file (`labelTaskDesc.json`): 96 | 97 | ``` 98 | { 99 | "name": "Nigeria 2015 - Relevance Labels", 100 | "question": "Is this tweet relevant to the Nigerian 2015 election?", 101 | "type": 2, 102 | "labels": [ 103 | "Relevant", 104 | "Not Relevant", 105 | "Not English", 106 | "Can't Decide" 107 | ] 108 | } 109 | ``` 110 | - Name: Name of the task as it will appear on the platform 111 | - Question: Question to ask of the coders 112 | - Type: 1 is a pairwise comparison task (omitted from this documentation for now), 2 is a 'classical' labeling task 113 | - labels: The choices of labels to give to the coders for each object 114 | 115 | 116 | ### Creating a task and importing data 117 | 118 | When the data is imported, tweets that are still available on Twitter will be displayed as a tweet would be on the platform itself. If the tweet is not available any longer, just the text of the tweet is displayed. To import your data you can use the following script (from the `collabortweet` directory): 119 | 120 | ``` 121 | python bin/embed_tweets_2_sql.py --task_path [task_file.json] --sqlite_path [database_file_path] --data_path [path_to_tweets_file.json] 122 | ``` 123 | 124 | ### Adding data to a running task 125 | 126 | To add tweets to a task, look up the task ID of the task you want to add tweets to. You can find the task ID either on the web interface in the header of each task, or by looking it up directly in the database table. Then use the following script: 127 | 128 | ``` 129 | python bin/add_tweets_to_task.py --sqlite_path [database_file_path] --task_id [integer_id] --data_path [path_to_additional_data.json] 130 | ``` 131 | 132 | ### Starting the server 133 | 134 | If you followed all steps above, you should now be ready to run the server. To do so, from the `collabortweet` directory, run: 135 | ``` 136 | npm start 137 | ``` 138 | 139 | The only output you should see is `Starting server...`. If you see any other error messages, check if you followed all the steps above and if so contact Megan, Cody or Frido. 140 | Now the application is available on the WWW at `http://178.128.157.54:XXXX/` where `XXXX` is the port number you chose above. 141 | 142 | ### Downloading the labeled data 143 | 144 | When the job is complete, you can download the data you can either use `bin/SqlToCsv.py` to get all tweets from all tasks, or `retrieve_partitioned_task.py` (see below for more info). 145 | 146 | 147 | ## Additional helpful scripts 148 | 149 | ### Dividing data into partially overlapping partitions for coder evaluation 150 | 151 | If you have a job with multiple undergraduate coders and you want to split up tweets among them while still keeping track of their individual performance, collabortweet offers utilities for the following approach: Split up the complete dataset into an evaluation set (e.g. 100 randomly sampled tweets) and the remainder of the data. The remainder is split up evenly among the n undergraduate coders (without overlap). The evaluation set is then added to the partition of each coder in order to have overlap for calculating reliability statistics. Additionally, if there is one 'gold standard' coder (e.g. the researcher running the task), this gold standard coder can label the evaluation set to get not only inter-coder reliability but also coder-accuracy for recovering the gold standard. 152 | 153 | To quickly set up such a task there are two scripts in the `bin/` directory. First, we partition the data: 154 | 155 | ``` 156 | python bin/partition_data.py --input_data [path_to_data.json] --n_partitions [n] --n_eval [number of eval elements] --output_prefix [prefix for output files] --seed [integer seed] 157 | ``` 158 | 159 | If no seed is set, the same seed is used every time the script is run, to be able to replicate specific partitioning. This script will generate the following set of files: 160 | - `[output_prefix]_eval.json` 161 | - `[output_prefix]_partition_[x].json for x in n_coders` 162 | 163 | These files now can be used to create a separate task for each coder (based on data in `[output_prefiix]_partition_[x].json` files) as well as for the gold standard coder (based on data in `[output_prefix]_eval.json`). That is, you need to create a separate task file for each partition, ideally with the name of the assigned coder in it. 164 | 165 | ### Retrieving partitioned data 166 | 167 | You can use `bin/retrieve_partitioned_task.py` to get the data from the partitioned task as well as statistics on reliability and accuracy. Use: 168 | ``` 169 | python bin/retrieve_partitioned_task.py -h 170 | ``` 171 | 172 | For help on how to use this script. 173 | --------------------------------------------------------------------------------