jsduck 3.6.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|