sequenceserver 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }