1 """GNUmed authentication widgets.
2
3 This module contains widgets and GUI
4 functions for authenticating users.
5 """
6
7
8
9 __version__ = "$Revision: 1.45 $"
10 __author__ = "karsten.hilbert@gmx.net, H.Herb, H.Berger, R.Terry"
11 __license__ = "GPL (details at http://www.gnu.org)"
12
13
14
15 import sys, os.path, cPickle, zlib, logging
16
17
18
19 import wx
20
21
22
23 if __name__ == '__main__':
24 sys.path.insert(0, '../../')
25 from Gnumed.pycommon import gmLoginInfo, gmPG2, gmBackendListener, gmTools, gmCfg2, gmI18N
26 from Gnumed.business import gmSurgery
27 from Gnumed.wxpython import gmGuiHelpers, gmExceptionHandlingWidgets
28
29
30 _log = logging.getLogger('gm.ui')
31 _log.info(__version__)
32 _cfg = gmCfg2.gmCfgData()
33
34 try:
35 _('dummy-no-need-to-translate-but-make-epydoc-happy')
36 except NameError:
37 _ = lambda x:x
38
39
40 msg_generic = _("""
41 GNUmed database version mismatch.
42
43 This database version cannot be used with this client:
44
45 client version: %s
46 database version detected: %s
47 database version needed: %s
48
49 Currently connected to database:
50
51 host: %s
52 database: %s
53 user: %s
54 """)
55
56 msg_time_skew_fail = _("""\
57 The server and client clocks are off
58 by more than %s minutes !
59
60 You must fix the time settings before
61 you can use this database with this
62 client.
63
64 You may have to contact your
65 administrator for help.""")
66
67 msg_time_skew_warn = _("""\
68 The server and client clocks are off
69 by more than %s minutes !
70
71 You should fix the time settings.
72 Otherwise clinical data may appear to
73 have been entered at the wrong time.
74
75 You may have to contact your
76 administrator for help.""")
77
78 msg_insanity = _("""
79 There is a serious problem with the database settings:
80
81 %s
82
83 You may have to contact your administrator for help.""")
84
85 msg_fail = _("""
86 You must connect to a different database in order
87 to use the GNUmed client. You may have to contact
88 your administrator for help.""")
89
90 msg_override = _("""
91 The client will, however, continue to start up because
92 you are running a development/test version of GNUmed.
93
94 There may be schema related errors. Please report and/or
95 fix them. Do not rely on this database to work properly
96 in all cases !""")
97
98
99
100
102 """Display the login dialog and try to log into the backend.
103
104 - up to max_attempts times
105 - returns True/False
106 """
107
108 expected_hash = gmPG2.known_schema_hashes[expected_version]
109 client_version = _cfg.get(option = u'client_version')
110 global current_db_name
111 current_db_name = u'gnumed_%s' % expected_version
112
113 attempt = 0
114
115 dlg = cLoginDialog(None, -1, client_version = client_version)
116 dlg.Centre(wx.BOTH)
117
118 while attempt < max_attempts:
119
120 _log.debug('login attempt %s of %s', (attempt+1), max_attempts)
121
122 connected = False
123
124 dlg.ShowModal()
125 login = dlg.panel.GetLoginInfo()
126 if login is None:
127 _log.info("user cancelled login dialog")
128 break
129
130
131 dsn = gmPG2.make_psycopg2_dsn (
132 database = login.database,
133 host = login.host,
134 port = login.port,
135 user = login.user,
136 password = login.password
137 )
138 try:
139 conn = gmPG2.get_raw_connection(dsn = dsn, verbose = True, readonly = True)
140 connected = True
141
142 except gmPG2.cAuthenticationError, e:
143 attempt += 1
144 _log.error(u"login attempt failed: %s", e)
145 if attempt < max_attempts:
146 gmGuiHelpers.gm_show_error (_(
147 "Unable to connect to database:\n\n"
148 "%s\n\n"
149 "Please retry with proper credentials or cancel.\n"
150 "\n"
151 'You may also need to check the PostgreSQL client\n'
152 'authentication configuration in pg_hba.conf. For\n'
153 'details see:\n'
154 '\n'
155 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL'
156 ) % e,
157 _('Connecting to backend')
158 )
159 del e
160 continue
161
162 except gmPG2.dbapi.OperationalError, e:
163 _log.error(u"login attempt failed: %s", e)
164 gmGuiHelpers.gm_show_error (_(
165 "Unable to connect to database:\n\n"
166 "%s\n\n"
167 "Please retry another backend / user / password combination !\n"
168 ) % gmPG2.extract_msg_from_pg_exception(e),
169 _('Connecting to backend')
170 )
171 del e
172 continue
173
174
175 gmPG2.set_default_login(login = login)
176 gmPG2.set_default_client_encoding(encoding = dlg.panel.backend_profile.encoding)
177
178 compatible = gmPG2.database_schema_compatible(version = expected_version)
179 if compatible or not require_version:
180 dlg.panel.save_state()
181
182 if not compatible:
183 connected_db_version = gmPG2.get_schema_version()
184 msg = msg_generic % (
185 client_version,
186 connected_db_version,
187 expected_version,
188 gmTools.coalesce(login.host, '<localhost>'),
189 login.database,
190 login.user
191 )
192 if require_version:
193 gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version'))
194 continue
195 gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version'))
196
197
198 max_skew = 1
199 if _cfg.get(option = 'debug'):
200 max_skew = 10
201 if not gmPG2.sanity_check_time_skew(tolerance = (max_skew * 60)):
202 if _cfg.get(option = 'debug'):
203 gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings'))
204 else:
205 gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings'))
206 continue
207
208 sanity_level, message = gmPG2.sanity_check_database_settings()
209 if sanity_level != 0:
210 gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings'))
211 if sanity_level == 2:
212 continue
213
214 gmExceptionHandlingWidgets.set_is_public_database(login.public_db)
215 gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk)
216
217 listener = gmBackendListener.gmBackendListener(conn = conn)
218 break
219
220 dlg.Destroy()
221
222 return connected
223
225 if procedure is None:
226 procedure = _('<restricted procedure>')
227
228
229 if dbo_password is None:
230 pwd_gm_dbo = wx.GetPasswordFromUser (
231 message = _("""
232 [%s]
233
234 This is a restricted procedure. We need the
235 password for the GNUmed database owner.
236
237 Please enter the password for <gm-dbo>:""") % procedure,
238 caption = procedure
239 )
240 if pwd_gm_dbo == '':
241 return None
242 else:
243 pwd_gm_dbo = dbo_password
244
245
246 login = gmPG2.get_default_login()
247 dsn = gmPG2.make_psycopg2_dsn(database=login.database, host=login.host, port=login.port, user='gm-dbo', password=pwd_gm_dbo)
248 try:
249 conn = gmPG2.get_connection(dsn=dsn, readonly=False, verbose=True, pooled=False)
250 except:
251 _log.exception('cannot connect')
252 gmGuiHelpers.gm_show_error (
253 aMessage = _('Cannot connect as the GNUmed database owner <gm-dbo>.'),
254 aTitle = procedure
255 )
256 return None
257
258 return conn
259
262
264 """cLoginDialog - window holding cLoginPanel"""
265
266 icon_serpent='x\xdae\x8f\xb1\x0e\x83 \x10\x86w\x9f\xe2\x92\x1blb\xf2\x07\x96\xeaH:0\xd6\
267 \xc1\x85\xd5\x98N5\xa5\xef?\xf5N\xd0\x8a\xdcA\xc2\xf7qw\x84\xdb\xfa\xb5\xcd\
268 \xd4\xda;\xc9\x1a\xc8\xb6\xcd<\xb5\xa0\x85\x1e\xeb\xbc\xbc7b!\xf6\xdeHl\x1c\
269 \x94\x073\xec<*\xf7\xbe\xf7\x99\x9d\xb21~\xe7.\xf5\x1f\x1c\xd3\xbdVlL\xc2\
270 \xcf\xf8ye\xd0\x00\x90\x0etH \x84\x80B\xaa\x8a\x88\x85\xc4(U\x9d$\xfeR;\xc5J\
271 \xa6\x01\xbbt9\xceR\xc8\x81e_$\x98\xb9\x9c\xa9\x8d,y\xa9t\xc8\xcf\x152\xe0x\
272 \xe9$\xf5\x07\x95\x0cD\x95t:\xb1\x92\xae\x9cI\xa8~\x84\x1f\xe0\xa3ec'
273
274 - def __init__(self, parent, id, title=_("Welcome to the"), client_version=u'*** unknown ***'):
275 wx.Dialog.__init__(self, parent, id, title)
276 self.panel = cLoginPanel(self, -1, isDialog=1, client_version = client_version)
277 self.Fit()
278 self.Centre()
279
280
281 icon_bmp_data = wx.BitmapFromXPMData(cPickle.loads(zlib.decompress(self.icon_serpent)))
282 icon = wx.EmptyIcon()
283 icon.CopyFromBitmap(icon_bmp_data)
284 self.SetIcon(icon)
285
287 """GUI panel class that interactively gets Postgres login parameters.
288
289 It features combo boxes which "remember" any number of
290 previously entered settings.
291 """
292 - def __init__(self, parent, id,
293 pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.TAB_TRAVERSAL,
294 isDialog = 0, client_version = u'*** unknown ***'):
295 """Create login panel.
296
297 isDialog: if this panel is the main panel of a dialog, the panel will
298 resize the dialog automatically to display everything neatly
299 if isDialog is set to True
300 """
301 wx.Panel.__init__(self, parent, id, pos, size, style)
302 self.parent = parent
303
304
305
306 self.cancelled = True
307
308
309 self.isDialog = isDialog
310
311 self.topsizer = wx.BoxSizer(wx.VERTICAL)
312
313
314 paths = gmTools.gmPaths(app_name = u'gnumed', wx = wx)
315 bitmap = os.path.join(paths.system_app_data_dir, 'bitmaps', 'gnumedlogo.png')
316 try:
317 png = wx.Image(bitmap, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
318 bmp = wx.StaticBitmap(self, -1, png, wx.Point(10, 10), wx.Size(png.GetWidth(), png.GetHeight()))
319 self.topsizer.Add (
320 bmp,
321 proportion = 0,
322 flag = wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
323 border = 10
324 )
325 except:
326 self.topsizer.Add (
327 wx.StaticText (
328 self,
329 -1,
330 label = _("Cannot find image") + bitmap,
331 style = wx.ALIGN_CENTRE
332 ),
333 proportion = 0,
334 flag = wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
335 border = 10
336 )
337
338 paramsbox_caption = _('"%s" (version %s)') % (gmSurgery.gmCurrentPractice().active_workplace, client_version)
339
340
341 self.paramsbox = wx.StaticBox( self, -1, paramsbox_caption, style = wx.ALIGN_CENTRE_HORIZONTAL)
342 self.paramsboxsizer = wx.StaticBoxSizer( self.paramsbox, wx.VERTICAL )
343 self.paramsbox.SetForegroundColour(wx.Colour(35, 35, 142))
344 self.paramsbox.SetFont(wx.Font(
345 pointSize = 12,
346 family = wx.SWISS,
347 style = wx.NORMAL,
348 weight = wx.BOLD,
349 underline = False
350 ))
351 self.pboxgrid = wx.FlexGridSizer(5, 2, 5, 5)
352 self.pboxgrid.AddGrowableCol(1)
353
354
355 label = wx.StaticText( self, -1, _('Log into'), wx.DefaultPosition, wx.DefaultSize, 0)
356 label.SetForegroundColour(wx.Colour(35, 35, 142))
357 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
358 self.__backend_profiles = self.__get_backend_profiles()
359 self._CBOX_profile = wx.ComboBox (
360 self,
361 -1,
362 self.__backend_profiles.keys()[0],
363 wx.DefaultPosition,
364 size = wx.Size(150,-1),
365 choices = self.__backend_profiles.keys(),
366 style = wx.CB_READONLY
367 )
368 self.pboxgrid.Add (self._CBOX_profile, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
369
370
371 label = wx.StaticText( self, -1, _("Username"), wx.DefaultPosition, wx.DefaultSize, 0 )
372 label.SetForegroundColour(wx.Colour(35, 35, 142))
373 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
374 self.__previously_used_accounts = self.__get_previously_used_accounts()
375 self._CBOX_user = wx.ComboBox (
376 self,
377 -1,
378 self.__previously_used_accounts[0],
379 wx.DefaultPosition,
380 wx.Size(150,-1),
381 self.__previously_used_accounts,
382 wx.CB_DROPDOWN
383 )
384 self.pboxgrid.Add( self._CBOX_user, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
385
386
387 label = wx.StaticText( self, -1, _("Password"), wx.DefaultPosition, wx.DefaultSize, 0 )
388 label.SetForegroundColour(wx.Colour(35, 35, 142))
389 self.pboxgrid.Add( label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
390 self.pwdentry = wx.TextCtrl( self, 1, '', wx.DefaultPosition, wx.Size(80,-1), wx.TE_PASSWORD )
391
392 self.pwdentry.SetFocus()
393 self.pboxgrid.Add( self.pwdentry, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
394
395
396 label = wx.StaticText(self, -1, _('Options'), wx.DefaultPosition, wx.DefaultSize, 0)
397 label.SetForegroundColour(wx.Colour(35, 35, 142))
398 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
399 self._CHBOX_debug = wx.CheckBox(self, -1, _('&Debug mode'))
400 self._CHBOX_debug.SetToolTipString(_('Check this to run GNUmed client in debugging mode.'))
401 self.pboxgrid.Add(self._CHBOX_debug, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
402
403
404 label = wx.StaticText(self, -1, '', wx.DefaultPosition, wx.DefaultSize, 0)
405 label.SetForegroundColour(wx.Colour(35, 35, 142))
406 self.pboxgrid.Add(label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
407 self._CHBOX_slave = wx.CheckBox(self, -1, _('Enable &remote control'))
408 self._CHBOX_slave.SetToolTipString(_('Check this to run GNUmed client in slave mode for remote control.'))
409 self.pboxgrid.Add(self._CHBOX_slave, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
410
411
412
413
414
415
416
417
418
419
420
421
422
423 self.button_gridsizer = wx.GridSizer(1,3,0,0)
424
425
426
427 ID_BUTTON_LOGIN = wx.NewId()
428 button_login_ok = wx.Button(self, ID_BUTTON_LOGIN, _("&Ok"), wx.DefaultPosition, wx.DefaultSize, 0 )
429 button_login_ok.SetToolTip(wx.ToolTip(_("Proceed with login.")) )
430 button_login_ok.SetDefault()
431
432
433
434
435 ID_BUTTON_CANCEL = wx.NewId()
436 button_cancel = wx.Button(self, ID_BUTTON_CANCEL, _("&Cancel"), wx.DefaultPosition, wx.DefaultSize, 0 )
437 button_cancel.SetToolTip(wx.ToolTip(_("Cancel Login.")) )
438
439
440
441 ID_BUTTON_HELP = wx.NewId()
442 button_help = wx.Button(self, ID_BUTTON_HELP, _("&Help"), wx.DefaultPosition, wx.DefaultSize, 0 )
443 button_help.SetToolTip(wx.ToolTip(_("Help for login screen")))
444
445
446
447 self.button_gridsizer.Add (button_help,0,wx.EXPAND|wx.ALL,5)
448 self.button_gridsizer.Add (button_login_ok,0,wx.EXPAND|wx.ALL,5)
449 self.button_gridsizer.Add (button_cancel,0,wx.EXPAND|wx.ALL,5)
450
451 self.paramsboxsizer.Add(self.pboxgrid, 1, wx.GROW|wx.ALL, 10)
452 self.topsizer.Add(self.paramsboxsizer, 1, wx.GROW|wx.ALL, 10)
453 self.topsizer.Add( self.button_gridsizer, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
454
455 self.__load_state()
456
457 self.SetAutoLayout(True)
458 self.SetSizer( self.topsizer)
459 self.topsizer.Fit( self )
460 if self.isDialog:
461 self.topsizer.SetSizeHints(parent)
462
463 wx.EVT_BUTTON(self, ID_BUTTON_HELP, self.OnHelp)
464 wx.EVT_BUTTON(self, ID_BUTTON_LOGIN, self.__on_login_button_pressed)
465 wx.EVT_BUTTON(self, ID_BUTTON_CANCEL, self.OnCancel)
466
467
468
469
471
472 accounts = gmTools.coalesce (
473 _cfg.get (
474 group = u'backend',
475 option = u'logins',
476 source_order = [
477 (u'explicit', u'extend'),
478 (u'user', u'extend'),
479 (u'workbase', u'extend')
480 ]
481 ),
482 ['any-doc']
483 )
484
485
486 return accounts
487
489 """Get server profiles from the configuration files.
490
491 1) from system-wide file
492 2) from user file
493
494 Profiles in the user file which have the same name
495 as a profile in the system file will override the
496 system file.
497 """
498
499 src_order = [
500 (u'explicit', u'extend'),
501 (u'system', u'extend'),
502 (u'user', u'extend'),
503 (u'workbase', u'extend')
504 ]
505
506 profile_names = gmTools.coalesce (
507 _cfg.get(group = u'backend', option = u'profiles', source_order = src_order),
508 []
509 )
510
511
512 src_order = [
513 (u'explicit', u'return'),
514 (u'workbase', u'return'),
515 (u'user', u'return'),
516 (u'system', u'return')
517 ]
518
519 profiles = {}
520
521 for profile_name in profile_names:
522
523
524 profile = cBackendProfile()
525 profile_section = 'profile %s' % profile_name
526
527 profile.name = profile_name
528 profile.host = gmTools.coalesce(_cfg.get(profile_section, u'host', src_order), u'').strip()
529 port = gmTools.coalesce(_cfg.get(profile_section, u'port', src_order), 5432)
530 try:
531 profile.port = int(port)
532 if profile.port < 1024:
533 raise ValueError('refusing to use priviledged port (< 1024)')
534 except ValueError:
535 _log.warning('invalid port definition: [%s], skipping profile [%s]', port, profile_name)
536 continue
537 profile.database = gmTools.coalesce(_cfg.get(profile_section, u'database', src_order), u'').strip()
538 if profile.database == u'':
539 _log.warning('database name not specified, skipping profile [%s]', profile_name)
540 continue
541 profile.encoding = gmTools.coalesce(_cfg.get(profile_section, u'encoding', src_order), u'UTF8')
542 profile.public_db = bool(_cfg.get(profile_section, u'public/open access', src_order))
543 profile.helpdesk = _cfg.get(profile_section, u'help desk', src_order)
544
545 label = u'%s (%s@%s)' % (profile_name, profile.database, profile.host)
546 profiles[label] = profile
547
548
549
550 if not (_cfg.get(option = 'debug') or current_db_name.endswith('_devel')):
551 profiles2remove = []
552 for label in profiles:
553 if profiles[label].database != current_db_name:
554 profiles2remove.append(label)
555 for label in profiles2remove:
556 del profiles[label]
557
558 if len(profiles) == 0:
559 host = u'salaam.homeunix.com'
560 label = u'public GNUmed database (%s@%s)' % (current_db_name, host)
561 profiles[label] = cBackendProfile()
562 profiles[label].name = label
563 profiles[label].host = host
564 profiles[label].port = 5432
565 profiles[label].database = current_db_name
566 profiles[label].encoding = u'UTF8'
567 profiles[label].public_db = True
568 profiles[label].helpdesk = u'http://wiki.gnumed.de'
569
570 return profiles
571
573
574 src_order = [
575 (u'explicit', u'return'),
576 (u'user', u'return'),
577 ]
578
579 self._CBOX_user.SetValue (
580 gmTools.coalesce (
581 _cfg.get(u'preferences', u'login', src_order),
582 self.__previously_used_accounts[0]
583 )
584 )
585
586 last_used_profile_label = _cfg.get(u'preferences', u'profile', src_order)
587 if last_used_profile_label in self.__backend_profiles.keys():
588 self._CBOX_profile.SetValue(last_used_profile_label)
589 else:
590 self._CBOX_profile.SetValue(self.__backend_profiles.keys()[0])
591
592 self._CHBOX_debug.SetValue(_cfg.get(option = 'debug'))
593 self._CHBOX_slave.SetValue(_cfg.get(option = 'slave'))
594
613
614
615
617 """convenience function for compatibility with gmLoginInfo.LoginInfo"""
618 if not self.cancelled:
619
620
621 profile = self.__backend_profiles[self._CBOX_profile.GetValue().encode('utf8').strip()]
622 _log.debug(u'backend profile "%s" selected', profile.name)
623 _log.debug(u' details: <%s> on %s@%s:%s (%s, %s)',
624 self._CBOX_user.GetValue(),
625 profile.database,
626 profile.host,
627 profile.port,
628 profile.encoding,
629 gmTools.bool2subst(profile.public_db, u'public', u'private')
630 )
631 _log.debug(u' helpdesk: "%s"', profile.helpdesk)
632 login = gmLoginInfo.LoginInfo (
633 user = self._CBOX_user.GetValue(),
634 password = self.pwdentry.GetValue(),
635 host = profile.host,
636 database = profile.database,
637 port = profile.port
638 )
639 login.public_db = profile.public_db
640 login.helpdesk = profile.helpdesk
641 return login
642
643 return None
644
645
646
648 praxis = gmSurgery.gmCurrentPractice()
649 wx.MessageBox(_(
650 """GNUmed main login screen
651
652 USER:
653 name of the GNUmed user
654 PASSWORD
655 password for this user
656
657 button OK:
658 proceed with login
659 button OPTIONS:
660 set advanced options
661 button CANCEL:
662 abort login and quit GNUmed client
663 button HELP:
664 this help screen
665
666 For assistance on using GNUmed please contact:
667 %s""") % praxis.helpdesk)
668
669
697
699 self.cancelled = True
700 self.parent.Close()
701
702
703
704
705 if __name__ == "__main__":
706
707 from Gnumed.pycommon import gmI18N
708
709 logging.basicConfig(level = logging.DEBUG)
710
711 gmI18N.activate_locale()
712 gmI18N.install_domain(domain='gnumed')
713
714
716 app = wx.PyWidgetTester(size = (300,400))
717
718
719
720 dlg = cLoginDialog(None, -1)
721 dlg.ShowModal()
722
723 lp = dlg.panel.GetLoginInfo()
724 if lp is None:
725 wx.MessageBox(_("Dialog was cancelled by user"))
726 else:
727 wx.MessageBox(_("You tried to log in as [%s] with password [%s].\nHost:%s, DB: %s, Port: %s") % (lp.GetUser(),lp.GetPassword(),lp.GetHost(),lp.GetDatabase(),lp.GetPort()))
728 dlg.Destroy()
729
730
731 if len(sys.argv) > 1 and sys.argv[1] == 'test':
732 print "no regression tests yet"
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225