summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEgil Moeller <egil.moller@freecode.no>2010-03-12 21:13:39 +0100
committerEgil Moeller <egil.moller@freecode.no>2010-03-12 21:13:39 +0100
commit16b37f7a1f9ff8e559e90261c8f6cd3da46dae49 (patch)
treeef0d0e9f4e9e0186d3ddb922818e949afcf9e1b4
parent648f6a226b076424e9df533107a5c0cf1804293a (diff)
downloadetherpad-16b37f7a1f9ff8e559e90261c8f6cd3da46dae49.tar.gz
etherpad-16b37f7a1f9ff8e559e90261c8f6cd3da46dae49.tar.bz2
etherpad-16b37f7a1f9ff8e559e90261c8f6cd3da46dae49.zip
Tag search works fully but looks awfull
-rw-r--r--trunk/etherpad/src/etherpad/control/tag/tag_control.js231
-rw-r--r--trunk/etherpad/src/etherpad/pad/model.js2
-rw-r--r--trunk/etherpad/src/main.js2
-rw-r--r--trunk/etherpad/src/static/js/pad_utils.js24
-rw-r--r--trunk/etherpad/src/templates/misc/pad_default.ejs3
-rw-r--r--trunk/etherpad/src/templates/tag/tag_search.ejs109
-rw-r--r--trunk/infrastructure/framework-src/modules/sqlbase/sqlobj.js50
7 files changed, 418 insertions, 3 deletions
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 <egil.moller@piratpartiet.se>
+ *
+ * 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('</a>');
}
}
+ if (tags) {
+ for(var j=0;j<tags.length;j++) {
+ var startIndex = tags[j][0];
+ var tag = tags[j][1];
+ advanceTo(startIndex);
+ pieces.push('<a ', (target?'target="'+target+'" ':''),
+ 'href="/ep/tag/?query=', tag, '">');
+ advanceTo(startIndex + href.length);
+ pieces.push('</a>');
+ }
+ }
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<style type="text/css" title="dynamicsyntax"></style>\n');
+%>
+
+<div id="padpage">
+ <div id="padtop">
+ <div id="topbar" style="margin: 7px; margin-top: 0px;">
+ <div id="topbarleft"><!-- --></div>
+ <div id="topbarright"><!-- --></div>
+ <div id="topbarcenter"><a href="/" id="topbaretherpad">EtherPad</a></div>
+ <% if (isProAccountHolder) { %>
+ <div id="accountnav"><%= toHTML(account.email) %><a href="/ep/account/sign-out">(sign out)</a></div>
+ <% } else if (isPro) { %>
+ <div id="accountnav"><a href="<%= signinUrl %>">sign in</a></div>
+ <% } %>
+ </div>
+ </div>
+ <div id="docbar" class="docbar-public">
+ <div id="docbarleft"><!-- --></div>
+ <div title="Browse pads by tag" id="docbarpadtitle"><span>Browse tags</span></div>
+
+ <div id="docbaroptions-outer"><a href="javascript:void(0)" id="docbaroptions">Pad Options</a></div>
+ <div id="docbarsavedrevs-outer"><a href="javascript:void(0)" id="docbarsavedrevs">Saved revisions</a></div>
+ <div id="docbarimpexp-outer"><a href="javascript:void(0)" id="docbarimpexp">Import/Export</a></div>
+ <div id="docbarslider-outer"><a target="_blank" href="/ep/pad/view/xx/latest" id="docbarslider">Time Slider</a></div>
+
+ </div>
+ </div>
+ <div id="padmain">
+
+ <div id="padsidebar">
+ <div id="padusers">
+ XYZZY
+ </div>
+
+ <div id="hdraggie"><!-- --></div>
+
+ <div id="padchat">NANANANA</div>
+ </div> <!-- /padsidebar -->
+
+ <div id="padeditor">
+ <div id="editbar" class="enabledtoolbar">
+ <div id="editbarleft"><!-- --></div>
+ <div id="editbarright"><!-- --></div>
+
+ <div id="editbarinner">
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('bold'));" class="editbarbutton bold" title="Bold (ctrl-B)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('italic'));" class="editbarbutton italic" title="Italics (ctrl-I)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('underline'));" class="editbarbutton underline" title="Underline (ctrl-U)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('strikethrough'));" class="editbarbutton strikethrough" title="Strikethrough">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('clearauthorship'));" class="editbarbutton clearauthorship" title="Clear Authorship Colors">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('undo'));" class="editbarbutton undo" title="Undo (ctrl-Z)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('redo'));" class="editbarbutton redo" title="Redo (ctrl-Y)">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('insertunorderedlist'));" class="editbarbutton insertunorderedlist" title="Toggle Bullet List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('indent'));" class="editbarbutton indent" title="Indent List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('outdent'));" class="editbarbutton outdent" title="Unindent List">&nbsp;</a>
+ <a unselectable="on" href="javascript:void (window.pad&amp;&amp;pad.editbarClick('save'));" class="editbarbutton save" title="Save Revision">&nbsp;</a>
+ </div>
+ </div>
+ <div style="height: 268px;" id="editorcontainerbox">
+ <div id="editorcontainer" style="padding:5pt; height: 600pt;">
+ <h1>Refine your query</h1>
+ <h2>Search for pads that have a tag</h2>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="/ep/tag/?query=<%= tagsToQuery(tags.concat([newTags[i].tagname]),antiTags) %>" class="tag" title="<%= newTags[i].matches %> matches"><%= newTags[i].tagname %></a>
+ <% } %>
+
+ <h2>Search for pads that <em>doesn't</em> have a tag</h2>
+ <% for (i = 0; i < newTags.length; i++) { %>
+ <a href="/ep/tag/?query=<%= tagsToQuery(tags,antiTags.concat([newTags[i].tagname])) %>" class="anti_tag" title="<%= newTags[i].antimatches %> matches"><%= newTags[i].tagname %></a>
+ <% } %>
+
+ <h1>Matching pads</h1>
+ <ul>
+ <% for (i = 0; i < matchingPads.length; i++) { %>
+ <li><a href="/<%= matchingPads[i].ID %>"><%= matchingPads[i].ID %></a></li>
+ <% } %>
+ </ul>
+ </div>
+ </div>
+ </div><!-- /padeditor -->
+
+ <div id="bottomarea">
+ <div id="widthprefcheck" class="widthprefunchecked"><!-- --></div>
+ <div id="sidebarcheck" class="sidebarchecked"><!-- --></div>
+ </div>
+ </div>
+</div>
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);