Sebastian Lohff 5 years ago
parent
commit
4cf5383d0d

+ 1
- 0
.gitignore View File

@@ -3,3 +3,4 @@
3 3
 .*.swp
4 4
 .*.swo
5 5
 db.sqlite3
6
+dnmapper/settings.py

+ 4
- 0
bgpdata/models.py View File

@@ -84,6 +84,10 @@ class AS(models.Model):
84 84
 	def getPeerings(self):
85 85
 		return Peering.objects.filter(Q(as1=self)|Q(as2=self))
86 86
 
87
+	def formatLastSeen(self):
88
+		if self.lastSeen:
89
+			return self.lastSeen.startTime.strftime("%d.%m.%Y %H:%I")
90
+
87 91
 class BorderRouter(models.Model):
88 92
 	# as id, ip, check method, pingable, reachable
89 93
 	# unique: (crawl_id, asno, as id)

+ 102
- 41
bgpdata/templates/bgpdata/map.html View File

@@ -25,7 +25,7 @@
25 25
 
26 26
 var asdata = [
27 27
 {% for AS in ASses %}
28
-	{id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.lastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}}{%if not forloop.last%},{%endif%}
28
+	{id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.formatLastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}, dismiss: true}{%if not forloop.last%},{%endif%}
29 29
 {%endfor%}
30 30
 ];
31 31
 
@@ -63,7 +63,6 @@ var svg = d3.select('#plotwin')
63 63
 			.attr('width', width)
64 64
 			.attr('height', height);
65 65
 
66
-console.log(asdata);
67 66
 var force = d3.layout.force()
68 67
 	.nodes(asdata)
69 68
 	.links(peerings)
@@ -72,9 +71,7 @@ var force = d3.layout.force()
72 71
 //    .linkDistance(70)
73 72
 	.linkStrength(0.65)
