├── FS-rpc.py ├── README ├── __init__.py ├── bin ├── jquery-1.3.1.min.js ├── json2.js ├── trimpath-template-1.0.38.js ├── ui.core.js └── ui.sortable.js ├── conference ├── __init__.py ├── admin.py ├── models.py ├── urls.py └── views.py ├── debug.py ├── dialplan ├── __init__.py ├── admin.py ├── models.py ├── templates │ ├── condition_inline.html │ ├── dialplan.html │ └── dialplan.xml ├── urls.py └── views.py ├── eventsocket ├── InboundOutboundExample.py ├── SimpleOriginateExample.py ├── __init__.py ├── eventsocket.py └── kuku.py ├── fs2web.wsgi ├── fsapi.py ├── local_settings.py.template ├── locale └── ru │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── manage.py ├── settings.py ├── superdict.py ├── templates ├── base.html ├── confrences.html ├── group.xml ├── index.html └── user.xml ├── urls.py └── users ├── __init__.py ├── admin.py ├── models.py ├── templates └── users │ ├── fsuser_detail.html │ └── fsuser_edit.html ├── urls.py └── views.py /FS-rpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from xmlrpclib import ServerProxy 4 | 5 | host = 'localhost' 6 | username = 'freeswitch' 7 | password = 'works' 8 | port = '8080' 9 | 10 | server = ServerProxy("http://%s:%s@%s:%s" % (username, password, host, port)) 11 | print server.freeswitch.api("show","channels") 12 | 13 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ============== 2 | Fs2web 3 | ============== 4 | 5 | 1. Introduction_ 6 | 2. Requirements_ 7 | 3. Installation_ 8 | 9 | Introduction 10 | ============ 11 | 12 | Fs2web is the Django based Web UI for FreeSWITCH. 13 | 14 | Django: http://www.djangoproject.com/ 15 | FreeSWITCH: http://freeswitch.org 16 | 17 | Requirements 18 | ============ 19 | 20 | - Django >= 1.0 21 | - FreeSWITCH - svn trunk 22 | 23 | Installation 24 | ============ 25 | 26 | Запуск - cd fs2web; ./manage.py runserver 27 | 28 | Для редактирования настроек надо зайти в административный интерфейс: http://127.0.0.1:8000/admin/ 29 | Логин admin, пароль kuku. 30 | 31 | В conf/autoload_configs/xml_curl.conf.xml: 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | И включить загрузку модуля xml_curl в conf/autoload_configs/modules.conf.xml 46 | 47 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/__init__.py -------------------------------------------------------------------------------- /bin/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.JSON.org/json2.js 3 | 2008-11-19 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | This file creates a global JSON object containing two methods: stringify 12 | and parse. 13 | 14 | JSON.stringify(value, replacer, space) 15 | value any JavaScript value, usually an object or array. 16 | 17 | replacer an optional parameter that determines how object 18 | values are stringified for objects. It can be a 19 | function or an array of strings. 20 | 21 | space an optional parameter that specifies the indentation 22 | of nested structures. If it is omitted, the text will 23 | be packed without extra whitespace. If it is a number, 24 | it will specify the number of spaces to indent at each 25 | level. If it is a string (such as '\t' or ' '), 26 | it contains the characters used to indent at each level. 27 | 28 | This method produces a JSON text from a JavaScript value. 29 | 30 | When an object value is found, if the object contains a toJSON 31 | method, its toJSON method will be called and the result will be 32 | stringified. A toJSON method does not serialize: it returns the 33 | value represented by the name/value pair that should be serialized, 34 | or undefined if nothing should be serialized. The toJSON method 35 | will be passed the key associated with the value, and this will be 36 | bound to the object holding the key. 37 | 38 | For example, this would serialize Dates as ISO strings. 39 | 40 | Date.prototype.toJSON = function (key) { 41 | function f(n) { 42 | // Format integers to have at least two digits. 43 | return n < 10 ? '0' + n : n; 44 | } 45 | 46 | return this.getUTCFullYear() + '-' + 47 | f(this.getUTCMonth() + 1) + '-' + 48 | f(this.getUTCDate()) + 'T' + 49 | f(this.getUTCHours()) + ':' + 50 | f(this.getUTCMinutes()) + ':' + 51 | f(this.getUTCSeconds()) + 'Z'; 52 | }; 53 | 54 | You can provide an optional replacer method. It will be passed the 55 | key and value of each member, with this bound to the containing 56 | object. The value that is returned from your method will be 57 | serialized. If your method returns undefined, then the member will 58 | be excluded from the serialization. 59 | 60 | If the replacer parameter is an array of strings, then it will be 61 | used to select the members to be serialized. It filters the results 62 | such that only members with keys listed in the replacer array are 63 | stringified. 64 | 65 | Values that do not have JSON representations, such as undefined or 66 | functions, will not be serialized. Such values in objects will be 67 | dropped; in arrays they will be replaced with null. You can use 68 | a replacer function to replace those with JSON values. 69 | JSON.stringify(undefined) returns undefined. 70 | 71 | The optional space parameter produces a stringification of the 72 | value that is filled with line breaks and indentation to make it 73 | easier to read. 74 | 75 | If the space parameter is a non-empty string, then that string will 76 | be used for indentation. If the space parameter is a number, then 77 | the indentation will be that many spaces. 78 | 79 | Example: 80 | 81 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 82 | // text is '["e",{"pluribus":"unum"}]' 83 | 84 | 85 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 86 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 87 | 88 | text = JSON.stringify([new Date()], function (key, value) { 89 | return this[key] instanceof Date ? 90 | 'Date(' + this[key] + ')' : value; 91 | }); 92 | // text is '["Date(---current time---)"]' 93 | 94 | 95 | JSON.parse(text, reviver) 96 | This method parses a JSON text to produce an object or array. 97 | It can throw a SyntaxError exception. 98 | 99 | The optional reviver parameter is a function that can filter and 100 | transform the results. It receives each of the keys and values, 101 | and its return value is used instead of the original value. 102 | If it returns what it received, then the structure is not modified. 103 | If it returns undefined then the member is deleted. 104 | 105 | Example: 106 | 107 | // Parse the text. Values that look like ISO date strings will 108 | // be converted to Date objects. 109 | 110 | myData = JSON.parse(text, function (key, value) { 111 | var a; 112 | if (typeof value === 'string') { 113 | a = 114 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 115 | if (a) { 116 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 117 | +a[5], +a[6])); 118 | } 119 | } 120 | return value; 121 | }); 122 | 123 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 124 | var d; 125 | if (typeof value === 'string' && 126 | value.slice(0, 5) === 'Date(' && 127 | value.slice(-1) === ')') { 128 | d = new Date(value.slice(5, -1)); 129 | if (d) { 130 | return d; 131 | } 132 | } 133 | return value; 134 | }); 135 | 136 | 137 | This is a reference implementation. You are free to copy, modify, or 138 | redistribute. 139 | 140 | This code should be minified before deployment. 141 | See http://javascript.crockford.com/jsmin.html 142 | 143 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 144 | NOT CONTROL. 145 | */ 146 | 147 | /*jslint evil: true */ 148 | 149 | /*global JSON */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | // Create a JSON object only if one does not already exist. We create the 159 | // methods in a closure to avoid creating global variables. 160 | 161 | if (!this.JSON) { 162 | JSON = {}; 163 | } 164 | (function () { 165 | 166 | function f(n) { 167 | // Format integers to have at least two digits. 168 | return n < 10 ? '0' + n : n; 169 | } 170 | 171 | if (typeof Date.prototype.toJSON !== 'function') { 172 | 173 | Date.prototype.toJSON = function (key) { 174 | 175 | return this.getUTCFullYear() + '-' + 176 | f(this.getUTCMonth() + 1) + '-' + 177 | f(this.getUTCDate()) + 'T' + 178 | f(this.getUTCHours()) + ':' + 179 | f(this.getUTCMinutes()) + ':' + 180 | f(this.getUTCSeconds()) + 'Z'; 181 | }; 182 | 183 | String.prototype.toJSON = 184 | Number.prototype.toJSON = 185 | Boolean.prototype.toJSON = function (key) { 186 | return this.valueOf(); 187 | }; 188 | } 189 | 190 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 191 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 192 | gap, 193 | indent, 194 | meta = { // table of character substitutions 195 | '\b': '\\b', 196 | '\t': '\\t', 197 | '\n': '\\n', 198 | '\f': '\\f', 199 | '\r': '\\r', 200 | '"' : '\\"', 201 | '\\': '\\\\' 202 | }, 203 | rep; 204 | 205 | 206 | function quote(string) { 207 | 208 | // If the string contains no control characters, no quote characters, and no 209 | // backslash characters, then we can safely slap some quotes around it. 210 | // Otherwise we must also replace the offending characters with safe escape 211 | // sequences. 212 | 213 | escapable.lastIndex = 0; 214 | return escapable.test(string) ? 215 | '"' + string.replace(escapable, function (a) { 216 | var c = meta[a]; 217 | return typeof c === 'string' ? c : 218 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 219 | }) + '"' : 220 | '"' + string + '"'; 221 | } 222 | 223 | 224 | function str(key, holder) { 225 | 226 | // Produce a string from holder[key]. 227 | 228 | var i, // The loop counter. 229 | k, // The member key. 230 | v, // The member value. 231 | length, 232 | mind = gap, 233 | partial, 234 | value = holder[key]; 235 | 236 | // If the value has a toJSON method, call it to obtain a replacement value. 237 | 238 | if (value && typeof value === 'object' && 239 | typeof value.toJSON === 'function') { 240 | value = value.toJSON(key); 241 | } 242 | 243 | // If we were called with a replacer function, then call the replacer to 244 | // obtain a replacement value. 245 | 246 | if (typeof rep === 'function') { 247 | value = rep.call(holder, key, value); 248 | } 249 | 250 | // What happens next depends on the value's type. 251 | 252 | switch (typeof value) { 253 | case 'string': 254 | return quote(value); 255 | 256 | case 'number': 257 | 258 | // JSON numbers must be finite. Encode non-finite numbers as null. 259 | 260 | return isFinite(value) ? String(value) : 'null'; 261 | 262 | case 'boolean': 263 | case 'null': 264 | 265 | // If the value is a boolean or null, convert it to a string. Note: 266 | // typeof null does not produce 'null'. The case is included here in 267 | // the remote chance that this gets fixed someday. 268 | 269 | return String(value); 270 | 271 | // If the type is 'object', we might be dealing with an object or an array or 272 | // null. 273 | 274 | case 'object': 275 | 276 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 277 | // so watch out for that case. 278 | 279 | if (!value) { 280 | return 'null'; 281 | } 282 | 283 | // Make an array to hold the partial results of stringifying this object value. 284 | 285 | gap += indent; 286 | partial = []; 287 | 288 | // Is the value an array? 289 | 290 | if (Object.prototype.toString.apply(value) === '[object Array]') { 291 | 292 | // The value is an array. Stringify every element. Use null as a placeholder 293 | // for non-JSON values. 294 | 295 | length = value.length; 296 | for (i = 0; i < length; i += 1) { 297 | partial[i] = str(i, value) || 'null'; 298 | } 299 | 300 | // Join all of the elements together, separated with commas, and wrap them in 301 | // brackets. 302 | 303 | v = partial.length === 0 ? '[]' : 304 | gap ? '[\n' + gap + 305 | partial.join(',\n' + gap) + '\n' + 306 | mind + ']' : 307 | '[' + partial.join(',') + ']'; 308 | gap = mind; 309 | return v; 310 | } 311 | 312 | // If the replacer is an array, use it to select the members to be stringified. 313 | 314 | if (rep && typeof rep === 'object') { 315 | length = rep.length; 316 | for (i = 0; i < length; i += 1) { 317 | k = rep[i]; 318 | if (typeof k === 'string') { 319 | v = str(k, value); 320 | if (v) { 321 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 322 | } 323 | } 324 | } 325 | } else { 326 | 327 | // Otherwise, iterate through all of the keys in the object. 328 | 329 | for (k in value) { 330 | if (Object.hasOwnProperty.call(value, k)) { 331 | v = str(k, value); 332 | if (v) { 333 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 334 | } 335 | } 336 | } 337 | } 338 | 339 | // Join all of the member texts together, separated with commas, 340 | // and wrap them in braces. 341 | 342 | v = partial.length === 0 ? '{}' : 343 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + 344 | mind + '}' : '{' + partial.join(',') + '}'; 345 | gap = mind; 346 | return v; 347 | } 348 | } 349 | 350 | // If the JSON object does not yet have a stringify method, give it one. 351 | 352 | if (typeof JSON.stringify !== 'function') { 353 | JSON.stringify = function (value, replacer, space) { 354 | 355 | // The stringify method takes a value and an optional replacer, and an optional 356 | // space parameter, and returns a JSON text. The replacer can be a function 357 | // that can replace values, or an array of strings that will select the keys. 358 | // A default replacer method can be provided. Use of the space parameter can 359 | // produce text that is more easily readable. 360 | 361 | var i; 362 | gap = ''; 363 | indent = ''; 364 | 365 | // If the space parameter is a number, make an indent string containing that 366 | // many spaces. 367 | 368 | if (typeof space === 'number') { 369 | for (i = 0; i < space; i += 1) { 370 | indent += ' '; 371 | } 372 | 373 | // If the space parameter is a string, it will be used as the indent string. 374 | 375 | } else if (typeof space === 'string') { 376 | indent = space; 377 | } 378 | 379 | // If there is a replacer, it must be a function or an array. 380 | // Otherwise, throw an error. 381 | 382 | rep = replacer; 383 | if (replacer && typeof replacer !== 'function' && 384 | (typeof replacer !== 'object' || 385 | typeof replacer.length !== 'number')) { 386 | throw new Error('JSON.stringify'); 387 | } 388 | 389 | // Make a fake root object containing our value under the key of ''. 390 | // Return the result of stringifying the value. 391 | 392 | return str('', {'': value}); 393 | }; 394 | } 395 | 396 | 397 | // If the JSON object does not yet have a parse method, give it one. 398 | 399 | if (typeof JSON.parse !== 'function') { 400 | JSON.parse = function (text, reviver) { 401 | 402 | // The parse method takes a text and an optional reviver function, and returns 403 | // a JavaScript value if the text is a valid JSON text. 404 | 405 | var j; 406 | 407 | function walk(holder, key) { 408 | 409 | // The walk method is used to recursively walk the resulting structure so 410 | // that modifications can be made. 411 | 412 | var k, v, value = holder[key]; 413 | if (value && typeof value === 'object') { 414 | for (k in value) { 415 | if (Object.hasOwnProperty.call(value, k)) { 416 | v = walk(value, k); 417 | if (v !== undefined) { 418 | value[k] = v; 419 | } else { 420 | delete value[k]; 421 | } 422 | } 423 | } 424 | } 425 | return reviver.call(holder, key, value); 426 | } 427 | 428 | 429 | // Parsing happens in four stages. In the first stage, we replace certain 430 | // Unicode characters with escape sequences. JavaScript handles many characters 431 | // incorrectly, either silently deleting them, or treating them as line endings. 432 | 433 | cx.lastIndex = 0; 434 | if (cx.test(text)) { 435 | text = text.replace(cx, function (a) { 436 | return '\\u' + 437 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 438 | }); 439 | } 440 | 441 | // In the second stage, we run the text against regular expressions that look 442 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 443 | // because they can cause invocation, and '=' because it can cause mutation. 444 | // But just to be safe, we want to reject all unexpected forms. 445 | 446 | // We split the second stage into 4 regexp operations in order to work around 447 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 448 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 449 | // replace all simple value tokens with ']' characters. Third, we delete all 450 | // open brackets that follow a colon or comma or that begin the text. Finally, 451 | // we look to see that the remaining characters are only whitespace or ']' or 452 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 453 | 454 | if (/^[\],:{}\s]*$/. 455 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). 456 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). 457 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 458 | 459 | // In the third stage we use the eval function to compile the text into a 460 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 461 | // in JavaScript: it can begin a block or an object literal. We wrap the text 462 | // in parens to eliminate the ambiguity. 463 | 464 | j = eval('(' + text + ')'); 465 | 466 | // In the optional fourth stage, we recursively walk the new structure, passing 467 | // each name/value pair to a reviver function for possible transformation. 468 | 469 | return typeof reviver === 'function' ? 470 | walk({'': j}, '') : j; 471 | } 472 | 473 | // If the text is not JSON parseable, then a SyntaxError is thrown. 474 | 475 | throw new SyntaxError('JSON.parse'); 476 | }; 477 | } 478 | })(); 479 | -------------------------------------------------------------------------------- /bin/trimpath-template-1.0.38.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TrimPath Template. Release 1.0.38. 3 | * Copyright (C) 2004, 2005 Metaha. 4 | * 5 | * TrimPath Template is licensed under the GNU General Public License 6 | * and the Apache License, Version 2.0, as follows: 7 | * 8 | * This program is free software; you can redistribute it and/or 9 | * modify it under the terms of the GNU General Public License 10 | * as published by the Free Software Foundation; either version 2 11 | * of the License, or (at your option) any later version. 12 | * 13 | * This program is distributed WITHOUT ANY WARRANTY; without even the 14 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | * See the GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program; if not, write to the Free Software 19 | * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | * 21 | * Licensed under the Apache License, Version 2.0 (the "License"); 22 | * you may not use this file except in compliance with the License. 23 | * You may obtain a copy of the License at 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software 28 | * distributed under the License is distributed on an "AS IS" BASIS, 29 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | * See the License for the specific language governing permissions and 31 | * limitations under the License. 32 | */ 33 | var TrimPath; 34 | 35 | // TODO: Debugging mode vs stop-on-error mode - runtime flag. 36 | // TODO: Handle || (or) characters and backslashes. 37 | // TODO: Add more modifiers. 38 | 39 | (function() { // Using a closure to keep global namespace clean. 40 | if (TrimPath == null) 41 | TrimPath = new Object(); 42 | if (TrimPath.evalEx == null) 43 | TrimPath.evalEx = function(src) { return eval(src); }; 44 | 45 | var UNDEFINED; 46 | if (Array.prototype.pop == null) // IE 5.x fix from Igor Poteryaev. 47 | Array.prototype.pop = function() { 48 | if (this.length === 0) {return UNDEFINED;} 49 | return this[--this.length]; 50 | }; 51 | if (Array.prototype.push == null) // IE 5.x fix from Igor Poteryaev. 52 | Array.prototype.push = function() { 53 | for (var i = 0; i < arguments.length; ++i) {this[this.length] = arguments[i];} 54 | return this.length; 55 | }; 56 | 57 | TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) { 58 | if (optEtc == null) 59 | optEtc = TrimPath.parseTemplate_etc; 60 | var funcSrc = parse(tmplContent, optTmplName, optEtc); 61 | var func = TrimPath.evalEx(funcSrc, optTmplName, 1); 62 | if (func != null) 63 | return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc); 64 | return null; 65 | } 66 | 67 | try { 68 | String.prototype.process = function(context, optFlags) { 69 | var template = TrimPath.parseTemplate(this, null); 70 | if (template != null) 71 | return template.process(context, optFlags); 72 | return this; 73 | } 74 | } catch (e) { // Swallow exception, such as when String.prototype is sealed. 75 | } 76 | 77 | TrimPath.parseTemplate_etc = {}; // Exposed for extensibility. 78 | TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro"; 79 | TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags. 80 | "if" : { delta: 1, prefix: "if (", suffix: ") {", paramMin: 1 }, 81 | "else" : { delta: 0, prefix: "} else {" }, 82 | "elseif" : { delta: 0, prefix: "} else if (", suffix: ") {", paramDefault: "true" }, 83 | "/if" : { delta: -1, prefix: "}" }, 84 | "for" : { delta: 1, paramMin: 3, 85 | prefixFunc : function(stmtParts, state, tmplName, etc) { 86 | if (stmtParts[2] != "in") 87 | throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' ')); 88 | var iterVar = stmtParts[1]; 89 | var listVar = "__LIST__" + iterVar; 90 | return [ "var ", listVar, " = ", stmtParts[3], ";", 91 | // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack. 92 | "var __LENGTH_STACK__;", 93 | "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", 94 | "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths. 95 | "if ((", listVar, ") != null) { ", 96 | "var ", iterVar, "_ct = 0;", // iterVar_ct variable, added by B. Bittman 97 | "for (var ", iterVar, "_index in ", listVar, ") { ", 98 | iterVar, "_ct++;", 99 | "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev. 100 | "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;", 101 | "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join(""); 102 | } }, 103 | "forelse" : { delta: 0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" }, 104 | "/for" : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths. 105 | "var" : { delta: 0, prefix: "var ", suffix: ";" }, 106 | "macro" : { delta: 1, 107 | prefixFunc : function(stmtParts, state, tmplName, etc) { 108 | var macroName = stmtParts[1].split('(')[0]; 109 | return [ "var ", macroName, " = function", 110 | stmtParts.slice(1).join(' ').substring(macroName.length), 111 | "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join(''); 112 | } }, 113 | "/macro" : { delta: -1, prefix: " return _OUT_arr.join(''); };" } 114 | } 115 | TrimPath.parseTemplate_etc.modifierDef = { 116 | "eat" : function(v) { return ""; }, 117 | "escape" : function(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); }, 118 | "capitalize" : function(s) { return String(s).toUpperCase(); }, 119 | "default" : function(s, d) { return s != null ? s : d; } 120 | } 121 | TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape; 122 | 123 | TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) { 124 | this.process = function(context, flags) { 125 | if (context == null) 126 | context = {}; 127 | if (context._MODIFIERS == null) 128 | context._MODIFIERS = {}; 129 | if (context.defined == null) 130 | context.defined = function(str) { return (context[str] != undefined); }; 131 | for (var k in etc.modifierDef) { 132 | if (context._MODIFIERS[k] == null) 133 | context._MODIFIERS[k] = etc.modifierDef[k]; 134 | } 135 | for (var k in TrimPath.global_modifiers) { 136 | if (context._MODIFIERS[k] == null) 137 | context._MODIFIERS[k] = TrimPath.global_modifiers[k]; 138 | } 139 | if (flags == null) 140 | flags = {}; 141 | var resultArr = []; 142 | var resultOut = { write: function(m) { resultArr.push(m); } }; 143 | try { 144 | func(resultOut, context, flags); 145 | } catch (e) { 146 | if (flags.throwExceptions == true) 147 | throw e; 148 | var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]"); 149 | result["exception"] = e; 150 | return result; 151 | } 152 | return resultArr.join(""); 153 | } 154 | this.name = tmplName; 155 | this.source = tmplContent; 156 | this.sourceFunc = funcSrc; 157 | this.toString = function() { return "TrimPath.Template [" + tmplName + "]"; } 158 | } 159 | TrimPath.parseTemplate_etc.ParseError = function(name, line, message) { 160 | this.name = name; 161 | this.line = line; 162 | this.message = message; 163 | } 164 | TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() { 165 | return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message); 166 | } 167 | 168 | var parse = function(body, tmplName, etc) { 169 | body = cleanWhiteSpace(body); 170 | var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ]; 171 | var state = { stack: [], line: 1 }; // TODO: Fix line number counting. 172 | var endStmtPrev = -1; 173 | while (endStmtPrev + 1 < body.length) { 174 | var begStmt = endStmtPrev; 175 | // Scan until we find some statement markup. 176 | begStmt = body.indexOf("{", begStmt + 1); 177 | while (begStmt >= 0) { 178 | var endStmt = body.indexOf('}', begStmt + 1); 179 | var stmt = body.substring(begStmt, endStmt); 180 | var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation. 181 | if (blockrx) { 182 | var blockType = blockrx[1]; 183 | var blockMarkerBeg = begStmt + blockType.length + 1; 184 | var blockMarkerEnd = body.indexOf('}', blockMarkerBeg); 185 | if (blockMarkerEnd >= 0) { 186 | var blockMarker; 187 | if( blockMarkerEnd - blockMarkerBeg <= 0 ) { 188 | blockMarker = "{/" + blockType + "}"; 189 | } else { 190 | blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd); 191 | } 192 | 193 | var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1); 194 | if (blockEnd >= 0) { 195 | emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText); 196 | 197 | var blockText = body.substring(blockMarkerEnd + 1, blockEnd); 198 | if (blockType == 'cdata') { 199 | emitText(blockText, funcText); 200 | } else if (blockType == 'minify') { 201 | emitText(scrubWhiteSpace(blockText), funcText); 202 | } else if (blockType == 'eval') { 203 | if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process(). 204 | funcText.push('_OUT.write( (function() { ' + blockText + ' })() );'); 205 | } 206 | begStmt = endStmtPrev = blockEnd + blockMarker.length - 1; 207 | } 208 | } 209 | } else if (body.charAt(begStmt - 1) != '$' && // Not an expression or backslashed, 210 | body.charAt(begStmt - 1) != '\\') { // so check if it is a statement tag. 211 | var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'. 212 | // 10 is larger than maximum statement tag length. 213 | if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) 214 | break; // Found a match. 215 | } 216 | begStmt = body.indexOf("{", begStmt + 1); 217 | } 218 | if (begStmt < 0) // In "a{for}c", begStmt will be 1. 219 | break; 220 | var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5. 221 | if (endStmt < 0) 222 | break; 223 | emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText); 224 | emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc); 225 | endStmtPrev = endStmt; 226 | } 227 | emitSectionText(body.substring(endStmtPrev + 1), funcText); 228 | if (state.stack.length != 0) 229 | throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(",")); 230 | funcText.push("}}; TrimPath_Template_TEMP"); 231 | return funcText.join(""); 232 | } 233 | 234 | var emitStatement = function(stmtStr, state, funcText, tmplName, etc) { 235 | var parts = stmtStr.slice(1, -1).split(' '); 236 | var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/... 237 | if (stmt == null) { // Not a real statement. 238 | emitSectionText(stmtStr, funcText); 239 | return; 240 | } 241 | if (stmt.delta < 0) { 242 | if (state.stack.length <= 0) 243 | throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr); 244 | state.stack.pop(); 245 | } 246 | if (stmt.delta > 0) 247 | state.stack.push(stmtStr); 248 | 249 | if (stmt.paramMin != null && 250 | stmt.paramMin >= parts.length) 251 | throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr); 252 | if (stmt.prefixFunc != null) 253 | funcText.push(stmt.prefixFunc(parts, state, tmplName, etc)); 254 | else 255 | funcText.push(stmt.prefix); 256 | if (stmt.suffix != null) { 257 | if (parts.length <= 1) { 258 | if (stmt.paramDefault != null) 259 | funcText.push(stmt.paramDefault); 260 | } else { 261 | for (var i = 1; i < parts.length; i++) { 262 | if (i > 1) 263 | funcText.push(' '); 264 | funcText.push(parts[i]); 265 | } 266 | } 267 | funcText.push(stmt.suffix); 268 | } 269 | } 270 | 271 | var emitSectionText = function(text, funcText) { 272 | if (text.length <= 0) 273 | return; 274 | var nlPrefix = 0; // Index to first non-newline in prefix. 275 | var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix. 276 | while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n')) 277 | nlPrefix++; 278 | while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t')) 279 | nlSuffix--; 280 | if (nlSuffix < nlPrefix) 281 | nlSuffix = nlPrefix; 282 | if (nlPrefix > 0) { 283 | funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("'); 284 | var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen. 285 | if (s.charAt(s.length - 1) == '\n') 286 | s = s.substring(0, s.length - 1); 287 | funcText.push(s); 288 | funcText.push('");'); 289 | } 290 | var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n'); 291 | for (var i = 0; i < lines.length; i++) { 292 | emitSectionTextLine(lines[i], funcText); 293 | if (i < lines.length - 1) 294 | funcText.push('_OUT.write("\\n");\n'); 295 | } 296 | if (nlSuffix + 1 < text.length) { 297 | funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("'); 298 | var s = text.substring(nlSuffix + 1).replace('\n', '\\n'); 299 | if (s.charAt(s.length - 1) == '\n') 300 | s = s.substring(0, s.length - 1); 301 | funcText.push(s); 302 | funcText.push('");'); 303 | } 304 | } 305 | 306 | var emitSectionTextLine = function(line, funcText) { 307 | var endMarkPrev = '}'; 308 | var endExprPrev = -1; 309 | while (endExprPrev + endMarkPrev.length < line.length) { 310 | var begMark = "${", endMark = "}"; 311 | var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1 312 | if (begExpr < 0) 313 | break; 314 | if (line.charAt(begExpr + 2) == '%') { 315 | begMark = "${%"; 316 | endMark = "%}"; 317 | } 318 | var endExpr = line.indexOf(endMark, begExpr + begMark.length); // In "a${b}c", endExpr == 4; 319 | if (endExpr < 0) 320 | break; 321 | emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText); 322 | // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|') 323 | var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|'); 324 | for (var k in exprArr) { 325 | if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev. 326 | exprArr[k] = exprArr[k].replace(/#@@#/g, '||'); 327 | } 328 | funcText.push('_OUT.write('); 329 | emitExpression(exprArr, exprArr.length - 1, funcText); 330 | funcText.push(');'); 331 | endExprPrev = endExpr; 332 | endMarkPrev = endMark; 333 | } 334 | emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); 335 | } 336 | 337 | var emitText = function(text, funcText) { 338 | if (text == null || 339 | text.length <= 0) 340 | return; 341 | text = text.replace(/\\/g, '\\\\'); 342 | text = text.replace(/\n/g, '\\n'); 343 | text = text.replace(/"/g, '\\"'); 344 | funcText.push('_OUT.write("'); 345 | funcText.push(text); 346 | funcText.push('");'); 347 | } 348 | 349 | var emitExpression = function(exprArr, index, funcText) { 350 | // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2) 351 | var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"] 352 | if (index <= 0) { // Ex: expr == 'default:"John Doe"' 353 | funcText.push(expr); 354 | return; 355 | } 356 | var parts = expr.split(':'); 357 | funcText.push('_MODIFIERS["'); 358 | funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize. 359 | funcText.push('"]('); 360 | emitExpression(exprArr, index - 1, funcText); 361 | if (parts.length > 1) { 362 | funcText.push(','); 363 | funcText.push(parts[1]); 364 | } 365 | funcText.push(')'); 366 | } 367 | 368 | var cleanWhiteSpace = function(result) { 369 | result = result.replace(/\t/g, " "); 370 | result = result.replace(/\r\n/g, "\n"); 371 | result = result.replace(/\r/g, "\n"); 372 | result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev. 373 | return result; 374 | } 375 | 376 | var scrubWhiteSpace = function(result) { 377 | result = result.replace(/^\s+/g, ""); 378 | result = result.replace(/\s+$/g, ""); 379 | result = result.replace(/\s+/g, " "); 380 | result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev. 381 | return result; 382 | } 383 | 384 | // The DOM helper functions depend on DOM/DHTML, so they only work in a browser. 385 | // However, these are not considered core to the engine. 386 | // 387 | TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) { 388 | if (optDocument == null) 389 | optDocument = document; 390 | var element = optDocument.getElementById(elementId); 391 | var content = element.value; // Like textarea.value. 392 | if (content == null) 393 | content = element.innerHTML; // Like textarea.innerHTML. 394 | content = content.replace(/</g, "<").replace(/>/g, ">"); 395 | return TrimPath.parseTemplate(content, elementId, optEtc); 396 | } 397 | 398 | TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) { 399 | return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags); 400 | } 401 | TrimPath.global_modifiers={}; 402 | }) (); 403 | -------------------------------------------------------------------------------- /bin/ui.core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI 1.6rc6 3 | * 4 | * Copyright (c) 2009 AUTHORS.txt (http://ui.jquery.com/about) 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | * 8 | * http://docs.jquery.com/UI 9 | */ 10 | ;(function($) { 11 | 12 | var _remove = $.fn.remove, 13 | isFF2 = $.browser.mozilla && (parseFloat($.browser.version) < 1.9); 14 | 15 | //Helper functions and ui object 16 | $.ui = { 17 | version: "1.6rc6", 18 | 19 | // $.ui.plugin is deprecated. Use the proxy pattern instead. 20 | plugin: { 21 | add: function(module, option, set) { 22 | var proto = $.ui[module].prototype; 23 | for(var i in set) { 24 | proto.plugins[i] = proto.plugins[i] || []; 25 | proto.plugins[i].push([option, set[i]]); 26 | } 27 | }, 28 | call: function(instance, name, args) { 29 | var set = instance.plugins[name]; 30 | if(!set) { return; } 31 | 32 | for (var i = 0; i < set.length; i++) { 33 | if (instance.options[set[i][0]]) { 34 | set[i][1].apply(instance.element, args); 35 | } 36 | } 37 | } 38 | }, 39 | 40 | contains: function(a, b) { 41 | return document.compareDocumentPosition 42 | ? a.compareDocumentPosition(b) & 16 43 | : a !== b && a.contains(b); 44 | }, 45 | 46 | cssCache: {}, 47 | css: function(name) { 48 | if ($.ui.cssCache[name]) { return $.ui.cssCache[name]; } 49 | var tmp = $('
').addClass(name).css({position:'absolute', top:'-5000px', left:'-5000px', display:'block'}).appendTo('body'); 50 | 51 | //if (!$.browser.safari) 52 | //tmp.appendTo('body'); 53 | 54 | //Opera and Safari set width and height to 0px instead of auto 55 | //Safari returns rgba(0,0,0,0) when bgcolor is not set 56 | $.ui.cssCache[name] = !!( 57 | (!(/auto|default/).test(tmp.css('cursor')) || (/^[1-9]/).test(tmp.css('height')) || (/^[1-9]/).test(tmp.css('width')) || 58 | !(/none/).test(tmp.css('backgroundImage')) || !(/transparent|rgba\(0, 0, 0, 0\)/).test(tmp.css('backgroundColor'))) 59 | ); 60 | try { $('body').get(0).removeChild(tmp.get(0)); } catch(e){} 61 | return $.ui.cssCache[name]; 62 | }, 63 | 64 | hasScroll: function(el, a) { 65 | 66 | //If overflow is hidden, the element might have extra content, but the user wants to hide it 67 | if ($(el).css('overflow') == 'hidden') { return false; } 68 | 69 | var scroll = (a && a == 'left') ? 'scrollLeft' : 'scrollTop', 70 | has = false; 71 | 72 | if (el[scroll] > 0) { return true; } 73 | 74 | // TODO: determine which cases actually cause this to happen 75 | // if the element doesn't have the scroll set, see if it's possible to 76 | // set the scroll 77 | el[scroll] = 1; 78 | has = (el[scroll] > 0); 79 | el[scroll] = 0; 80 | return has; 81 | }, 82 | 83 | isOverAxis: function(x, reference, size) { 84 | //Determines when x coordinate is over "b" element axis 85 | return (x > reference) && (x < (reference + size)); 86 | }, 87 | 88 | isOver: function(y, x, top, left, height, width) { 89 | //Determines when x, y coordinates is over "b" element 90 | return $.ui.isOverAxis(y, top, height) && $.ui.isOverAxis(x, left, width); 91 | }, 92 | 93 | keyCode: { 94 | BACKSPACE: 8, 95 | CAPS_LOCK: 20, 96 | COMMA: 188, 97 | CONTROL: 17, 98 | DELETE: 46, 99 | DOWN: 40, 100 | END: 35, 101 | ENTER: 13, 102 | ESCAPE: 27, 103 | HOME: 36, 104 | INSERT: 45, 105 | LEFT: 37, 106 | NUMPAD_ADD: 107, 107 | NUMPAD_DECIMAL: 110, 108 | NUMPAD_DIVIDE: 111, 109 | NUMPAD_ENTER: 108, 110 | NUMPAD_MULTIPLY: 106, 111 | NUMPAD_SUBTRACT: 109, 112 | PAGE_DOWN: 34, 113 | PAGE_UP: 33, 114 | PERIOD: 190, 115 | RIGHT: 39, 116 | SHIFT: 16, 117 | SPACE: 32, 118 | TAB: 9, 119 | UP: 38 120 | } 121 | }; 122 | 123 | // WAI-ARIA normalization 124 | if (isFF2) { 125 | var attr = $.attr, 126 | removeAttr = $.fn.removeAttr, 127 | ariaNS = "http://www.w3.org/2005/07/aaa", 128 | ariaState = /^aria-/, 129 | ariaRole = /^wairole:/; 130 | 131 | $.attr = function(elem, name, value) { 132 | var set = value !== undefined; 133 | 134 | return (name == 'role' 135 | ? (set 136 | ? attr.call(this, elem, name, "wairole:" + value) 137 | : (attr.apply(this, arguments) || "").replace(ariaRole, "")) 138 | : (ariaState.test(name) 139 | ? (set 140 | ? elem.setAttributeNS(ariaNS, 141 | name.replace(ariaState, "aaa:"), value) 142 | : attr.call(this, elem, name.replace(ariaState, "aaa:"))) 143 | : attr.apply(this, arguments))); 144 | }; 145 | 146 | $.fn.removeAttr = function(name) { 147 | return (ariaState.test(name) 148 | ? this.each(function() { 149 | this.removeAttributeNS(ariaNS, name.replace(ariaState, "")); 150 | }) : removeAttr.call(this, name)); 151 | }; 152 | } 153 | 154 | //jQuery plugins 155 | $.fn.extend({ 156 | remove: function() { 157 | // Safari has a native remove event which actually removes DOM elements, 158 | // so we have to use triggerHandler instead of trigger (#3037). 159 | $("*", this).add(this).each(function() { 160 | $(this).triggerHandler("remove"); 161 | }); 162 | return _remove.apply(this, arguments ); 163 | }, 164 | 165 | enableSelection: function() { 166 | return this 167 | .attr('unselectable', 'off') 168 | .css('MozUserSelect', '') 169 | .unbind('selectstart.ui'); 170 | }, 171 | 172 | disableSelection: function() { 173 | return this 174 | .attr('unselectable', 'on') 175 | .css('MozUserSelect', 'none') 176 | .bind('selectstart.ui', function() { return false; }); 177 | }, 178 | 179 | scrollParent: function() { 180 | var scrollParent; 181 | if(($.browser.msie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) { 182 | scrollParent = this.parents().filter(function() { 183 | return (/(relative|absolute|fixed)/).test($.curCSS(this,'position',1)) && (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1)); 184 | }).eq(0); 185 | } else { 186 | scrollParent = this.parents().filter(function() { 187 | return (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1)); 188 | }).eq(0); 189 | } 190 | 191 | return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent; 192 | } 193 | }); 194 | 195 | 196 | //Additional selectors 197 | $.extend($.expr[':'], { 198 | data: function(elem, i, match) { 199 | return !!$.data(elem, match[3]); 200 | }, 201 | 202 | focusable: function(element) { 203 | var nodeName = element.nodeName.toLowerCase(), 204 | tabIndex = $.attr(element, 'tabindex'); 205 | return (/input|select|textarea|button|object/.test(nodeName) 206 | ? !element.disabled 207 | : 'a' == nodeName || 'area' == nodeName 208 | ? element.href || !isNaN(tabIndex) 209 | : !isNaN(tabIndex)) 210 | // the element and all of its ancestors must be visible 211 | // the browser may report that the area is hidden 212 | && !$(element)['area' == nodeName ? 'parents' : 'closest'](':hidden').length; 213 | }, 214 | 215 | tabbable: function(element) { 216 | var tabIndex = $.attr(element, 'tabindex'); 217 | return (isNaN(tabIndex) || tabIndex >= 0) && $(element).is(':focusable'); 218 | } 219 | }); 220 | 221 | 222 | // $.widget is a factory to create jQuery plugins 223 | // taking some boilerplate code out of the plugin code 224 | function getter(namespace, plugin, method, args) { 225 | function getMethods(type) { 226 | var methods = $[namespace][plugin][type] || []; 227 | return (typeof methods == 'string' ? methods.split(/,?\s+/) : methods); 228 | } 229 | 230 | var methods = getMethods('getter'); 231 | if (args.length == 1 && typeof args[0] == 'string') { 232 | methods = methods.concat(getMethods('getterSetter')); 233 | } 234 | return ($.inArray(method, methods) != -1); 235 | } 236 | 237 | $.widget = function(name, prototype) { 238 | var namespace = name.split(".")[0]; 239 | name = name.split(".")[1]; 240 | 241 | // create plugin method 242 | $.fn[name] = function(options) { 243 | var isMethodCall = (typeof options == 'string'), 244 | args = Array.prototype.slice.call(arguments, 1); 245 | 246 | // prevent calls to internal methods 247 | if (isMethodCall && options.substring(0, 1) == '_') { 248 | return this; 249 | } 250 | 251 | // handle getter methods 252 | if (isMethodCall && getter(namespace, name, options, args)) { 253 | var instance = $.data(this[0], name); 254 | return (instance ? instance[options].apply(instance, args) 255 | : undefined); 256 | } 257 | 258 | // handle initialization and non-getter methods 259 | return this.each(function() { 260 | var instance = $.data(this, name); 261 | 262 | // constructor 263 | (!instance && !isMethodCall && 264 | $.data(this, name, new $[namespace][name](this, options))._init()); 265 | 266 | // method call 267 | (instance && isMethodCall && $.isFunction(instance[options]) && 268 | instance[options].apply(instance, args)); 269 | }); 270 | }; 271 | 272 | // create widget constructor 273 | $[namespace] = $[namespace] || {}; 274 | $[namespace][name] = function(element, options) { 275 | var self = this; 276 | 277 | this.namespace = namespace; 278 | this.widgetName = name; 279 | this.widgetEventPrefix = $[namespace][name].eventPrefix || name; 280 | this.widgetBaseClass = namespace + '-' + name; 281 | 282 | this.options = $.extend({}, 283 | $.widget.defaults, 284 | $[namespace][name].defaults, 285 | $.metadata && $.metadata.get(element)[name], 286 | options); 287 | 288 | this.element = $(element) 289 | .bind('setData.' + name, function(event, key, value) { 290 | if (event.target == element) { 291 | return self._setData(key, value); 292 | } 293 | }) 294 | .bind('getData.' + name, function(event, key) { 295 | if (event.target == element) { 296 | return self._getData(key); 297 | } 298 | }) 299 | .bind('remove', function() { 300 | return self.destroy(); 301 | }); 302 | }; 303 | 304 | // add widget prototype 305 | $[namespace][name].prototype = $.extend({}, $.widget.prototype, prototype); 306 | 307 | // TODO: merge getter and getterSetter properties from widget prototype 308 | // and plugin prototype 309 | $[namespace][name].getterSetter = 'option'; 310 | }; 311 | 312 | $.widget.prototype = { 313 | _init: function() {}, 314 | destroy: function() { 315 | this.element.removeData(this.widgetName) 316 | .removeClass(this.widgetBaseClass + '-disabled' + ' ' + this.namespace + '-state-disabled') 317 | .removeAttr('aria-disabled'); 318 | }, 319 | 320 | option: function(key, value) { 321 | var options = key, 322 | self = this; 323 | 324 | if (typeof key == "string") { 325 | if (value === undefined) { 326 | return this._getData(key); 327 | } 328 | options = {}; 329 | options[key] = value; 330 | } 331 | 332 | $.each(options, function(key, value) { 333 | self._setData(key, value); 334 | }); 335 | }, 336 | _getData: function(key) { 337 | return this.options[key]; 338 | }, 339 | _setData: function(key, value) { 340 | this.options[key] = value; 341 | 342 | if (key == 'disabled') { 343 | this.element 344 | [value ? 'addClass' : 'removeClass']( 345 | this.widgetBaseClass + '-disabled' + ' ' + 346 | this.namespace + '-state-disabled') 347 | .attr("aria-disabled", value); 348 | } 349 | }, 350 | 351 | enable: function() { 352 | this._setData('disabled', false); 353 | }, 354 | disable: function() { 355 | this._setData('disabled', true); 356 | }, 357 | 358 | _trigger: function(type, event, data) { 359 | var callback = this.options[type], 360 | eventName = (type == this.widgetEventPrefix 361 | ? type : this.widgetEventPrefix + type); 362 | 363 | event = $.Event(event); 364 | event.type = eventName; 365 | 366 | // copy original event properties over to the new event 367 | // this would happen if we could call $.event.fix instead of $.Event 368 | // but we don't have a way to force an event to be fixed multiple times 369 | if (event.originalEvent) { 370 | for (var i = $.event.props.length, prop; i;) { 371 | prop = $.event.props[--i]; 372 | event[prop] = event.originalEvent[prop]; 373 | } 374 | } 375 | 376 | this.element.trigger(event, data); 377 | 378 | return !($.isFunction(callback) && callback.call(this.element[0], event, data) === false 379 | || event.isDefaultPrevented()); 380 | } 381 | }; 382 | 383 | $.widget.defaults = { 384 | disabled: false 385 | }; 386 | 387 | 388 | /** Mouse Interaction Plugin **/ 389 | 390 | $.ui.mouse = { 391 | _mouseInit: function() { 392 | var self = this; 393 | 394 | this.element 395 | .bind('mousedown.'+this.widgetName, function(event) { 396 | return self._mouseDown(event); 397 | }) 398 | .bind('click.'+this.widgetName, function(event) { 399 | if(self._preventClickEvent) { 400 | self._preventClickEvent = false; 401 | return false; 402 | } 403 | }); 404 | 405 | // Prevent text selection in IE 406 | if ($.browser.msie) { 407 | this._mouseUnselectable = this.element.attr('unselectable'); 408 | this.element.attr('unselectable', 'on'); 409 | } 410 | 411 | this.started = false; 412 | }, 413 | 414 | // TODO: make sure destroying one instance of mouse doesn't mess with 415 | // other instances of mouse 416 | _mouseDestroy: function() { 417 | this.element.unbind('.'+this.widgetName); 418 | 419 | // Restore text selection in IE 420 | ($.browser.msie 421 | && this.element.attr('unselectable', this._mouseUnselectable)); 422 | }, 423 | 424 | _mouseDown: function(event) { 425 | // don't let more than one widget handle mouseStart 426 | if (event.originalEvent.mouseHandled) { return; } 427 | 428 | // we may have missed mouseup (out of window) 429 | (this._mouseStarted && this._mouseUp(event)); 430 | 431 | this._mouseDownEvent = event; 432 | 433 | var self = this, 434 | btnIsLeft = (event.which == 1), 435 | elIsCancel = (typeof this.options.cancel == "string" ? $(event.target).parents().add(event.target).filter(this.options.cancel).length : false); 436 | if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { 437 | return true; 438 | } 439 | 440 | this.mouseDelayMet = !this.options.delay; 441 | if (!this.mouseDelayMet) { 442 | this._mouseDelayTimer = setTimeout(function() { 443 | self.mouseDelayMet = true; 444 | }, this.options.delay); 445 | } 446 | 447 | if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { 448 | this._mouseStarted = (this._mouseStart(event) !== false); 449 | if (!this._mouseStarted) { 450 | event.preventDefault(); 451 | return true; 452 | } 453 | } 454 | 455 | // these delegates are required to keep context 456 | this._mouseMoveDelegate = function(event) { 457 | return self._mouseMove(event); 458 | }; 459 | this._mouseUpDelegate = function(event) { 460 | return self._mouseUp(event); 461 | }; 462 | $(document) 463 | .bind('mousemove.'+this.widgetName, this._mouseMoveDelegate) 464 | .bind('mouseup.'+this.widgetName, this._mouseUpDelegate); 465 | 466 | // preventDefault() is used to prevent the selection of text here - 467 | // however, in Safari, this causes select boxes not to be selectable 468 | // anymore, so this fix is needed 469 | ($.browser.safari || event.preventDefault()); 470 | 471 | event.originalEvent.mouseHandled = true; 472 | return true; 473 | }, 474 | 475 | _mouseMove: function(event) { 476 | // IE mouseup check - mouseup happened when mouse was out of window 477 | if ($.browser.msie && !event.button) { 478 | return this._mouseUp(event); 479 | } 480 | 481 | if (this._mouseStarted) { 482 | this._mouseDrag(event); 483 | return event.preventDefault(); 484 | } 485 | 486 | if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { 487 | this._mouseStarted = 488 | (this._mouseStart(this._mouseDownEvent, event) !== false); 489 | (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); 490 | } 491 | 492 | return !this._mouseStarted; 493 | }, 494 | 495 | _mouseUp: function(event) { 496 | $(document) 497 | .unbind('mousemove.'+this.widgetName, this._mouseMoveDelegate) 498 | .unbind('mouseup.'+this.widgetName, this._mouseUpDelegate); 499 | 500 | if (this._mouseStarted) { 501 | this._mouseStarted = false; 502 | this._preventClickEvent = true; 503 | this._mouseStop(event); 504 | } 505 | 506 | return false; 507 | }, 508 | 509 | _mouseDistanceMet: function(event) { 510 | return (Math.max( 511 | Math.abs(this._mouseDownEvent.pageX - event.pageX), 512 | Math.abs(this._mouseDownEvent.pageY - event.pageY) 513 | ) >= this.options.distance 514 | ); 515 | }, 516 | 517 | _mouseDelayMet: function(event) { 518 | return this.mouseDelayMet; 519 | }, 520 | 521 | // These are placeholder methods, to be overriden by extending plugin 522 | _mouseStart: function(event) {}, 523 | _mouseDrag: function(event) {}, 524 | _mouseStop: function(event) {}, 525 | _mouseCapture: function(event) { return true; } 526 | }; 527 | 528 | $.ui.mouse.defaults = { 529 | cancel: null, 530 | distance: 1, 531 | delay: 0 532 | }; 533 | 534 | })(jQuery); 535 | -------------------------------------------------------------------------------- /bin/ui.sortable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI Sortable 1.6rc6 3 | * 4 | * Copyright (c) 2009 AUTHORS.txt (http://ui.jquery.com/about) 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | * 8 | * http://docs.jquery.com/UI/Sortables 9 | * 10 | * Depends: 11 | * ui.core.js 12 | */ 13 | (function($) { 14 | 15 | $.widget("ui.sortable", $.extend({}, $.ui.mouse, { 16 | _init: function() { 17 | 18 | var o = this.options; 19 | this.containerCache = {}; 20 | (this.options.cssNamespace && this.element.addClass(this.options.cssNamespace+"-sortable")); 21 | 22 | //Get the items 23 | this.refresh(); 24 | 25 | //Let's determine if the items are floating 26 | this.floating = this.items.length ? (/left|right/).test(this.items[0].item.css('float')) : false; 27 | 28 | //Let's determine the parent's offset 29 | this.offset = this.element.offset(); 30 | 31 | //Initialize mouse events for interaction 32 | this._mouseInit(); 33 | 34 | }, 35 | 36 | destroy: function() { 37 | this.element 38 | .removeClass(this.options.cssNamespace+"-sortable "+this.options.cssNamespace+"-sortable-disabled") 39 | .removeData("sortable") 40 | .unbind(".sortable"); 41 | this._mouseDestroy(); 42 | 43 | for ( var i = this.items.length - 1; i >= 0; i-- ) 44 | this.items[i].item.removeData("sortable-item"); 45 | }, 46 | 47 | _mouseCapture: function(event, overrideHandle) { 48 | 49 | if (this.reverting) { 50 | return false; 51 | } 52 | 53 | if(this.options.disabled || this.options.type == 'static') return false; 54 | 55 | //We have to refresh the items data once first 56 | this._refreshItems(event); 57 | 58 | //Find out if the clicked node (or one of its parents) is a actual item in this.items 59 | var currentItem = null, self = this, nodes = $(event.target).parents().each(function() { 60 | if($.data(this, 'sortable-item') == self) { 61 | currentItem = $(this); 62 | return false; 63 | } 64 | }); 65 | if($.data(event.target, 'sortable-item') == self) currentItem = $(event.target); 66 | 67 | if(!currentItem) return false; 68 | if(this.options.handle && !overrideHandle) { 69 | var validHandle = false; 70 | 71 | $(this.options.handle, currentItem).find("*").andSelf().each(function() { if(this == event.target) validHandle = true; }); 72 | if(!validHandle) return false; 73 | } 74 | 75 | this.currentItem = currentItem; 76 | this._removeCurrentsFromItems(); 77 | return true; 78 | 79 | }, 80 | 81 | _mouseStart: function(event, overrideHandle, noActivation) { 82 | 83 | var o = this.options, self = this; 84 | this.currentContainer = this; 85 | 86 | //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture 87 | this.refreshPositions(); 88 | 89 | //Create and append the visible helper 90 | this.helper = this._createHelper(event); 91 | 92 | //Cache the helper size 93 | this._cacheHelperProportions(); 94 | 95 | /* 96 | * - Position generation - 97 | * This block generates everything position related - it's the core of draggables. 98 | */ 99 | 100 | //Cache the margins of the original element 101 | this._cacheMargins(); 102 | 103 | //Get the next scrolling parent 104 | this.scrollParent = this.helper.scrollParent(); 105 | 106 | //The element's absolute position on the page minus margins 107 | this.offset = this.currentItem.offset(); 108 | this.offset = { 109 | top: this.offset.top - this.margins.top, 110 | left: this.offset.left - this.margins.left 111 | }; 112 | 113 | // Only after we got the offset, we can change the helper's position to absolute 114 | // TODO: Still need to figure out a way to make relative sorting possible 115 | this.helper.css("position", "absolute"); 116 | this.cssPosition = this.helper.css("position"); 117 | 118 | $.extend(this.offset, { 119 | click: { //Where the click happened, relative to the element 120 | left: event.pageX - this.offset.left, 121 | top: event.pageY - this.offset.top 122 | }, 123 | parent: this._getParentOffset(), 124 | relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper 125 | }); 126 | 127 | //Generate the original position 128 | this.originalPosition = this._generatePosition(event); 129 | this.originalPageX = event.pageX; 130 | this.originalPageY = event.pageY; 131 | 132 | //Adjust the mouse offset relative to the helper if 'cursorAt' is supplied 133 | if(o.cursorAt) 134 | this._adjustOffsetFromHelper(o.cursorAt); 135 | 136 | //Cache the former DOM position 137 | this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; 138 | 139 | //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way 140 | if(this.helper[0] != this.currentItem[0]) { 141 | this.currentItem.hide(); 142 | } 143 | 144 | //Create the placeholder 145 | this._createPlaceholder(); 146 | 147 | //Set a containment if given in the options 148 | if(o.containment) 149 | this._setContainment(); 150 | 151 | if(o.cursor) { // cursor option 152 | if ($('body').css("cursor")) this._storedCursor = $('body').css("cursor"); 153 | $('body').css("cursor", o.cursor); 154 | } 155 | 156 | if(o.opacity) { // opacity option 157 | if (this.helper.css("opacity")) this._storedOpacity = this.helper.css("opacity"); 158 | this.helper.css("opacity", o.opacity); 159 | } 160 | 161 | if(o.zIndex) { // zIndex option 162 | if (this.helper.css("zIndex")) this._storedZIndex = this.helper.css("zIndex"); 163 | this.helper.css("zIndex", o.zIndex); 164 | } 165 | 166 | //Prepare scrolling 167 | if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') 168 | this.overflowOffset = this.scrollParent.offset(); 169 | 170 | //Call callbacks 171 | this._trigger("start", event, this._uiHash()); 172 | 173 | //Recache the helper size 174 | if(!this._preserveHelperProportions) 175 | this._cacheHelperProportions(); 176 | 177 | 178 | //Post 'activate' events to possible containers 179 | if(!noActivation) { 180 | for (var i = this.containers.length - 1; i >= 0; i--) { this.containers[i]._trigger("activate", event, self._uiHash(this)); } 181 | } 182 | 183 | //Prepare possible droppables 184 | if($.ui.ddmanager) 185 | $.ui.ddmanager.current = this; 186 | 187 | if ($.ui.ddmanager && !o.dropBehaviour) 188 | $.ui.ddmanager.prepareOffsets(this, event); 189 | 190 | this.dragging = true; 191 | 192 | this.helper.addClass(o.cssNamespace+'-sortable-helper'); 193 | this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position 194 | return true; 195 | 196 | }, 197 | 198 | _mouseDrag: function(event) { 199 | 200 | //Compute the helpers position 201 | this.position = this._generatePosition(event); 202 | this.positionAbs = this._convertPositionTo("absolute"); 203 | 204 | if (!this.lastPositionAbs) { 205 | this.lastPositionAbs = this.positionAbs; 206 | } 207 | 208 | //Do scrolling 209 | if(this.options.scroll) { 210 | var o = this.options, scrolled = false; 211 | if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') { 212 | 213 | if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) 214 | this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; 215 | else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) 216 | this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; 217 | 218 | if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) 219 | this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; 220 | else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) 221 | this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; 222 | 223 | } else { 224 | 225 | if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) 226 | scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); 227 | else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) 228 | scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); 229 | 230 | if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) 231 | scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); 232 | else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) 233 | scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); 234 | 235 | } 236 | 237 | if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) 238 | $.ui.ddmanager.prepareOffsets(this, event); 239 | } 240 | 241 | //Regenerate the absolute position used for position checks 242 | this.positionAbs = this._convertPositionTo("absolute"); 243 | 244 | //Set the helper position 245 | if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px'; 246 | if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px'; 247 | 248 | //Rearrange 249 | for (var i = this.items.length - 1; i >= 0; i--) { 250 | 251 | //Cache variables and intersection, continue if no intersection 252 | var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item); 253 | if (!intersection) continue; 254 | 255 | if(itemElement != this.currentItem[0] //cannot intersect with itself 256 | && this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before 257 | && !$.ui.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked 258 | && (this.options.type == 'semi-dynamic' ? !$.ui.contains(this.element[0], itemElement) : true) 259 | ) { 260 | 261 | this.direction = intersection == 1 ? "down" : "up"; 262 | 263 | if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) { 264 | this.options.sortIndicator.call(this, event, item); 265 | } else { 266 | break; 267 | } 268 | 269 | this._trigger("change", event, this._uiHash()); 270 | break; 271 | } 272 | } 273 | 274 | //Post events to containers 275 | this._contactContainers(event); 276 | 277 | //Interconnect with droppables 278 | if($.ui.ddmanager) $.ui.ddmanager.drag(this, event); 279 | 280 | //Call callbacks 281 | this._trigger('sort', event, this._uiHash()); 282 | 283 | this.lastPositionAbs = this.positionAbs; 284 | return false; 285 | 286 | }, 287 | 288 | _mouseStop: function(event, noPropagation) { 289 | 290 | if(!event) return; 291 | 292 | //If we are using droppables, inform the manager about the drop 293 | if ($.ui.ddmanager && !this.options.dropBehaviour) 294 | $.ui.ddmanager.drop(this, event); 295 | 296 | if(this.options.revert) { 297 | var self = this; 298 | var cur = self.placeholder.offset(); 299 | 300 | self.reverting = true; 301 | 302 | $(this.helper).animate({ 303 | left: cur.left - this.offset.parent.left - self.margins.left + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollLeft), 304 | top: cur.top - this.offset.parent.top - self.margins.top + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollTop) 305 | }, parseInt(this.options.revert, 10) || 500, function() { 306 | self._clear(event); 307 | }); 308 | } else { 309 | this._clear(event, noPropagation); 310 | } 311 | 312 | return false; 313 | 314 | }, 315 | 316 | cancel: function() { 317 | 318 | var self = this; 319 | 320 | if(this.dragging) { 321 | 322 | this._mouseUp(); 323 | 324 | if(this.options.helper == "original") 325 | this.currentItem.css(this._storedCSS).removeClass(this.options.cssNamespace+"-sortable-helper"); 326 | else 327 | this.currentItem.show(); 328 | 329 | //Post deactivating events to containers 330 | for (var i = this.containers.length - 1; i >= 0; i--){ 331 | this.containers[i]._trigger("deactivate", null, self._uiHash(this)); 332 | if(this.containers[i].containerCache.over) { 333 | this.containers[i]._trigger("out", null, self._uiHash(this)); 334 | this.containers[i].containerCache.over = 0; 335 | } 336 | } 337 | 338 | } 339 | 340 | //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! 341 | if(this.placeholder[0].parentNode) this.placeholder[0].parentNode.removeChild(this.placeholder[0]); 342 | if(this.options.helper != "original" && this.helper && this.helper[0].parentNode) this.helper.remove(); 343 | 344 | $.extend(this, { 345 | helper: null, 346 | dragging: false, 347 | reverting: false, 348 | _noFinalSort: null 349 | }); 350 | 351 | if(this.domPosition.prev) { 352 | $(this.domPosition.prev).after(this.currentItem); 353 | } else { 354 | $(this.domPosition.parent).prepend(this.currentItem); 355 | } 356 | 357 | return true; 358 | 359 | }, 360 | 361 | serialize: function(o) { 362 | 363 | var items = this._getItemsAsjQuery(o && o.connected); 364 | var str = []; o = o || {}; 365 | 366 | $(items).each(function() { 367 | var res = ($(o.item || this).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/)); 368 | if(res) str.push((o.key || res[1]+'[]')+'='+(o.key && o.expression ? res[1] : res[2])); 369 | }); 370 | 371 | return str.join('&'); 372 | 373 | }, 374 | 375 | toArray: function(o) { 376 | 377 | var items = this._getItemsAsjQuery(o && o.connected); 378 | var ret = []; o = o || {}; 379 | 380 | items.each(function() { ret.push($(o.item || this).attr(o.attribute || 'id') || ''); }); 381 | return ret; 382 | 383 | }, 384 | 385 | /* Be careful with the following core functions */ 386 | _intersectsWith: function(item) { 387 | 388 | var x1 = this.positionAbs.left, 389 | x2 = x1 + this.helperProportions.width, 390 | y1 = this.positionAbs.top, 391 | y2 = y1 + this.helperProportions.height; 392 | 393 | var l = item.left, 394 | r = l + item.width, 395 | t = item.top, 396 | b = t + item.height; 397 | 398 | var dyClick = this.offset.click.top, 399 | dxClick = this.offset.click.left; 400 | 401 | var isOverElement = (y1 + dyClick) > t && (y1 + dyClick) < b && (x1 + dxClick) > l && (x1 + dxClick) < r; 402 | 403 | if( this.options.tolerance == "pointer" 404 | || this.options.forcePointerForContainers 405 | || (this.options.tolerance != "pointer" && this.helperProportions[this.floating ? 'width' : 'height'] > item[this.floating ? 'width' : 'height']) 406 | ) { 407 | return isOverElement; 408 | } else { 409 | 410 | return (l < x1 + (this.helperProportions.width / 2) // Right Half 411 | && x2 - (this.helperProportions.width / 2) < r // Left Half 412 | && t < y1 + (this.helperProportions.height / 2) // Bottom Half 413 | && y2 - (this.helperProportions.height / 2) < b ); // Top Half 414 | 415 | } 416 | }, 417 | 418 | _intersectsWithPointer: function(item) { 419 | 420 | var isOverElementHeight = $.ui.isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), 421 | isOverElementWidth = $.ui.isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), 422 | isOverElement = isOverElementHeight && isOverElementWidth, 423 | verticalDirection = this._getDragVerticalDirection(), 424 | horizontalDirection = this._getDragHorizontalDirection(); 425 | 426 | if (!isOverElement) 427 | return false; 428 | 429 | return this.floating ? 430 | ( ((horizontalDirection && horizontalDirection == "right") || verticalDirection == "down") ? 2 : 1 ) 431 | : ( verticalDirection && (verticalDirection == "down" ? 2 : 1) ); 432 | 433 | }, 434 | 435 | _intersectsWithSides: function(item) { 436 | 437 | var isOverBottomHalf = $.ui.isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), 438 | isOverRightHalf = $.ui.isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), 439 | verticalDirection = this._getDragVerticalDirection(), 440 | horizontalDirection = this._getDragHorizontalDirection(); 441 | 442 | if (this.floating && horizontalDirection) { 443 | return ((horizontalDirection == "right" && isOverRightHalf) || (horizontalDirection == "left" && !isOverRightHalf)); 444 | } else { 445 | return verticalDirection && ((verticalDirection == "down" && isOverBottomHalf) || (verticalDirection == "up" && !isOverBottomHalf)); 446 | } 447 | 448 | }, 449 | 450 | _getDragVerticalDirection: function() { 451 | var delta = this.positionAbs.top - this.lastPositionAbs.top; 452 | return delta != 0 && (delta > 0 ? "down" : "up"); 453 | }, 454 | 455 | _getDragHorizontalDirection: function() { 456 | var delta = this.positionAbs.left - this.lastPositionAbs.left; 457 | return delta != 0 && (delta > 0 ? "right" : "left"); 458 | }, 459 | 460 | refresh: function(event) { 461 | this._refreshItems(event); 462 | this.refreshPositions(); 463 | }, 464 | 465 | _getItemsAsjQuery: function(connected) { 466 | 467 | var self = this; 468 | var items = []; 469 | var queries = []; 470 | 471 | if(this.options.connectWith && connected) { 472 | var connectWith = this.options.connectWith.constructor == String ? [this.options.connectWith] : this.options.connectWith; 473 | for (var i = connectWith.length - 1; i >= 0; i--){ 474 | var cur = $(connectWith[i]); 475 | for (var j = cur.length - 1; j >= 0; j--){ 476 | var inst = $.data(cur[j], 'sortable'); 477 | if(inst && inst != this && !inst.options.disabled) { 478 | queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not("."+inst.options.cssNamespace+"-sortable-helper"), inst]); 479 | } 480 | }; 481 | }; 482 | } 483 | 484 | queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not("."+this.options.cssNamespace+"-sortable-helper"), this]); 485 | 486 | for (var i = queries.length - 1; i >= 0; i--){ 487 | queries[i][0].each(function() { 488 | items.push(this); 489 | }); 490 | }; 491 | 492 | return $(items); 493 | 494 | }, 495 | 496 | _removeCurrentsFromItems: function() { 497 | 498 | var list = this.currentItem.find(":data(sortable-item)"); 499 | 500 | for (var i=0; i < this.items.length; i++) { 501 | 502 | for (var j=0; j < list.length; j++) { 503 | if(list[j] == this.items[i].item[0]) 504 | this.items.splice(i,1); 505 | }; 506 | 507 | }; 508 | 509 | }, 510 | 511 | _refreshItems: function(event) { 512 | 513 | this.items = []; 514 | this.containers = [this]; 515 | var items = this.items; 516 | var self = this; 517 | var queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]]; 518 | 519 | if(this.options.connectWith) { 520 | for (var i = this.options.connectWith.length - 1; i >= 0; i--){ 521 | var cur = $(this.options.connectWith[i]); 522 | for (var j = cur.length - 1; j >= 0; j--){ 523 | var inst = $.data(cur[j], 'sortable'); 524 | if(inst && inst != this && !inst.options.disabled) { 525 | queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); 526 | this.containers.push(inst); 527 | } 528 | }; 529 | }; 530 | } 531 | 532 | for (var i = queries.length - 1; i >= 0; i--) { 533 | var targetData = queries[i][1]; 534 | var _queries = queries[i][0]; 535 | 536 | for (var j=0, queriesLength = _queries.length; j < queriesLength; j++) { 537 | var item = $(_queries[j]); 538 | 539 | item.data('sortable-item', targetData); // Data for target checking (mouse manager) 540 | 541 | items.push({ 542 | item: item, 543 | instance: targetData, 544 | width: 0, height: 0, 545 | left: 0, top: 0 546 | }); 547 | }; 548 | }; 549 | 550 | }, 551 | 552 | refreshPositions: function(fast) { 553 | 554 | //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change 555 | if(this.offsetParent && this.helper) { 556 | this.offset.parent = this._getParentOffset(); 557 | } 558 | 559 | for (var i = this.items.length - 1; i >= 0; i--){ 560 | var item = this.items[i]; 561 | 562 | //We ignore calculating positions of all connected containers when we're not over them 563 | if(item.instance != this.currentContainer && this.currentContainer && item.item[0] != this.currentItem[0]) 564 | continue; 565 | 566 | var t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; 567 | 568 | if (!fast) { 569 | if (this.options.accurateIntersection) { 570 | item.width = t.outerWidth(); 571 | item.height = t.outerHeight(); 572 | } 573 | else { 574 | item.width = t[0].offsetWidth; 575 | item.height = t[0].offsetHeight; 576 | } 577 | } 578 | 579 | var p = t.offset(); 580 | item.left = p.left; 581 | item.top = p.top; 582 | }; 583 | 584 | if(this.options.custom && this.options.custom.refreshContainers) { 585 | this.options.custom.refreshContainers.call(this); 586 | } else { 587 | for (var i = this.containers.length - 1; i >= 0; i--){ 588 | var p = this.containers[i].element.offset(); 589 | this.containers[i].containerCache.left = p.left; 590 | this.containers[i].containerCache.top = p.top; 591 | this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); 592 | this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); 593 | }; 594 | } 595 | 596 | }, 597 | 598 | _createPlaceholder: function(that) { 599 | 600 | var self = that || this, o = self.options; 601 | 602 | if(!o.placeholder || o.placeholder.constructor == String) { 603 | var className = o.placeholder; 604 | o.placeholder = { 605 | element: function() { 606 | 607 | var el = $(document.createElement(self.currentItem[0].nodeName)) 608 | .addClass(className || self.currentItem[0].className+" "+self.options.cssNamespace+"-sortable-placeholder") 609 | .removeClass(self.options.cssNamespace+'-sortable-helper')[0]; 610 | 611 | if(!className) 612 | el.style.visibility = "hidden"; 613 | 614 | return el; 615 | }, 616 | update: function(container, p) { 617 | 618 | // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that 619 | // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified 620 | if(className && !o.forcePlaceholderSize) return; 621 | 622 | //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item 623 | if(!p.height()) { p.height(self.currentItem.innerHeight() - parseInt(self.currentItem.css('paddingTop')||0, 10) - parseInt(self.currentItem.css('paddingBottom')||0, 10)); }; 624 | if(!p.width()) { p.width(self.currentItem.innerWidth() - parseInt(self.currentItem.css('paddingLeft')||0, 10) - parseInt(self.currentItem.css('paddingRight')||0, 10)); }; 625 | } 626 | }; 627 | } 628 | 629 | //Create the placeholder 630 | self.placeholder = $(o.placeholder.element.call(self.element, self.currentItem)); 631 | 632 | //Append it after the actual current item 633 | self.currentItem.after(self.placeholder); 634 | 635 | //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) 636 | o.placeholder.update(self, self.placeholder); 637 | 638 | }, 639 | 640 | _contactContainers: function(event) { 641 | for (var i = this.containers.length - 1; i >= 0; i--){ 642 | 643 | if(this._intersectsWith(this.containers[i].containerCache)) { 644 | if(!this.containers[i].containerCache.over) { 645 | 646 | if(this.currentContainer != this.containers[i]) { 647 | 648 | //When entering a new container, we will find the item with the least distance and append our item near it 649 | var dist = 10000; var itemWithLeastDistance = null; var base = this.positionAbs[this.containers[i].floating ? 'left' : 'top']; 650 | for (var j = this.items.length - 1; j >= 0; j--) { 651 | if(!$.ui.contains(this.containers[i].element[0], this.items[j].item[0])) continue; 652 | var cur = this.items[j][this.containers[i].floating ? 'left' : 'top']; 653 | if(Math.abs(cur - base) < dist) { 654 | dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; 655 | } 656 | } 657 | 658 | if(!itemWithLeastDistance && !this.options.dropOnEmpty) //Check if dropOnEmpty is enabled 659 | continue; 660 | 661 | this.currentContainer = this.containers[i]; 662 | itemWithLeastDistance ? this.options.sortIndicator.call(this, event, itemWithLeastDistance, null, true) : this.options.sortIndicator.call(this, event, null, this.containers[i].element, true); 663 | this._trigger("change", event, this._uiHash()); 664 | this.containers[i]._trigger("change", event, this._uiHash(this)); 665 | 666 | //Update the placeholder 667 | this.options.placeholder.update(this.currentContainer, this.placeholder); 668 | 669 | } 670 | 671 | this.containers[i]._trigger("over", event, this._uiHash(this)); 672 | this.containers[i].containerCache.over = 1; 673 | } 674 | } else { 675 | if(this.containers[i].containerCache.over) { 676 | this.containers[i]._trigger("out", event, this._uiHash(this)); 677 | this.containers[i].containerCache.over = 0; 678 | } 679 | } 680 | 681 | }; 682 | }, 683 | 684 | _createHelper: function(event) { 685 | 686 | var o = this.options; 687 | var helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper == 'clone' ? this.currentItem.clone() : this.currentItem); 688 | 689 | if(!helper.parents('body').length) //Add the helper to the DOM if that didn't happen already 690 | $(o.appendTo != 'parent' ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); 691 | 692 | if(helper[0] == this.currentItem[0]) 693 | this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; 694 | 695 | if(helper[0].style.width == '' || o.forceHelperSize) helper.width(this.currentItem.width()); 696 | if(helper[0].style.height == '' || o.forceHelperSize) helper.height(this.currentItem.height()); 697 | 698 | return helper; 699 | 700 | }, 701 | 702 | _adjustOffsetFromHelper: function(obj) { 703 | if(obj.left != undefined) this.offset.click.left = obj.left + this.margins.left; 704 | if(obj.right != undefined) this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; 705 | if(obj.top != undefined) this.offset.click.top = obj.top + this.margins.top; 706 | if(obj.bottom != undefined) this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; 707 | }, 708 | 709 | _getParentOffset: function() { 710 | 711 | 712 | //Get the offsetParent and cache its position 713 | this.offsetParent = this.helper.offsetParent(); 714 | var po = this.offsetParent.offset(); 715 | 716 | // This is a special case where we need to modify a offset calculated on start, since the following happened: 717 | // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent 718 | // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that 719 | // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag 720 | if(this.cssPosition == 'absolute' && this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) { 721 | po.left += this.scrollParent.scrollLeft(); 722 | po.top += this.scrollParent.scrollTop(); 723 | } 724 | 725 | if((this.offsetParent[0] == document.body && $.browser.mozilla) //Ugly FF3 fix 726 | || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() == 'html' && $.browser.msie)) //Ugly IE fix 727 | po = { top: 0, left: 0 }; 728 | 729 | return { 730 | top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), 731 | left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) 732 | }; 733 | 734 | }, 735 | 736 | _getRelativeOffset: function() { 737 | 738 | if(this.cssPosition == "relative") { 739 | var p = this.currentItem.position(); 740 | return { 741 | top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), 742 | left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() 743 | }; 744 | } else { 745 | return { top: 0, left: 0 }; 746 | } 747 | 748 | }, 749 | 750 | _cacheMargins: function() { 751 | this.margins = { 752 | left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), 753 | top: (parseInt(this.currentItem.css("marginTop"),10) || 0) 754 | }; 755 | }, 756 | 757 | _cacheHelperProportions: function() { 758 | this.helperProportions = { 759 | width: this.helper.outerWidth(), 760 | height: this.helper.outerHeight() 761 | }; 762 | }, 763 | 764 | _setContainment: function() { 765 | 766 | var o = this.options; 767 | if(o.containment == 'parent') o.containment = this.helper[0].parentNode; 768 | if(o.containment == 'document' || o.containment == 'window') this.containment = [ 769 | 0 - this.offset.relative.left - this.offset.parent.left, 770 | 0 - this.offset.relative.top - this.offset.parent.top, 771 | $(o.containment == 'document' ? document : window).width() - this.helperProportions.width - this.margins.left, 772 | ($(o.containment == 'document' ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top 773 | ]; 774 | 775 | if(!(/^(document|window|parent)$/).test(o.containment)) { 776 | var ce = $(o.containment)[0]; 777 | var co = $(o.containment).offset(); 778 | var over = ($(ce).css("overflow") != 'hidden'); 779 | 780 | this.containment = [ 781 | co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, 782 | co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, 783 | co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, 784 | co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top 785 | ]; 786 | } 787 | 788 | }, 789 | 790 | _convertPositionTo: function(d, pos) { 791 | 792 | if(!pos) pos = this.position; 793 | var mod = d == "absolute" ? 1 : -1; 794 | var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); 795 | 796 | return { 797 | top: ( 798 | pos.top // The absolute mouse position 799 | + this.offset.relative.top * mod // Only for relative positioned nodes: Relative offset from element to offset parent 800 | + this.offset.parent.top * mod // The offsetParent's offset without borders (offset + border) 801 | - ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod 802 | ), 803 | left: ( 804 | pos.left // The absolute mouse position 805 | + this.offset.relative.left * mod // Only for relative positioned nodes: Relative offset from element to offset parent 806 | + this.offset.parent.left * mod // The offsetParent's offset without borders (offset + border) 807 | - ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod 808 | ) 809 | }; 810 | 811 | }, 812 | 813 | _generatePosition: function(event) { 814 | 815 | var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); 816 | 817 | // This is another very weird special case that only happens for relative elements: 818 | // 1. If the css position is relative 819 | // 2. and the scroll parent is the document or similar to the offset parent 820 | // we have to refresh the relative offset during the scroll so there are no jumps 821 | if(this.cssPosition == 'relative' && !(this.scrollParent[0] != document && this.scrollParent[0] != this.offsetParent[0])) { 822 | this.offset.relative = this._getRelativeOffset(); 823 | } 824 | 825 | var pageX = event.pageX; 826 | var pageY = event.pageY; 827 | 828 | /* 829 | * - Position constraining - 830 | * Constrain the position to a mix of grid, containment. 831 | */ 832 | 833 | if(this.originalPosition) { //If we are not dragging yet, we won't check for options 834 | 835 | if(this.containment) { 836 | if(event.pageX - this.offset.click.left < this.containment[0]) pageX = this.containment[0] + this.offset.click.left; 837 | if(event.pageY - this.offset.click.top < this.containment[1]) pageY = this.containment[1] + this.offset.click.top; 838 | if(event.pageX - this.offset.click.left > this.containment[2]) pageX = this.containment[2] + this.offset.click.left; 839 | if(event.pageY - this.offset.click.top > this.containment[3]) pageY = this.containment[3] + this.offset.click.top; 840 | } 841 | 842 | if(o.grid) { 843 | var top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; 844 | pageY = this.containment ? (!(top - this.offset.click.top < this.containment[1] || top - this.offset.click.top > this.containment[3]) ? top : (!(top - this.offset.click.top < this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; 845 | 846 | var left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; 847 | pageX = this.containment ? (!(left - this.offset.click.left < this.containment[0] || left - this.offset.click.left > this.containment[2]) ? left : (!(left - this.offset.click.left < this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; 848 | } 849 | 850 | } 851 | 852 | return { 853 | top: ( 854 | pageY // The absolute mouse position 855 | - this.offset.click.top // Click offset (relative to the element) 856 | - this.offset.relative.top // Only for relative positioned nodes: Relative offset from element to offset parent 857 | - this.offset.parent.top // The offsetParent's offset without borders (offset + border) 858 | + ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) 859 | ), 860 | left: ( 861 | pageX // The absolute mouse position 862 | - this.offset.click.left // Click offset (relative to the element) 863 | - this.offset.relative.left // Only for relative positioned nodes: Relative offset from element to offset parent 864 | - this.offset.parent.left // The offsetParent's offset without borders (offset + border) 865 | + ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) 866 | ) 867 | }; 868 | 869 | }, 870 | 871 | _rearrange: function(event, i, a, hardRefresh) { 872 | 873 | a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction == 'down' ? i.item[0] : i.item[0].nextSibling)); 874 | 875 | //Various things done here to improve the performance: 876 | // 1. we create a setTimeout, that calls refreshPositions 877 | // 2. on the instance, we have a counter variable, that get's higher after every append 878 | // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same 879 | // 4. this lets only the last addition to the timeout stack through 880 | this.counter = this.counter ? ++this.counter : 1; 881 | var self = this, counter = this.counter; 882 | 883 | window.setTimeout(function() { 884 | if(counter == self.counter) self.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove 885 | },0); 886 | 887 | }, 888 | 889 | _clear: function(event, noPropagation) { 890 | 891 | this.reverting = false; 892 | // We delay all events that have to be triggered to after the point where the placeholder has been removed and 893 | // everything else normalized again 894 | var delayedTriggers = [], self = this; 895 | 896 | //We first have to update the dom position of the actual currentItem 897 | if(!this._noFinalSort) this.placeholder.before(this.currentItem); 898 | this._noFinalSort = null; 899 | 900 | if(this.helper[0] == this.currentItem[0]) { 901 | for(var i in this._storedCSS) { 902 | if(this._storedCSS[i] == 'auto' || this._storedCSS[i] == 'static') this._storedCSS[i] = ''; 903 | } 904 | this.currentItem.css(this._storedCSS).removeClass(this.options.cssNamespace+"-sortable-helper"); 905 | } else { 906 | this.currentItem.show(); 907 | } 908 | 909 | if(this.fromOutside && !noPropagation) delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); 910 | if((this.fromOutside || this.domPosition.prev != this.currentItem.prev().not("."+this.options.cssNamespace+"-sortable-helper")[0] || this.domPosition.parent != this.currentItem.parent()[0]) && !noPropagation) delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed 911 | if(!$.ui.contains(this.element[0], this.currentItem[0])) { //Node was moved out of the current element 912 | if(!noPropagation) delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); 913 | for (var i = this.containers.length - 1; i >= 0; i--){ 914 | if($.ui.contains(this.containers[i].element[0], this.currentItem[0]) && !noPropagation) { 915 | delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.containers[i])); 916 | delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.containers[i])); 917 | } 918 | }; 919 | }; 920 | 921 | //Post events to containers 922 | for (var i = this.containers.length - 1; i >= 0; i--){ 923 | if(!noPropagation) delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); }; }).call(this, this.containers[i])); 924 | if(this.containers[i].containerCache.over) { 925 | delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); }; }).call(this, this.containers[i])); 926 | this.containers[i].containerCache.over = 0; 927 | } 928 | } 929 | 930 | //Do what was originally in plugins 931 | if(this._storedCursor) $('body').css("cursor", this._storedCursor); //Reset cursor 932 | if(this._storedOpacity) this.helper.css("opacity", this._storedCursor); //Reset cursor 933 | if(this._storedZIndex) this.helper.css("zIndex", this._storedZIndex == 'auto' ? '' : this._storedZIndex); //Reset z-index 934 | 935 | this.dragging = false; 936 | if(this.cancelHelperRemoval) { 937 | if(!noPropagation) { 938 | this._trigger("beforeStop", event, this._uiHash()); 939 | for (var i=0; i < delayedTriggers.length; i++) { delayedTriggers[i].call(this, event); }; //Trigger all delayed events 940 | this._trigger("stop", event, this._uiHash()); 941 | } 942 | return false; 943 | } 944 | 945 | if(!noPropagation) this._trigger("beforeStop", event, this._uiHash()); 946 | 947 | //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! 948 | this.placeholder[0].parentNode.removeChild(this.placeholder[0]); 949 | 950 | if(this.helper[0] != this.currentItem[0]) this.helper.remove(); this.helper = null; 951 | 952 | if(!noPropagation) { 953 | for (var i=0; i < delayedTriggers.length; i++) { delayedTriggers[i].call(this, event); }; //Trigger all delayed events 954 | this._trigger("stop", event, this._uiHash()); 955 | } 956 | 957 | this.fromOutside = false; 958 | return true; 959 | 960 | }, 961 | 962 | _trigger: function() { 963 | if ($.widget.prototype._trigger.apply(this, arguments) === false) { 964 | this.cancel(); 965 | } 966 | }, 967 | 968 | _uiHash: function(inst) { 969 | var self = inst || this; 970 | return { 971 | helper: self.helper, 972 | placeholder: self.placeholder || $([]), 973 | position: self.position, 974 | absolutePosition: self.positionAbs, //deprecated 975 | offset: self.positionAbs, 976 | item: self.currentItem, 977 | sender: inst ? inst.element : null 978 | }; 979 | } 980 | 981 | })); 982 | 983 | $.extend($.ui.sortable, { 984 | getter: "serialize toArray", 985 | version: "1.6rc6", 986 | defaults: { 987 | accurateIntersection: true, 988 | appendTo: "parent", 989 | cancel: ":input,option", 990 | connectWith: false, 991 | cssNamespace: 'ui', 992 | delay: 0, 993 | distance: 1, 994 | dropOnEmpty: true, 995 | forcePlaceholderSize: false, 996 | forceHelperSize: false, 997 | handle: false, 998 | helper: "original", 999 | items: '> *', 1000 | placeholder: false, 1001 | scope: "default", 1002 | scroll: true, 1003 | scrollSensitivity: 20, 1004 | scrollSpeed: 20, 1005 | sortIndicator: $.ui.sortable.prototype._rearrange, 1006 | tolerance: "intersect", 1007 | zIndex: 1000 1008 | } 1009 | }); 1010 | 1011 | })(jQuery); 1012 | -------------------------------------------------------------------------------- /conference/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/conference/__init__.py -------------------------------------------------------------------------------- /conference/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import * 3 | 4 | def start_conf(modeladmin, request, queryset): 5 | for obj in queryset: 6 | obj.start() 7 | start_conf.short_description = "Start conference" 8 | 9 | class ConferenceAdmin(admin.ModelAdmin): 10 | actions=[start_conf] 11 | 12 | 13 | admin.site.register(Conference,ConferenceAdmin) 14 | admin.site.register(Phone) 15 | 16 | -------------------------------------------------------------------------------- /conference/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models as m 2 | from django.conf import settings 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django import forms 5 | from fsapi import * 6 | 7 | # Create your models here. 8 | 9 | class Conference(m.Model): 10 | name = m.CharField(_(u"Name"),max_length=254,blank=False) 11 | number = m.CharField(_(u"Number"),max_length=254,blank=False) 12 | pin = m.IntegerField(_(u"Password"),max_length=254,blank=True,null=True) 13 | participants = m.ManyToManyField("Phone",verbose_name=_(u"Phone"),through='Participant') 14 | is_active = m.BooleanField(_(u"Is active?"),default=False,editable=False) 15 | def start(self): 16 | #participants = list(self.participants.filter(auto_call=True)) 17 | # TODO: auto_call filter 18 | participants = self.participant_set.all() 19 | if participants: 20 | for p in participants: 21 | call_from_conference(self.number,p.phone.number,vars='participant=%s'%p.id) 22 | def participants_form(self): 23 | return AddParticipant(instance=self) 24 | def __unicode__(self): 25 | return self.name 26 | class Meta: 27 | verbose_name = _(u"Conference") 28 | verbose_name_plural = _(u"Conferences") 29 | 30 | class Phone(m.Model): 31 | name = m.CharField(_(u"Name"),max_length=254) 32 | number = m.IntegerField(_(u"Number"),max_length=254) 33 | auto_call = m.BooleanField(_(u"Auto call"),default=True) 34 | def caller_id_name(self): 35 | return self.name 36 | def caller_id_number(self): 37 | return self.number 38 | def member(self): 39 | return self 40 | def __unicode__(self): 41 | return "%s (%s)%s"%(self.name,self.number,"" if self.auto_call else "!") 42 | class Meta: 43 | verbose_name = _(u"Phone") 44 | verbose_name_plural = _(u"Phones") 45 | 46 | class Participant(m.Model): 47 | conference = m.ForeignKey(Conference) 48 | phone = m.ForeignKey(Phone) 49 | active = m.BooleanField(_(u"Active"),default=False,editable=False) 50 | mute = m.BooleanField(_(u"Mute"),default=False,editable=False) 51 | talk = m.BooleanField(_(u"Talk"),default=False,editable=False) 52 | deaf = m.BooleanField(_(u"Deaf"),default=False,editable=False) 53 | 54 | # Forms 55 | class AddParticipant(forms.ModelForm): 56 | class Meta: 57 | model = Conference 58 | fields = ['participants'] 59 | def save(self): 60 | for p in self.cleaned_data['participants']: 61 | Participant.objects.get_or_create(conference=self.instance,phone=p) 62 | Participant.objects.filter(conference=self.instance).\ 63 | exclude(phone__in=self.cleaned_data['participants']).delete() 64 | 65 | 66 | class InviteParticipantForm(forms.Form): 67 | conference = forms.CharField(widget=forms.widgets.HiddenInput()) 68 | phone = forms.CharField() 69 | 70 | class AddConference(forms.ModelForm): 71 | class Meta: 72 | model = Conference 73 | fields = ['name','number'] 74 | -------------------------------------------------------------------------------- /conference/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('', 4 | (r'^$','conference.views.list'), 5 | (r'^(?P[-.0-9a-zA-Z]+)/(?Pkick|mute|unmute|start)/(?P\d+)/$','conference.views.list'), 6 | (r'^add/$','conference.views.add'), 7 | (r'^add/participants/(?P\d+)/$','conference.views.add_partcipants'), 8 | (r'^delete/(?P\d+)/$','conference.views.del_conf'), 9 | ) 10 | -------------------------------------------------------------------------------- /conference/views.py: -------------------------------------------------------------------------------- 1 | from lxml import objectify as O 2 | from django.shortcuts import render_to_response, get_object_or_404 3 | from django.http import HttpResponseRedirect 4 | from django.conf import settings 5 | from django.views.generic import list_detail 6 | from models import * 7 | from fsapi import * 8 | 9 | def list(request,do="",id="",cnf="",param=""): 10 | # Data from FreeSWITCH 11 | r = O.fromstring(fsapi("conference","xml_list")) 12 | 13 | # Commands to FreeSWITCH 14 | if do=="start": 15 | conference = Conference.objects.get(id=int(cnf)) 16 | conference.start() 17 | return HttpResponseRedirect('/confs/') 18 | if do and id: 19 | fsapi("conference","%s %s %s"%(cnf,do,id)) 20 | return HttpResponseRedirect('/confs/') 21 | 22 | # Invite participant 23 | if request.GET: 24 | form = InviteParticipantForm(request.GET) 25 | if form.is_valid(): 26 | conf = form.cleaned_data.get('conference') 27 | phone = form.cleaned_data.get('phone') 28 | call_from_conference(conf,phone) 29 | return HttpResponseRedirect('/confs/') 30 | 31 | # Parse data for display 32 | conferences = [] 33 | active_confs=[] 34 | if r.countchildren() > 0: 35 | for conf in r.conference: 36 | conf_name =conf.get('name') 37 | conference = Conference.objects.filter(number=conf_name) 38 | if conference: 39 | members = dict([(str(m.number),m) for m in conference[0].participants.all()]) 40 | conference[0].is_active = True 41 | conference[0].save() 42 | active_confs.append(conference[0].id) 43 | else: 44 | members = {} 45 | fs_members = [m for m in conf.members.member] 46 | for fm in fs_members: 47 | if str(fm.caller_id_name) in members: 48 | fm.member = members[str(fm.caller_id_name)] 49 | del members[str(fm.caller_id_name)] 50 | else: 51 | fm.member = None 52 | fs_members.extend(members.values()) 53 | conf_res = [conf_name, fs_members,InviteParticipantForm({'conference':conf.get('name')}), 54 | conference[0] if conference else None] 55 | conferences.append(conf_res) 56 | Conference.objects.exclude(id__in=active_confs).update(is_active=False) 57 | return list_detail.object_list(request,queryset=Conference.objects.filter(is_active=False), 58 | template_name='confrences.html',extra_context={'confs':conferences,'addconf':AddConference()}) 59 | #return render_to_response('confrences.html',{'confs':conferences}) 60 | 61 | def add(request): 62 | # Add conference 63 | if request.method == "POST": 64 | new_conf = AddConference(request.POST) 65 | if new_conf.is_valid(): 66 | new_conf.save() 67 | return HttpResponseRedirect('/confs/') 68 | return HttpResponseRedirect('/confs/') 69 | 70 | def add_partcipants(request,object_id): 71 | # Update participants 72 | conf=get_object_or_404(Conference,pk=object_id) 73 | if request.method == "POST": 74 | new_conf = AddParticipant(request.POST,instance=conf) 75 | if new_conf.is_valid(): 76 | new_conf.save() 77 | return HttpResponseRedirect('/confs/') 78 | return HttpResponseRedirect('/confs/') 79 | 80 | def del_conf(request,object_id): 81 | # Update participants 82 | conf=get_object_or_404(Conference,pk=object_id) 83 | conf.delete() 84 | return HttpResponseRedirect('/confs/') 85 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import syslog 5 | 6 | def debug(msg): 7 | print msg 8 | syslog.syslog(msg) 9 | -------------------------------------------------------------------------------- /dialplan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/dialplan/__init__.py -------------------------------------------------------------------------------- /dialplan/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import * 3 | 4 | #class VarInline(admin.TabularInline): 5 | # model = Agent 6 | # extra = 3 7 | # 8 | #class FSGAdmin(admin.ModelAdmin): 9 | # inlines = [VarInline] 10 | # 11 | #admin.site.register(HuntGroup,FSGAdmin) 12 | 13 | class ExtInline(admin.TabularInline): 14 | model = Extension 15 | extra = 5 16 | template = "condition_inline.html" 17 | class CondInline(admin.TabularInline): 18 | model = Condition 19 | extra = 3 20 | template = "condition_inline.html" 21 | class ActInline(admin.TabularInline): 22 | model = Action 23 | extra = 10 24 | 25 | class Cont_Admin(admin.ModelAdmin): 26 | inlines = [ExtInline] 27 | class Ext_Admin(admin.ModelAdmin): 28 | inlines = [CondInline] 29 | class Cond_Admin(admin.ModelAdmin): 30 | inlines = [ActInline] 31 | 32 | admin.site.register(Extension, Ext_Admin) 33 | admin.site.register(Condition, Cond_Admin) 34 | admin.site.register(Context,Cont_Admin) 35 | 36 | admin.site.register(DPApp) 37 | 38 | -------------------------------------------------------------------------------- /dialplan/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models as m 2 | from django.utils.translation import ugettext_lazy as _ 3 | #from users.models import PhoneNumber 4 | 5 | # Create your models here. 6 | class Context(m.Model): 7 | name = m.CharField(_(u"Name"),max_length=255) 8 | def __unicode__(self): 9 | return self.name 10 | class Meta: 11 | verbose_name = _(u"Context") 12 | verbose_name_plural = _(u"Contexts") 13 | 14 | class Extension(m.Model): 15 | name = m.CharField(_(u"Name"),max_length=255) 16 | continue_on = m.BooleanField(_(u"Continue on")) 17 | context = m.ForeignKey(Context) 18 | pref = m.IntegerField(_(u"Preference")) 19 | def __unicode__(self): 20 | return self.name 21 | class Meta: 22 | verbose_name = _(u"Extension") 23 | verbose_name_plural = _(u"Extensions") 24 | 25 | BREAK = ( 26 | ("on-false",_(u"on-false")), 27 | ("on-true",_(u"on-true")), 28 | ("always",_(u"always")), 29 | ("never",_(u"never")), 30 | ) 31 | 32 | class Condition(m.Model): 33 | field = m.CharField(_(u"Field"),max_length=255) 34 | expression = m.CharField(_(u"Expression"),max_length=255) 35 | break_on = m.CharField(_(u"Break on"),max_length=10,choices=BREAK,default="on-false") 36 | extension = m.ForeignKey(Extension) 37 | pref = m.IntegerField(_(u"Preference"),) 38 | def __unicode__(self): 39 | return "%s === %s"%(self.field,self.expression) 40 | class Meta: 41 | ordering = ['pref'] 42 | verbose_name = _(u"Condition") 43 | verbose_name_plural = _(u"Conditions") 44 | 45 | class DPApp(m.Model): 46 | name = m.CharField(_(u"Name"),max_length=255) 47 | def __unicode__(self): 48 | return self.name 49 | class Meta: 50 | verbose_name = _(u"Dialplan application") 51 | verbose_name_plural = _(u"Dialplan applications") 52 | 53 | def get_dpapp(app): 54 | f = DPApp.objects.filter(name=app) 55 | if f: 56 | return f[0] 57 | else: 58 | new = DPApp(name=app) 59 | new.save() 60 | return new 61 | 62 | class Action(m.Model): 63 | app = m.ForeignKey(DPApp) 64 | condition = m.ForeignKey(Condition) 65 | params = m.CharField(_(u"Name"),max_length=255) 66 | anti = m.BooleanField(_(u"Anti action")) 67 | pref = m.IntegerField(_(u"Preference")) 68 | def __unicode__(self): 69 | return "%s(%s)"%(self.app.name,self.params) 70 | class Meta: 71 | ordering = ['pref'] 72 | verbose_name = _(u"Action") 73 | verbose_name_plural = _(u"Actions") 74 | 75 | -------------------------------------------------------------------------------- /dialplan/templates/condition_inline.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | 66 | 67 | {# #} 70 | 71 |
72 | -------------------------------------------------------------------------------- /dialplan/templates/dialplan.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block head %} 4 | 13 | 14 | 15 | 16 | 17 | 18 | 205 | {% endblock %} 206 | {% block contents %} 207 |

{% trans "Edit dialplan" %} {% trans "Click to save" %}

208 | 209 | 225 | 248 | 272 | 280 |
281 |
282 | {% endblock %} 283 | -------------------------------------------------------------------------------- /dialplan/templates/dialplan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | {% for extension in context.extension_set.all %} 7 | 8 | {% for condition in extension.condition_set.all %} 9 | 10 | {% for action in condition.action_set.all %} 11 | <{% if action.anti %}anti-{% endif %}action application="{{ action.app.name }}" data="{{ action.params }}"/> 12 | {% endfor %} 13 | 14 | {% endfor %} 15 | 16 | {% endfor %} 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /dialplan/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.views.generic import list_detail 3 | 4 | from models import * 5 | 6 | urlpatterns = patterns('', 7 | (r'get/$','dialplan.views.get_dialplan'), 8 | (r'edit/$','dialplan.views.edit_dialplan'), 9 | (r'save/$','dialplan.views.save_dialplan'), 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /dialplan/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from django.http import HttpResponse 3 | from django.shortcuts import render_to_response, get_object_or_404 4 | from models import * 5 | from django.core import serializers 6 | from django.utils import simplejson 7 | 8 | def pyser(i,d={}): 9 | res = dict([(f.name,f.value_from_object(i)) for f in i._meta.fields]) 10 | res.update(d) 11 | return res 12 | 13 | def all_ser(): 14 | return simplejson.dumps( 15 | [pyser(i,{'exts': 16 | [pyser(j,{'conditions': 17 | [pyser(k,{'actions': 18 | [pyser(a,{'app_name':a.app.name} ) for a in k.action_set.all()]}) 19 | for k in j.condition_set.all()]}) 20 | for j in i.extension_set.all()]}) 21 | for i in Context.objects.all()]) 22 | 23 | def get_dialplan(request): 24 | if len(request.GET): 25 | data = request.GET 26 | else: 27 | data = request.POST 28 | context = get_object_or_404(Context,name=data.get('Hunt-Context')) 29 | return render_to_response('dialplan.xml',{'context':context,'data':request.POST}) 30 | 31 | def edit_dialplan(request): 32 | #print context 33 | #context = get_object_or_404(Context,name=context) 34 | #return render_to_response('dialplan.html',{'context':context,'data':all_ser()}) 35 | return render_to_response('dialplan.html',{'data':all_ser()}) 36 | 37 | def save_obj(obj,Model): 38 | def udict2dict(udict,filter): 39 | return dict([(str(k),udict[k]) for k in udict if k in filter]) 40 | 41 | # Action hook 42 | if Model==Action: 43 | obj['app']=get_dpapp(obj['app_name']) 44 | 45 | objid = obj['id'] 46 | if objid!='': q = Model.objects.filter(id=int(objid)) 47 | else: q=[] 48 | if q: 49 | inst=q[0] 50 | for f in obj: 51 | setattr(inst,f,obj[f]) 52 | inst.save() 53 | else: 54 | del obj['id'] 55 | filter = [i.name for i in Model._meta.fields] 56 | inst=Model(**udict2dict(obj,filter)) 57 | inst.save() 58 | return inst 59 | 60 | _dp_next=[Extension,'exts','context',[Condition,'conditions','extension',[Action,'actions','condition',[]]]] 61 | def save_objs(objs,Model,prnt_f,parent,next=[]): 62 | ids=[] 63 | pref=10 64 | for obj in objs: 65 | if parent and prnt_f: 66 | obj.update({prnt_f:parent}) 67 | obj['pref']=pref 68 | pref+=10 69 | new_obj = save_obj(obj,Model) 70 | ids.append(int(new_obj.id)) 71 | if next: 72 | next_obj = obj[next[1]] 73 | save_objs(next_obj, next[0], next[2], new_obj, next[3]) 74 | 75 | # Delete removed objs 76 | if prnt_f and prnt_f: 77 | db_ids = Model.objects.filter(**{prnt_f:parent}) 78 | else: db_ids = Model.objects.all() 79 | db_ids = [i.id for i in db_ids] 80 | for_rm = list(set(db_ids)-set(ids)) 81 | print ids, db_ids, for_rm 82 | for rm in for_rm: 83 | o = Model.objects.filter(**{prnt_f:parent,'id':rm})[0] 84 | print "Remove ",o 85 | o.delete() 86 | 87 | 88 | def save_dialplan(request): 89 | if len(request.GET): 90 | data = request.GET 91 | else: 92 | data = request.POST 93 | print data['data'] 94 | dp = simplejson.loads(data['data']) 95 | print dp 96 | save_objs(dp,Context,'',None,_dp_next) 97 | 98 | return HttpResponse('Saved') 99 | 100 | 101 | -------------------------------------------------------------------------------- /eventsocket/InboundOutboundExample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # freeswitch's event socket protocol for twisted 4 | # Copyright (C) 2009 Alexandre Fiori & Arnaldo Pereira 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | 20 | from Queue import Queue 21 | from eventsocket import EventProtocol 22 | from twisted.internet import reactor, protocol 23 | 24 | port = 1905 25 | unique_id = {} 26 | 27 | class InboundProxy(EventProtocol): 28 | def __init__(self): 29 | self.job_uuid = {} 30 | self.calls = Queue() 31 | EventProtocol.__init__(self) 32 | 33 | def authSuccess(self, ev): 34 | self.eventplain('CHANNEL_HANGUP BACKGROUND_JOB') 35 | 36 | def authFailure(self, failure): 37 | self.factory.reconnect = False 38 | 39 | def eventplainSuccess(self, ev): 40 | for ext in ['1000', '1001', '1002']: 41 | self.calls.put(ext) 42 | print '[inboundproxy] originating call to extension %s' % ext 43 | 44 | # FreeSWITCH will park the call immediately if the destination is socket(). 45 | # when we receive this call on the outbound side, there won't be an ANSWER 46 | # nor a PARK event we can watch; hence, we must start processing right away. 47 | self.bgapi("originate sofia/internal/%s%%192.168.0.3 '&socket(127.0.0.1:%d async full)'" % (ext, port)) 48 | 49 | def eventplainFailure(self, failure): 50 | self.factory.reconnect = False 51 | self.exit() 52 | 53 | def bgapiSuccess(self, ev): 54 | ext = self.calls.get() 55 | self.calls.task_done() 56 | self.job_uuid[ev.Job_UUID] = ext 57 | 58 | def bgapiFailure(self, failure): 59 | print '[inboundproxy] bgapi failure: %s' % failure.value 60 | self.exit() 61 | 62 | def onBackgroundJob(self, data): 63 | ext = self.job_uuid[data.Job_UUID] 64 | del self.job_uuid[data.Job_UUID] 65 | response, content = data.rawresponse.split() 66 | if response == '+OK': unique_id[content] = ext 67 | else: print '[inboundproxy] cannot call %s: %s' % (ext, content) 68 | 69 | def onChannelHangup(self, data): 70 | if unique_id.has_key(data.Unique_ID): 71 | ext = unique_id[data.Unique_ID] 72 | del unique_id[data.Unique_ID] 73 | print '[inboundproxy] extension %s hung up: %s' % (ext, data.Hangup_Cause) 74 | 75 | class InboundFactory(protocol.ClientFactory): 76 | protocol = InboundProxy 77 | 78 | def __init__(self, password): 79 | self.password = password 80 | self.reconnect = True 81 | 82 | def clientConnectionLost(self, connector, reason): 83 | if self.reconnect: connector.connect() 84 | else: 85 | print '[inboundfactory] stopping reactor' 86 | reactor.stop() 87 | 88 | def clientConnectionFailed(self, connector, reason): 89 | print '[inboundfactoy] cannot connect: %s' % reason 90 | reactor.stop() 91 | 92 | class OutboundProxy(EventProtocol): 93 | def __init__(self): 94 | self.ext = None 95 | self.debug_enabled = False 96 | EventProtocol.__init__(self) 97 | 98 | # when we get connection from fs, send "connect" 99 | def connectionMade(self): 100 | self.connect() 101 | 102 | # when we get OK from connect, send "myevents" 103 | def connectSuccess(self, ev): 104 | self.ext = unique_id[ev.Unique_ID] 105 | print '[outboundproxy] started controlling extension %s' % self.ext 106 | self.myevents() 107 | 108 | # after we get OK for myevents, send "answer". 109 | def myeventsSuccess(self, ev): 110 | self.answer() 111 | 112 | # when we get OK from answer, play something. Note: on default FreeSWITCH 113 | # deployments, the sounds directory is usually /usr/local/freeswitch/sounds 114 | def answerSuccess(self, ev): 115 | print '[outboundproxy] going to play audio file for extension %s' % self.ext 116 | self.playback('/opt/freeswitch/sounds/en/us/callie/ivr/8000/ivr-sample_submenu.wav', 117 | terminators='123*#') 118 | 119 | # well well... 120 | def onDtmf(self, data): 121 | print '[outboundproxy] got dtmf "%s" from extension %s' % (data.DTMF_Digit, self.ext) 122 | 123 | # finished executing something 124 | def onChannelExecuteComplete(self, data): 125 | app = data.variable_current_application 126 | if app == 'playback': 127 | terminator = data.get('variable_playback_terminator_used') 128 | response = data.get('variable_current_application_response') 129 | print '[outboundproxy] extension %s finished playing file, terminator=%s, response=%s' % (self.ext, terminator, response) 130 | print '[outboundproxy] bridging extension %s to public conference 888' % self.ext 131 | self.bridge('sofia/external/888@conference.freeswitch.org') 132 | 133 | # it could also be done by onChannelUnbridge 134 | elif app == 'bridge': 135 | print '[outboundproxy] extension %s finished the bridge' % self.ext 136 | self.hangup() 137 | 138 | # goodbye... 139 | def exitSuccess(self, ev): 140 | print '[outboundproxy] control of extension %s has finished' % self.ext 141 | 142 | class OutboundFactory(protocol.ServerFactory): 143 | protocol = OutboundProxy 144 | 145 | if __name__ == '__main__': 146 | reactor.listenTCP(port, OutboundFactory()) 147 | reactor.connectTCP('localhost', 8021, InboundFactory('ClueCon')) 148 | reactor.run() 149 | -------------------------------------------------------------------------------- /eventsocket/SimpleOriginateExample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # freeswitch's event socket protocol for twisted 4 | # Copyright (C) 2009 Alexandre Fiori & Arnaldo Pereira 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | 20 | from eventsocket import EventProtocol 21 | from twisted.internet import reactor, protocol 22 | 23 | class InboundProxy(EventProtocol): 24 | def authSuccess(self, ev): 25 | self.eventplain('CHANNEL_CREATE BACKGROUND_JOB') 26 | self.bgapi('originate sofia/internal/1001%192.168.0.3 9888 XML default') 27 | 28 | def authFailure(self, ev): 29 | print 'auth failure.' 30 | self.factory.reconnect = False 31 | 32 | def exitSuccess(self, ev): 33 | print 'exit success. ev:', ev 34 | self.factory.reconnect = False 35 | 36 | def bgapiSuccess(self, ev): 37 | print 'calling you. ev:', ev 38 | 39 | def bgapiFailure(self, ev): 40 | print 'bgapi failure. ev:', ev 41 | 42 | def onBackgroundJob(self, ev): 43 | print 'background job. ev:', ev 44 | 45 | class InboundFactory(protocol.ClientFactory): 46 | protocol = InboundProxy 47 | 48 | def __init__(self, password): 49 | self.password = password 50 | self.reconnect = True 51 | 52 | def clientConnectionLost(self, connector, reason): 53 | if self.reconnect: connector.connect() 54 | else: 55 | print '[inboundfactory] stopping reactor' 56 | reactor.stop() 57 | 58 | def clientConnectionFailed(self, connector, reason): 59 | print '[inboundfactoy] cannot connect: %s' % reason 60 | reactor.stop() 61 | 62 | if __name__ == '__main__': 63 | factory = InboundFactory('ClueCon') 64 | reactor.connectTCP('127.0.0.1', 8021, factory) 65 | reactor.run() 66 | -------------------------------------------------------------------------------- /eventsocket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/eventsocket/__init__.py -------------------------------------------------------------------------------- /eventsocket/eventsocket.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # freeswitch's event socket protocol for twisted 3 | # Copyright (C) 2009 Alexandre Fiori & Arnaldo Pereira 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | import re, urllib 20 | from Queue import Queue 21 | from cStringIO import StringIO 22 | from twisted.protocols import basic 23 | from twisted.internet import defer, reactor, protocol 24 | from superdict import superdict 25 | 26 | def debug(s): print s 27 | 28 | class EventSocket(basic.LineReceiver): 29 | delimiter = '\n' 30 | debug_enabled = False 31 | 32 | def __init__(self): 33 | self.ctx = None 34 | self.rawlen = None 35 | self.io = StringIO() 36 | self.crlf = re.compile(r'[\r\n]+') 37 | self.rawresponse = [ 38 | 'api/response', 39 | 'text/disconnect-notice', 40 | ] 41 | 42 | def send(self, cmd): 43 | try: 44 | if type(cmd) is unicode: cmd = cmd.encode('utf-8') 45 | self.transport.write(cmd) 46 | self.transport.write('\n\n') 47 | except Exception, e: 48 | if self.debug_enabled: debug('[eventsocket] send: %s' % e) 49 | 50 | def sendmsg(self, name, arg=None, uuid='', lock=False): 51 | if type(name) is unicode: name = name.encode('utf-8') 52 | if type(arg) is unicode: arg = arg.encode('utf-8') 53 | 54 | self.transport.write('sendmsg %s\ncall-command: execute\n' % uuid) 55 | self.transport.write('execute-app-name: %s\n' % name) 56 | if arg: self.transport.write('execute-app-arg: %s\n' % arg) 57 | if lock or True: self.transport.write('event-lock: true\n') 58 | self.transport.write('\n\n') 59 | 60 | def processLine(self, cur, line): 61 | try: 62 | k, v = self.crlf.sub('', line).split(':', 1) 63 | k = k.replace('-', '_').strip() 64 | v = urllib.url2pathname(v.strip()) 65 | cur[k] = v 66 | #cur[k] = v.isdigit() and int(v) or v 67 | except: pass 68 | 69 | def parseEvent(self, isctx=False): 70 | ev = superdict() 71 | self.io.reset() 72 | 73 | for line in self.io: 74 | if line == '\n': break 75 | self.processLine(ev, line) 76 | 77 | if not isctx: 78 | rawlength = ev.get('Content_Length') 79 | if rawlength: ev.rawresponse = self.io.read(int(rawlength)) 80 | 81 | self.io.reset() 82 | self.io.truncate() 83 | return ev 84 | 85 | def readRawResponse(self): 86 | self.io.reset() 87 | chunk = self.io.read(int(self.ctx.Content_Length)) 88 | self.io.reset() 89 | self.io.truncate() 90 | return superdict(rawresponse=chunk) 91 | 92 | def dispatchEvent(self, ctx, event): 93 | ctx.data = superdict(event.copy()) 94 | reactor.callLater(0, self.eventHandler, superdict(ctx.copy())) 95 | self.ctx = self.rawlen = None 96 | 97 | def eventHandler(self, ctx): 98 | pass 99 | 100 | def lineReceived(self, line): 101 | if line: self.io.write(line+'\n') 102 | else: 103 | ctx = self.parseEvent(True) 104 | rawlength = ctx.get('Content_Length') 105 | if rawlength: 106 | self.ctx = ctx 107 | self.rawlen = int(rawlength) 108 | self.setRawMode() 109 | else: 110 | self.dispatchEvent(ctx, {}) 111 | 112 | def rawDataReceived(self, data): 113 | if self.rawlen is not None: 114 | data, rest = data[:self.rawlen], data[self.rawlen:] 115 | self.rawlen -= len(data) 116 | else: rest = '' 117 | 118 | self.io.write(data) 119 | if self.rawlen == 0: 120 | if self.ctx.get('Content_Type') in self.rawresponse: 121 | self.dispatchEvent(self.ctx, self.readRawResponse()) 122 | else: 123 | self.dispatchEvent(self.ctx, self.parseEvent()) 124 | self.setLineMode(rest) 125 | 126 | class EventProtocol(EventSocket): 127 | def __init__(self): 128 | EventSocket.__init__(self) 129 | self._queue_ = Queue() 130 | 131 | # content type dispatcher 132 | self._content_ = { 133 | 'auth/request': self.authRequest, 134 | 'api/response': self.apiResponse, 135 | 'command/reply': self.commandReply, 136 | 'text/event-plain': self.eventPlain, 137 | 'text/disconnect-notice': self.disconnectNotice, 138 | } 139 | 140 | # plain event name dispatcher 141 | self._events_ = { 142 | 'CUSTOM': self.onCustom, 143 | 'CHANNEL_CREATE': self.onChannelCreate, 144 | 'CHANNEL_DESTROY': self.onChannelDestroy, 145 | 'CHANNEL_STATE': self.onChannelState, 146 | 'CHANNEL_ANSWER': self.onChannelAnswer, 147 | 'CHANNEL_HANGUP': self.onChannelHangup, 148 | 'CHANNEL_EXECUTE': self.onChannelExecute, 149 | 'CHANNEL_EXECUTE_COMPLETE': self.onChannelExecuteComplete, 150 | 'CHANNEL_BRIDGE': self.onChannelBridge, 151 | 'CHANNEL_UNBRIDGE': self.onChannelUnbridge, 152 | 'CHANNEL_PROGRESS': self.onChannelProgress, 153 | 'CHANNEL_PROGRESS_MEDIA': self.onChannelProgressMedia, 154 | 'CHANNEL_OUTGOING': self.onChannelOutgoing, 155 | 'CHANNEL_PARK': self.onChannelPark, 156 | 'CHANNEL_UNPARK': self.onChannelUnpark, 157 | 'CHANNEL_APPLICATION': self.onChannelApplication, 158 | 'CHANNEL_ORIGINATE': self.onChannelOriginate, 159 | 'CHANNEL_UUID': self.onChannelUuid, 160 | 'API': self.onApi, 161 | 'LOG': self.onLog, 162 | 'INBOUND_CHAN': self.onInboundChannel, 163 | 'OUTBOUND_CHAN': self.onOutboundChannel, 164 | 'STARTUP': self.onStartup, 165 | 'SHUTDOWN': self.onShutdown, 166 | 'PUBLISH': self.onPublish, 167 | 'UNPUBLISH': self.onUnpublish, 168 | 'TALK': self.onTalk, 169 | 'NOTALK': self.onNotalk, 170 | 'SESSION_CRASH': self.onSessionCrash, 171 | 'MODULE_LOAD': self.onModuleLoad, 172 | 'MODULE_UNLOAD': self.onModuleUnload, 173 | 'DTMF': self.onDtmf, 174 | 'MESSAGE': self.onMessage, 175 | 'PRESENCE_IN': self.onPresenceIn, 176 | 'NOTIFY_IN': self.onNotifyIn, 177 | 'PRESENCE_OUT': self.onPresenceOut, 178 | 'PRESENCE_PROBE': self.onPresenceProbe, 179 | 'MESSAGE_WAITING': self.onMessageWaiting, 180 | 'MESSAGE_QUERY': self.onMessageQuery, 181 | 'ROSTER': self.onRoster, 182 | 'CODEC': self.onCodec, 183 | 'BACKGROUND_JOB': self.onBackgroundJob, 184 | 'DETECTED_SPEECH': self.onDetectSpeech, 185 | 'DETECTED_TONE': self.onDetectTone, 186 | 'PRIVATE_COMMAND': self.onPrivateCommand, 187 | 'HEARTBEAT': self.onHeartbeat, 188 | 'TRAP': self.onTrap, 189 | 'ADD_SCHEDULE': self.onAddSchedule, 190 | 'DEL_SCHEDULE': self.onDelSchedule, 191 | 'EXE_SCHEDULE': self.onExeSchedule, 192 | 'RE_SCHEDULE': self.onReSchedule, 193 | 'RELOADXML': self.onReloadxml, 194 | 'NOTIFY': self.onNotify, 195 | 'SEND_MESSAGE': self.onSendMessage, 196 | 'RECV_MESSAGE': self.onRecvMessage, 197 | 'REQUEST_PARAMS': self.onRequestParams, 198 | 'CHANNEL_DATA': self.onChannelData, 199 | 'GENERAL': self.onGeneral, 200 | 'COMMAND': self.onCommand, 201 | 'SESSION_HEARTBEAT': self.onSessionHeartbeat, 202 | 'CLIENT_DISCONNECTED': self.onClientDisconnected, 203 | 'SERVER_DISCONNECTED': self.onServerDisconnected, 204 | 'ALL': self.onAll 205 | } 206 | 207 | # successful command reply by type 208 | self._commands_ = { 209 | 'auth': '+OK accepted', 210 | 'bgapi': '+OK Job-UUID', 211 | 'eventplain': '+OK event', 212 | 'exit': '+OK bye', 213 | 'connect': '+OK', 214 | 'myevents': '+OK Events Enabled', 215 | 'answer': '+OK', 216 | 'bridge': '+OK', 217 | 'playback': '+OK', 218 | 'hangup': '+OK', 219 | 'sched_api': '+OK', 220 | 'ring_ready': '+OK', 221 | 'record_session': '+OK', 222 | 'wait_for_silence': '+OK', 223 | 'sleep': '+OK', 224 | 'vmd': '+OK', 225 | 'set': '+OK', 226 | 'play_fsv': '+OK', 227 | 'record_fsv': '+OK', 228 | } 229 | 230 | # callbacks by content type 231 | def authSuccess(self, ev): pass 232 | def authFailure(self, failure): pass # self.factory.reconnect = False 233 | def bgapiSuccess(self, ev): pass 234 | def bgapiFailure(self, failure): pass 235 | def eventplainSuccess(self, ev): pass 236 | def eventplainFailure(self, failure): pass 237 | def myeventsSuccess(self, ev): pass 238 | def myeventsFailure(self, failure): pass 239 | def exitSuccess(self, ev): pass 240 | def exitFailure(self, failure): pass 241 | def connectSuccess(self, ev): pass 242 | def connectFailure(self, failure): pass 243 | def answerSuccess(self, ev): pass 244 | def answerFailure(self, failure): pass 245 | def bridgeSuccess(self, ev): pass 246 | def bridgeFailure(self, failure): pass 247 | def playbackSuccess(self, ev): pass 248 | def playbackFailure(self, failure): pass 249 | def hangupSuccess(self, ev): pass 250 | def hangupFailure(self, failure): pass 251 | def sched_apiSuccess(self, ev): pass 252 | def sched_apiFailure(self, failure): pass 253 | def ring_readySuccess(self, ev): pass 254 | def ring_readyFailure(self, failure): pass 255 | def record_sessionSuccess(self, ev): pass 256 | def record_sessionFailure(self, failure): pass 257 | def wait_for_silenceSuccess(self, ev): pass 258 | def wait_for_silenceFailure(self, failure): pass 259 | def sleepSuccess(self, ev): pass 260 | def sleepFailure(self, ev): pass 261 | def vmdSuccess(self, ev): pass 262 | def vmdFailure(self, failure): pass 263 | def setSuccess(self, ev): pass 264 | def setFailure(self, failure): pass 265 | def apiSuccess(self, ev): pass 266 | def apiFailure(self, failure): pass 267 | def play_fsvSuccess(self, ev): pass 268 | def play_fsvFailure(self, failure): pass 269 | def record_fsvSuccess(self, ev): pass 270 | def record_fsvFailure(self, failure): pass 271 | 272 | # callbacks by event name (plain) 273 | def onCustom(self, data): pass 274 | def onChannelCreate(self, data): pass 275 | def onChannelDestroy(self, data): pass 276 | def onChannelState(self, data): pass 277 | def onChannelAnswer(self, data): pass 278 | def onChannelHangup(self, data): pass 279 | def onChannelExecute(self, data): pass 280 | def onChannelExecuteComplete(self, data): pass 281 | def onChannelBridge(self, data): pass 282 | def onChannelUnbridge(self, data): pass 283 | def onChannelProgress(self, data): pass 284 | def onChannelProgressMedia(self, data): pass 285 | def onChannelOutgoing(self, data): pass 286 | def onChannelPark(self, data): pass 287 | def onChannelUnpark(self, data): pass 288 | def onChannelApplication(self, data): pass 289 | def onChannelOriginate(self, data): pass 290 | def onChannelUuid(self, data): pass 291 | def onApi(self, data): pass 292 | def onLog(self, data): pass 293 | def onInboundChannel(self, data): pass 294 | def onOutboundChannel(self, data): pass 295 | def onStartup(self, data): pass 296 | def onShutdown(self, data): pass 297 | def onPublish(self, data): pass 298 | def onUnpublish(self, data): pass 299 | def onTalk(self, data): pass 300 | def onNotalk(self, data): pass 301 | def onSessionCrash(self, data): pass 302 | def onModuleLoad(self, data): pass 303 | def onModuleUnload(self, data): pass 304 | def onDtmf(self, data): pass 305 | def onMessage(self, data): pass 306 | def onPresenceIn(self, data): pass 307 | def onNotifyIn(self, data): pass 308 | def onPresenceOut(self, data): pass 309 | def onPresenceProbe(self, data): pass 310 | def onMessageWaiting(self, data): pass 311 | def onMessageQuery(self, data): pass 312 | def onRoster(self, data): pass 313 | def onCodec(self, data): pass 314 | def onBackgroundJob(self, data): pass 315 | def onDetectSpeech(self, data): pass 316 | def onDetectTone(self, data): pass 317 | def onPrivateCommand(self, data): pass 318 | def onHeartbeat(self, data): pass 319 | def onTrap(self, data): pass 320 | def onAddSchedule(self, data): pass 321 | def onDelSchedule(self, data): pass 322 | def onExeSchedule(self, data): pass 323 | def onReSchedule(self, data): pass 324 | def onReloadxml(self, data): pass 325 | def onNotify(self, data): pass 326 | def onSendMessage(self, data): pass 327 | def onRecvMessage(self, data): pass 328 | def onRequestParams(self, data): pass 329 | def onChannelData(self, data): pass 330 | def onGeneral(self, data): pass 331 | def onCommand(self, data): pass 332 | def onSessionHeartbeat(self, data): pass 333 | def onClientDisconnected(self, data): pass 334 | def onServerDisconnected(self, data): pass 335 | def onAll(self, data): pass 336 | def onUnknownEvent(self, data): pass 337 | 338 | def _defer_(self, name): 339 | deferred = defer.Deferred() 340 | deferred.addCallback(getattr(self, '%sSuccess' % name)) 341 | deferred.addErrback(getattr(self, '%sFailure' % name)) 342 | self._queue_.put((name, deferred)) 343 | return deferred 344 | 345 | def _exec_(self, name, args=''): 346 | deferred = self._defer_(name) 347 | self.send('%s %s' % (name, args)) 348 | return deferred 349 | 350 | def _execmsg_(self, name, args=None, uuid='', lock=False): 351 | deferred = self._defer_(name) 352 | self.sendmsg(name, args, uuid, lock) 353 | return deferred 354 | 355 | def api(self, text): self._exec_('api', text) 356 | def bgapi(self, text): self._exec_('bgapi', text) 357 | def exit(self): self._exec_('exit') 358 | def eventplain(self, text): self._exec_('eventplain', text) 359 | def auth(self, text): deferred = self._exec_('auth', text) # inbound only 360 | def connect(self): self._exec_('connect') # everything below is outbound only 361 | def myevents(self): self._exec_('myevents') 362 | def answer(self): self._execmsg_('answer', lock=True) 363 | def bridge(self, text): self._execmsg_('bridge', text, lock=True) 364 | def hangup(self): self._execmsg_('hangup', lock=True) 365 | def sched_api(self, text): self._execmsg_('sched_api', text, lock=True) 366 | def ring_ready(self): self._execmsg_('ring_ready') 367 | def record_session(self, text): self._execmsg_('record_session', text, lock=True) 368 | def wait_for_silence(self, text): self._execmsg_('wait_for_silence', text, lock=True) 369 | def sleep(self, text): self._execmsg_('sleep', text, lock=True) 370 | def vmd(self, text): self._execmsg_('vmd', text, lock=True) 371 | def set(self, text): self._execmsg_('set', text, lock=True) 372 | def play_fsv(self, text): self._execmsg_('play_fsv', text, lock=True) 373 | def record_fsv(self, text): self._execmsg_('record_fsv', text, lock=True) 374 | def playback(self, text, terminators=None): 375 | self.set('playback_terminators=%s' % terminators or 'none') 376 | self._execmsg_('playback', text, lock=True) 377 | 378 | def eventHandler(self, ctx): 379 | if self.debug_enabled: debug('GOT EVENT: %s\n' % repr(ctx)) 380 | method = self._content_.get(ctx.get('Content_Type'), self.eventUnknown) 381 | return method(ctx) 382 | #try: method(ctx) 383 | #except Exception, e: 384 | # debug('[eventsocket] cannot dispatch (content) event: "%s"' % repr(e)) 385 | 386 | def authRequest(self, ctx): 387 | self.auth(self.factory.password) 388 | 389 | def disconnectNotice(self, ctx): 390 | pass 391 | 392 | def apiResponse(self, ctx): 393 | cmd, deferred = self._queue_.get() 394 | if cmd == 'api': deferred.callback(ctx) 395 | elif self.debug_enabled: debug('[eventsocket] apiResponse on "%s": out of sync?' % cmd) 396 | 397 | def commandReply(self, ctx): 398 | cmd, deferred = self._queue_.get() 399 | if self._commands_.has_key(cmd): 400 | if ctx.Reply_Text.startswith(self._commands_.get(cmd)): 401 | deferred.callback(ctx) 402 | else: 403 | deferred.errback(ctx) 404 | 405 | def eventPlain(self, ctx): 406 | name = ctx.data.get('Event_Name') 407 | method = self._events_.has_key(name) and \ 408 | self._events_[name] or self.onUnknownEvent 409 | return method(ctx.data) 410 | #try: method(ctx.data) 411 | #except Exception, e: 412 | # debug('[eventsocket] cannot dispatch (plain) event: %s, "%s"' % (name, repr(e))) 413 | 414 | def eventUnknown(self, ctx): 415 | pass 416 | -------------------------------------------------------------------------------- /eventsocket/kuku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from debug import debug 5 | 6 | from eventsocket import EventProtocol, superdict 7 | from twisted.internet import reactor, protocol 8 | from twisted.web import xmlrpc, server 9 | from Queue import Queue 10 | import pickle 11 | import pprint 12 | 13 | # Connect Django ORM 14 | from django.core.management import setup_environ 15 | import settings 16 | setup_environ(settings) 17 | from conference.models import * 18 | 19 | port = 1905 20 | unique_id = {} 21 | 22 | conferences = {} 23 | channels = {} 24 | 25 | class InboundProxy(EventProtocol): 26 | def __init__(self): 27 | self.job_uuid = {} 28 | self.conf_msgs = Queue() 29 | self.api_type = Queue() 30 | EventProtocol.__init__(self) 31 | 32 | def queue_api(self,command,type): 33 | print "Queue ",type 34 | self.api_type.put(type) 35 | self.api(command) 36 | 37 | def authSuccess(self, ev): 38 | self.queue_api("show channels",{"name":"show","subtype":"channels"}) 39 | self.eventplain('CHANNEL_CREATE CHANNEL_DESTROY CUSTOM conference::maintenance conference::dtmf') 40 | 41 | def authFailure(self, failure): 42 | self.factory.reconnect = False 43 | 44 | def eventplainFailure(self, failure): 45 | self.factory.reconnect = False 46 | self.exit() 47 | 48 | def onChannelCreate(self,data): 49 | pprint.pprint(data) 50 | channels[data.Unique_ID] = data 51 | print channels 52 | self.queue_api("uuid_dump %s"%data.Unique_ID,{"name":"uuid_dump"}) 53 | 54 | def onChannelDestroy(self,data): 55 | pprint.pprint(data) 56 | del channels[data.Unique_ID] 57 | print channels 58 | 59 | def apiFailure(self, ev): 60 | print "Error!", ev 61 | def apiSuccess(self, ev): 62 | # Update channel data with uuid_dump result 63 | type = self.api_type.get() 64 | raw = ev['data']['rawresponse'] 65 | if '-ERR' == raw[:4]: 66 | print 'ERROR!' 67 | return 68 | 69 | print "Type:",type,raw 70 | if type["name"] == "uuid_dump": 71 | temp = [line.strip().split(': ',1) for line in raw.strip().split('\n') if line] 72 | apidata = superdict([(line[0].replace('-','_'),line[1]) for line in temp]) 73 | if apidata.Unique_ID in channels: 74 | channels[apidata.Unique_ID].update(apidata) 75 | else: 76 | channels[apidata.Unique_ID] = apidata 77 | channel = channels[apidata.Unique_ID] 78 | kuku = channel.get('variable_kuku',None) 79 | print "Channel ",channel.Unique_ID,kuku 80 | 81 | elif type["name"] == "show": 82 | lines = raw.split("\n\n")[0].strip().splitlines() 83 | headers = [header.strip() for header in lines[0].split(',')] 84 | response = [superdict(zip(headers,[e.strip() for e in line.split(',')])) for line in lines[1:]] 85 | self.process_api_response(type,response) 86 | 87 | def process_api_response(self,type,response): 88 | print response 89 | if type["subtype"] == "channels": 90 | for r in response: 91 | self.queue_api("uuid_dump %s"%r.uuid,{"name":"uuid_dump"}) 92 | 93 | def onCustom(self, data): 94 | #pprint.pprint(data) 95 | channels[data.Unique_ID].update(data) 96 | channel = channels[data.Unique_ID] 97 | 98 | if data.Event_Subclass == 'conference::dtmf': 99 | print data.conference 100 | elif data.Event_Subclass == 'conference::maintenance': 101 | print data.Action 102 | #self.conf_msgs.put({'data':data,'context':'conference'}) 103 | #self.api("uuid_dump %s"%data.Unique_ID) 104 | if data.Action == 'add-member': 105 | if not data.Conference_Name in conferences: 106 | conferences[data.Conference_Name] = {} 107 | conferences[data.Conference_Name][channel.Unique_ID]=channel 108 | pprint.pprint(conferences[data.Conference_Name]) 109 | elif data.Action == 'del-member': 110 | if data.Conference_Name in conferences and channel.Unique_ID in conferences[data.Conference_Name]: 111 | del conferences[data.Conference_Name][channel.Unique_ID] 112 | if not conferences[data.Conference_Name]: 113 | del conferences[data.Conference_Name] 114 | print channels 115 | elif data.Action == 'start-talking': 116 | print "start talk" 117 | elif data.Action == 'stop-talking': 118 | print "stop talk" 119 | elif data.Action == 'mute-member': 120 | print "mute" 121 | elif data.Action == 'unmute-member': 122 | print "unmute" 123 | 124 | class InboundFactory(protocol.ClientFactory): 125 | protocol = InboundProxy 126 | 127 | def __init__(self, password): 128 | self.password = password 129 | self.reconnect = True 130 | 131 | def clientConnectionLost(self, connector, reason): 132 | if self.reconnect: connector.connect() 133 | else: 134 | print '[inboundfactory] stopping reactor' 135 | reactor.stop() 136 | 137 | def clientConnectionFailed(self, connector, reason): 138 | print '[inboundfactoy] cannot connect: %s' % reason 139 | reactor.stop() 140 | 141 | class XMLRPCInterface(xmlrpc.XMLRPC): 142 | def xmlrpc_info(self): 143 | print conferences 144 | return pickle.dumps(conferences) 145 | 146 | if __name__ == '__main__': 147 | reactor.connectTCP('localhost', 8021, InboundFactory('ClueCon')) 148 | xmlrpc_interface = XMLRPCInterface() 149 | reactor.listenTCP(7080,server.Site(xmlrpc_interface)) 150 | reactor.run() 151 | -------------------------------------------------------------------------------- /fs2web.wsgi: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.dirname(__file__) + '/../') 5 | sys.path.append(os.path.dirname(__file__) + '/') 6 | os.environ['DJANGO_SETTINGS_MODULE'] = 'fs2web.settings' 7 | 8 | import django.core.handlers.wsgi 9 | application = django.core.handlers.wsgi.WSGIHandler() 10 | 11 | -------------------------------------------------------------------------------- /fsapi.py: -------------------------------------------------------------------------------- 1 | from xmlrpclib import ServerProxy 2 | from django.conf import settings 3 | #from django.views.decorators.cache import cache 4 | import syslog 5 | 6 | def fsapi(*args,**kwargs): 7 | #TODO caching 8 | server = ServerProxy(settings.FS_CONNECT_STR) 9 | syslog.syslog(str(args)+"; "+str(kwargs)) 10 | return server.freeswitch.api(*args,**kwargs) 11 | 12 | def call_from_conference(conf,number,conf_cid=settings.CONFERENCE_CID,vars=''): 13 | #fsapi("bgapi","{%s}conference %s@default dial "%(vars,conf) + settings.DIALTEMPLATE%number+" %s %s"%(conf_cid,number)) 14 | fsapi("bgapi","originate {%s}"%vars+settings.DIALTEMPLATE%number+" &conference(%s@default)"%conf) 15 | 16 | -------------------------------------------------------------------------------- /local_settings.py.template: -------------------------------------------------------------------------------- 1 | #DIALTEMPLATE="user/%s@10.254.131.115" 2 | DIALTEMPLATE="sofia/gateway/asterisk/%s" 3 | CONFERENCE_CID="300" 4 | 5 | FS_CONNECT_STR = "http://freeswitch:works@127.0.0.1:8080" 6 | -------------------------------------------------------------------------------- /locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2009-06-15 13:03+0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: conference/models.py:10 conference/models.py:29 dialplan/models.py:7 20 | #: dialplan/models.py:15 dialplan/models.py:46 dialplan/models.py:65 21 | #: users/models.py:7 users/models.py:33 22 | msgid "Name" 23 | msgstr "Имя" 24 | 25 | #: conference/models.py:11 conference/models.py:30 26 | msgid "Number" 27 | msgstr "Номер" 28 | 29 | #: conference/models.py:12 users/models.py:17 30 | msgid "Password" 31 | msgstr "Пароль" 32 | 33 | #: conference/models.py:13 conference/models.py:41 34 | msgid "Phone" 35 | msgstr "Телефон" 36 | 37 | #: conference/models.py:14 38 | msgid "Is active?" 39 | msgstr "Активен?" 40 | 41 | #: conference/models.py:25 42 | msgid "Conference" 43 | msgstr "Конференция" 44 | 45 | #: conference/models.py:26 templates/base.html:30 templates/confrences.html:29 46 | #: templates/index.html:8 47 | msgid "Conferences" 48 | msgstr "Конференции" 49 | 50 | #: conference/models.py:31 51 | msgid "Auto call" 52 | msgstr "Автоматический вызов" 53 | 54 | #: conference/models.py:42 55 | msgid "Phones" 56 | msgstr "Телефоны" 57 | 58 | #: dialplan/models.py:11 users/models.py:18 59 | msgid "Context" 60 | msgstr "Контекст" 61 | 62 | #: dialplan/models.py:12 63 | msgid "Contexts" 64 | msgstr "Контексты" 65 | 66 | #: dialplan/models.py:16 67 | msgid "Continue on" 68 | msgstr "Продолжение в случае" 69 | 70 | #: dialplan/models.py:18 dialplan/models.py:37 dialplan/models.py:67 71 | msgid "Preference" 72 | msgstr "Порядок" 73 | 74 | #: dialplan/models.py:22 75 | msgid "Extension" 76 | msgstr "Модуль номерного плана" 77 | 78 | #: dialplan/models.py:23 79 | msgid "Extensions" 80 | msgstr "Модули номерного плана" 81 | 82 | #: dialplan/models.py:26 83 | msgid "on-false" 84 | msgstr "в случае ксли ложь" 85 | 86 | #: dialplan/models.py:27 87 | msgid "on-true" 88 | msgstr "в случае если правда" 89 | 90 | #: dialplan/models.py:28 91 | msgid "always" 92 | msgstr "в любой случае" 93 | 94 | #: dialplan/models.py:29 95 | msgid "never" 96 | msgstr "никогда" 97 | 98 | #: dialplan/models.py:33 99 | msgid "Field" 100 | msgstr "Критерий" 101 | 102 | #: dialplan/models.py:34 103 | msgid "Expression" 104 | msgstr "Выражение" 105 | 106 | #: dialplan/models.py:35 107 | msgid "Break on" 108 | msgstr "Прервать выполнение в случае" 109 | 110 | #: dialplan/models.py:42 111 | msgid "Condition" 112 | msgstr "Условие" 113 | 114 | #: dialplan/models.py:43 115 | msgid "Conditions" 116 | msgstr "Условия" 117 | 118 | #: dialplan/models.py:50 119 | msgid "Dialplan application" 120 | msgstr "Приложение норменого плана" 121 | 122 | #: dialplan/models.py:51 123 | msgid "Dialplan applications" 124 | msgstr "Приложения номерного плана" 125 | 126 | #: dialplan/models.py:66 127 | msgid "Anti action" 128 | msgstr "Анти-действие" 129 | 130 | #: dialplan/models.py:72 131 | msgid "Action" 132 | msgstr "Действие" 133 | 134 | #: dialplan/models.py:73 135 | msgid "Actions" 136 | msgstr "Действия" 137 | 138 | #: dialplan/templates/condition_inline.html:15 139 | msgid "Delete?" 140 | msgstr "Удалить?" 141 | 142 | #: dialplan/templates/condition_inline.html:16 143 | msgid "Edit" 144 | msgstr "Редактировать" 145 | 146 | #: dialplan/templates/condition_inline.html:28 147 | msgid "View on site" 148 | msgstr "Смотреть на сайте" 149 | 150 | #: dialplan/templates/dialplan.html:204 templates/base.html:31 151 | msgid "Edit dialplan" 152 | msgstr "Редактирование номерного плана" 153 | 154 | #: dialplan/templates/dialplan.html:204 155 | msgid "Click to save" 156 | msgstr "Нажмите для сохранения" 157 | 158 | #: dialplan/templates/dialplan.html:261 159 | msgid "Add action" 160 | msgstr "Добавить действие" 161 | 162 | #: dialplan/templates/dialplan.html:264 163 | msgid "Anti?" 164 | msgstr "Анти?" 165 | 166 | #: templates/base.html:29 167 | msgid "Main" 168 | msgstr "Главная" 169 | 170 | #: templates/confrences.html:15 171 | msgid "click to toggle" 172 | msgstr "нажмите чтобы скрыть/раскрыть" 173 | 174 | #: templates/confrences.html:30 175 | msgid "Renew" 176 | msgstr "Обновить" 177 | 178 | #: templates/confrences.html:33 179 | msgid "conference" 180 | msgstr "конференция" 181 | 182 | #: templates/confrences.html:36 183 | msgid "Invite phone" 184 | msgstr "Вызвать телефонный номер" 185 | 186 | #: templates/confrences.html:39 187 | msgid "caller name" 188 | msgstr "Имя вызывающего" 189 | 190 | #: templates/confrences.html:40 191 | msgid "caller number" 192 | msgstr "Номер вызывающего" 193 | 194 | #: templates/confrences.html:41 195 | msgid "member id" 196 | msgstr "ID участника" 197 | 198 | #: templates/confrences.html:42 199 | msgid "can hear" 200 | msgstr "Слышит" 201 | 202 | #: templates/confrences.html:43 203 | msgid "can speak" 204 | msgstr "Может говорить" 205 | 206 | #: templates/confrences.html:44 207 | msgid "talking" 208 | msgstr "Говорит" 209 | 210 | #: templates/confrences.html:45 211 | msgid "uuid" 212 | msgstr "UUID" 213 | 214 | #: templates/confrences.html:57 215 | msgid "kick" 216 | msgstr "Выгнать" 217 | 218 | #: templates/confrences.html:58 219 | msgid "mute" 220 | msgstr "Заглушить" 221 | 222 | #: templates/confrences.html:59 223 | msgid "unmute" 224 | msgstr "Вернуть голос" 225 | 226 | #: templates/confrences.html:61 227 | msgid "Reinvite" 228 | msgstr "Вызвать снова" 229 | 230 | #: templates/confrences.html:67 231 | msgid "Unactive conferences" 232 | msgstr "Неактивные конференции" 233 | 234 | #: templates/confrences.html:72 235 | msgid "Start" 236 | msgstr "Запустить" 237 | 238 | #: templates/confrences.html:77 239 | msgid "Update participants list" 240 | msgstr "Редактировать список участников" 241 | 242 | #: templates/confrences.html:81 243 | msgid "Update" 244 | msgstr "Сохранить" 245 | 246 | #: templates/confrences.html:83 247 | msgid "Delete conference" 248 | msgstr "Удалить конференцию" 249 | 250 | #: templates/confrences.html:88 251 | msgid "Create new conference" 252 | msgstr "Создать новую конференцию" 253 | 254 | #: templates/confrences.html:91 255 | msgid "Create" 256 | msgstr "Создать" 257 | 258 | #: templates/index.html:4 259 | msgid "Start page" 260 | msgstr "Стартовая страница" 261 | 262 | #: templates/index.html:7 263 | msgid "Admin interface" 264 | msgstr "Интерфейс администратора" 265 | 266 | #: templates/index.html:9 267 | msgid "Dialplan" 268 | msgstr "Номерной план" 269 | 270 | #: users/models.py:11 users/models.py:23 271 | msgid "Domain" 272 | msgstr "Домен" 273 | 274 | #: users/models.py:12 275 | msgid "Domains" 276 | msgstr "Домены" 277 | 278 | #: users/models.py:16 279 | msgid "User number" 280 | msgstr "Пользовательский номер" 281 | 282 | #: users/models.py:19 283 | msgid "Caller name" 284 | msgstr "Имя вызывающего" 285 | 286 | #: users/models.py:20 287 | msgid "Caller number" 288 | msgstr "Номер вызывающего" 289 | 290 | #: users/models.py:21 291 | msgid "Mailbox number" 292 | msgstr "Номер ящика голосовой почты" 293 | 294 | #: users/models.py:22 295 | msgid "Mailbox password" 296 | msgstr "Пароль от голосовой почты" 297 | 298 | #: users/models.py:24 users/models.py:59 299 | msgid "Group" 300 | msgstr "Группа" 301 | 302 | #: users/models.py:28 303 | msgid "User" 304 | msgstr "Пользователь" 305 | 306 | #: users/models.py:29 307 | msgid "Users" 308 | msgstr "Пользователи" 309 | 310 | #: users/models.py:34 311 | msgid "Param?" 312 | msgstr "Параметр?" 313 | 314 | #: users/models.py:38 users/models.py:42 users/models.py:48 315 | msgid "Variable" 316 | msgstr "Переменная" 317 | 318 | #: users/models.py:39 users/models.py:49 319 | msgid "Variables" 320 | msgstr "Переменные" 321 | 322 | #: users/models.py:43 323 | msgid "Value" 324 | msgstr "Значение" 325 | 326 | #: users/models.py:60 327 | msgid "Groups" 328 | msgstr "Группы" 329 | 330 | #~ msgid "Save" 331 | #~ msgstr "Сохранить" 332 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for fs2web project. 2 | from local_settings import * 3 | 4 | import os 5 | BP = os.path.realpath(os.path.curdir) + '/' 6 | BP = os.path.dirname(__file__) + '/' 7 | 8 | DEBUG = True 9 | TEMPLATE_DEBUG = DEBUG 10 | 11 | ADMINS = ( 12 | # ('Your Name', 'your_email@domain.com'), 13 | ) 14 | 15 | MANAGERS = ADMINS 16 | 17 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 18 | DATABASE_NAME = BP + 'base.sq3' # Or path to database file if using sqlite3. 19 | DATABASE_USER = '' # Not used with sqlite3. 20 | DATABASE_PASSWORD = '' # Not used with sqlite3. 21 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 22 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 23 | 24 | # Local time zone for this installation. Choices can be found here: 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # although not all choices may be available on all operating systems. 27 | # If running in a Windows environment this must be set to the same as your 28 | # system time zone. 29 | TIME_ZONE = 'Asia/Novosibirsk' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'ru-RU' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # Absolute path to the directory that holds media. 42 | # Example: "/home/media/media.lawrence.com/" 43 | MEDIA_ROOT = BP + 'data' 44 | 45 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 46 | # trailing slash if there is a path component (optional in other cases). 47 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 48 | MEDIA_URL = '' 49 | 50 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 51 | # trailing slash. 52 | # Examples: "http://foo.com/media/", "/media/". 53 | ADMIN_MEDIA_PREFIX = '/media/' 54 | 55 | # Make this unique, and don't share it with anybody. 56 | SECRET_KEY = 'frehx-stg#_gxuhoeou(2o5u^5t4cygt*(g9u^6+2v#_qp8$kf' 57 | 58 | # List of callables that know how to import templates from various sources. 59 | TEMPLATE_LOADERS = ( 60 | 'django.template.loaders.filesystem.load_template_source', 61 | 'django.template.loaders.app_directories.load_template_source', 62 | # 'django.template.loaders.eggs.load_template_source', 63 | ) 64 | 65 | MIDDLEWARE_CLASSES = ( 66 | 'django.middleware.common.CommonMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 69 | 'django.middleware.doc.XViewMiddleware', 70 | ) 71 | 72 | ROOT_URLCONF = 'fs2web.urls' 73 | 74 | TEMPLATE_DIRS = ( 75 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 76 | # Always use forward slashes, even on Windows. 77 | # Don't forget to use absolute paths, not relative paths. 78 | BP + 'templates', 79 | ) 80 | 81 | INSTALLED_APPS = ( 82 | 'django.contrib.auth', 83 | 'django.contrib.contenttypes', 84 | 'django.contrib.sessions', 85 | 'django.contrib.sites', 86 | 'django.contrib.admin', 87 | 'users', 88 | 'conference', 89 | 'dialplan', 90 | ) 91 | 92 | -------------------------------------------------------------------------------- /superdict.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | class superdict(dict): 4 | """Translates dictionary keys to instance attributes""" 5 | def __setattr__(self, k, v): 6 | dict.__setitem__(self, k, v) 7 | 8 | def __delattr__(self, k): 9 | dict.__delitem__(self, k) 10 | 11 | def __getattribute__(self, k): 12 | try: return dict.__getitem__(self, k) 13 | except KeyError: return dict.__getattribute__(self, k) 14 | 15 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 23 | {% block head %} 24 | {% endblock %} 25 | 26 | 27 |

28 | 33 |
34 | {% block contents %} 35 | {% endblock %} 36 | {% block admin_contents %} 37 | {% endblock %} 38 |
39 | 40 | -------------------------------------------------------------------------------- /templates/confrences.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block head %} 4 | 19 | 20 | 28 | {% endblock %} 29 | {% block contents %} 30 |

{% trans "Conferences" %}

31 | {% trans "Renew" %} 32 | {% for c in confs %} 33 |

34 | {% trans "conference" %} {{ c.0 }}

35 |
36 |

{{ c.2.conference }}{{ c.2.phone }} 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% for m in c.1 %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | {% if not m.uuid %}{% endif %} 59 | 60 | 61 | 62 | {% endfor %} 63 |
{% trans "caller name" %}{% trans "caller number" %}{% trans "can hear" %}{% trans "can speak" %}{% trans "talking" %}
{{ m.caller_id_name }}{{ m.caller_id_number }}{{ m.flags.can_hear }}{{ m.flags.can_speak }}{{ m.flags.talking }}{{ m.member }}X
55 | M
56 | m
58 | {% trans "Reinvite" %}{{ m.id }}{{ m.uuid }}
64 |
65 | {% endfor %} 66 |

{% trans "Unactive conferences" %}

67 |
68 | {% for conf in object_list %} 69 |
70 |

{{ conf }} 71 | {% trans "Start" %} 72 |

73 | {% for phone in conf.participants.all %} 74 | {{ phone }}{% if not forloop.last %}, {% endif %} 75 | {% endfor %} 76 |

{% trans "Update participants list" %}

77 |
78 |
79 |
{{ conf.participants_form.participants }}
80 | 81 |
82 | {% trans "Delete conference" %} 83 |
84 |
85 | {% endfor %} 86 |
87 |

{% trans "Create new conference" %}

88 |
89 | {{ addconf.as_p }} 90 | 91 |
92 |
93 |
94 | {% endblock %} 95 | -------------------------------------------------------------------------------- /templates/group.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for user in group.users.all %} 12 | 13 | {% endfor %} 14 | 15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block contents %} 4 |

