sequenceserver 0.6.7

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.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

@@ -0,0 +1,525 @@
1
+ # sequenceserver.rb
2
+
3
+ # ensure 'lib/' is in the load path
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+
6
+ require 'rubygems'
7
+ require 'sinatra/base'
8
+ require 'yaml'
9
+ require 'logger'
10
+ require 'fileutils'
11
+ require 'sequenceserver/helpers'
12
+ require 'sequenceserver/blast'
13
+ require 'sequenceserver/sequencehelpers'
14
+ require 'sequenceserver/sinatralikeloggerformatter'
15
+ require 'sequenceserver/customisation'
16
+
17
+ # Helper module - initialize the blast server.
18
+ module SequenceServer
19
+ class App < Sinatra::Base
20
+ include Helpers
21
+ include SequenceHelpers
22
+ include SequenceServer::Customisation
23
+
24
+ # Basic configuration settings for app.
25
+ configure do
26
+ # enable some builtin goodies
27
+ enable :session, :logging
28
+
29
+ # main application file
30
+ set :app_file, File.expand_path(__FILE__)
31
+
32
+ # app root is SequenceServer's installation directory
33
+ #
34
+ # SequenceServer figures out different settings, location of static
35
+ # assets or templates for example, based on app root.
36
+ set :root, File.dirname(File.dirname(app_file))
37
+
38
+ # path to test database
39
+ #
40
+ # SequenceServer ships with test database (fire ant genome) so users can
41
+ # launch and preview SequenceServer without any configuration, and/or run
42
+ # test suite.
43
+ set :test_database, File.join(root, 'tests', 'database')
44
+
45
+ # path to example configuration file
46
+ #
47
+ # SequenceServer ships with a dummy configuration file. Users can simply
48
+ # copy it and make necessary changes to get started.
49
+ set :example_config_file, File.join(root, 'example.config.yml')
50
+
51
+ # path to SequenceServer's configuration file
52
+ #
53
+ # The configuration file is a simple, YAML data store.
54
+ set :config_file, Proc.new{ File.expand_path('~/.sequenceserver.yml') }
55
+
56
+ set :log, Proc.new { Logger.new(STDERR) }
57
+ log.formatter = SinatraLikeLogFormatter.new()
58
+ end
59
+
60
+ # Local, app configuration settings derived from config.yml.
61
+ #
62
+ # A config.yml should contain the settings described in the following
63
+ # configure block as key, value pairs. See example.config.yml in the
64
+ # installation directory.
65
+ configure do
66
+ # store the settings hash from config.yml; further configuration values
67
+ # are derived from it
68
+ set :config, {}
69
+
70
+ # absolute path to the blast binaries
71
+ #
72
+ # A default of 'nil' is indicative of blast binaries being present in
73
+ # system PATH.
74
+ set :bin, Proc.new{ File.expand_path(config['bin']) rescue nil }
75
+
76
+ # absolute path to the database directory
77
+ #
78
+ # As a default use 'database' directory relative to current working
79
+ # directory of the running app.
80
+ set :database, Proc.new{ File.expand_path(config['database']) rescue test_database }
81
+
82
+ # the port number to run Sequence Server standalone
83
+ set :port, Proc.new{ (config['port'] or 4567).to_i }
84
+
85
+ # number of threads to be used during blasting
86
+ #
87
+ # This option is passed directly to BLAST+. We use a default value of 1
88
+ # as a higher value may cause BLAST+ to crash if it was not compiled with
89
+ # threading support.
90
+ set :num_threads, Proc.new{ (config['num_threads'] or 1).to_i }
91
+ end
92
+
93
+ # Lookup tables used by Sequence Server to pick up the right blast binary,
94
+ # or database. These tables should be populated during app initialization
95
+ # by scanning bin, and database directories.
96
+ configure do
97
+ # blast methods (executables) and their corresponding absolute path
98
+ set :binaries, {}
99
+
100
+ # list of sorted blast databases grouped by databse type:
101
+ # 'protein', or 'nucleotide'
102
+ set :databases, {}
103
+ end
104
+
105
+ configure :development do
106
+ log.level = Logger::DEBUG
107
+ end
108
+
109
+ configure(:production) do
110
+ log.level = Logger::INFO
111
+ error do
112
+ erb :'500'
113
+ end
114
+ not_found do
115
+ erb :'500'
116
+ end
117
+ end
118
+
119
+ class << self
120
+ # Run SequenceServer as a self-hosted server.
121
+ #
122
+ # By default SequenceServer uses Thin, Mongrel or WEBrick (in that
123
+ # order). This can be configured by setting the 'server' option.
124
+ def run!(options={})
125
+ set options
126
+
127
+ # perform SequenceServer initializations
128
+ puts "\n== Initializing SequenceServer..."
129
+ init
130
+
131
+ # find out the what server to host SequenceServer with
132
+ handler = detect_rack_handler
133
+ handler_name = handler.name.gsub(/.*::/, '')
134
+
135
+ puts
136
+ log.info("Using #{handler_name} web server.")
137
+
138
+ if handler_name == 'WEBrick'
139
+ puts "\n== We recommend using Thin web server for better performance."
140
+ puts "== To install Thin: [sudo] gem install thin"
141
+ end
142
+
143
+ url = "http://#{bind}:#{port}"
144
+ puts "\n== Launched SequenceServer at: #{url}"
145
+ puts "== Press CTRL + C to quit."
146
+ handler.run(self, :Host => bind, :Port => port, :Logger => Logger.new('/dev/null')) do |server|
147
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler) } }
148
+ set :running, true
149
+
150
+ # for Thin
151
+ server.silent = true if handler_name == 'Thin'
152
+ end
153
+ rescue Errno::EADDRINUSE, RuntimeError => e
154
+ puts "\n== Failed to start SequenceServer."
155
+ puts "== Is SequenceServer already running at: #{url}"
156
+ end
157
+
158
+ # Stop SequenceServer.
159
+ def quit!(server, handler_name)
160
+ # Use Thin's hard #stop! if available, otherwise just #stop.
161
+ server.respond_to?(:stop!) ? server.stop! : server.stop
162
+ puts "\n== Thank you for using SequenceServer :)." +
163
+ "\n== Please cite: " +
164
+ "\n== Priyam A., Woodcroft B.J., Wurm Y (in prep)." +
165
+ "\n== Sequenceserver: BLAST searching made easy." unless handler_name =~/cgi/i
166
+ end
167
+
168
+ # Initializes the blast server : executables, database. Exit if blast
169
+ # executables, and databses can not be found. Logs the result if logging
170
+ # has been enabled.
171
+ def init
172
+ # first read the user supplied configuration options
173
+ self.config = parse_config
174
+
175
+ # empty config file
176
+ unless config
177
+ log.warn("Empty configuration file: #{config_file} - will assume default settings")
178
+ self.config = {}
179
+ end
180
+
181
+ # scan for blast binaries
182
+ self.binaries = scan_blast_executables(bin).freeze
183
+
184
+ # Log the discovery of binaries.
185
+ binaries.each do |command, path|
186
+ log.info("Found #{command} at #{path}")
187
+ end
188
+
189
+ # scan for blast database
190
+ self.databases = scan_blast_db(database, binaries['blastdbcmd']).freeze
191
+
192
+ # Log the discovery of databases.
193
+ databases.each do |type, dbs|
194
+ dbs.each do |d|
195
+ log.info("Found #{type} database: #{d.title} at #{d.name}")
196
+ end
197
+ end
198
+ rescue IOError => error
199
+ log.fatal("Fail: #{error}")
200
+ exit
201
+ rescue ArgumentError => error
202
+ log.fatal("Error in config.yml: #{error}")
203
+ puts "YAML is white space sensitive. Is your config.yml properly indented?"
204
+ exit
205
+ rescue Errno::ENOENT # config file not found
206
+ log.info('Configuration file not found')
207
+ FileUtils.cp(example_config_file, config_file)
208
+ log.info('Generated a dummy configuragion file')
209
+ puts "\nPlease edit #{config_file} to indicate the location of your BLAST databases and run SequenceServer again."
210
+ exit
211
+ end
212
+
213
+ # Parse config.yml, and return the resulting hash.
214
+ #
215
+ # This method uses YAML.load_file to read config.yml. Absence of a
216
+ # config.yml is safely ignored as the app should then fall back on
217
+ # default configuration values. Any other error raised by YAML.load_file
218
+ # is not rescued.
219
+ def parse_config
220
+ YAML.load_file( config_file )
221
+ end
222
+ end
223
+
224
+ get '/' do
225
+ erb :search
226
+ end
227
+
228
+ post '/' do
229
+ method = params['method']
230
+ db_type_param = params['db']
231
+ sequence = params[:sequence]
232
+
233
+ # evaluate empty sequence as nil, otherwise as fasta
234
+ sequence = sequence.empty? ? nil : to_fasta(sequence)
235
+
236
+ if request.xhr?
237
+ return (sequence && type_of_sequences(sequence)).to_s
238
+ end
239
+
240
+ # Raise ArgumentError if there is no database selected
241
+ if db_type_param.nil?
242
+ raise ArgumentError, "No BLAST database selected"
243
+ end
244
+ db_type = db_type_param.first.first
245
+
246
+ # can not proceed if one of these is missing
247
+ raise ArgumentError unless sequence and db_type and method
248
+ settings.log.info("requested #{method} against #{db_type.to_s} database")
249
+
250
+ # only allowed blast methods should be used
251
+ blast_methods = %w|blastn blastp blastx tblastn tblastx|
252
+ raise ArgumentError, "wrong method: #{method}" unless blast_methods.include?(method)
253
+
254
+ # check if input_fasta is compatible with the selected blast method
255
+ sequence_type = type_of_sequences(sequence)
256
+ settings.log.debug('sequence: ' + sequence)
257
+ settings.log.debug('input seq type: ' + sequence_type.to_s)
258
+ settings.log.debug('blast db type: ' + db_type)
259
+ settings.log.debug('blast method: ' + method)
260
+
261
+ unless blast_methods_for(sequence_type).include?(method)
262
+ raise ArgumentError, "Cannot #{method} a #{sequence_type} query."
263
+ end
264
+
265
+ # check if selected db is comaptible with the selected blast method
266
+ allowed_db_type = db_type_for(method)
267
+ unless allowed_db_type.to_s == db_type
268
+ raise ArgumentError, "Cannot #{method} against a #{db_type} database.
269
+ Need #{allowed_db_type} database."
270
+ end
271
+
272
+ advanced_opts = params['advanced']
273
+ validate_advanced_parameters(advanced_opts) #check the advanced options are sensible
274
+
275
+ # blastn implies blastn, not megablast
276
+ advanced_opts << '-task blastn ' if method == 'blastn'
277
+
278
+ method = settings.binaries[ method ]
279
+ settings.log.debug('settings.databases: ' + settings.databases.inspect)
280
+ databases = params['db'][db_type].map{|index|
281
+ settings.databases[db_type][index.to_i].name
282
+ }
283
+
284
+ # run blast to blast archive
285
+ blast = Blast.blast_string_to_blast_archive(method, databases.join(' '), sequence, advanced_opts)
286
+ # log the command that was run
287
+ settings.log.info('Ran to blast archive: ' + blast.command) if settings.logging
288
+
289
+ # convert blast archive to HTML version
290
+ blast.convert_blast_archive_to_html_result(settings.binaries['blast_formatter'])
291
+ # log the command that was run
292
+ settings.log.info('Ran to get HTML output: ' + blast.command) if settings.logging
293
+
294
+ @blast = format_blast_results(blast.result, databases)
295
+
296
+ erb :search
297
+ end
298
+
299
+ # get '/get_sequence/?id=sequence_ids&db=retreival_databases'
300
+ #
301
+ # Use whitespace to separate entries in sequence_ids (all other chars exist
302
+ # in identifiers) and retreival_databases (we don't allow whitespace in a
303
+ # database's name, so it's safe).
304
+ get '/get_sequence/' do
305
+ sequenceids = params[:id].split(/\s/).uniq # in a multi-blast
306
+ # query some may have been found multiply
307
+ retrieval_databases = params[:db].split(/\s/)
308
+
309
+ settings.log.info("Looking for: '#{sequenceids.join(', ')}' in '#{retrieval_databases.join(', ')}'")
310
+
311
+ # the results do not indicate which database a hit is from.
312
+ # Thus if several databases were used for blasting, we must check them all
313
+ # if it works, refactor with "inject" or "collect"?
314
+ found_sequences = ''
315
+
316
+ retrieval_databases.each do |database| # we need to populate this session variable from the erb.
317
+ sequence = sequence_from_blastdb(sequenceids, database)
318
+ if sequence.empty?
319
+ settings.log.debug("'#{sequenceids.join(', ')}' not found in #{database}")
320
+ else
321
+ found_sequences += sequence
322
+ end
323
+ end
324
+
325
+ found_sequences_count = found_sequences.count('>')
326
+
327
+ out = ''
328
+ # just in case, checking we found right number of sequences
329
+ if found_sequences_count != sequenceids.length
330
+ out <<<<HEADER
331
+ <h1>ERROR: incorrect number of sequences found.</h1>
332
+ <p>Dear user,</p>
333
+
334
+ <p><strong>We have found
335
+ <em>#{found_sequences_count > sequenceids.length ? 'more' : 'less'}</em>
336
+ sequence than expected.</strong></p>
337
+
338
+ <p>This is likely due to a problem with how databases are formatted.
339
+ <strong>Please share this text with the person managing this website so
340
+ they can resolve the issue.</strong></p>
341
+
342
+ <p> You requested #{sequenceids.length} sequence#{sequenceids.length > 1 ? 's' : ''}
343
+ with the following identifiers: <code>#{sequenceids.join(', ')}</code>,
344
+ from the following databases: <code>#{retrieval_databases.join(', ')}</code>.
345
+ But we found #{found_sequences_count} sequence#{found_sequences_count> 1 ? 's' : ''}.
346
+ </p>
347
+
348
+ <p>If sequences were retrieved, you can find them below (but some may be incorrect, so be careful!).</p>
349
+ <hr/>
350
+ HEADER
351
+ end
352
+
353
+ out << "<pre><code>#{found_sequences}</pre></code>"
354
+ out
355
+ end
356
+
357
+ # Ensure a '>sequence_identifier\n' at the start of a sequence.
358
+ #
359
+ # An empty query line appears in the blast report if the leading
360
+ # '>sequence_identifier\n' in the sequence is missing. We prepend
361
+ # the input sequence with user info in such case.
362
+ #
363
+ # > to_fasta("acgt")
364
+ # => 'Submitted_By_127.0.0.1_at_110214-15:33:34\nacgt'
365
+ def to_fasta(sequence)
366
+ sequence.lstrip!
367
+ if sequence[0,1] != '>'
368
+ ip = request.ip.to_s
369
+ time = Time.now.strftime("%y%m%d-%H:%M:%S")
370
+ sequence.insert(0, ">Submitted_By_#{ip}_at_#{time}\n")
371
+ end
372
+ return sequence
373
+ end
374
+
375
+ def format_blast_results(result, databases)
376
+ raise ArgumentError, 'Problem: empty result! Maybe your query was invalid?' if !result.class == String
377
+ raise ArgumentError, 'Problem: empty result! Maybe your query was invalid?' if result.empty?
378
+
379
+ formatted_result = ''
380
+ @all_retrievable_ids = []
381
+ string_of_used_databases = databases.join(' ')
382
+ blast_database_number = 0
383
+ line_number = 0
384
+ started_query = false
385
+ finished_database_summary = false
386
+ finished_alignments = false
387
+ reference_string = ''
388
+ database_summary_string = ''
389
+ result.each do |line|
390
+ line_number += 1
391
+ next if line_number <= 5 #skip the first 5 lines
392
+
393
+ # Add the reference to the end, not the start, of the blast result
394
+ if line_number >= 7 and line_number <= 15
395
+ reference_string += line
396
+ next
397
+ end
398
+
399
+ if !finished_database_summary and line_number > 15
400
+ database_summary_string += line
401
+ finished_database_summary = true if line.match(/total letters/)
402
+ next
403
+ end
404
+
405
+ # Remove certain lines from the output
406
+ skipped_lines = [/^<\/BODY>/,/^<\/HTML>/,/^<\/PRE>/]
407
+ skip = false
408
+ skipped_lines.each do |skippy|
409
+ # $stderr.puts "`#{line}' matches #{skippy}?"
410
+ if skippy.match(line)
411
+ skip = true
412
+ # $stderr.puts 'yes'
413
+ else
414
+ # $stderr.puts 'no'
415
+ end
416
+ end
417
+ next if skip
418
+
419
+ # Remove the javascript inclusion
420
+ line.gsub!(/^<script src=\"blastResult.js\"><\/script>/, '')
421
+
422
+ if line.match(/^>/) # If line to possibly replace
423
+ # Reposition the anchor to the end of the line, so that it both still works and
424
+ # doesn't interfere with the diagnostic space at the beginning of the line.
425
+ #
426
+ # There are two cases:
427
+ #
428
+ # database formatted _with_ -parse_seqids
429
+ line.gsub!(/^>(.+)(<a.*><\/a>)(.*)/, '>\1\3\2')
430
+ #
431
+ # database formatted _without_ -parse_seqids
432
+ line.gsub!(/^>(<a.*><\/a>)(.*)/, '>\2\1')
433
+
434
+ # get hit coordinates -- useful for linking to genome browsers
435
+ hit_length = result[line_number..-1].index{|l| l =~ />lcl|Lambda/}
436
+ hit_coordinates = result[line_number, hit_length].grep(/Sbjct/).
437
+ map(&:split).map{|l| [l[1], l[-1]]}.flatten.map(&:to_i).minmax
438
+
439
+ # Create the hyperlink (if required)
440
+ formatted_result += construct_sequence_hyperlink_line(line, databases, hit_coordinates)
441
+ else
442
+ # Surround each query's result in <div> tags so they can be coloured by CSS
443
+ if matches = line.match(/^<b>Query=<\/b> (.*)/) # If starting a new query, then surround in new <div> tag, and finish the last one off
444
+ line = "<div class=\"resultn\" id=\"#{matches[1]}\">\n<h3>Query= #{matches[1]}</h3><pre>"
445
+ unless blast_database_number == 0
446
+ line = "</pre></div>\n#{line}"
447
+ end
448
+ blast_database_number += 1
449
+ elsif line.match(/^ Database: /) and !finished_alignments
450
+ formatted_result += "</div>\n<pre>#{database_summary_string}\n\n"
451
+ finished_alignments = true
452
+ end
453
+ formatted_result += line
454
+ end
455
+ end
456
+ formatted_result << "</pre>"
457
+
458
+ link_to_fasta_of_all = "/get_sequence/?id=#{@all_retrievable_ids.join(' ')}&db=#{string_of_used_databases}"
459
+ # #dbs must be sep by ' '
460
+ retrieval_text = @all_retrievable_ids.empty? ? '' : "<a href='#{url(link_to_fasta_of_all)}'>FASTA of #{@all_retrievable_ids.length} retrievable hit(s)</a>"
461
+
462
+ "<h2>Results</h2>"+
463
+ retrieval_text +
464
+ "<br/><br/>" +
465
+ formatted_result +
466
+ "<br/>" +
467
+ "<pre>#{reference_string.strip}</pre>"
468
+ end
469
+
470
+ def construct_sequence_hyperlink_line(line, databases, hit_coordinates)
471
+ matches = line.match(/^>(.+)/)
472
+ sequence_id = matches[1]
473
+
474
+ link = nil
475
+
476
+ # If a custom sequence hyperlink method has been defined,
477
+ # use that.
478
+ options = {
479
+ :sequence_id => sequence_id,
480
+ :databases => databases,
481
+ :hit_coordinates => hit_coordinates
482
+ }
483
+
484
+ # First precedence: construct the whole line to be customised
485
+ if self.respond_to?(:construct_custom_sequence_hyperlinking_line)
486
+ settings.log.debug("Using custom hyperlinking line creator with sequence #{options.inspect}")
487
+ link_line = construct_custom_sequence_hyperlinking_line(options)
488
+ unless link_line.nil?
489
+ return link_line
490
+ end
491
+ end
492
+
493
+ # If we have reached here, custom construction of the
494
+ # whole line either wasn't defined, or returned nil
495
+ # (indicating failure)
496
+ if self.respond_to?(:construct_custom_sequence_hyperlink)
497
+ settings.log.debug("Using custom hyperlink creator with sequence #{options.inspect}")
498
+ link = construct_custom_sequence_hyperlink(options)
499
+ else
500
+ settings.log.debug("Using standard hyperlink creator with sequence `#{options.inspect}'")
501
+ link = construct_standard_sequence_hyperlink(options)
502
+ end
503
+
504
+ # Return the BLAST output line with the link in it
505
+ if link.nil?
506
+ settings.log.debug('No link added link for: `'+ sequence_id +'\'')
507
+ return line
508
+ else
509
+ settings.log.debug('Added link for: `'+ sequence_id +'\''+ link)
510
+ return "><a href='#{url(link)}'>#{sequence_id}</a> \n"
511
+ end
512
+
513
+ end
514
+
515
+ # Advanced options are specified by the user. Here they are checked for interference with SequenceServer operations.
516
+ # raise ArgumentError if an error has occurred, otherwise return without value
517
+ def validate_advanced_parameters(advanced_options)
518
+ raise ArgumentError, "Invalid characters detected in the advanced options" unless advanced_options =~ /\A[a-z0-9\-_\. ']*\Z/i
519
+ disallowed_options = %w(-out -html -outfmt -db -query)
520
+ disallowed_options.each do |o|
521
+ raise ArgumentError, "The advanced BLAST option \"#{o}\" is used internally by SequenceServer and so cannot be specified by the you" if advanced_options =~ /#{o}/i
522
+ end
523
+ end
524
+ end
525
+ end
@@ -0,0 +1,99 @@
1
+
2
+ function finalSubmit(dbIsNt, targetForm, inputName, submitFormName, dbType){
3
+ var formArr=targetForm.split(":");
4
+ var idArray=new Array();
5
+ if(dbType == 0){
6
+ if(dbIsNt==1){
7
+ document.forms[submitFormName].db.value="Nucleotide";
8
+ }
9
+ else{
10
+ document.forms[submitFormName].db.value="Protein";
11
+ }
12
+ }
13
+ for(j=0; j<formArr.length; j++){
14
+ for(var i=0; i<document.forms[formArr[j]].elements.length; i++){
15
+ var theElem=document.forms[formArr[j]].elements[i];
16
+ if(theElem.type=="checkbox"&&theElem.name==inputName&&theElem.checked){
17
+ if(!isIdIn(theElem.value, idArray)){
18
+
19
+ idArray[idArray.length]=theElem.value;
20
+ }
21
+ }
22
+ }
23
+ if(dbType == 0){
24
+ document.forms[submitFormName].term.value="";
25
+ for(i=0; i<idArray.length; i++){
26
+ if(i ==0 ) {
27
+ document.forms[submitFormName].term.value += idArray[i];
28
+ } else {
29
+ document.forms[submitFormName].term.value += "," + idArray[i];
30
+ }
31
+ }
32
+ } else if (dbType == 1){
33
+ document.forms[submitFormName].val.value="";
34
+ for(i=0; i<idArray.length; i++){
35
+ if(i ==0 ) {
36
+ document.forms[submitFormName].val.value += idArray[i];
37
+ } else {
38
+ document.forms[submitFormName].val.value += "," + idArray[i];
39
+ }
40
+ }
41
+ }
42
+ }
43
+ document.forms[submitFormName].submit();
44
+ }
45
+
46
+ function isIdIn(id, idArray){
47
+ var idSeen=false;
48
+
49
+ for(i=0; i<idArray.length; i++){
50
+ if(id==idArray[i]){
51
+ idSeen=true;
52
+ break;
53
+ }
54
+ }
55
+ return idSeen;
56
+ }
57
+
58
+ function handleCheckAll(mode, targetForm, inputName){
59
+
60
+ var formArr=targetForm.split(":");
61
+ for(j=0; j<formArr.length; j++){
62
+ for(var i=0; i<document.forms[formArr[j]].elements.length; i++){
63
+ var theElem=document.forms[formArr[j]].elements[i];
64
+ if(theElem.type=="checkbox"&&theElem.name==inputName){
65
+ if(mode=="select"){
66
+ theElem.checked=true;
67
+ }
68
+ else if(mode=="deselect"){
69
+ theElem.checked=false;
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ }
76
+
77
+ function synchronizeCheck(id, formName, inputName, isChecked){
78
+
79
+ for(var i=0; i<document.forms[formName].elements.length; i++){
80
+ var theElem=document.forms[formName].elements[i];
81
+ if(theElem.type=="checkbox"&&theElem.name==inputName&&id==theElem.value){
82
+ theElem.checked=isChecked;
83
+
84
+ }
85
+ }
86
+
87
+ }
88
+
89
+
90
+ function uncheckable(formName, inputName){
91
+ for(var i=0; i<document.forms[formName].elements.length; i++){
92
+ var theElem=document.forms[formName].elements[i];
93
+ if(theElem.type=="checkbox"&&theElem.name==inputName){
94
+ theElem.checked=0;
95
+
96
+ }
97
+ }
98
+
99
+ }