74 73
 	.linkDistance(function(l) {
75
-		console.log(l);
76 74
 		neighs = Math.min(l.source.neighbors, l.target.neighbors);
77
-		console.log(neighs, "neighbors");
78 75
 		switch(neighs) {
79 76
 			case 0: return 40;
80 77
 			case 1: 
@@ -103,6 +100,7 @@ var node = svg.selectAll('.node')
103 100
 			.data(asdata)
104 101
 			.enter()
105 102
 			.append("g")
103
+			.attr("id", function(d) { return "node-" + d.id; })
106 104
 			.call(force.drag);
107 105
 
108 106
 node.append("ellipse")
@@ -120,6 +118,7 @@ node.append("ellipse")
120 118
 	.attr("stroke", "black")
121 119
 	.attr("stroke-width", "1px");
122 120
 
121
+
123 122
 node.append('text')
124 123
 	.attr("font-family", "sans-serif")
125 124
 	.attr("font-size", "13px")
@@ -128,6 +127,57 @@ node.append('text')
128 127
 	.attr("text-anchor", "middle")
129 128
 	.text(function(d) { return d.label; });
130 129
 
130
+var lastMouseNode = null;
131
+node.on("mouseover", function(d) {
132
+		if(lastMouseNode) {
133
+			if(!d.dismiss && lastMouseNode.id == d.id)
134
+				return;
135
+			else
136
+				$("#node-"+lastMouseNode.id).tipsy("hide");
137
+		}
138
+
139
+		d.dismiss = true;
140
+		$("#node-"+d.id).tipsy("show");
141
+
142
+		lastMouseNode = d;
143
+	})
144
+	.on("mouseout", function(d) {
145
+		if(d.dismiss)
146
+			$("#node-"+d.id).tipsy("hide");
147
+	});
148
+
149
+$('svg g').tipsy({
150
+	gravity: 'e',
151
+	offset: 20,
152
+	html: true,
153
+	trigger: 'manual',
154
+	title: function() {
155
+		var d = this.__data__;
156
+		var content = '';
157
+		if(d.nodetype == 'AS') {
158
+			var state = null;
159
+			if(d.online)
160
+				state = '<span class="label label-success">Online</span>';
161
+			else if(d.lastSeen)
162
+				state = '<span class="label label-danger">Offline</span>';
163
+			else
164
+				state = '<span class="label label-default">Never seen</span>';
165
+
166
+			content  = '<b>AS '+d.asnumber+'</b><br>';
167
+			content += '<table style="opacity: 1.0" class="table table-bordered">';
168
+			content += '<tr><th>State</th><td>'+state+'</td>';
169
+			if(d.lastSeen) {
170
+				content += '<tr><th>Last seen</th><td><a href="/map/'+d.lastSeen+'/">Crawl '+d.lastSeen+'<br>'+d.lastSeenDate+'</a></td></tr>';
171
+			}
172
+			if(d.crawled) {
173
+				content += '<tr><th>Note</th><td>Directly crawled</td></tr>';
174
+			}
175
+			content += '</table>';
176
+		}
177
+		return content;
178
+	}
179
+});
180
+
131 181
 force.on("tick", function() {
132 182
 	//node.attr('cx', function(d) { return d.x; })
133 183
 	//	.attr('cy', function(d) { return d.y; });
@@ -139,51 +189,62 @@ force.on("tick", function() {
139 189
 		.attr("y2", function(d) { return d.target.y; });
140 190
 });
141 191
 
192
+
142 193
 var lastNode = null;
143 194
 function click(d) {
144 195
 	if(d3.event.defaultPrevented) return;
145
-	if(lastNode)
146
-		console.log("last time you clicked on another node", d);
196
+
197
+	//if(lastNode)
198
+	//	$("#node-" + lastNode.id).tipsy("hide");
199
+	d.dismiss = false;
200
+	//$("#node-" + d.id).tipsy("show");
201
+
202
+
147 203
 	$("#infowin").fadeOut('fast', function() {
148 204
 		// set progress bar
149 205
 		$("#infowin").html('<div class="progress"><div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"><span class="sr-only">Loading...</span></div></div>');
150 206
 
151 207
 
152 208
 
153
-		console.log("fading done");
154 209
 		var content = '';
155
-		if(d.nodetype == 'AS' && d.crawled) {
156
-			console.log("doing javascript...");
157
-			$.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) {
158
-				console.log("SUCCESS", result);
159
-				$("#infowin").html('');
160
-				$("#infowin").fadeIn('fast', function() {});
161
-				for(var i=0; i<result.objects.length; i++) {
162
-					(function(currRouter) {
163
-						$.ajax({url:"/map/api/announcement/?router="+currRouter.id, success: function(result2) {
164
-							console.log("Current router on success", currRouter);
165
-							astable  = 'Table for AS'+d.asnumber+' router ID ' + currRouter.routerID;
166
-							astable += '<table class="table table-bordered">';
167
-							astable += '<tr><th>Network</th><th>Next Hop</th><th>AS Path</th></tr>';
168
-							for(var j=0; j<result2.objects.length; j++) {
169
-								ann = result2.objects[j];
170
-								astable += '<tr><td>'+ann.ip+'/'+ann.prefix+'</td><td>'+ann.nextHop+'</td><td>'+ann.ASPath+'</td></tr>';
171
-							}
172
-							astable += '</table>';
173
-							console.log("Sending stuff for", currRouter.id, "aka", currRouter.routerID);
174
-							$('#infowin').append(astable);
175
-						}});
176
-					})(result.objects[i]);
177
-				}
178
-
179
-			}});
180
-			astable = '<table class="table table-bordered table-hover">';
181
-			astable += '<tr><th scope="row">AS</th><td>'+ d.asnumber +'<td></tr>';
182
-			astable += '<tr><th>Directly crawled</th><td><td></tr>';
183
-			astable += '<tr><th>Online</th><td><td></tr>';
184
-			astable += '<tr><th></th><td><td></tr>';
185
-				
186
-			astable += '</table><div id="announcement"></div>';
210
+		if(d.nodetype == 'AS') {
211
+			//$("#node-"+d.id).popover({content: "Hallo Katze! :)"});
212
+			//$("#node-"+d.id).popover('show');
213
+			//console.log("My element", $("#node-"+d.id));
214
+			//$("#node-"+d.id).tipsy({
215
+			//	gravity: 'e',
216
+			//	html: true,
217
+			//	//trigger: 'focus',
218
+			//	trigger: 'hover',
219
+			//	title: function() {
220
+			//		var d = this.__data__;
221
+			//		astable = '<table class="table table-bordered"><tr><td>FOOO</td><td>bar</td></table>';
222
+			//		return astable;
223
+			//	}
224
+			//});
225
+
226
+			if(d.crawled) {
227
+				$.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) {
228
+					$("#infowin").html('');
229
+					$("#infowin").fadeIn('fast', function() {});
230
+					for(var i=0; i<result.objects.length; i++) {
231
+						(function(currRouter) {
232
+							$.ajax({url:"/map/api/announcement/?router="+currRouter.id, success: function(result2) {
233
+								astable  = 'Table for AS'+d.asnumber+' router ID ' + currRouter.routerID;
234
+								astable += '<table class="table table-bordered">';
235
+								astable += '<tr><th>Network</th><th>Next Hop</th><th>AS Path</th></tr>';
236
+								for(var j=0; j<result2.objects.length; j++) {
237
+									ann = result2.objects[j];
238
+									astable += '<tr><td>'+ann.ip+'/'+ann.prefix+'</td><td>'+ann.nextHop+'</td><td>'+ann.ASPath+'</td></tr>';
239
+								}
240
+								astable += '</table>';
241
+								$('#infowin').append(astable);
242
+							}});
243
+						})(result.objects[i]);
244
+					}
245
+
246
+				}});
247
+			}
187 248
 
188 249
 
189 250
 		} else {
@@ -191,10 +252,10 @@ function click(d) {
191 252
 			$("#infowin").fadeIn('fast', function() {});
192 253
 		}
193 254
 	});
194
-	console.log("Single click on", d);
195 255
 	lastNode = d;
196 256
 }
197
-node.on("click", click)
257
+node.on("click", click);
258
+
198 259
 
199 260
 force.start();
200 261
 

+ 93
- 0
dnmapper/settings.default.py View File

@@ -0,0 +1,93 @@
1
+"""
2
+Django settings for dnmapper project.
3
+
4
+For more information on this file, see
5
+https://docs.djangoproject.com/en/1.7/topics/settings/
6
+
7
+For the full list of settings and their values, see
8
+https://docs.djangoproject.com/en/1.7/ref/settings/
9
+"""
10
+
11
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12
+import os
13
+BASE_DIR = os.path.dirname(os.path.dirname(__file__))
14
+
15
+
16
+# Quick-start development settings - unsuitable for production
17
+# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
18
+
19
+# SECURITY WARNING: keep the secret key used in production secret!
20
+SECRET_KEY = 'crv*hfx2pkxvq1s!)dbz*hdu+r7u2$y4djf6_#6mm)shk9e!58'
21
+
22
+# SECURITY WARNING: don't run with debug turned on in production!
23
+DEBUG = True
24
+
25
+TEMPLATE_DEBUG = True
26
+
27
+ALLOWED_HOSTS = []
28
+
29
+STATICFILES_DIRS = (
30
+	'static/',
31
+)
32
+
33
+# Application definition
34
+
35
+INSTALLED_APPS = (
36
+    'django.contrib.admin',
37
+    'django.contrib.auth',
38
+    'django.contrib.contenttypes',
39
+    'django.contrib.sessions',
40
+    'django.contrib.messages',
41
+    'django.contrib.staticfiles',
42
+	'bgpdata',
43
+	'tastypie',
44
+)
45
+
46
+API_LIMIT_PER_PAGE = 100
47
+
48
+MIDDLEWARE_CLASSES = (
49
+    'django.contrib.sessions.middleware.SessionMiddleware',
50
+    'django.middleware.common.CommonMiddleware',
51
+    'django.middleware.csrf.CsrfViewMiddleware',
52
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
53
+    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
54
+    'django.contrib.messages.middleware.MessageMiddleware',
55
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
56
+)
57
+
58
+ROOT_URLCONF = 'dnmapper.urls'
59
+
60
+WSGI_APPLICATION = 'dnmapper.wsgi.application'
61
+
62
+
63
+# Database
64
+# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
65
+
66
+DATABASES = {
67
+    'default': {
68
+        'ENGINE': 'django.db.backends.sqlite3',
69
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
70
+    }
71
+}
72
+
73
+# Internationalization
74
+# https://docs.djangoproject.com/en/1.7/topics/i18n/
75
+
76
+LANGUAGE_CODE = 'de-de'
77
+
78
+TIME_ZONE = 'CET'
79
+
80
+USE_I18N = True
81
+
82
+USE_L10N = True
83
+
84
+USE_TZ = True
85
+TEMPLATE_DIRS = (
86
+	'templates/',
87
+)
88
+
89
+
90
+# Static files (CSS, JavaScript, Images)
91
+# https://docs.djangoproject.com/en/1.7/howto/static-files/
92
+
93
+STATIC_URL = '/static/'

+ 25
- 0
static/css/tipsy.css View File

@@ -0,0 +1,25 @@
1
+.tipsy { font-size: 14px; position: absolute; padding: 5px; z-index: 100000; }
2
+  .tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; }
3
+
4
+  /* Rounded corners */
5
+  .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; }
6
+  
7
+  /* Uncomment for shadow */
8
+  /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/
9
+  
10
+  .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; }
11
+  
12
+  /* Rules to colour arrows */
13
+  .tipsy-arrow-n { border-bottom-color: #000; }
14
+  .tipsy-arrow-s { border-top-color: #000; }
15
+  .tipsy-arrow-e { border-left-color: #000; }
16
+  .tipsy-arrow-w { border-right-color: #000; }
17
+  
18
+	.tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; }
19
+    .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
20
+    .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none;  border-left-color: transparent; border-right-color: transparent;}
21
+  .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none;  border-left-color: transparent; border-right-color: transparent; }
22
+    .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none;  border-left-color: transparent; border-right-color: transparent; }
23
+    .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
24
+  .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; }
25
+  .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; }

