From 16b37f7a1f9ff8e559e90261c8f6cd3da46dae49 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Fri, 12 Mar 2010 21:13:39 +0100 Subject: Tag search works fully but looks awfull --- .../src/etherpad/control/tag/tag_control.js | 231 +++++++++++++++++++++ trunk/etherpad/src/etherpad/pad/model.js | 2 +- trunk/etherpad/src/main.js | 2 + trunk/etherpad/src/static/js/pad_utils.js | 24 +++ trunk/etherpad/src/templates/misc/pad_default.ejs | 3 + trunk/etherpad/src/templates/tag/tag_search.ejs | 109 ++++++++++ .../framework-src/modules/sqlbase/sqlobj.js | 50 ++++- 7 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 trunk/etherpad/src/etherpad/control/tag/tag_control.js create mode 100644 trunk/etherpad/src/templates/tag/tag_search.ejs diff --git a/trunk/etherpad/src/etherpad/control/tag/tag_control.js b/trunk/etherpad/src/etherpad/control/tag/tag_control.js new file mode 100644 index 0000000..e56ee8e --- /dev/null +++ b/trunk/etherpad/src/etherpad/control/tag/tag_control.js @@ -0,0 +1,231 @@ +/** + * Copyright 2009 RedHog, Egil Möller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import("faststatic"); +import("dispatch.{Dispatcher,PrefixMatcher,forward}"); + +import("etherpad.utils.*"); +import("etherpad.globals.*"); +import("etherpad.log"); +import("etherpad.pad.padusers"); +import("etherpad.pro.pro_utils"); +import("etherpad.helpers"); +import("etherpad.pro.pro_accounts.getSessionProAccount"); +import("sqlbase.sqlbase"); +import("sqlbase.sqlcommon"); +import("sqlbase.sqlobj"); + +function tagsToQuery(tags, antiTags) { + var prefixed = []; + for (i = 0; i < antiTags.length; i++) + prefixed[i] = '!' + antiTags[i]; + return tags.concat(prefixed).join(','); +} + +function stringFormat(text, obj) { + var name; + for (name in obj) { + //iterate through the params and replace their placeholders from the original text + text = text.replace(new RegExp('%\\(' + name + '\\)s', 'gi' ), obj[name]); + } + return text; +} + +function getQueryToSql(tags, antiTags, querySql) { + var queryTable; + var queryParams; + + if (querySql == null) { + queryTable = 'PAD_META'; + queryParams = []; + } else { + queryTable = querySql.sql; + queryParams = querySql.params; + } + + var exceptArray = []; + var joinArray = []; + var exceptParamArray = []; + var joinParamArray = []; + + var info = new Object(); + info.queryTable = queryTable; + info.n = 0; + var i; + + for (i = 0; i < antiTags.length; i++) { + tag = antiTags[i]; + exceptArray.push(stringFormat('' + + 'except ' + + ' select ' + + ' pt%(n)s.PAD_ID ' + + ' from ' + + ' PAD_TAG as pt%(n)s, ' + + ' TAG as t%(n)s ' + + ' where ' + + ' t%(n)s.ID = pt%(n)s.TAG_ID ' + + ' and t%(n)s.NAME = ? ' + + '', info)); + exceptParamArray.push(tag); + info.n += 1; + } + for (i = 0; i < tags.length; i++) { + tag = tags[i]; + joinArray.push(stringFormat('' + + 'join PAD_TAG as pt%(n)s on ' + + ' pt%(n)s.PAD_ID = p.ID ' + + 'join TAG as t%(n)s on ' + + ' t%(n)s.ID = pt%(n)s.TAG_ID ' + + ' and t%(n)s.NAME = ? ' + + '', info)); + joinParamArray.push(tag); + info.n += 1; + } + + info["joins"] = joinArray.join(""); + info["excepts"] = exceptArray.join(""); + + return { + sql: stringFormat('' + + '(select ' + + ' p.ID ' + + ' from ' + + ' %(queryTable)s as p ' + + ' %(joins)s ' + + ' %(excepts)s ' + + ') ' + + '', info), + params: queryParams.concat(joinParamArray).concat(exceptParamArray)}; +} + +function nrSql(querySql) { + var queryTable; + var queryParams; + + if (querySql == null) { + queryTable = 'PAD_META'; + queryParams = []; + } else { + queryTable = querySql.sql; + queryParams = querySql.params; + } + + var info = []; + info['query_sql'] = queryTable + return { + sql: stringFormat('(select count(*) as total from %(query_sql)s as q)', info), + params: queryParams}; +} + +function newTagsSql(querySql) { + var queryTable; + var queryParams; + + if (querySql == null) { + queryTable = 'PAD_META'; + queryParams = []; + } else { + queryTable = querySql.sql; + queryParams = querySql.params; + } + + var info = []; + info["query_post_table"] = queryTable; + var queryNrSql = nrSql(querySql); + info["query_nr_sql"] = queryNrSql.sql; + queryNrParams = queryNrSql.params; + + return { + sql: stringFormat('' + + 'select ' + + ' t.NAME tagname, ' + + ' count(tp.PAD_ID) as matches, ' + + ' tn.total - count(tp.PAD_ID) as antimatches, ' + + ' abs(count(tp.PAD_ID) - (tn.total / 2)) as weight ' + + 'from ' + + ' TAG as t, ' + + ' PAD_TAG as tp, ' + + ' %(query_nr_sql)s as tn ' + + 'where ' + + ' tp.TAG_ID = t.ID ' + + ' and tp.PAD_ID in %(query_post_table)s ' + + 'group by t.NAME, tn.total ' + + 'having ' + + ' count(tp.PAD_ID) > 0 and count(tp.PAD_ID) < tn.total ' + + 'order by ' + + ' abs(count(tp.PAD_ID) - (tn.total / 2)) asc ' + + 'limit 10 ' + + '', info), + params: queryNrParams.concat(queryParams)}; +} + + +function onRequest() { + var tags = new Array(); + var antiTags = new Array(); + + if (request.params.query != undefined) { + var query = request.params.query.split(','); + for (i = 0; i < query.length; i++) + if (query[i][0] == '!') + antiTags.push(query[i].substring(1)); + else + tags.push(query[i]); + } + + var querySql = getQueryToSql(tags.concat(['public']), antiTags); + + var queryNewTagsSql = newTagsSql(querySql); + var newTags = sqlobj.executeRaw(queryNewTagsSql.sql, queryNewTagsSql.params); + + var matchingPads; + if (tags.length > 0 || antiTags.length > 0) { + var sql = "select p.ID from PAD_META as p, " + querySql.sql + " as q where p.ID = q.ID limit 10" + matchingPads = sqlobj.executeRaw(sql, querySql.params); + } else { + matchingPads = []; + } + + var isPro = pro_utils.isProDomainRequest(); + var userId = padusers.getUserId(); + + helpers.addClientVars({ + userAgent: request.headers["User-Agent"], + debugEnabled: request.params.djs, + clientIp: request.clientAddr, + colorPalette: COLOR_PALETTE, + serverTimestamp: +(new Date), + isProPad: isPro, + userIsGuest: padusers.isGuest(userId), + userId: userId, + }); + + var isProUser = (isPro && ! padusers.isGuest(userId)); + + renderHtml("tag/tag_search.ejs", + { + tagsToQuery: tagsToQuery, + tags: tags, + antiTags: antiTags, + newTags: newTags, + matchingPads: matchingPads, + bodyClass: 'nonpropad', + isPro: isPro, + isProAccountHolder: isProUser, + account: getSessionProAccount(), // may be falsy + }); + return true; +} diff --git a/trunk/etherpad/src/etherpad/pad/model.js b/trunk/etherpad/src/etherpad/pad/model.js index ea0d68d..c557a2c 100644 --- a/trunk/etherpad/src/etherpad/pad/model.js +++ b/trunk/etherpad/src/etherpad/pad/model.js @@ -259,7 +259,7 @@ function accessPadGlobal(padId, padFunc, rwMode) { sqlbase.putJSON("PAD_META", padId, meta2); /* Update tags for the pad. Should maybe be in a separate function? */ - var new_tags = pad.text().match(new RegExp("#[^,# \t\n\r][^,# \t\n\r]*", "g")) + var new_tags = pad.text().match(new RegExp("#[^,#!\\s][^,#!\\s]*", "g")); if (new_tags == null) new_tags = new Array(); for (i = 0; i < new_tags.length; i++) new_tags[i] = new_tags[i].substring(1); diff --git a/trunk/etherpad/src/main.js b/trunk/etherpad/src/main.js index 9cc1db2..61e365f 100644 --- a/trunk/etherpad/src/main.js +++ b/trunk/etherpad/src/main.js @@ -42,6 +42,7 @@ import("etherpad.control.historycontrol"); import("etherpad.control.loadtestcontrol"); import("etherpad.control.maincontrol"); import("etherpad.control.pad.pad_control"); +import("etherpad.control.tag.tag_control"); import("etherpad.control.pne_manual_control"); import("etherpad.control.pne_tracker_control"); import("etherpad.control.pro.admin.license_manager_control"); @@ -362,6 +363,7 @@ function handlePath() { [PrefixMatcher('/static/'), forward(static_control)], [PrefixMatcher('/ep/genimg/'), genimg.renderPath], [PrefixMatcher('/ep/pad/'), forward(pad_control)], + [PrefixMatcher('/ep/tag/'), forward(tag_control)], [PrefixMatcher('/ep/script/'), forward(scriptcontrol)], [/^\/([^\/]+)$/, pad_control.render_pad], [DirMatcher('/ep/unit-tests/'), forward(testcontrol)], diff --git a/trunk/etherpad/src/static/js/pad_utils.js b/trunk/etherpad/src/static/js/pad_utils.js index de606ad..ccedac9 100644 --- a/trunk/etherpad/src/static/js/pad_utils.js +++ b/trunk/etherpad/src/static/js/pad_utils.js @@ -106,6 +106,18 @@ var padutils = { var hourmin = d.getHours()+":"+("0"+d.getMinutes()).slice(-2); return dayOfWeek+' '+month+' '+dayOfMonth+' '+year+' '+hourmin; }, + findTags: function(text) { + var tagExp = new RegExp("#[^,#!\\s][^,#!\\s]*", "g"); + var tags = null; + var execResult; + while ((execResult = tagExp.exec(text))) { + tags = (tags || []); + var startIndex = execResult.index; + var url = execResult[0]; + tags.push([startIndex, url]); + } + return tags; + }, findURLs: function(text) { // copied from ACE var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; @@ -133,6 +145,7 @@ var padutils = { var idx = 0; var pieces = []; var urls = padutils.findURLs(text); + var tags = padutils.findTags(text); function advanceTo(i) { if (i > idx) { pieces.push(padutils.escapeHtml(text.substring(idx, i))); @@ -150,6 +163,17 @@ var padutils = { pieces.push(''); } } + if (tags) { + for(var j=0;j'); + advanceTo(startIndex + href.length); + pieces.push(''); + } + } advanceTo(text.length); return pieces.join(''); }, diff --git a/trunk/etherpad/src/templates/misc/pad_default.ejs b/trunk/etherpad/src/templates/misc/pad_default.ejs index 96b7e25..2287096 100644 --- a/trunk/etherpad/src/templates/misc/pad_default.ejs +++ b/trunk/etherpad/src/templates/misc/pad_default.ejs @@ -14,3 +14,6 @@ limitations under the License. */ %> Welcome to EtherPad! This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents! + +You can make this pad public by tagging it with #public, and categorize it using twitter-style tags, like this: #newpage #uncategorized +If you don't want this pad to be public, just delete the above tags :) diff --git a/trunk/etherpad/src/templates/tag/tag_search.ejs b/trunk/etherpad/src/templates/tag/tag_search.ejs new file mode 100644 index 0000000..d7fe351 --- /dev/null +++ b/trunk/etherpad/src/templates/tag/tag_search.ejs @@ -0,0 +1,109 @@ +<% /* Copyright 2009 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ %> +<% + helpers.setHtmlTitle("Browse tags"); + helpers.setBodyId("padbody"); + helpers.addBodyClass("limwidth nonpropad nonprouser"); + helpers.includeCss("pad2_ejs.css"); + helpers.setRobotsPolicy({index: false, follow: false}) + helpers.includeJQuery(); + helpers.includeCometJs(); + helpers.includeJs("json2.js"); + helpers.addToHead('\n\n'); +%> + +
+
+
+
+
+ + <% if (isProAccountHolder) { %> +
<%= toHTML(account.email) %>(sign out)
+ <% } else if (isPro) { %> + + <% } %> +
+
+
+
+
Browse tags
+ + + + + + +
+
+
+ +
+
+ XYZZY +
+ +
+ +
NANANANA
+
+ +
+
+
+
+ +
+   +   +   +   +   +   +   +   +   +   +   +
+
+
+
+

Refine your query

+

Search for pads that have a tag

+ <% for (i = 0; i < newTags.length; i++) { %> + <%= newTags[i].tagname %> + <% } %> + +

Search for pads that doesn't have a tag

+ <% for (i = 0; i < newTags.length; i++) { %> + <%= newTags[i].tagname %> + <% } %> + +

Matching pads

+ +
+
+
+ +
+
+
+
+
+ diff --git a/trunk/infrastructure/framework-src/modules/sqlbase/sqlobj.js b/trunk/infrastructure/framework-src/modules/sqlbase/sqlobj.js index 4bc1263..e599c92 100644 --- a/trunk/infrastructure/framework-src/modules/sqlbase/sqlobj.js +++ b/trunk/infrastructure/framework-src/modules/sqlbase/sqlobj.js @@ -17,6 +17,7 @@ import("cache_utils.syncedWithCache"); import("sqlbase.sqlcommon.*"); import("jsutils.*"); +import("etherpad.log"); jimport("java.lang.System.out.println"); jimport("java.sql.Statement"); @@ -112,10 +113,13 @@ function _getJsValFromResultSet(rs, type, colName) { } else { r = null; } - } else if (type == java.sql.Types.INTEGER || + } else if (type == java.sql.Types.BIGINT || + type == java.sql.Types.INTEGER || type == java.sql.Types.SMALLINT || type == java.sql.Types.TINYINT) { r = rs.getInt(colName); + } else if (type == java.sql.Types.DECIMAL) { + r = rs.getFloat(colName); } else if (type == java.sql.Types.BIT) { r = rs.getBoolean(colName); } else { @@ -192,8 +196,9 @@ function _resultRowToJsObj(resultSet) { var metaData = resultSet.getMetaData(); var colCount = metaData.getColumnCount(); + for (var i = 1; i <= colCount; i++) { - var colName = metaData.getColumnName(i); + var colName = metaData.getColumnLabel(i); var type = metaData.getColumnType(i); resultObj[colName] = _getJsValFromResultSet(resultSet, type, colName); } @@ -338,6 +343,47 @@ function selectMulti(tableName, constraints, options) { }); } +function executeRaw(stmnt, params) { + return withConnection(function(conn) { + var pstmnt = conn.prepareStatement(stmnt); + return closing(pstmnt, function() { + for (var i = 0; i < params.length; i++) { + var v = params[i]; + + if (v === undefined) { + throw Error("value is undefined for key "+i); + } + + if (typeof(v) == 'object' && v.isnull) { + pstmnt.setNull(i+1, v.type); + } else if (typeof(v) == 'string') { + pstmnt.setString(i+1, v); + } else if (typeof(v) == 'number') { + pstmnt.setInt(i+1, v); + } else if (typeof(v) == 'boolean') { + pstmnt.setBoolean(i+1, v); + } else if (v.valueOf && v.getDate && v.getHours) { + pstmnt.setTimestamp(i+1, new java.sql.Timestamp(+v)); + } else { + throw Error("Cannot insert this type of javascript object: "+typeof(v)+" (key="+i+", value = "+v+")"); + } + } + + _qdebug(stmnt); + var resultSet = pstmnt.executeQuery(); + var resultArray = []; + + return closing(resultSet, function() { + while (resultSet.next()) { + resultArray.push(_resultRowToJsObj(resultSet)); + } + + return resultArray; + }); + }); + }); +} + /* returns number of rows updated */ function update(tableName, constraints, obj) { var objKeys = keys(obj); -- cgit v1.2.3-1-g7c22