jsduck 3.6.1 → 3.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +41 -86
- data/jsduck.gemspec +2 -2
- data/lib/jsduck/class.rb +6 -0
- data/lib/jsduck/doc_parser.rb +15 -12
- data/lib/jsduck/examples.rb +15 -10
- data/lib/jsduck/inherit_doc.rb +53 -29
- data/lib/jsduck/lint.rb +1 -1
- data/lib/jsduck/options.rb +2 -2
- data/opt/comments-server-side/.gitignore +2 -0
- data/opt/comments-server-side/ForumUser.js +80 -0
- data/opt/comments-server-side/app.js +332 -0
- data/opt/comments-server-side/database.js +36 -0
- data/opt/comments-server-side/package.json +20 -0
- data/opt/comments-server-side/util.js +245 -0
- metadata +19 -8
data/Rakefile
CHANGED
@@ -127,14 +127,10 @@ def compress
|
|
127
127
|
system "rm -rf #{dir}/resources/codemirror"
|
128
128
|
system "rm -rf #{dir}/resources/.sass-cache"
|
129
129
|
|
130
|
-
# Empty the extjs dir, leave only the main JS
|
130
|
+
# Empty the extjs dir, leave only the main JS file and images
|
131
131
|
system "rm -rf #{dir}/extjs"
|
132
132
|
system "mkdir #{dir}/extjs"
|
133
|
-
system "cp
|
134
|
-
system "cp #{EXT_DIR}/ext-all-debug.js #{dir}/extjs"
|
135
|
-
system "cp #{EXT_DIR}/bootstrap.js #{dir}/extjs"
|
136
|
-
system "mkdir -p #{dir}/extjs/resources/css"
|
137
|
-
system "cp #{EXT_DIR}/resources/css/ext-all.css #{dir}/extjs/resources/css"
|
133
|
+
system "cp template/extjs/ext-all.js #{dir}/extjs"
|
138
134
|
system "mkdir -p #{dir}/extjs/resources/themes/images"
|
139
135
|
system "cp -r #{EXT_DIR}/resources/themes/images/default #{dir}/extjs/resources/themes/images"
|
140
136
|
end
|
@@ -158,27 +154,28 @@ class JsDuckRunner
|
|
158
154
|
# This excludes Opera and IE < 8.
|
159
155
|
# We check explicitly for IE version to make sure the code works the
|
160
156
|
# same way in both real IE7 and IE7-mode of IE8/9.
|
161
|
-
def add_comments(db_name)
|
157
|
+
def add_comments(db_name, version)
|
162
158
|
comments_base_url = "http://projects.sencha.com/auth"
|
163
159
|
@options += ["--head-html", <<-EOHTML]
|
164
160
|
<script type="text/javascript">
|
165
161
|
Docs.enableComments = ("withCredentials" in new XMLHttpRequest()) || (Ext.ieVersion >= 8);
|
166
162
|
Docs.baseUrl = "#{comments_base_url}";
|
167
163
|
Docs.commentsDb = "#{db_name}";
|
164
|
+
Docs.commentsVersion = "#{version}";
|
168
165
|
</script>
|
169
166
|
EOHTML
|
170
167
|
end
|
171
168
|
|
172
169
|
# For export of ExtJS, reference extjs from the parent dir
|
173
|
-
def
|
174
|
-
|
175
|
-
["#{@out_dir}/eg-iframe.html", "#{@out_dir}/index.html"].each do |file|
|
170
|
+
def make_extjs_path_relative
|
171
|
+
["#{@out_dir}/index.html"].each do |file|
|
176
172
|
out = []
|
177
173
|
IO.read(file).each_line do |line|
|
178
|
-
out << line.sub(/(
|
174
|
+
out << line.sub(/(src|href)="extjs\//, '\1="../')
|
179
175
|
end
|
180
176
|
File.open(file, 'w') {|f| f.write(out) }
|
181
177
|
end
|
178
|
+
system "rm -rf #{@out_dir}/extjs"
|
182
179
|
end
|
183
180
|
|
184
181
|
def add_ext4
|
@@ -194,54 +191,12 @@ class JsDuckRunner
|
|
194
191
|
]
|
195
192
|
end
|
196
193
|
|
197
|
-
def
|
198
|
-
@options += [
|
199
|
-
"--json",
|
200
|
-
"--output", "#{@out_dir}/../export/touch1",
|
201
|
-
"--external=google.maps.Map,google.maps.LatLng",
|
202
|
-
"#{@sdk_dir}/touch/sencha-touch.jsb3",
|
203
|
-
]
|
204
|
-
end
|
205
|
-
|
206
|
-
def add_touch2_export
|
207
|
-
@options += [
|
208
|
-
"--export", "full",
|
209
|
-
"--output", "#{@out_dir}/../export/touch2",
|
210
|
-
"--external=google.maps.Map,google.maps.LatLng",
|
211
|
-
"#{@sdk_dir}/touch/touch.jsb3",
|
212
|
-
]
|
213
|
-
end
|
214
|
-
|
215
|
-
def set_touch2_src
|
216
|
-
relative_touch_path = "../"
|
217
|
-
system("cp", "-r", "#{@sdk_dir}/touch/docs/build-welcome.html", "template-min/welcome.html")
|
218
|
-
system("cp", "-r", "#{@sdk_dir}/touch/docs/eg-iframe.html", "template-min/eg-iframe.html")
|
219
|
-
|
220
|
-
["template-min/eg-iframe.html", "template-min/welcome.html"].each do |file|
|
221
|
-
html = IO.read(file);
|
222
|
-
|
223
|
-
touch_src_re = /((src|href)="touch)/m
|
224
|
-
out = []
|
225
|
-
|
226
|
-
html.each_line do |line|
|
227
|
-
out << line.sub(/((src|href)="touch\/)/, '\2="' + relative_touch_path)
|
228
|
-
end
|
229
|
-
|
230
|
-
File.open(file, 'w') {|f| f.write(out) }
|
231
|
-
end
|
232
|
-
|
233
|
-
head_html = <<-EOHTML
|
194
|
+
def add_phone_redirect
|
195
|
+
@options += ["--body-html", <<-EOHTML]
|
234
196
|
<script type="text/javascript">
|
235
|
-
if (Ext.is.Phone) { window.location = "
|
197
|
+
if (Ext.is.Phone) { window.location = "../examples/"; }
|
236
198
|
</script>
|
237
199
|
EOHTML
|
238
|
-
|
239
|
-
@options += [
|
240
|
-
"--body-html", head_html,
|
241
|
-
"--welcome", "template-min/welcome.html",
|
242
|
-
"--eg-iframe", "template-min/eg-iframe.html",
|
243
|
-
"--examples-base-url", "#{relative_touch_path}examples/",
|
244
|
-
]
|
245
200
|
end
|
246
201
|
|
247
202
|
def add_debug
|
@@ -346,13 +301,8 @@ class JsDuckRunner
|
|
346
301
|
|
347
302
|
end
|
348
303
|
|
349
|
-
|
350
|
-
|
351
|
-
system "mkdir #{@out_dir}/extjs/builds"
|
352
|
-
system "cp #{@ext_dir}/builds/ext-core.js #{@out_dir}/extjs/builds/ext-core.js"
|
353
|
-
system "cp #{@ext_dir}/release-notes.html #{@out_dir}/extjs"
|
354
|
-
system "cp -r #{@ext_dir}/examples #{@out_dir}/extjs"
|
355
|
-
system "cp -r #{@ext_dir}/welcome #{@out_dir}/extjs"
|
304
|
+
def copy_extjs_build
|
305
|
+
system "cp -r #{@ext_dir} #{@out_dir}/extjs-build"
|
356
306
|
end
|
357
307
|
|
358
308
|
# Copy over Sencha Touch
|
@@ -386,11 +336,20 @@ task :sdk, [:mode] => :sass do |t, args|
|
|
386
336
|
runner.add_seo if mode == "debug" || mode == "live"
|
387
337
|
runner.add_export_notice("ext-js/4-0") if mode == "export"
|
388
338
|
runner.add_google_analytics if mode == "live"
|
389
|
-
runner.add_comments('
|
339
|
+
runner.add_comments('ext-js', '4') if mode == "debug" || mode == "live"
|
340
|
+
if mode == "export"
|
341
|
+
runner.add_options ["--eg-iframe", "#{SDK_DIR}/extjs/docs/eg-iframe-build.html"]
|
342
|
+
runner.add_options ["--examples-base-url", "../examples/"]
|
343
|
+
else
|
344
|
+
runner.add_options ["--examples-base-url", "extjs-build/examples/"]
|
345
|
+
end
|
390
346
|
runner.run
|
391
347
|
|
392
|
-
|
393
|
-
|
348
|
+
if mode == "export"
|
349
|
+
runner.make_extjs_path_relative
|
350
|
+
else
|
351
|
+
runner.copy_extjs_build
|
352
|
+
end
|
394
353
|
end
|
395
354
|
|
396
355
|
desc "Run JSDuck on Docs app itself"
|
@@ -464,17 +423,26 @@ task :touch2, [:mode] => :sass do |t, args|
|
|
464
423
|
compress if mode == "live" || mode == "export"
|
465
424
|
|
466
425
|
runner = JsDuckRunner.new
|
467
|
-
runner.add_options [
|
468
|
-
|
469
|
-
|
426
|
+
runner.add_options [
|
427
|
+
"--output", OUT_DIR,
|
428
|
+
"--config", "#{SDK_DIR}/touch/docs/config.json"
|
429
|
+
]
|
430
|
+
|
470
431
|
if mode == "export"
|
471
|
-
runner.
|
472
|
-
|
473
|
-
|
432
|
+
runner.add_export_notice("touch/2-0")
|
433
|
+
runner.add_phone_redirect
|
434
|
+
# override settings in config.json
|
435
|
+
runner.add_options [
|
436
|
+
"--welcome", "#{SDK_DIR}/touch/docs/build-welcome.html",
|
437
|
+
"--eg-iframe", "#{SDK_DIR}/touch/docs/build-eg-iframe.html",
|
438
|
+
"--examples-base-url", "../examples/",
|
439
|
+
]
|
474
440
|
end
|
441
|
+
|
442
|
+
runner.add_debug if mode == "debug"
|
475
443
|
runner.add_seo if mode == "debug" || mode == "live"
|
476
444
|
runner.add_google_analytics if mode == "live"
|
477
|
-
runner.add_comments('
|
445
|
+
runner.add_comments('touch', '2') if mode == "debug" || mode == "live"
|
478
446
|
runner.run
|
479
447
|
|
480
448
|
runner.copy_touch2_build if mode != "export"
|
@@ -531,19 +499,6 @@ task :animator, [:mode] => :sass do |t, args|
|
|
531
499
|
runner.run
|
532
500
|
end
|
533
501
|
|
534
|
-
desc "Run JSDuck JSON Export (for internal use at Sencha)\n" +
|
535
|
-
"export[touch] - creates export for Touch 1\n" +
|
536
|
-
"export[touch2] - creates export for Touch 2"
|
537
|
-
task :export, [:mode] do |t, args|
|
538
|
-
mode = args[:mode]
|
539
|
-
throw "Unknown mode #{mode}" unless ["touch", "touch2"].include?(mode)
|
540
|
-
|
541
|
-
runner = JsDuckRunner.new
|
542
|
-
runner.add_touch_export if mode == "touch"
|
543
|
-
runner.add_touch2_export if mode == "touch2"
|
544
|
-
runner.run
|
545
|
-
end
|
546
|
-
|
547
502
|
desc "Build JSDuck gem"
|
548
503
|
task :gem => :sass do
|
549
504
|
compress
|
data/jsduck.gemspec
CHANGED
@@ -2,8 +2,8 @@ Gem::Specification.new do |s|
|
|
2
2
|
s.required_rubygems_version = ">= 1.3.5"
|
3
3
|
|
4
4
|
s.name = 'jsduck'
|
5
|
-
s.version = '3.
|
6
|
-
s.date = '2012-02-
|
5
|
+
s.version = '3.7.0'
|
6
|
+
s.date = '2012-02-29'
|
7
7
|
s.summary = "Simple JavaScript Duckumentation generator"
|
8
8
|
s.description = "Documentation generator for Sencha JS frameworks"
|
9
9
|
s.homepage = "https://github.com/senchalabs/jsduck"
|
data/lib/jsduck/class.rb
CHANGED
@@ -33,10 +33,16 @@ module JsDuck
|
|
33
33
|
@doc = doc
|
34
34
|
end
|
35
35
|
|
36
|
+
# Accessor to internal hash
|
36
37
|
def [](key)
|
37
38
|
@doc[key]
|
38
39
|
end
|
39
40
|
|
41
|
+
# Assignment to internal hash keys
|
42
|
+
def []=(key, value)
|
43
|
+
@doc[key] = value
|
44
|
+
end
|
45
|
+
|
40
46
|
# Returns instance of parent class, or nil if there is none
|
41
47
|
def parent
|
42
48
|
@doc[:extends] ? lookup(@doc[:extends]) : nil
|
data/lib/jsduck/doc_parser.rb
CHANGED
@@ -124,7 +124,7 @@ module JsDuck
|
|
124
124
|
at_member
|
125
125
|
elsif look(/@inherit[dD]oc\b/)
|
126
126
|
at_inheritdoc
|
127
|
-
elsif look(/@alias\s+[\w.]
|
127
|
+
elsif look(/@alias\s+([\w.]+)?#\w+/)
|
128
128
|
# For backwards compatibility.
|
129
129
|
# @alias tag was used as @inheritdoc before
|
130
130
|
at_inheritdoc
|
@@ -325,21 +325,24 @@ module JsDuck
|
|
325
325
|
|
326
326
|
add_tag(:inheritdoc)
|
327
327
|
skip_horiz_white
|
328
|
+
|
328
329
|
if look(@ident_chain_pattern)
|
329
330
|
@current_tag[:cls] = ident_chain
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
@
|
331
|
+
end
|
332
|
+
|
333
|
+
if look(/#\w/)
|
334
|
+
@input.scan(/#/)
|
335
|
+
if look(/static-/)
|
336
|
+
@current_tag[:static] = true
|
337
|
+
@input.scan(/static-/)
|
338
|
+
end
|
339
|
+
if look(/(cfg|property|method|event|css_var|css_mixin)-/)
|
340
|
+
@current_tag[:type] = ident.to_sym
|
341
|
+
@input.scan(/-/)
|
341
342
|
end
|
343
|
+
@current_tag[:member] = ident
|
342
344
|
end
|
345
|
+
|
343
346
|
skip_white
|
344
347
|
end
|
345
348
|
|
data/lib/jsduck/examples.rb
CHANGED
@@ -33,24 +33,29 @@ module JsDuck
|
|
33
33
|
#
|
34
34
|
def fix_examples_data
|
35
35
|
each_item do |ex|
|
36
|
+
ex["name"] = ex["url"] unless ex["name"]
|
37
|
+
|
36
38
|
unless ex["url"] =~ /^https?:\/\//
|
37
39
|
ex["url"] = @opts.examples_base_url + ex["url"]
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
end
|
41
|
+
unless ex["icon"] =~ /^https?:\/\//
|
42
|
+
ex["icon"] = @opts.examples_base_url + ex["icon"]
|
43
|
+
end
|
44
|
+
|
45
|
+
unless ex["title"]
|
46
|
+
ex["title"] = ex["text"]
|
47
|
+
ex.delete("text")
|
48
|
+
end
|
49
|
+
unless ex["description"]
|
50
|
+
ex["description"] = ex["desc"]
|
51
|
+
ex.delete("desc")
|
47
52
|
end
|
48
53
|
end
|
49
54
|
end
|
50
55
|
|
51
56
|
# Extracts example icon URL from example hash
|
52
57
|
def icon_url(example)
|
53
|
-
|
58
|
+
example["icon"]
|
54
59
|
end
|
55
60
|
|
56
61
|
end
|
data/lib/jsduck/inherit_doc.rb
CHANGED
@@ -12,6 +12,7 @@ module JsDuck
|
|
12
12
|
# Performs all inheriting
|
13
13
|
def resolve_all
|
14
14
|
@relations.each do |cls|
|
15
|
+
resolve_class(cls) if cls[:inheritdoc]
|
15
16
|
cls.all_local_members.each do |member|
|
16
17
|
if member[:inheritdoc]
|
17
18
|
resolve(member)
|
@@ -33,57 +34,80 @@ module JsDuck
|
|
33
34
|
#
|
34
35
|
# If the parent also has @inheritdoc, continues recursively.
|
35
36
|
def find_parent(m)
|
36
|
-
|
37
|
-
|
37
|
+
if m[:inheritdoc][:cls]
|
38
|
+
# @inheritdoc MyClass#member
|
39
|
+
parent_cls = @relations[m[:inheritdoc][:cls]]
|
40
|
+
return warn("class not found", m) unless parent_cls
|
41
|
+
|
42
|
+
parent = lookup_member(parent_cls, m)
|
43
|
+
return warn("member not found", m) unless parent
|
44
|
+
|
45
|
+
elsif m[:inheritdoc][:member]
|
46
|
+
# @inheritdoc #member
|
47
|
+
parent = lookup_member(@relations[m[:owner]], m)
|
48
|
+
return warn("member not found", m) unless parent
|
38
49
|
|
39
|
-
if inherit[:cls]
|
40
|
-
parent_cls = @relations[inherit[:cls]]
|
41
|
-
unless parent_cls
|
42
|
-
warn("@inheritdoc #{inherit[:cls]}##{inherit[:member]} - class not found", context)
|
43
|
-
return m
|
44
|
-
end
|
45
|
-
parent = parent_cls.get_members(inherit[:member], inherit[:type] || m[:tagname], inherit[:static] || m[:meta][:static])[0]
|
46
|
-
unless parent
|
47
|
-
warn("@inheritdoc #{inherit[:cls]}##{inherit[:member]} - member not found", context)
|
48
|
-
return m
|
49
|
-
end
|
50
50
|
else
|
51
|
+
# @inheritdoc
|
51
52
|
parent_cls = @relations[m[:owner]].parent
|
52
53
|
mixins = @relations[m[:owner]].mixins
|
54
|
+
|
53
55
|
# Warn when no parent or mixins at all
|
54
|
-
|
55
|
-
|
56
|
-
return m
|
57
|
-
end
|
56
|
+
return warn("parent class not found", m) unless parent_cls || mixins.length > 0
|
57
|
+
|
58
58
|
# First check for the member in all mixins, because members
|
59
59
|
# from mixins override those from parent class. Looking first
|
60
60
|
# from mixins is probably a bit slower, but it's the correct
|
61
61
|
# order to do things.
|
62
62
|
if mixins.length > 0
|
63
|
-
parent = mixins.map
|
64
|
-
mix.get_members(m[:name], m[:tagname], m[:meta][:static])[0]
|
65
|
-
end.compact.first
|
63
|
+
parent = mixins.map {|mix| lookup_member(mix, m) }.compact.first
|
66
64
|
end
|
65
|
+
|
67
66
|
# When not found, try looking from parent class
|
68
67
|
if !parent && parent_cls
|
69
|
-
parent = parent_cls
|
68
|
+
parent = lookup_member(parent_cls, m)
|
70
69
|
end
|
70
|
+
|
71
71
|
# Only when both parent and mixins fail, throw warning
|
72
|
-
|
73
|
-
warn("@inheritdoc - parent member not found", context)
|
74
|
-
return m
|
75
|
-
end
|
72
|
+
return warn("parent member not found", m) unless parent
|
76
73
|
end
|
77
74
|
|
78
|
-
|
79
|
-
|
75
|
+
return parent[:inheritdoc] ? find_parent(parent) : parent
|
76
|
+
end
|
77
|
+
|
78
|
+
def lookup_member(cls, m)
|
79
|
+
inherit = m[:inheritdoc]
|
80
|
+
cls.get_members(inherit[:member] || m[:name], inherit[:type] || m[:tagname], inherit[:static] || m[:meta][:static])[0]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Copy over doc from parent class.
|
84
|
+
def resolve_class(cls)
|
85
|
+
parent = find_class_parent(cls)
|
86
|
+
cls[:doc] = (cls[:doc] + "\n\n" + parent[:doc]).strip
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_class_parent(cls)
|
90
|
+
if cls[:inheritdoc][:cls]
|
91
|
+
# @inheritdoc MyClass
|
92
|
+
parent = @relations[cls[:inheritdoc][:cls]]
|
93
|
+
return warn("class not found", cls) unless parent
|
80
94
|
else
|
81
|
-
|
95
|
+
# @inheritdoc
|
96
|
+
parent = cls.parent
|
97
|
+
return warn("parent class not found", cls) unless parent
|
82
98
|
end
|
99
|
+
|
100
|
+
return parent[:inheritdoc] ? find_class_parent(parent) : parent
|
83
101
|
end
|
84
102
|
|
85
|
-
def warn(msg,
|
103
|
+
def warn(msg, item)
|
104
|
+
context = item[:files][0]
|
105
|
+
i_member = item[:inheritdoc][:member]
|
106
|
+
|
107
|
+
msg = "@inheritdoc #{item[:inheritdoc][:cls]}"+ (i_member ? "#" + i_member : "") + " - " + msg
|
86
108
|
Logger.instance.warn(:inheritdoc, msg, context[:filename], context[:linenr])
|
109
|
+
|
110
|
+
return item
|
87
111
|
end
|
88
112
|
|
89
113
|
end
|
data/lib/jsduck/lint.rb
CHANGED
@@ -41,7 +41,7 @@ module JsDuck
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
each_member do |member|
|
44
|
-
if member[:doc] == "" && !member[:private]
|
44
|
+
if member[:doc] == "" && !member[:private] && !member[:meta][:hide]
|
45
45
|
warn(:no_doc, "No documentation for #{member[:owner]}##{member[:name]}", member)
|
46
46
|
end
|
47
47
|
end
|
data/lib/jsduck/options.rb
CHANGED
@@ -72,7 +72,7 @@ module JsDuck
|
|
72
72
|
]
|
73
73
|
@meta_tag_paths = []
|
74
74
|
|
75
|
-
@version = "3.
|
75
|
+
@version = "3.7.0"
|
76
76
|
|
77
77
|
# Customizing output
|
78
78
|
@title = "Sencha Docs - Ext JS"
|
@@ -95,7 +95,7 @@ module JsDuck
|
|
95
95
|
@export = nil
|
96
96
|
@seo = false
|
97
97
|
@eg_iframe = nil
|
98
|
-
@examples_base_url = "extjs/examples/"
|
98
|
+
@examples_base_url = "extjs-build/examples/"
|
99
99
|
|
100
100
|
# Debugging
|
101
101
|
# Turn multiprocessing off by default in Windows
|
@@ -0,0 +1,80 @@
|
|
1
|
+
/**
|
2
|
+
* Authentication with a vBulletin user database
|
3
|
+
*/
|
4
|
+
|
5
|
+
var crypto = require('crypto'),
|
6
|
+
_ = require('underscore');
|
7
|
+
|
8
|
+
var ForumUser = exports.ForumUser = function(client) {
|
9
|
+
this.client = client;
|
10
|
+
};
|
11
|
+
|
12
|
+
ForumUser.prototype = {
|
13
|
+
|
14
|
+
login: function(username, password, callback) {
|
15
|
+
|
16
|
+
var sql = "SELECT userid, usergroupid, membergroupids, email, username, password, salt FROM user WHERE username = ?",
|
17
|
+
self = this;
|
18
|
+
|
19
|
+
this.client.query(sql, [username],
|
20
|
+
|
21
|
+
function selectCb(err, results, fields) {
|
22
|
+
if (err) {
|
23
|
+
callback(err);
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
|
27
|
+
if (results.length == 0) {
|
28
|
+
callback("No such user");
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
if (!self.checkPassword(password, results[0].salt, results[0].password)) {
|
33
|
+
callback("Invalid password");
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
var user = self.getUserFromResult(results[0]);
|
38
|
+
|
39
|
+
callback(null, user);
|
40
|
+
}
|
41
|
+
);
|
42
|
+
},
|
43
|
+
|
44
|
+
clientUser: function(user) {
|
45
|
+
|
46
|
+
crypto.createHash('md5').update(user.email).digest("hex");
|
47
|
+
|
48
|
+
return {
|
49
|
+
emailHash: user.email,
|
50
|
+
userName: user.username,
|
51
|
+
userId: user.userid,
|
52
|
+
mod: _.include(user.membergroupids, 7)
|
53
|
+
};
|
54
|
+
},
|
55
|
+
|
56
|
+
checkPassword: function(password, salt, saltedPassword) {
|
57
|
+
|
58
|
+
password = crypto.createHash('md5').update(password).digest("hex") + salt;
|
59
|
+
password = crypto.createHash('md5').update(password).digest("hex");
|
60
|
+
|
61
|
+
return password == saltedPassword;
|
62
|
+
},
|
63
|
+
|
64
|
+
getUserFromResult: function(result) {
|
65
|
+
|
66
|
+
var ids, id;
|
67
|
+
|
68
|
+
if (result.membergroupids) {
|
69
|
+
ids = result.membergroupids.split(',');
|
70
|
+
result.membergroupids = [];
|
71
|
+
for (id in ids) {
|
72
|
+
result.membergroupids.push(Number(ids[id]));
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
result.moderator = _.include(result.membergroupids, 7);
|
77
|
+
|
78
|
+
return result;
|
79
|
+
}
|
80
|
+
};
|
@@ -0,0 +1,332 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
* JSDuck authentication / commenting server side element. Requires Node.js + MongoDB.
|
4
|
+
*
|
5
|
+
* Authentication assumes a vBulletin forum database, but could easily be adapted (see ForumUser.js)
|
6
|
+
*
|
7
|
+
* Expects a config file, config.js, that looks like this:
|
8
|
+
*
|
9
|
+
* exports.db = {
|
10
|
+
* user: 'forumUsername',
|
11
|
+
* password: 'forumPassword',
|
12
|
+
* host: 'forumHost',
|
13
|
+
* dbName: 'forumDb'
|
14
|
+
* };
|
15
|
+
*
|
16
|
+
* exports.sessionSecret = 'random string for session cookie encryption';
|
17
|
+
*
|
18
|
+
* exports.mongoDb = 'mongodb://mongoHost:port/comments';
|
19
|
+
*
|
20
|
+
*/
|
21
|
+
|
22
|
+
config = require('./config');
|
23
|
+
mongoose = require('mongoose');
|
24
|
+
require('./database');
|
25
|
+
require('express-namespace');
|
26
|
+
|
27
|
+
var mysql = require('mysql'),
|
28
|
+
client = mysql.createClient({
|
29
|
+
host: config.db.host,
|
30
|
+
user: config.db.user,
|
31
|
+
password: config.db.password,
|
32
|
+
database: config.db.dbName
|
33
|
+
}),
|
34
|
+
express = require('express'),
|
35
|
+
connect = require('connect'),
|
36
|
+
MongoStore = require('connect-session-mongo'),
|
37
|
+
_ = require('underscore'),
|
38
|
+
ForumUser = require('./ForumUser').ForumUser,
|
39
|
+
forumUser = new ForumUser(client),
|
40
|
+
util = require('./util'),
|
41
|
+
crypto = require('crypto');
|
42
|
+
|
43
|
+
|
44
|
+
var app = express.createServer(
|
45
|
+
|
46
|
+
// Headers for Cross Origin Resource Sharing (CORS)
|
47
|
+
function (req, res, next) {
|
48
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
49
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
50
|
+
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
|
51
|
+
next();
|
52
|
+
},
|
53
|
+
|
54
|
+
express.cookieParser(),
|
55
|
+
|
56
|
+
// Hack to set session cookie if session ID is set as a URL param.
|
57
|
+
// This is because not all browsers support sending cookies via CORS
|
58
|
+
function(req, res, next) {
|
59
|
+
if (req.query.sid) {
|
60
|
+
var sid = req.query.sid.replace(/ /g, '+');
|
61
|
+
req.cookies = req.cookies || {};
|
62
|
+
req.cookies['sencha_docs'] = sid;
|
63
|
+
}
|
64
|
+
next();
|
65
|
+
},
|
66
|
+
|
67
|
+
// Use MongoDB for session storage
|
68
|
+
connect.session({ store: new MongoStore, secret: config.sessionSecret, key: 'sencha_docs' }),
|
69
|
+
|
70
|
+
function(req, res, next) {
|
71
|
+
// IE doesn't get content-type, so default to form encoded.
|
72
|
+
if (!req.headers['content-type']) {
|
73
|
+
req.headers['content-type'] = 'application/x-www-form-urlencoded';
|
74
|
+
}
|
75
|
+
next();
|
76
|
+
},
|
77
|
+
express.bodyParser(),
|
78
|
+
express.methodOverride()
|
79
|
+
);
|
80
|
+
|
81
|
+
app.enable('jsonp callback');
|
82
|
+
|
83
|
+
// All URLs start with /auth
|
84
|
+
app.namespace('/auth', function(){
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Authentication
|
88
|
+
*/
|
89
|
+
|
90
|
+
app.get('/session', function(req, res) {
|
91
|
+
var result = req.session && req.session.user && forumUser.clientUser(req.session.user);
|
92
|
+
res.json(result || false);
|
93
|
+
});
|
94
|
+
|
95
|
+
app.post('/login', function(req, res){
|
96
|
+
|
97
|
+
forumUser.login(req.body.username, req.body.password, function(err, result) {
|
98
|
+
|
99
|
+
if (err) {
|
100
|
+
res.json({ success: false, reason: err });
|
101
|
+
return;
|
102
|
+
}
|
103
|
+
|
104
|
+
req.session = req.session || {};
|
105
|
+
req.session.user = result;
|
106
|
+
|
107
|
+
var response = _.extend(forumUser.clientUser(result), {
|
108
|
+
sessionID: req.sessionID,
|
109
|
+
success: true
|
110
|
+
});
|
111
|
+
|
112
|
+
res.json(response);
|
113
|
+
});
|
114
|
+
});
|
115
|
+
|
116
|
+
// Remove session
|
117
|
+
app.post('/logout', function(req, res){
|
118
|
+
req.session.user = null;
|
119
|
+
res.json({ success: true });
|
120
|
+
});
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Handles comment unsubscription requests.
|
124
|
+
*/
|
125
|
+
app.get('/unsubscribe/:subscriptionId', function(req, res) {
|
126
|
+
|
127
|
+
Subscription.findOne({ _id: req.params.subscriptionId }, function(err, subscription) {
|
128
|
+
if (err) throw(err);
|
129
|
+
|
130
|
+
if (subscription) {
|
131
|
+
if (req.query.all == 'true') {
|
132
|
+
Subscription.remove({ userId: subscription.userId }, function(err) {
|
133
|
+
res.send("You have been unsubscribed from all threads.");
|
134
|
+
});
|
135
|
+
} else {
|
136
|
+
Subscription.remove({ _id: req.params.subscriptionId }, function(err) {
|
137
|
+
res.send("You have been unsubscribed from that thread.");
|
138
|
+
});
|
139
|
+
}
|
140
|
+
} else {
|
141
|
+
res.send("You are already unsubscribed.")
|
142
|
+
}
|
143
|
+
});
|
144
|
+
});
|
145
|
+
|
146
|
+
});
|
147
|
+
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Commenting
|
151
|
+
*/
|
152
|
+
app.namespace('/auth/:sdk/:version', function(){
|
153
|
+
|
154
|
+
/**
|
155
|
+
* Returns a list of comments for a particular target (eg class, guide, video)
|
156
|
+
*/
|
157
|
+
app.get('/comments', function(req, res) {
|
158
|
+
|
159
|
+
Comment.find({
|
160
|
+
target: JSON.parse(req.query.startkey),
|
161
|
+
deleted: { '$ne': true },
|
162
|
+
sdk: req.params.sdk,
|
163
|
+
version: req.params.version
|
164
|
+
}).sort('createdAt', 1).run(function(err, comments){
|
165
|
+
res.json(util.formatComments(comments, req));
|
166
|
+
});
|
167
|
+
});
|
168
|
+
|
169
|
+
/**
|
170
|
+
* Returns 100 most recent comments.
|
171
|
+
*/
|
172
|
+
app.get('/comments_recent', function(req, res) {
|
173
|
+
Comment.find({
|
174
|
+
deleted: { '$ne': true },
|
175
|
+
sdk: req.params.sdk,
|
176
|
+
version: req.params.version
|
177
|
+
}).sort('createdAt', -1).limit(100).run(function(err, comments){
|
178
|
+
res.json(util.formatComments(comments, req));
|
179
|
+
});
|
180
|
+
});
|
181
|
+
|
182
|
+
/**
|
183
|
+
* Returns number of comments for each class / method
|
184
|
+
*/
|
185
|
+
app.get('/comments_meta', util.getCommentsMeta, util.getCommentSubscriptions, function(req, res) {
|
186
|
+
res.send({ comments: req.commentsMeta, subscriptions: req.commentSubscriptions || [] });
|
187
|
+
});
|
188
|
+
|
189
|
+
/**
|
190
|
+
* Returns an individual comment (used when editing a comment)
|
191
|
+
*/
|
192
|
+
app.get('/comments/:commentId', util.findComment, function(req, res) {
|
193
|
+
res.json({ success: true, content: req.comment.content });
|
194
|
+
});
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Creates a new comment
|
198
|
+
*/
|
199
|
+
app.post('/comments', util.requireLoggedInUser, function(req, res) {
|
200
|
+
|
201
|
+
var target = JSON.parse(req.body.target);
|
202
|
+
|
203
|
+
if (target.length === 2) {
|
204
|
+
target.push('');
|
205
|
+
}
|
206
|
+
|
207
|
+
var comment = new Comment({
|
208
|
+
author: req.session.user.username,
|
209
|
+
userId: req.session.user.userid,
|
210
|
+
content: req.body.comment,
|
211
|
+
action: req.body.action,
|
212
|
+
rating: Number(req.body.rating),
|
213
|
+
contentHtml: util.sanitize(req.body.comment),
|
214
|
+
downVotes: [],
|
215
|
+
upVotes: [],
|
216
|
+
createdAt: new Date,
|
217
|
+
target: target,
|
218
|
+
emaiHash: crypto.createHash('md5').update(req.session.user.email).digest("hex"),
|
219
|
+
sdk: req.params.sdk,
|
220
|
+
version: req.params.version,
|
221
|
+
moderator: req.session.user.moderator,
|
222
|
+
title: req.body.title,
|
223
|
+
url: req.body.url
|
224
|
+
});
|
225
|
+
|
226
|
+
comment.save(function(err, response) {
|
227
|
+
res.json({ success: true, id: response._id, action: req.body.action });
|
228
|
+
|
229
|
+
util.sendEmailUpdates(comment);
|
230
|
+
});
|
231
|
+
});
|
232
|
+
|
233
|
+
/**
|
234
|
+
* Updates an existing comment (for voting or updating contents)
|
235
|
+
*/
|
236
|
+
app.post('/comments/:commentId', util.requireLoggedInUser, util.findComment, function(req, res) {
|
237
|
+
|
238
|
+
var voteDirection,
|
239
|
+
comment = req.comment;
|
240
|
+
|
241
|
+
if (req.body.vote) {
|
242
|
+
util.vote(req, res, comment);
|
243
|
+
} else {
|
244
|
+
var canUpdate = _.include(req.session.user.membergroupids, 7) || req.session.user.username == comment.author;
|
245
|
+
|
246
|
+
if (!canUpdate) {
|
247
|
+
res.json({success: false, reason: 'Forbidden'}, 403);
|
248
|
+
return;
|
249
|
+
}
|
250
|
+
|
251
|
+
comment.content = req.body.content;
|
252
|
+
comment.contentHtml = util.sanitize(req.body.content);
|
253
|
+
|
254
|
+
comment.updates = comment.updates || [];
|
255
|
+
comment.updates.push({
|
256
|
+
updatedAt: String(new Date()),
|
257
|
+
author: req.session.user.username
|
258
|
+
});
|
259
|
+
|
260
|
+
comment.save(function(err, response) {
|
261
|
+
res.json({ success: true, content: comment.contentHtml });
|
262
|
+
});
|
263
|
+
}
|
264
|
+
});
|
265
|
+
|
266
|
+
/**
|
267
|
+
* Deletes a comment
|
268
|
+
*/
|
269
|
+
app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, function(req, res) {
|
270
|
+
|
271
|
+
var canDelete = false,
|
272
|
+
comment = req.comment;
|
273
|
+
|
274
|
+
canDelete = _.include(req.session.user.membergroupids, 7) || req.session.user.username == req.comment.author;
|
275
|
+
|
276
|
+
if (!canDelete) {
|
277
|
+
res.json({ success: false, reason: 'Forbidden' }, 403);
|
278
|
+
return;
|
279
|
+
}
|
280
|
+
|
281
|
+
comment.deleted = true;
|
282
|
+
|
283
|
+
comment.save(function(err, response) {
|
284
|
+
res.send({ success: true });
|
285
|
+
});
|
286
|
+
});
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Get email subscriptions
|
290
|
+
*/
|
291
|
+
app.get('/subscriptions', util.getCommentSubscriptions, function(req, res) {
|
292
|
+
res.json({ subscriptions: req.commentSubscriptions });
|
293
|
+
});
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Subscibe / unsubscribe to a comment thread
|
297
|
+
*/
|
298
|
+
app.post('/subscribe', util.requireLoggedInUser, function(req, res) {
|
299
|
+
|
300
|
+
var subscriptionBody = {
|
301
|
+
sdk: req.params.sdk,
|
302
|
+
version: req.params.version,
|
303
|
+
target: JSON.parse(req.body.target),
|
304
|
+
userId: req.session.user.userid
|
305
|
+
};
|
306
|
+
|
307
|
+
Subscription.findOne(subscriptionBody, function(err, subscription) {
|
308
|
+
|
309
|
+
if (subscription && req.body.subscribed == 'false') {
|
310
|
+
|
311
|
+
subscription.remove(function(err, ok) {
|
312
|
+
res.send({ success: true });
|
313
|
+
});
|
314
|
+
|
315
|
+
} else if (!subscription && req.body.subscribed == 'true') {
|
316
|
+
|
317
|
+
subscription = new Subscription(subscriptionBody);
|
318
|
+
subscription.email = req.session.user.email;
|
319
|
+
|
320
|
+
subscription.save(function(err, ok) {
|
321
|
+
res.send({ success: true });
|
322
|
+
});
|
323
|
+
}
|
324
|
+
});
|
325
|
+
});
|
326
|
+
|
327
|
+
});
|
328
|
+
|
329
|
+
|
330
|
+
app.listen(3000);
|
331
|
+
console.log("Server started...");
|
332
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
/**
|
2
|
+
* Defines comment schema and connects to database
|
3
|
+
*/
|
4
|
+
Comment = mongoose.model('Comment', new mongoose.Schema({
|
5
|
+
sdk: String,
|
6
|
+
version: String,
|
7
|
+
|
8
|
+
action: String,
|
9
|
+
author: String,
|
10
|
+
userId: Number,
|
11
|
+
content: String,
|
12
|
+
contentHtml: String,
|
13
|
+
createdAt: Date,
|
14
|
+
downVotes: Array,
|
15
|
+
emailHash: String,
|
16
|
+
rating: Number,
|
17
|
+
target: Array,
|
18
|
+
upVotes: Array,
|
19
|
+
deleted: Boolean,
|
20
|
+
updates: Array,
|
21
|
+
mod: Boolean,
|
22
|
+
title: String,
|
23
|
+
url: String
|
24
|
+
}));
|
25
|
+
|
26
|
+
Subscription = mongoose.model('Subscription', new mongoose.Schema({
|
27
|
+
sdk: String,
|
28
|
+
version: String,
|
29
|
+
|
30
|
+
createdAt: Date,
|
31
|
+
userId: Number,
|
32
|
+
email: String,
|
33
|
+
target: Array
|
34
|
+
}));
|
35
|
+
|
36
|
+
mongoose.connect(config.mongoDb);
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"name": "jsduck_comments",
|
3
|
+
"version": "0.5.0",
|
4
|
+
"description": "Commenting backend for JSDuck Documentation",
|
5
|
+
"author": "Nick Poudlen <nick@sencha.com>",
|
6
|
+
"dependencies": {
|
7
|
+
"connect": "",
|
8
|
+
"connect-session-mongo": "",
|
9
|
+
"express": "",
|
10
|
+
"express-namespace": "",
|
11
|
+
"marked": "",
|
12
|
+
"mongoose": "",
|
13
|
+
"mysql": "",
|
14
|
+
"sanitizer": "",
|
15
|
+
"step": "",
|
16
|
+
"underscore": "",
|
17
|
+
"nodemailer": ""
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
@@ -0,0 +1,245 @@
|
|
1
|
+
|
2
|
+
var marked = require('marked'),
|
3
|
+
_ = require('underscore'),
|
4
|
+
sanitizer = require('sanitizer'),
|
5
|
+
nodemailer = require("nodemailer");
|
6
|
+
|
7
|
+
exports.sanitize = function(content, opts) {
|
8
|
+
|
9
|
+
var markdowned, sanitized_output, urlFunc;
|
10
|
+
|
11
|
+
try {
|
12
|
+
markdowned = marked(content);
|
13
|
+
} catch(e) {
|
14
|
+
markdowned = content;
|
15
|
+
}
|
16
|
+
|
17
|
+
var exp = /(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/igm;
|
18
|
+
markdowned = markdowned.replace(exp, "<a href='$1'>$1</a>");
|
19
|
+
|
20
|
+
if (opts && opts.stripUrls) {
|
21
|
+
urlFunc = function(str) {
|
22
|
+
if (str.match(/^(http:\/\/(www\.)?sencha.com|#))/)) {
|
23
|
+
return str;
|
24
|
+
} else {
|
25
|
+
return '';
|
26
|
+
}
|
27
|
+
};
|
28
|
+
}
|
29
|
+
|
30
|
+
sanitized_output = sanitizer.sanitize(markdowned, urlFunc);
|
31
|
+
sanitized_output = sanitized_output.replace(/'/g, ''');
|
32
|
+
|
33
|
+
return sanitized_output;
|
34
|
+
};
|
35
|
+
|
36
|
+
exports.formatComments = function(comments, req) {
|
37
|
+
|
38
|
+
return _.map(comments, function(comment) {
|
39
|
+
|
40
|
+
comment = _.extend(comment._doc, {
|
41
|
+
score: comment.upVotes.length - comment.downVotes.length,
|
42
|
+
createdAt: String(comment.createdAt)
|
43
|
+
});
|
44
|
+
|
45
|
+
if (req.session.user) {
|
46
|
+
comment.upVote = _.contains(comment.upVotes, req.session.user.username);
|
47
|
+
comment.downVote = _.contains(comment.downVotes, req.session.user.username);
|
48
|
+
}
|
49
|
+
|
50
|
+
return comment;
|
51
|
+
});
|
52
|
+
};
|
53
|
+
|
54
|
+
exports.vote = function(req, res, comment) {
|
55
|
+
|
56
|
+
var voteDirection;
|
57
|
+
|
58
|
+
if (req.session.user.username == comment.author) {
|
59
|
+
|
60
|
+
// Ignore votes from the author
|
61
|
+
res.json({success: false, reason: 'You cannot vote on your own content'});
|
62
|
+
return;
|
63
|
+
|
64
|
+
} else if (req.body.vote == 'up' && !_.include(comment.upVotes, req.session.user.username)) {
|
65
|
+
|
66
|
+
var voted = _.include(comment.downVotes, req.session.user.username);
|
67
|
+
|
68
|
+
comment.downVotes = _.reject(comment.downVotes, function(v) {
|
69
|
+
return v == req.session.user.username;
|
70
|
+
});
|
71
|
+
|
72
|
+
if (!voted) {
|
73
|
+
voteDirection = 'up';
|
74
|
+
comment.upVotes.push(req.session.user.username);
|
75
|
+
}
|
76
|
+
} else if (req.body.vote == 'down' && !_.include(comment.downVotes, req.session.user.username)) {
|
77
|
+
|
78
|
+
var voted = _.include(comment.upVotes, req.session.user.username);
|
79
|
+
|
80
|
+
comment.upVotes = _.reject(comment.upVotes, function(v) {
|
81
|
+
return v == req.session.user.username;
|
82
|
+
});
|
83
|
+
|
84
|
+
if (!voted) {
|
85
|
+
voteDirection = 'down';
|
86
|
+
comment.downVotes.push(req.session.user.username);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
comment.save(function(err, response) {
|
91
|
+
res.json({
|
92
|
+
success: true,
|
93
|
+
direction: voteDirection,
|
94
|
+
total: (comment.upVotes.length - comment.downVotes.length)
|
95
|
+
});
|
96
|
+
});
|
97
|
+
};
|
98
|
+
|
99
|
+
|
100
|
+
exports.requireLoggedInUser = function(req, res, next) {
|
101
|
+
|
102
|
+
if (!req.session || !req.session.user) {
|
103
|
+
res.json({success: false, reason: 'Forbidden'}, 403);
|
104
|
+
} else {
|
105
|
+
next();
|
106
|
+
}
|
107
|
+
};
|
108
|
+
|
109
|
+
exports.findComment = function(req, res, next) {
|
110
|
+
|
111
|
+
if (req.params.commentId) {
|
112
|
+
Comment.findById(req.params.commentId, function(err, comment) {
|
113
|
+
req.comment = comment;
|
114
|
+
next();
|
115
|
+
});
|
116
|
+
} else {
|
117
|
+
res.json({success: false, reason: 'No such comment'});
|
118
|
+
}
|
119
|
+
|
120
|
+
};
|
121
|
+
|
122
|
+
exports.sendEmailUpdates = function(comment) {
|
123
|
+
|
124
|
+
var mailTransport = nodemailer.createTransport("SMTP",{
|
125
|
+
host: 'localhost',
|
126
|
+
port: 25
|
127
|
+
});
|
128
|
+
|
129
|
+
var sendSubscriptionEmail = function(emails) {
|
130
|
+
|
131
|
+
var email = emails.shift();
|
132
|
+
|
133
|
+
if (email) {
|
134
|
+
nodemailer.sendMail(email, function(err){
|
135
|
+
if (err){
|
136
|
+
console.log(err);
|
137
|
+
} else{
|
138
|
+
console.log("Sent email to " + email.to);
|
139
|
+
sendSubscriptionEmail(emails);
|
140
|
+
}
|
141
|
+
});
|
142
|
+
} else {
|
143
|
+
console.log("Finished sending emails");
|
144
|
+
mailTransport.close();
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
var subscriptionBody = {
|
149
|
+
sdk: comment.sdk,
|
150
|
+
version: comment.version,
|
151
|
+
target: comment.target
|
152
|
+
};
|
153
|
+
|
154
|
+
var emails = [];
|
155
|
+
|
156
|
+
Subscription.find(subscriptionBody, function(err, subscriptions) {
|
157
|
+
|
158
|
+
_.each(subscriptions, function(subscription) {
|
159
|
+
var mailOptions = {
|
160
|
+
transport: mailTransport,
|
161
|
+
from: "Sencha Documentation <no-reply@sencha.com>",
|
162
|
+
to: subscription.email,
|
163
|
+
subject: "Comment on '" + comment.title + "'",
|
164
|
+
text: [
|
165
|
+
"A comment by " + comment.author + " on '" + comment.title + "' was posted on the Sencha Documentation:\n",
|
166
|
+
comment.content + "\n",
|
167
|
+
"--",
|
168
|
+
"Original thread: " + comment.url,
|
169
|
+
"Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id,
|
170
|
+
"Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true'
|
171
|
+
].join("\n")
|
172
|
+
}
|
173
|
+
|
174
|
+
if (Number(comment.userId) != Number(subscription.userId)) {
|
175
|
+
emails.push(mailOptions);
|
176
|
+
}
|
177
|
+
});
|
178
|
+
|
179
|
+
if (emails.length) {
|
180
|
+
sendSubscriptionEmail(emails);
|
181
|
+
} else {
|
182
|
+
console.log("No emails to send");
|
183
|
+
}
|
184
|
+
});
|
185
|
+
}
|
186
|
+
|
187
|
+
|
188
|
+
exports.getCommentsMeta = function(req, res, next) {
|
189
|
+
|
190
|
+
var map = function() {
|
191
|
+
if (this.target) {
|
192
|
+
emit(this.target.slice(0,3).join('__'), 1);
|
193
|
+
} else {
|
194
|
+
return;
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
var reduce = function(key, values) {
|
199
|
+
var i = 0, total = 0;
|
200
|
+
|
201
|
+
for (; i< values.length; i++) {
|
202
|
+
total += values[i];
|
203
|
+
}
|
204
|
+
|
205
|
+
return total;
|
206
|
+
}
|
207
|
+
|
208
|
+
mongoose.connection.db.executeDbCommand({
|
209
|
+
mapreduce: 'comments',
|
210
|
+
map: map.toString(),
|
211
|
+
reduce: reduce.toString(),
|
212
|
+
out: 'commentCounts',
|
213
|
+
query: {
|
214
|
+
deleted: { '$ne': true },
|
215
|
+
sdk: req.params.sdk,
|
216
|
+
version: req.params.version
|
217
|
+
}
|
218
|
+
}, function(err, dbres) {
|
219
|
+
mongoose.connection.db.collection('commentCounts', function(err, collection) {
|
220
|
+
collection.find({}).toArray(function(err, comments) {
|
221
|
+
req.commentsMeta = comments;
|
222
|
+
next()
|
223
|
+
});
|
224
|
+
});
|
225
|
+
});
|
226
|
+
}
|
227
|
+
|
228
|
+
exports.getCommentSubscriptions = function(req, res, next) {
|
229
|
+
if (req.session.user) {
|
230
|
+
Subscription.find({
|
231
|
+
sdk: req.params.sdk,
|
232
|
+
version: req.params.version,
|
233
|
+
userId: req.session.user.userid
|
234
|
+
}, function(err, subscriptions) {
|
235
|
+
req.commentSubscriptions = _.map(subscriptions, function(subscription) {
|
236
|
+
return subscription.target;
|
237
|
+
});
|
238
|
+
next();
|
239
|
+
})
|
240
|
+
} else {
|
241
|
+
next();
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsduck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 3
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 3.
|
8
|
+
- 7
|
9
|
+
- 0
|
10
|
+
version: 3.7.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Rene Saarsoo
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2012-02-
|
19
|
+
date: 2012-02-29 00:00:00 +02:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -150,6 +150,12 @@ files:
|
|
150
150
|
- lib/jsduck/type_parser.rb
|
151
151
|
- lib/jsduck/videos.rb
|
152
152
|
- lib/jsduck/welcome.rb
|
153
|
+
- opt/comments-server-side/.gitignore
|
154
|
+
- opt/comments-server-side/ForumUser.js
|
155
|
+
- opt/comments-server-side/app.js
|
156
|
+
- opt/comments-server-side/database.js
|
157
|
+
- opt/comments-server-side/package.json
|
158
|
+
- opt/comments-server-side/util.js
|
153
159
|
- opt/example.js
|
154
160
|
- template-min/README.md
|
155
161
|
- template-min/template.html
|
@@ -213,8 +219,6 @@ files:
|
|
213
219
|
- template-min/app.js
|
214
220
|
- template-min/print-template.html
|
215
221
|
- template-min/favicon.ico
|
216
|
-
- template-min/extjs/bootstrap.js
|
217
|
-
- template-min/extjs/ext-all-debug.js
|
218
222
|
- template-min/extjs/resources/themes/images/default/tab/tab-default-bottom-sides.gif
|
219
223
|
- template-min/extjs/resources/themes/images/default/tab/tab-default-top-disabled-corners.gif
|
220
224
|
- template-min/extjs/resources/themes/images/default/tab/tab-default-top-active-sides.gif
|
@@ -476,12 +480,20 @@ files:
|
|
476
480
|
- template-min/extjs/resources/themes/images/default/form/text-bg.gif
|
477
481
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-bottom-corners.gif
|
478
482
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-right-sides.gif
|
483
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-bottom-corners.gif
|
484
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-right-sides.gif
|
479
485
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-top-sides.gif
|
486
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-left-sides.gif
|
480
487
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-top-corners.gif
|
481
488
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-left-corners.gif
|
489
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-right-corners.gif
|
490
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-top-corners.gif
|
482
491
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-left-sides.gif
|
492
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-bottom-sides.gif
|
493
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-left-corners.gif
|
483
494
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-right-corners.gif
|
484
495
|
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-bottom-sides.gif
|
496
|
+
- template-min/extjs/resources/themes/images/default/window-header/window-header-default-collapsed-top-sides.gif
|
485
497
|
- template-min/extjs/resources/themes/images/default/tab-bar/scroll-left.gif
|
486
498
|
- template-min/extjs/resources/themes/images/default/tab-bar/tab-bar-default-bg.gif
|
487
499
|
- template-min/extjs/resources/themes/images/default/tab-bar/scroll-right.gif
|
@@ -577,7 +589,6 @@ files:
|
|
577
589
|
- template-min/extjs/resources/themes/images/default/slider/slider-v-bg.png
|
578
590
|
- template-min/extjs/resources/themes/images/default/slider/slider-v-bg.gif
|
579
591
|
- template-min/extjs/resources/themes/images/default/slider/slider-thumb.png
|
580
|
-
- template-min/extjs/resources/css/ext-all.css
|
581
592
|
- template-min/extjs/ext-all.js
|
582
593
|
- template-min/build-js.html
|
583
594
|
- template-min/index.php
|