{% trans "Start page" %}

5 | 6 |
    7 |
  • {% trans "Admin interface" %}
  • 8 |
  • {% trans "Conferences" %}
  • 9 |
  • {% trans "Dialplan" %}
  • 10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/user.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% if user.fsuvariable_set %} 16 | {% for i in user.fsuvariable_set.all %} 17 | {% if i.is_param %} 18 | 19 | {% endif %} 20 | {% endfor %} 21 | {% endif %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% if user.fsuvariable_set %} 29 | {% for i in user.fsuvariable_set.all %} 30 | {% if i.is_param %} 31 | {% else %} 32 | 33 | {% endif %} 34 | {% endfor %} 35 | {% endif %} 36 | 37 | 38 | {% for gw in user.fsgateway_set.all %} 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% if gw.from_user %} 46 | 47 | {% endif %} 48 | 49 | {% if gw.from_domain %} 50 | 51 | {% endif %} 52 | 53 | 54 | 55 | 56 | {% if gw.extension %} 57 | 58 | 59 | {% endif %} 60 | {% if gw.proxy %} 61 | 62 | 63 | {% endif %} 64 | {% if gw.register_proxy %} 65 | 66 | 67 | {% endif %} 68 | {% if gw.expire_seconds %} 69 | 70 | 71 | {% endif %} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {% if gw.ping %} 81 | 82 | 83 | {% endif %} 84 | 85 | {% endfor %} 86 | 87 | 88 | 89 | 90 | 91 | 92 |
    93 |
    94 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.views.generic import list_detail 3 | from django.views.generic import simple 4 | 5 | #from users.views 6 | 7 | # Uncomment the next two lines to enable the admin: 8 | from django.contrib import admin 9 | admin.autodiscover() 10 | 11 | urlpatterns = patterns('', 12 | # Example: 13 | (r'^user/', include('users.urls')), 14 | (r'^dialplan/', include('dialplan.urls')), 15 | (r'^confs/', include('conference.urls')), 16 | 17 | (r'^$',simple.direct_to_template,{'template': 'index.html'}), 18 | # Uncomment the next line to enable admin documentation: 19 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 20 | (r'^bin/(?P.*)$', 'django.views.static.serve', 21 | {'document_root': 'bin/'}), 22 | 23 | # Uncomment the next line for to enable the admin: 24 | (r'^admin/(.*)', admin.site.root), 25 | ) 26 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deepwalker/fs2web/a8807ae30d02d4a11e47fdbf9a9948a1bea8c1d4/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from models import * 3 | 4 | class VarInline(admin.TabularInline): 5 | model = FSUVariable 6 | extra = 3 7 | 8 | class FSUAdmin(admin.ModelAdmin): 9 | inlines = [VarInline] 10 | 11 | 12 | admin.site.register(Variable) 13 | admin.site.register(FSUser,FSUAdmin) 14 | admin.site.register(FSGroup) 15 | admin.site.register(FSDomain) 16 | 17 | admin.site.register(FSGateway) 18 | -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models as m 2 | from django.utils.translation import ugettext_lazy as _ 3 | from dialplan.models import Context 4 | 5 | # Create your models here. 6 | class FSDomain(m.Model): 7 | name = m.CharField(_(u"Name"),max_length=255) 8 | def __unicode__(self): 9 | return self.name 10 | class Meta: 11 | verbose_name = _(u"Domain") 12 | verbose_name_plural = _(u"Domains") 13 | 14 | class FSUser(m.Model): 15 | "FreeSWITCH user" 16 | uid = m.CharField(_(u"User number"),max_length=255) 17 | password = m.CharField(_(u"Password"),max_length=255) 18 | user_context = m.ForeignKey(Context,verbose_name=_(u"Context")) 19 | effective_caller_id_name = m.CharField(_(u"Caller name"),max_length=255) 20 | effective_caller_id_number = m.CharField(_(u"Caller number"),max_length=255) 21 | mailbox = m.CharField(_(u"Mailbox number"),max_length=255) 22 | mailbox_pwd = m.CharField(_(u"Mailbox password"),max_length=255) 23 | domain = m.ForeignKey(FSDomain,verbose_name=_(u"Domain")) 24 | group = m.ForeignKey("FSGroup",verbose_name=_(u"Group")) 25 | def __unicode__(self): 26 | return "%s (%s)"%(self.uid,self.user_context.name) 27 | class Meta: 28 | verbose_name = _(u"User") 29 | verbose_name_plural = _(u"Users") 30 | 31 | class Variable(m.Model): 32 | "Variable, in fact - type of variable" 33 | name = m.CharField(_(u"Name"),max_length=255) 34 | is_param = m.BooleanField(_(u"Param?")) 35 | def __unicode__(self): 36 | return self.name+' ('+['var','param'][int(self.is_param)]+')' 37 | class Meta: 38 | verbose_name = _(u"Variable") 39 | verbose_name_plural = _(u"Variables") 40 | 41 | class FSUVariable(m.Model): 42 | variable = m.ForeignKey(Variable,verbose_name=_(u"Variable")) 43 | value = m.CharField(_(u"Value"),max_length=255) 44 | user = m.ForeignKey(FSUser) 45 | def __unicode__(self): 46 | return self.variable.name+' = '+self.value 47 | class Meta: 48 | verbose_name = _(u"Variable") 49 | verbose_name_plural = _(u"Variables") 50 | 51 | class FSGroup(m.Model): 52 | "Group of FSUsers" 53 | name = m.CharField(max_length=255) 54 | users = m.ManyToManyField(FSUser,blank=True) 55 | domain = m.ForeignKey(FSDomain) 56 | def __unicode__(self): 57 | return self.name 58 | class Meta: 59 | verbose_name = _(u"Group") 60 | verbose_name_plural = _(u"Groups") 61 | 62 | TRANSPORT = ( 63 | ("udp","udp"), 64 | ("tcp","tcp"), 65 | ) 66 | class FSGateway(m.Model): 67 | "Gateway model" 68 | name = m.CharField(max_length=255) 69 | username = m.CharField(max_length=255) 70 | realm = m.CharField(max_length=255) 71 | from_user = m.CharField(max_length=255,blank=True,null=True) 72 | from_domain = m.CharField(max_length=255,blank=True,null=True) 73 | password = m.CharField(max_length=255) 74 | extension = m.CharField(max_length=255,blank=True,null=True) 75 | proxy = m.CharField(max_length=255,blank=True,null=True) 76 | register_proxy = m.CharField(max_length=255,blank=True,null=True) 77 | expire_seconds = m.IntegerField(blank=True,null=True) 78 | register = m.BooleanField(default=True) 79 | register_transport = m.CharField(max_length=4,choices=TRANSPORT) 80 | retry_seconds = m.IntegerField(default=10) 81 | caller_id_in_form = m.BooleanField(default=False) 82 | contact_params = m.CharField(max_length=255,blank=True,null=True) 83 | ping = m.IntegerField(blank=True,null=True) 84 | 85 | user = m.ForeignKey(FSUser,blank=True,null=True) 86 | domain = m.ForeignKey(FSDomain) 87 | def __unicode__(self): 88 | return self.name 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /users/templates/users/fsuser_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block contents %} 3 | User: {{ object }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /users/templates/users/fsuser_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block contents %} 3 |

    Edit user

    4 |
    5 |

    {{ form.forms.0.as_p }}

    6 |
    7 | {% for form in form.var_forms.forms %} 8 |
    9 |

    {{ form.as_p }}

    10 |
    11 | {% endfor %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.views.generic import list_detail 3 | 4 | from models import * 5 | 6 | urlpatterns = patterns('', 7 | (r'view/(?P\d+)/$',list_detail.object_detail,{'queryset':FSUser.objects.all()}), 8 | (r'edit/$','users.views.edit_user'), 9 | (r'edit/(?P\d+)/$','users.views.edit_user'), 10 | (r'get/$','users.views.get_user_info'), 11 | ) 12 | 13 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from models import * 3 | from django.shortcuts import render_to_response, get_object_or_404 4 | from django.http import HttpResponse 5 | from django.forms.models import modelformset_factory, inlineformset_factory 6 | 7 | def get_user_info(request): 8 | if len(request.GET): 9 | data = request.GET 10 | else: 11 | data = request.POST 12 | 13 | if data.get('action')=='group_call': 14 | return get_group(data) 15 | else: 16 | return get_user(data) 17 | 18 | def get_user(data): 19 | user = get_object_or_404(FSUser,uid=data.get('user'),domain__name=data['domain']) 20 | return render_to_response('user.xml',{'user':user,'data':data}) 21 | 22 | def get_group(data): 23 | group = get_object_or_404(FSGroup,name=data.get('group')) 24 | return render_to_response('group.xml',{'group':group,'data':data}) 25 | 26 | def get_gates(data): 27 | gtws = get_object_or_404() 28 | return render_to_response('gateway.xml',{'gateways':gtws,'data':data}) 29 | 30 | # Edit 31 | def edit_user(request,object_id=None): 32 | FSUserFormSet = modelformset_factory(FSUser) 33 | FSUVarFormSet = inlineformset_factory(FSUser,FSUVariable) 34 | if request.method == 'POST': # If the form has been submitted... 35 | form = FSUserFormSet(request.POST) # A form bound to the POST data 36 | if form.is_valid(): # All validation rules pass 37 | return HttpResponseRedirect('/users/get/%i/'%object_id) # Redirect after POST 38 | else: 39 | form = FSUserFormSet() 40 | form.var_forms = FSUVarFormSet() 41 | 42 | return render_to_response('users/fsuser_edit.html', { 43 | 'form': form, 44 | 'object_id':object_id, 45 | }) 46 | 47 | --------------------------------------------------------------------------------