+ 260
- 0
static/js/jquery.tipsy.js View File

@@ -0,0 +1,260 @@
1
+// tipsy, facebook style tooltips for jquery
2
+// version 1.0.0a
3
+// (c) 2008-2010 jason frame [jason@onehackoranother.com]
4
+// released under the MIT license
5
+
6
+(function($) {
7
+    
8
+    function maybeCall(thing, ctx) {
9
+        return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
10
+    };
11
+    
12
+    function isElementInDOM(ele) {
13
+      while (ele = ele.parentNode) {
14
+        if (ele == document) return true;
15
+      }
16
+      return false;
17
+    };
18
+    
19
+    function Tipsy(element, options) {
20
+        this.$element = $(element);
21
+        this.options = options;
22
+        this.enabled = true;
23
+        this.fixTitle();
24
+    };
25
+    
26
+    Tipsy.prototype = {
27
+        show: function() {
28
+            var title = this.getTitle();
29
+            if (title && this.enabled) {
30
+                var $tip = this.tip();
31
+                
32
+                $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
33
+                $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
34
+                $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
35
+                
36
+                var pos = $.extend({}, this.$element.offset(), {
37
+                    width: this.$element[0].offsetWidth,
38
+                    height: this.$element[0].offsetHeight
39
+                });
40
+                
41
+                var actualWidth = $tip[0].offsetWidth,
42
+                    actualHeight = $tip[0].offsetHeight,
43
+                    gravity = maybeCall(this.options.gravity, this.$element[0]);
44
+                
45
+                var tp;
46
+                switch (gravity.charAt(0)) {
47
+                    case 'n':
48
+                        tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
49
+                        break;
50
+                    case 's':
51
+                        tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
52
+                        break;
53
+                    case 'e':
54
+                        tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
55
+						// XXX HACK: i want to set another offset so apparently I have to hardcode this into the code. obviously.
56
+                        tp = {top: pos.top + pos.height / 2 - actualHeight / 2 + this.options.offset, left: pos.left - actualWidth};
57
+                        break;
58
+                    case 'w':
59
+                        tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
60
+                        break;
61
+                }
62
+                
63
+                if (gravity.length == 2) {
64
+                    if (gravity.charAt(1) == 'w') {
65
+                        tp.left = pos.left + pos.width / 2 - 15;
66
+                    } else {
67
+                        tp.left = pos.left + pos.width / 2 - actualWidth + 15;
68
+                    }
69
+                }
70
+                
71
+                $tip.css(tp).addClass('tipsy-' + gravity);
72
+                $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0);
73
+                if (this.options.className) {
74
+                    $tip.addClass(maybeCall(this.options.className, this.$element[0]));
75
+                }
76
+                
77
+                if (this.options.fade) {
78
+                    $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity});
79
+                } else {
80
+                    $tip.css({visibility: 'visible', opacity: this.options.opacity});
81
+                }
82
+            }
83
+        },
84
+        
85
+        hide: function() {
86
+            if (this.options.fade) {
87
+                this.tip().stop().fadeOut(function() { $(this).remove(); });
88
+            } else {
89
+                this.tip().remove();
90
+            }
91
+        },
92
+        
93
+        fixTitle: function() {
94
+            var $e = this.$element;
95
+            if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
96
+                $e.attr('original-title', $e.attr('title') || '').removeAttr('title');
97
+            }
98
+        },
99
+        
100
+        getTitle: function() {
101
+            var title, $e = this.$element, o = this.options;
102
+            this.fixTitle();
103
+            var title, o = this.options;
104
+            if (typeof o.title == 'string') {
105
+                title = $e.attr(o.title == 'title' ? 'original-title' : o.title);
106
+            } else if (typeof o.title == 'function') {
107
+                title = o.title.call($e[0]);
108
+            }
109
+            title = ('' + title).replace(/(^\s*|\s*$)/, "");
110
+            return title || o.fallback;
111
+        },
112
+        
113
+        tip: function() {
114
+            if (!this.$tip) {
115
+                this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
116
+                this.$tip.data('tipsy-pointee', this.$element[0]);
117
+            }
118
+            return this.$tip;
119
+        },
120
+        
121
+        validate: function() {
122
+            if (!this.$element[0].parentNode) {
123
+                this.hide();
124
+                this.$element = null;
125
+                this.options = null;
126
+            }
127
+        },
128
+        
129
+        enable: function() { this.enabled = true; },
130
+        disable: function() { this.enabled = false; },
131
+        toggleEnabled: function() { this.enabled = !this.enabled; }
132
+    };
133
+    
134
+    $.fn.tipsy = function(options) {
135
+        
136
+        if (options === true) {
137
+            return this.data('tipsy');
138
+        } else if (typeof options == 'string') {
139
+            var tipsy = this.data('tipsy');
140
+            if (tipsy) tipsy[options]();
141
+            return this;
142
+        }
143
+        
144
+        options = $.extend({}, $.fn.tipsy.defaults, options);
145
+        
146
+        function get(ele) {
147
+            var tipsy = $.data(ele, 'tipsy');
148
+            if (!tipsy) {
149
+                tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options));
150
+                $.data(ele, 'tipsy', tipsy);
151
+            }
152
+            return tipsy;
153
+        }
154
+        
155
+        function enter() {
156
+            var tipsy = get(this);
157
+            tipsy.hoverState = 'in';
158
+            if (options.delayIn == 0) {
159
+                tipsy.show();
160
+            } else {
161
+                tipsy.fixTitle();
162
+                setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn);
163
+            }
164
+        };
165
+        
166
+        function leave() {
167
+            var tipsy = get(this);
168
+            tipsy.hoverState = 'out';
169
+            if (options.delayOut == 0) {
170
+                tipsy.hide();
171
+            } else {
172
+                setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
173
+            }
174
+        };
175
+        
176
+        if (!options.live) this.each(function() { get(this); });
177
+        
178
+        if (options.trigger != 'manual') {
179
+            var binder   = options.live ? 'live' : 'bind',
180
+                eventIn  = options.trigger == 'hover' ? 'mouseenter' : 'focus',
181
+                eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
182
+            this[binder](eventIn, enter)[binder](eventOut, leave);
183
+        }
184
+        
185
+        return this;
186
+        
187
+    };
188
+    
189
+    $.fn.tipsy.defaults = {
190
+        className: null,
191
+        delayIn: 0,
192
+        delayOut: 0,
193
+        fade: false,
194
+        fallback: '',
195
+        gravity: 'n',
196
+        html: false,
197
+        live: false,
198
+        offset: 0,
199
+        opacity: 0.8,
200
+        title: 'title',
201
+        trigger: 'hover'
202
+    };
203
+    
204
+    $.fn.tipsy.revalidate = function() {
205
+      $('.tipsy').each(function() {
206
+        var pointee = $.data(this, 'tipsy-pointee');
207
+        if (!pointee || !isElementInDOM(pointee)) {
208
+          $(this).remove();
209
+        }
210
+      });
211
+    };
212
+    
213
+    // Overwrite this method to provide options on a per-element basis.
214
+    // For example, you could store the gravity in a 'tipsy-gravity' attribute:
215
+    // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
216
+    // (remember - do not modify 'options' in place!)
217
+    $.fn.tipsy.elementOptions = function(ele, options) {
218
+        return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
219
+    };
220
+    
221
+    $.fn.tipsy.autoNS = function() {
222
+        return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
223
+    };
224
+    
225
+    $.fn.tipsy.autoWE = function() {
226
+        return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
227
+    };
228
+    
229
+    /**
230
+     * yields a closure of the supplied parameters, producing a function that takes
231
+     * no arguments and is suitable for use as an autogravity function like so:
232
+     *
233
+     * @param margin (int) - distance from the viewable region edge that an
234
+     *        element should be before setting its tooltip's gravity to be away
235
+     *        from that edge.
236
+     * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
237
+     *        if there are no viewable region edges effecting the tooltip's
238
+     *        gravity. It will try to vary from this minimally, for example,
239
+     *        if 'sw' is preferred and an element is near the right viewable 
240
+     *        region edge, but not the top edge, it will set the gravity for
241
+     *        that element's tooltip to be 'se', preserving the southern
242
+     *        component.
243
+     */
244
+     $.fn.tipsy.autoBounds = function(margin, prefer) {
245
+		return function() {
246
+			var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)},
247
+			    boundTop = $(document).scrollTop() + margin,
248
+			    boundLeft = $(document).scrollLeft() + margin,
249
+			    $this = $(this);
250
+
251
+			if ($this.offset().top < boundTop) dir.ns = 'n';
252
+			if ($this.offset().left < boundLeft) dir.ew = 'w';
253
+			if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e';
254
+			if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's';
255
+
256
+			return dir.ns + (dir.ew ? dir.ew : '');
257
+		}
258
+	};
259
+    
260
+})(jQuery);

+ 2
- 0
templates/base.html View File

@@ -4,10 +4,12 @@
4 4
 <head>
5 5
 	<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
6 6
 	<link href="{% static "css/main.css" %}" rel="stylesheet">
7
+	<link href="{% static "css/tipsy.css" %}" rel="stylesheet">
7 8
 {% block head %}{% endblock %}
8 9
 </head>
9 10
 <body>
10 11
 	<script src="{% static "js/jquery.min.js" %}"></script>
12
+	<script src="{% static "js/jquery.tipsy.js" %}"></script>
11 13
 	<script src="{% static "js/bootstrap.min.js" %}"></script>
12 14
 
13 15
     <!-- Static navbar -->

Loading…
Cancel
Save