From 8fdedb70181526a2f581d0d737a28c31e7f40de9 Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Mon, 13 Jun 2016 12:17:45 +0200 Subject: [PATCH 001/436] Add maxtextlenth option to SQLFORM.grid HTML exporter --- gluon/sqlhtml.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 5cdb9147..23b9642e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2476,7 +2476,8 @@ class SQLFORM(FORM): response.headers['Content-Type'] = oExp.content_type response.headers['Content-Disposition'] = \ 'attachment;filename=' + filename + ';' - raise HTTP(200, oExp.export(), **response.headers) + document = oExp.export(maxtextlength) if oExp.__class__.__name__== 'ExporterHTML' else oExp.export() + raise HTTP(200, document, **response.headers) elif request.vars.records and not isinstance( request.vars.records, list): @@ -3549,9 +3550,9 @@ class ExporterHTML(ExportClass): def __init__(self, rows): ExportClass.__init__(self, rows) - def export(self): - xml = self.rows.xml() if self.rows else '' - return '\n\n\n\n\n%s\n\n' % (xml or '') + def export(self, truncate=16): + table = SQLTABLE(self.rows, truncate=truncate) if self.rows else '' + return '\n\n\n\n\n%s\n\n' % (table or '') class ExporterXML(ExportClass): From 180ada57da1ae2cd113b82e5da884d8355a12de8 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Mon, 13 Jun 2016 18:00:14 +0200 Subject: [PATCH 002/436] fix request.json, close #1337 --- gluon/contrib/shell.py | 2 +- gluon/globals.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index 341b32a2..638d3c81 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -48,7 +48,7 @@ _HISTORY_KIND = '_Shell_History' UNPICKLABLE_TYPES = [ types.ModuleType, type, - ClassType + ClassType, types.FunctionType, ] diff --git a/gluon/globals.py b/gluon/globals.py index 43079593..3af951c1 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -40,7 +40,7 @@ import threading import cgi import copy import tempfile -import json +import json as json_parser FMT = '%a, %d-%b-%Y %H:%M:%S PST' @@ -215,7 +215,7 @@ class Request(Storage): if is_json: try: - json_vars = json.load(body) + json_vars = json_parser.load(body) except: # incoherent request bodies can still be parsed "ad-hoc" json_vars = {} From 7259516627cd6c399dae618c25a41f76e988be3e Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 11 Jun 2016 14:54:02 +0200 Subject: [PATCH 003/436] fix tools --- gluon/_compat.py | 7 ++- gluon/contenttype.py | 3 +- gluon/languages.py | 6 +-- gluon/serializers.py | 2 + gluon/sqlhtml.py | 8 +-- gluon/tests/__init__.py | 4 +- gluon/tests/test_tools.py | 14 ++--- gluon/tools.py | 111 ++++++++++++++++++-------------------- 8 files changed, 78 insertions(+), 77 deletions(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index d95b0e82..0baea6bb 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -21,7 +21,10 @@ if PY2: import ConfigParser as configparser from email.MIMEBase import MIMEBase from email.Header import Header - from email import MIMEMultipart, MIMEText, Encoders, Charset + from email import Encoders, Charset + from email.MIMEMultipart import MIMEMultipart + from email.MIMEText import MIMEText + from email.Charset import add_charset, QP as charset_QP from urllib import FancyURLopener, urlencode, urlopen from urllib import quote as urllib_quote, unquote as urllib_unquote from string import maketrans @@ -88,7 +91,7 @@ else: from email.mime.text import MIMEText from email import encoders as Encoders from email.header import Header - from email.charset import Charset + from email.charset import Charset, add_charset, QP as charset_QP from urllib.request import FancyURLopener, urlopen from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode import html diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 209f8e15..7dd28f35 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -19,6 +19,7 @@ Additions: - .pickle: application/python-pickle - .w2p': application/w2p """ +from gluon._compat import to_native __all__ = ['contenttype'] @@ -842,7 +843,7 @@ def contenttype(filename, default='text/plain'): """ Returns the Content-Type string matching extension of the given filename. """ - + filename=to_native(filename) i = filename.rfind('.') if i >= 0: default = CONTENT_TYPE.get(filename[i:].lower(), default) diff --git a/gluon/languages.py b/gluon/languages.py index 763479ef..2d29a51a 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -18,7 +18,7 @@ import pkgutil import logging from cgi import escape from threading import RLock -from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems +from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, _local_html_escape, to_native from gluon.portalocker import read_locked, LockedFile from gluon.utf8 import Utf8 @@ -426,7 +426,7 @@ class lazyT(object): return len(str(self)) def xml(self): - return str(self) if self.M else escape(str(self)) + return str(self) if self.M else _local_html_escape(str(self), quote=False) def encode(self, *a, **b): return str(self).encode(*a, **b) @@ -821,7 +821,7 @@ class translator(object): self.language_file != self.default_language_file: write_dict(self.language_file, self.t) return regex_backslash.sub( - lambda m: m.group(1).translate(ttab_in), str(mt)) + lambda m: m.group(1).translate(ttab_in), to_native(mt)) def params_substitution(self, message, symbols): """ diff --git a/gluon/serializers.py b/gluon/serializers.py index e578fd82..61fe88bb 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -83,6 +83,8 @@ def custom_json(o): return int(o) elif isinstance(o, decimal.Decimal): return str(o) + elif isinstance(o, (bytes, bytearray)): + return str(o) elif isinstance(o, lazyT): return str(o) elif isinstance(o, XmlComponent): diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 5cdb9147..84e25d3e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -19,7 +19,7 @@ import urllib import re import os -from gluon._compat import StringIO, unichr, urllib_quote, iteritems +from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT from gluon.http import HTTP, redirect from gluon.html import XmlComponent, truncate_string from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG @@ -1141,7 +1141,7 @@ class SQLFORM(FORM): # try to retrieve the indicated record using its id # otherwise ignore it - if record and isinstance(record, (int, long, str, unicode)): + if record and isinstance(record, (int, long, str, unicodeT)): if not str(record).isdigit(): raise HTTP(404, "Object not found") record = table._db(table._id == record).select().first() @@ -1651,7 +1651,7 @@ class SQLFORM(FORM): original_filename = os.path.split(f)[1] elif hasattr(f, 'file'): (source_file, original_filename) = (f.file, f.filename) - elif isinstance(f, (str, unicode)): + elif isinstance(f, (str, unicodeT)): # do not know why this happens, it should not (source_file, original_filename) = \ (StringIO(f), 'file.txt') @@ -3426,7 +3426,7 @@ class ExportClass(object): """ if value is None: return '' - elif isinstance(value, unicode): + elif isinstance(value, unicodeT): return value.encode('utf8') elif isinstance(value, Reference): return int(value) diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 59691e4d..f587b65e 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -14,14 +14,14 @@ from .test_contribs import * from .test_routes import * from .test_router import * from .test_validators import * - +from .test_tools import * if sys.version[:3] == '2.7': from .test_compileapp import * from .test_is_url import * from .test_languages import * from .test_serializers import * from .test_utils import * - from .test_tools import * + from .test_appadmin import * from .test_scheduler import * from .test_web import * diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index 374810c0..ed1b6483 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -547,7 +547,7 @@ class TestAuth(unittest.TestCase): def test_basic_blank_forms(self): for f in ['login', 'retrieve_password', 'retrieve_username', 'register']: html_form = getattr(self.auth, f)().xml() - self.assertTrue('name="_formkey"' in html_form) + self.assertTrue(b'name="_formkey"' in html_form) for f in ['logout', 'verify_email', 'reset_password', 'change_password', 'profile', 'groups']: self.assertRaisesRegexp(HTTP, "303*", getattr(self.auth, f)) @@ -712,7 +712,7 @@ class TestAuth(unittest.TestCase): self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() self.auth.settings.bulk_register_enabled = True bulk_register_form = self.auth.bulk_register(max_emails=10).xml() - self.assertTrue('name="_formkey"' in bulk_register_form) + self.assertTrue(b'name="_formkey"' in bulk_register_form) # TODO: def test_manage_tokens(self): # TODO: def test_reset_password(self): @@ -723,12 +723,12 @@ class TestAuth(unittest.TestCase): def test_change_password(self): self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() change_password_form = getattr(self.auth, 'change_password')().xml() - self.assertTrue('name="_formkey"' in change_password_form) + self.assertTrue(b'name="_formkey"' in change_password_form) def test_profile(self): self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() profile_form = getattr(self.auth, 'profile')().xml() - self.assertTrue('name="_formkey"' in profile_form) + self.assertTrue(b'name="_formkey"' in profile_form) # TODO: def test_run_login_onaccept(self): # TODO: def test_jwt(self): @@ -766,7 +766,7 @@ class TestAuth(unittest.TestCase): # basic impersonate() test that return a read form self.assertEqual(self.auth.impersonate().xml(), - '
') + b'
') # bart impersonate itself self.assertEqual(self.auth.impersonate(bart_id), None) self.assertFalse(self.auth.is_impersonating()) # User shouldn't impersonate itself? @@ -776,7 +776,7 @@ class TestAuth(unittest.TestCase): self.assertTrue(self.auth.is_impersonating()) self.assertEqual(self.auth.user_id, omer_id) # we make it really sure self.assertEqual(impersonate_form.xml(), - '
2
Omer
Simpson
omer@test.com
omer
') + b'
2
Omer
Simpson
omer@test.com
omer
') self.auth.logout_bare() # Failing impersonation # User lacking impersonate membership @@ -800,7 +800,7 @@ class TestAuth(unittest.TestCase): def test_groups(self): self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() self.assertEqual(self.auth.groups().xml(), - '

user_1(1)

') + b'

user_1(1)

') def test_not_authorized(self): self.current.request.ajax = 'facke_ajax_request' diff --git a/gluon/tools.py b/gluon/tools.py index 8eb6e74e..8736b814 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -13,7 +13,8 @@ Auth, Mail, PluginManager and various utilities import base64 from functools import reduce from gluon._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ - MIMEText, Encoders, Charset, long, urllib_quote, iteritems + MIMEText, Encoders, Charset, long, urllib_quote, iteritems, to_bytes, to_native, add_charset, \ + charset_QP, basestring, unicodeT, to_unicode import datetime import logging import sys @@ -241,6 +242,7 @@ class Mail(object): if filename is None: raise Exception('Missing attachment name') payload = payload.read() + #FIXME PY3 can be used to_native? filename = filename.encode(encoding) if content_type is None: content_type = contenttype(filename) @@ -248,9 +250,9 @@ class Mail(object): self.my_payload = payload MIMEBase.__init__(self, *content_type.split('/', 1)) self.set_payload(payload) - self['Content-Disposition'] = 'attachment; filename="%s"' % filename + self['Content-Disposition'] = 'attachment; filename="%s"' % to_native(filename, encoding) if content_id is not None: - self['Content-Id'] = '<%s>' % content_id.encode(encoding) + self['Content-Id'] = '<%s>' % to_native(content_id, encoding) Encoders.encode_base64(self) def __init__(self, server=None, sender=None, login=None, tls=True): @@ -405,7 +407,7 @@ class Mail(object): """ # We don't want to use base64 encoding for unicode mail - Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') + add_charset('utf-8', charset_QP, charset_QP, 'utf-8') def encode_header(key): if [c for c in key if 32 > ord(c) or ord(c) > 127]: @@ -428,12 +430,12 @@ class Mail(object): if not raw and attachments: # Use multipart/mixed if there is attachments - payload_in = MIMEMultipart.MIMEMultipart('mixed') + payload_in = MIMEMultipart('mixed') elif raw: # no encoding configuration for raw messages if not isinstance(message, basestring): message = message.read() - if isinstance(message, unicode): + if isinstance(message, unicodeT): text = message.encode('utf-8') elif not encoding == 'utf-8': text = message.decode(encoding).encode('utf-8') @@ -442,7 +444,7 @@ class Mail(object): # No charset passed to avoid transport encoding # NOTE: some unicode encoded strings will produce # unreadable mail contents. - payload_in = MIMEText.MIMEText(text) + payload_in = MIMEText(text) if to: if not isinstance(to, (list, tuple)): to = [to] @@ -471,14 +473,14 @@ class Mail(object): if text is not None: if not isinstance(text, basestring): text = text.read() - if isinstance(text, unicode): + if isinstance(text, unicodeT): text = text.encode('utf-8') elif not encoding == 'utf-8': text = text.decode(encoding).encode('utf-8') if html is not None: if not isinstance(html, basestring): html = html.read() - if isinstance(html, unicode): + if isinstance(html, unicodeT): html = html.encode('utf-8') elif not encoding == 'utf-8': html = html.decode(encoding).encode('utf-8') @@ -486,15 +488,13 @@ class Mail(object): # Construct mime part only if needed if text is not None and html: # We have text and html we need multipart/alternative - attachment = MIMEMultipart.MIMEMultipart('alternative') - attachment.attach(MIMEText.MIMEText(text, _charset='utf-8')) - attachment.attach( - MIMEText.MIMEText(html, 'html', _charset='utf-8')) + attachment = MIMEMultipart('alternative') + attachment.attach(MIMEText(text, _charset='utf-8')) + attachment.attach(MIMEText(html, 'html', _charset='utf-8')) elif text is not None: - attachment = MIMEText.MIMEText(text, _charset='utf-8') + attachment = MIMEText(text, _charset='utf-8') elif html: - attachment = \ - MIMEText.MIMEText(html, 'html', _charset='utf-8') + attachment = MIMEText(html, 'html', _charset='utf-8') if attachments: # If there is attachments put text and html into @@ -560,12 +560,11 @@ class Mail(object): c.op_sign(plain, sig, mode.DETACH) sig.seek(0, 0) # make it part of the email - payload = \ - MIMEMultipart.MIMEMultipart('signed', - boundary=None, - _subparts=None, - **dict(micalg="pgp-sha1", - protocol="application/pgp-signature")) + payload = MIMEMultipart('signed', + boundary=None, + _subparts=None, + **dict(micalg="pgp-sha1", + protocol="application/pgp-signature")) # insert the origin payload payload.attach(payload_in) # insert the detached signature @@ -605,10 +604,10 @@ class Mail(object): c.op_encrypt(recipients, 1, plain, cipher) cipher.seek(0, 0) # make it a part of the email - payload = MIMEMultipart.MIMEMultipart('encrypted', - boundary=None, - _subparts=None, - **dict(protocol="application/pgp-encrypted")) + payload = MIMEMultipart('encrypted', + boundary=None, + _subparts=None, + **dict(protocol="application/pgp-encrypted")) p = MIMEBase("application", 'pgp-encrypted') p.set_payload("Version: 1\r\n") payload.attach(p) @@ -729,29 +728,29 @@ class Mail(object): payload = payload_in if from_address: - payload['From'] = encoded_or_raw(from_address.decode(encoding)) + payload['From'] = encoded_or_raw(to_unicode(from_address, encoding)) else: - payload['From'] = encoded_or_raw(sender.decode(encoding)) + payload['From'] = encoded_or_raw(to_unicode(sender, encoding)) origTo = to[:] if to: - payload['To'] = encoded_or_raw(', '.join(to).decode(encoding)) + payload['To'] = encoded_or_raw(to_unicode(', '.join(to), encoding)) if reply_to: - payload['Reply-To'] = encoded_or_raw(reply_to.decode(encoding)) + payload['Reply-To'] = encoded_or_raw(to_unicode(reply_to, encoding)) if cc: - payload['Cc'] = encoded_or_raw(', '.join(cc).decode(encoding)) + payload['Cc'] = encoded_or_raw(to_unicode(', '.join(cc), encoding)) to.extend(cc) if bcc: to.extend(bcc) - payload['Subject'] = encoded_or_raw(subject.decode(encoding)) + payload['Subject'] = encoded_or_raw(to_unicode(subject, encoding)) payload['Date'] = email.utils.formatdate() for k, v in iteritems(headers): - payload[k] = encoded_or_raw(v.decode(encoding)) + payload[k] = encoded_or_raw(to_unicode(v, encoding)) result = {} try: if self.settings.server == 'logging': entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \ ('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40) - logger.warn(entry) + logger.warning(entry) elif self.settings.server.startswith('logging:'): entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \ ('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40) @@ -773,16 +772,16 @@ class Mail(object): if attachments: result = mail.send_mail( sender=sender, to=origTo, - subject=unicode(subject, encoding), body=unicode(text, encoding), html=html, + subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), html=html, attachments=attachments, **xcc) elif html and (not raw): result = mail.send_mail( sender=sender, to=origTo, - subject=unicode(subject, encoding), body=unicode(text, encoding), html=html, **xcc) + subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), html=html, **xcc) else: result = mail.send_mail( sender=sender, to=origTo, - subject=unicode(subject, encoding), body=unicode(text, encoding), **xcc) + subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), **xcc) else: smtp_args = self.settings.server.split(':') kwargs = dict(timeout=self.settings.timeout) @@ -800,7 +799,7 @@ class Mail(object): sender, to, payload.as_string()) server.quit() except Exception as e: - logger.warn('Mail.send failure:%s' % e) + logger.warning('Mail.send failure:%s' % e) self.result = result self.error = e return False @@ -1250,8 +1249,7 @@ class AuthJWT(object): @staticmethod def jwt_b64e(string): - if isinstance(string, unicode): - string = string.encode('utf-8', 'strict') + string = to_bytes(string) return base64.urlsafe_b64encode(string).strip(b'=') @staticmethod @@ -1260,47 +1258,44 @@ class AuthJWT(object): called with a unicode string). The result is also a bytestring. """ - if isinstance(string, unicode): - string = string.encode('ascii', 'ignore') - return base64.urlsafe_b64decode(string + '=' * (-len(string) % 4)) + string = to_bytes(string, 'ascii', 'ignore') + return base64.urlsafe_b64decode(string + b'=' * (-len(string) % 4)) def generate_token(self, payload): - secret = self.secret_key + secret = to_bytes(self.secret_key) if self.salt: if callable(self.salt): secret = "%s$%s" % (secret, self.salt(payload)) else: secret = "%s$%s" % (secret, self.salt) - if isinstance(secret, unicode): + if isinstance(secret, unicodeT): secret = secret.encode('ascii', 'ignore') b64h = self.cached_b64h b64p = self.jwt_b64e(serializers.json(payload)) - jbody = b64h + '.' + b64p + jbody = b64h + b'.' + b64p mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod) jsign = self.jwt_b64e(mauth.digest()) - return jbody + '.' + jsign + return to_native(jbody + b'.' + jsign) def verify_signature(self, body, signature, secret): mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod) return compare(self.jwt_b64e(mauth.digest()), signature) def load_token(self, token): - if isinstance(token, unicode): - token = token.encode('utf-8', 'strict') - body, sig = token.rsplit('.', 1) - b64h, b64b = body.split('.', 1) + token = to_bytes(token, 'utf-8', 'strict') + body, sig = token.rsplit(b'.', 1) + b64h, b64b = body.split(b'.', 1) if b64h != self.cached_b64h: # header not the same raise HTTP(400, u'Invalid JWT Header') secret = self.secret_key - tokend = serializers.loads_json(self.jwt_b64d(b64b)) + tokend = serializers.loads_json(to_native(self.jwt_b64d(b64b))) if self.salt: if callable(self.salt): secret = "%s$%s" % (secret, self.salt(tokend)) else: secret = "%s$%s" % (secret, self.salt) - if isinstance(secret, unicode): - secret = secret.encode('ascii', 'ignore') + secret = to_bytes(secret, 'ascii', 'ignore') if not self.verify_signature(body, sig, secret): # signature verification failed raise HTTP(400, u'Token signature is invalid') @@ -3560,8 +3555,8 @@ class Auth(object): password = '' specials = r'!#$*' for i in range(0, 3): - password += random.choice(string.lowercase) - password += random.choice(string.uppercase) + password += random.choice(string.ascii_lowercase) + password += random.choice(string.ascii_uppercase) password += random.choice(string.digits) password += random.choice(specials) return ''.join(random.sample(password, len(password))) @@ -3994,7 +3989,7 @@ class Auth(object): requires = table_user[passfield].requires if not isinstance(requires, (list, tuple)): requires = [requires] - requires = filter(lambda t: isinstance(t, CRYPT), requires) + requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) if requires: requires[0].min_length = 0 form = SQLFORM.factory( @@ -5621,7 +5616,7 @@ class Service(object): args = request.args def none_exception(value): - if isinstance(value, unicode): + if isinstance(value, unicodeT): return value.encode('utf8') if hasattr(value, 'isoformat'): return value.isoformat()[:19].replace('T', ' ') From ab2cdd595b47d71bfcd4c9ba4e54aadf42f0d94a Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 11 Jun 2016 15:26:15 +0200 Subject: [PATCH 004/436] fix utils --- gluon/tests/__init__.py | 4 ++-- gluon/tests/test_utils.py | 6 +++--- gluon/utils.py | 16 ++++++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index f587b65e..46a0f107 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -15,13 +15,13 @@ from .test_routes import * from .test_router import * from .test_validators import * from .test_tools import * +from .test_utils import * + if sys.version[:3] == '2.7': from .test_compileapp import * from .test_is_url import * from .test_languages import * from .test_serializers import * - from .test_utils import * - from .test_appadmin import * from .test_scheduler import * from .test_web import * diff --git a/gluon/tests/test_utils.py b/gluon/tests/test_utils.py index 01507cc5..21db1d88 100644 --- a/gluon/tests/test_utils.py +++ b/gluon/tests/test_utils.py @@ -12,7 +12,7 @@ from gluon.utils import md5_hash, compare, is_valid_ip_address, web2py_uuid import hashlib from hashlib import md5, sha1, sha224, sha256, sha384, sha512 -from gluon.utils import simple_hash, get_digest, secure_dumps, secure_loads +from gluon.utils import simple_hash, get_digest, secure_dumps, secure_loads, basestring class TestUtils(unittest.TestCase): @@ -94,8 +94,8 @@ class TestUtils(unittest.TestCase): secured = secure_dumps(testobj, testkey) original = secure_loads(secured, testkey) self.assertEqual(testobj, original) - self.assertTrue(isinstance(secured, basestring)) - self.assertTrue(':' in secured) + self.assertTrue(isinstance(secured, bytes)) + self.assertTrue(b':' in secured) large_testobj = [x for x in range(1000)] secured_comp = secure_dumps(large_testobj, testkey, compression_level=9) diff --git a/gluon/utils.py b/gluon/utils.py index 6933a72b..540f4e1f 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -85,7 +85,7 @@ def compare(a, b): def md5_hash(text): """ Generates a md5 hash with the given text """ - return md5(text).hexdigest() + return md5(to_bytes(text)).hexdigest() def simple_hash(text, key='', salt='', digest_alg='md5'): @@ -157,11 +157,12 @@ def get_callable_argspec(fn): return inspect.getargspec(inspectable) -def pad(s, n=32, padchar=' '): +def pad(s, n=32, padchar=b' '): return s + (32 - len(s) % 32) * padchar def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): + encryption_key = to_bytes(encryption_key) if not hash_key: hash_key = sha1(encryption_key).hexdigest() dump = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) @@ -170,17 +171,20 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): key = pad(encryption_key)[:32] cipher, IV = AES_new(key) encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) - signature = hmac.new(hash_key, encrypted_data).hexdigest() - return signature + ':' + encrypted_data + signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data).hexdigest()) + return signature + b':' + encrypted_data def secure_loads(data, encryption_key, hash_key=None, compression_level=None): + encryption_key = to_bytes(encryption_key) + data = to_native(data) if ':' not in data: return None if not hash_key: hash_key = sha1(encryption_key).hexdigest() signature, encrypted_data = data.split(':', 1) - actual_signature = hmac.new(hash_key, encrypted_data).hexdigest() + encrypted_data = to_bytes(encrypted_data) + actual_signature = hmac.new(to_bytes(hash_key), encrypted_data).hexdigest() if not compare(signature, actual_signature): return None key = pad(encryption_key)[:32] @@ -189,7 +193,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None): cipher, _ = AES_new(key, IV=IV) try: data = cipher.decrypt(encrypted_data) - data = data.rstrip(' ') + data = data.rstrip(b' ') if compression_level: data = zlib.decompress(data) return pickle.loads(data) From a27f6f88efdc1f830e94b422d58d3b29b66c8908 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 11 Jun 2016 15:45:27 +0200 Subject: [PATCH 005/436] fix serializers, websocket_messaging --- gluon/contrib/websocket_messaging.py | 34 ++++++++++++++++++++++------ gluon/custom_import.py | 4 ++-- gluon/serializers.py | 8 +++---- gluon/sqlhtml.py | 2 +- gluon/tests/__init__.py | 2 +- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/gluon/contrib/websocket_messaging.py b/gluon/contrib/websocket_messaging.py index 5531629a..84fe55cc 100644 --- a/gluon/contrib/websocket_messaging.py +++ b/gluon/contrib/websocket_messaging.py @@ -84,7 +84,6 @@ Tornado code inspired by http://thomas.pelletier.im/2010/08/websocket-tornado-re """ from __future__ import print_function - import tornado.httpserver import tornado.websocket import tornado.ioloop @@ -92,17 +91,38 @@ import tornado.web import hmac import sys import optparse -import urllib import time +import sys +if (sys.version_info[0] == 2): + from urllib import urlencode, urlopen + def to_bytes(obj, charset='utf-8', errors='strict'): + if obj is None: + return None + if isinstance(obj, (bytes, bytearray, buffer)): + return bytes(obj) + if isinstance(obj, unicode): + return obj.encode(charset, errors) + raise TypeError('Expected bytes') +else: + from urllib.request import urlopen + from urllib.parse import urlencode + def to_bytes(obj, charset='utf-8', errors='strict'): + if obj is None: + return None + if isinstance(obj, (bytes, bytearray, memoryview)): + return bytes(obj) + if isinstance(obj, str): + return obj.encode(charset, errors) + raise TypeError('Expected bytes') listeners, names, tokens = {}, {}, {} def websocket_send(url, message, hmac_key=None, group='default'): - sig = hmac_key and hmac.new(hmac_key, message).hexdigest() or '' - params = urllib.urlencode( + sig = hmac_key and hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() or '' + params = urlencode( {'message': message, 'signature': sig, 'group': group}) - f = urllib.urlopen(url, params) + f = urlopen(url, to_bytes(params)) data = f.read() f.close() return data @@ -121,7 +141,7 @@ class PostHandler(tornado.web.RequestHandler): print('%s:MESSAGE to %s:%s' % (time.time(), group, message)) if hmac_key: signature = self.request.arguments['signature'][0] - if not hmac.new(hmac_key, message).hexdigest() == signature: + if not to_bytes(hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()) == signature: self.send_error(401) for client in listeners.get(group, []): client.write_message(message) @@ -140,7 +160,7 @@ class TokenHandler(tornado.web.RequestHandler): message = self.request.arguments['message'][0] if hmac_key: signature = self.request.arguments['signature'][0] - if not hmac.new(hmac_key, message).hexdigest() == signature: + if not to_bytes(hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()) == signature: self.send_error(401) tokens[message] = None diff --git a/gluon/custom_import.py b/gluon/custom_import.py index 9dfb937c..4bd3ab5f 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -8,7 +8,7 @@ Support for smart import syntax for web2py applications ------------------------------------------------------- """ -from gluon._compat import builtin +from gluon._compat import builtin, unicodeT, PY2 import os import sys import threading @@ -47,7 +47,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): If the import fails, it falls back on naive_importer """ - if isinstance(name, unicode): + if isinstance(name, unicodeT) and PY2: name = name.encode('utf8') globals = globals or {} diff --git a/gluon/serializers.py b/gluon/serializers.py index 61fe88bb..559eb725 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -10,7 +10,7 @@ from gluon.html import TAG, XmlComponent, xmlescape from gluon.languages import lazyT import gluon.contrib.rss2 as rss2 import json as json_parser -from gluon._compat import long +from gluon._compat import long, to_native, unicodeT have_yaml = True try: @@ -43,7 +43,7 @@ def cast_keys(o, cast=str, encoding="utf-8"): else: newobj = Storage() for k, v in o.items(): - if (cast == str) and isinstance(k, unicode): + if (cast == str) and isinstance(k, unicodeT): key = k.encode(encoding) else: key = cast(k) @@ -88,7 +88,7 @@ def custom_json(o): elif isinstance(o, lazyT): return str(o) elif isinstance(o, XmlComponent): - return str(o) + return to_native(o.xml()) elif isinstance(o, set): return list(o) elif hasattr(o, 'as_list') and callable(o.as_list): @@ -161,7 +161,7 @@ def ics(events, title=None, link=None, timeshift=0, calname=True, return s def safe_encode(text): - if not isinstance(text, (str, unicode)): + if not isinstance(text, (str, unicodeT)): text = str(text) try: text = text.encode('utf8','replace') diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 84e25d3e..6bf336b4 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -3487,7 +3487,7 @@ class ExporterTSV(ExportClass): import codecs final.write(codecs.BOM_UTF16) writer.writerow( - [unicode(col).encode("utf8") for col in self.rows.colnames]) + [to_unicode(col, "utf8") for col in self.rows.colnames]) data = out.getvalue().decode("utf8") data = data.encode("utf-16") data = data[2:] diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 46a0f107..cb3f7c13 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -16,12 +16,12 @@ from .test_router import * from .test_validators import * from .test_tools import * from .test_utils import * +from .test_serializers import * if sys.version[:3] == '2.7': from .test_compileapp import * from .test_is_url import * from .test_languages import * - from .test_serializers import * from .test_appadmin import * from .test_scheduler import * from .test_web import * From 34f753be56f05f67e652c2d83e7f2424b3c19d55 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 11 Jun 2016 20:12:30 +0200 Subject: [PATCH 006/436] fix languages --- gluon/languages.py | 20 ++++++++------------ gluon/tests/__init__.py | 2 +- gluon/tests/test_languages.py | 7 +++++-- gluon/utf8.py | 26 +++++++++++++------------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index 2d29a51a..b40cabd3 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -18,7 +18,8 @@ import pkgutil import logging from cgi import escape from threading import RLock -from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, _local_html_escape, to_native +from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, _local_html_escape, to_native, \ + pjoin from gluon.portalocker import read_locked, LockedFile from gluon.utf8 import Utf8 @@ -34,8 +35,6 @@ __all__ = ['translator', 'findT', 'update_all_languages'] ostat = os.stat oslistdir = os.listdir -pjoin = os.path.join -pexists = os.path.exists pdirname = os.path.dirname isdir = os.path.isdir @@ -167,7 +166,7 @@ def read_dict_aux(filename): lang_text = read_locked(filename).replace(b'\r\n', b'\n') clear_cache(filename) try: - return safe_eval(lang_text) or {} + return safe_eval(to_native(lang_text)) or {} except Exception: e = sys.exc_info()[1] status = 'Syntax error in %s (%s)' % (filename, e) @@ -623,7 +622,6 @@ class translator(object): of them matches possible_languages. """ pl_info = read_possible_languages(self.langpath) - def set_plural(language): """ initialize plural forms subsystem @@ -800,18 +798,16 @@ class translator(object): the ## notation is ignored in multiline strings and strings that start with ##. This is needed to allow markmin syntax to be translated """ - if isinstance(message, unicodeT): - message = message.encode('utf8') - if isinstance(prefix, unicodeT): - prefix = prefix.encode('utf8') + message = to_native(message, 'utf8') + prefix = to_native(prefix, 'utf8') key = prefix + message mt = self.t.get(key, None) if mt is not None: return mt # we did not find a translation - if message.find(to_bytes('##')) > 0: + if message.find('##') > 0: pass - if message.find(to_bytes('##')) > 0 and not '\n' in message: + if message.find('##') > 0 and not '\n' in message: # remove comments message = message.rsplit('##', 1)[0] # guess translation same as original @@ -966,7 +962,7 @@ def findT(path, language=DEFAULT_LANGUAGE): for filename in \ listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\ + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): - data = read_locked(filename) + data = to_native(read_locked(filename)) items = regex_translate.findall(data) items += regex_translate_m.findall(data) for item in items: diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index cb3f7c13..5db0c683 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -17,11 +17,11 @@ from .test_validators import * from .test_tools import * from .test_utils import * from .test_serializers import * +from .test_languages import * if sys.version[:3] == '2.7': from .test_compileapp import * from .test_is_url import * - from .test_languages import * from .test_appadmin import * from .test_scheduler import * from .test_web import * diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index bed7ff42..0df6f21b 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -15,6 +15,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon import languages +from gluon._compat import PY2 MP_WORKING = 0 try: @@ -105,8 +106,10 @@ class TestTranslations(unittest.TestCase): '1 quark') self.assertEqual(str(T('%s %%{quark[0]}', 2)), '2 quarks') - self.assertEqual(str(T.M('**Hello World**')), - 'Hello World') + if PY2: + # FIXME PY3 markmin is not supported yet + self.assertEqual(str(T.M('**Hello World**')), + 'Hello World') T.force('it') self.assertEqual(str(T('Hello World')), 'Salve Mondo') diff --git a/gluon/utf8.py b/gluon/utf8.py index 6fbab2dd..21fd12c4 100644 --- a/gluon/utf8.py +++ b/gluon/utf8.py @@ -11,7 +11,7 @@ Utilities and class for UTF8 strings managing ---------------------------------------------- """ from __future__ import print_function -from gluon._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode +from gluon._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode, to_native __all__ = ['Utf8'] @@ -51,10 +51,10 @@ def sort_key(s): from gluon.contrib.pyuca import unicode_collator unicode_sort_key = unicode_collator.sort_key sort_key = lambda s: unicode_sort_key( - unicode(s, 'utf-8') if isinstance(s, str) else s) + to_unicode(s, 'utf-8') if isinstance(s, str) else s) except: sort_key = lambda s: ( - unicode(s, 'utf-8') if isinstance(s, str) else s).lower() + to_unicode(s, 'utf-8') if isinstance(s, str) else s).lower() return sort_key(s) @@ -64,7 +64,7 @@ def ord(char): """ if isinstance(char, unicodeT): return __builtin__.ord(char) - return __builtin__.ord(unicode(char, 'utf-8')) + return __builtin__.ord(to_unicode(char, 'utf-8')) def chr(code): @@ -92,8 +92,8 @@ def truncate(string, length, dots='...'): Returns: (utf8-str): original or cutted string """ - text = unicode(string, 'utf-8') - dots = unicode(dots, 'utf-8') if isinstance(dots, str) else dots + text = to_unicode(string, 'utf-8') + dots = to_unicode(dots, 'utf-8') if isinstance(dots, str) else dots if len(text) > length: text = text[:length - len(dots)] + dots return str.__new__(Utf8, text.encode('utf-8')) @@ -120,11 +120,11 @@ class Utf8(str): """ def __new__(cls, content='', codepage='utf-8'): if isinstance(content, unicodeT): - return str.__new__(cls, unicode.encode(content, 'utf-8')) + return str.__new__(cls, to_native(content, 'utf-8')) elif codepage in ('utf-8', 'utf8') or isinstance(content, cls): return str.__new__(cls, content) else: - return str.__new__(cls, unicode(content, codepage).encode('utf-8')) + return str.__new__(cls, to_native(to_unicode(content, codepage), 'utf-8')) def __repr__(self): r''' # note that we use raw strings to avoid having to use double back slashes below @@ -156,9 +156,9 @@ class Utf8(str): True ''' if str.find(self, "'") >= 0 and str.find(self, '"') < 0: # only single quote exists - return '"' + unicode(self, 'utf-8').translate(repr_escape_tab).encode('utf-8') + '"' + return '"' + to_native(to_unicode(self, 'utf-8').translate(repr_escape_tab), 'utf-8') + '"' else: - return "'" + unicode(self, 'utf-8').translate(repr_escape_tab2).encode('utf-8') + "'" + return "'" + to_native(to_unicode(self, 'utf-8').translate(repr_escape_tab2), 'utf-8') + "'" def __size__(self): """ length of utf-8 string in bytes """ @@ -168,17 +168,17 @@ class Utf8(str): return str.__contains__(self, Utf8(other)) def __getitem__(self, index): - return str.__new__(Utf8, unicode(self, 'utf-8')[index].encode('utf-8')) + return str.__new__(Utf8, to_native(to_unicode(self, 'utf-8')[index], 'utf-8')) def __getslice__(self, begin, end): - return str.__new__(Utf8, unicode(self, 'utf-8')[begin:end].encode('utf-8')) + return str.__new__(Utf8, to_native(to_unicode(self, 'utf-8')[begin:end], 'utf-8')) def __add__(self, other): return str.__new__(Utf8, str.__add__(self, unicode.encode(other, 'utf-8') if isinstance(other, unicode) else other)) def __len__(self): - return len(unicode(self, 'utf-8')) + return len(to_unicode(self, 'utf-8')) def __mul__(self, integer): return str.__new__(Utf8, str.__mul__(self, integer)) From 48209f5bdfff6f6a52c2980b5277354c5ee596b2 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 12 Jun 2016 13:22:34 +0200 Subject: [PATCH 007/436] fix compileapp --- gluon/compileapp.py | 6 +++--- gluon/custom_import.py | 6 +++--- gluon/fileutils.py | 6 +++++- gluon/globals.py | 4 ++-- gluon/template.py | 7 +++---- gluon/tests/__init__.py | 2 +- gluon/tools.py | 4 ++-- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 679732c3..7c28fcbb 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2 +from gluon._compat import builtin, PY2, unicodeT, to_native from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 @@ -650,8 +650,8 @@ def run_controller_in(controller, function, environment): vars = response._vars if response.postprocessing: vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) - if isinstance(vars, unicode): - vars = vars.encode('utf8') + if isinstance(vars, unicodeT): + vars = to_native(vars) elif hasattr(vars, 'xml') and callable(vars.xml): vars = vars.xml() return vars diff --git a/gluon/custom_import.py b/gluon/custom_import.py index 4bd3ab5f..ad055153 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -8,7 +8,7 @@ Support for smart import syntax for web2py applications ------------------------------------------------------- """ -from gluon._compat import builtin, unicodeT, PY2 +from gluon._compat import builtin, unicodeT, PY2, to_native import os import sys import threading @@ -47,8 +47,8 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): If the import fails, it falls back on naive_importer """ - if isinstance(name, unicodeT) and PY2: - name = name.encode('utf8') + if isinstance(name, unicodeT): + name = to_native(name) globals = globals or {} locals = locals or {} diff --git a/gluon/fileutils.py b/gluon/fileutils.py index 84e65365..e9d869b3 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -21,6 +21,7 @@ import logging from gluon.http import HTTP from gzip import open as gzopen from gluon.recfile import generate +from gluon._compat import PY2 __all__ = [ 'parse_version', @@ -100,7 +101,10 @@ def read_file(filename, mode='r'): """Returns content from filename, making sure to close the file explicitly on exit. """ - f = open(filename, mode) + if PY2 or 'b' in mode: + f = open(filename, mode) + else: + f = open(filename, mode, encoding="utf8") try: return f.read() finally: diff --git a/gluon/globals.py b/gluon/globals.py index 43079593..717d923b 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,7 @@ Contains the classes for the global used variables: - Session """ -from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native +from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -569,7 +569,7 @@ class Response(Storage): if not request: request = current.request - if isinstance(stream, (str, unicode)): + if isinstance(stream, (str, unicodeT)): stream_file_or_304_or_206(stream, chunk_size=chunk_size, request=request, diff --git a/gluon/template.py b/gluon/template.py index 7d79c573..2afc29ab 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -263,7 +263,6 @@ class TemplateParser(object): # This will end up as # "%s(%s, escape=False)" % (self.writer, value) self.writer = writer - # Dictionary of custom name lexers to use. if isinstance(lexers, dict): self.lexers = lexers @@ -448,7 +447,7 @@ class TemplateParser(object): fileobj.close() except IOError: self._raise_error('Unable to open included view file: ' + filepath) - + text = to_native(text) return text def include(self, content, filename): @@ -788,7 +787,7 @@ def parse_template(filename, raise RestrictedError(filename, '', 'Unable to find the file') else: text = filename.read() - + text = to_native(text) # Use the file contents to get a parsed template and return it. return str(TemplateParser(text, context=context, path=path, lexers=lexers, delimiters=delimiters)) @@ -885,7 +884,7 @@ def render(content="hello world", """ # here to avoid circular Imports try: - from globals import Response + from gluon.globals import Response except ImportError: # Working standalone. Build a mock Response object. Response = DummyResponse diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 5db0c683..8a43f01c 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -18,9 +18,9 @@ from .test_tools import * from .test_utils import * from .test_serializers import * from .test_languages import * +from .test_compileapp import * if sys.version[:3] == '2.7': - from .test_compileapp import * from .test_is_url import * from .test_appadmin import * from .test_scheduler import * diff --git a/gluon/tools.py b/gluon/tools.py index 8736b814..7bfa4b7f 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1457,7 +1457,7 @@ class AuthJWT(object): def f(*args, **kwargs): try: token = self.get_jwt_token_from_request(token_param=token_param) - except HTTP, e: + except HTTP as e: if required: raise e token = None @@ -4759,7 +4759,7 @@ class Auth(object): self._wiki.automenu() -class Crud(object): +class Crud(object): # pragma: no cover def url(self, f=None, args=None, vars=None): """ From 11fec259279648bac1d3f7b3dff914b1d26e8fed Mon Sep 17 00:00:00 2001 From: Oscar Fonts Date: Tue, 14 Jun 2016 11:06:14 +0200 Subject: [PATCH 008/436] Don't truncate texts on SQLFORM.grid HTML Export --- gluon/sqlhtml.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 23b9642e..51055c6c 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2476,8 +2476,7 @@ class SQLFORM(FORM): response.headers['Content-Type'] = oExp.content_type response.headers['Content-Disposition'] = \ 'attachment;filename=' + filename + ';' - document = oExp.export(maxtextlength) if oExp.__class__.__name__== 'ExporterHTML' else oExp.export() - raise HTTP(200, document, **response.headers) + raise HTTP(200, oExp.export(), **response.headers) elif request.vars.records and not isinstance( request.vars.records, list): @@ -3550,8 +3549,8 @@ class ExporterHTML(ExportClass): def __init__(self, rows): ExportClass.__init__(self, rows) - def export(self, truncate=16): - table = SQLTABLE(self.rows, truncate=truncate) if self.rows else '' + def export(self): + table = SQLTABLE(self.rows, truncate=None) if self.rows else '' return '\n\n\n\n\n%s\n\n' % (table or '') From 0f648eee561184c8da50a894f0b5308412a894b8 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 14 Jun 2016 18:01:12 +0200 Subject: [PATCH 009/436] enabled pg8000 pymysql --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 73433e70..a55f3cf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ install: virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install --download-cache $HOME/.pip-cache pycrypto; fi; + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install --download-cache $HOME/.pip-cache pycrypto pg8000 pymysql; fi; - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; before_script: From 2aeb063890d6177de354d6f7c324a2f449c184e4 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 14 Jun 2016 20:31:41 +0200 Subject: [PATCH 010/436] enabled test_appadmin, fix markmin2html, fix main.py --- gluon/compileapp.py | 6 +++--- gluon/contrib/markmin/markmin2html.py | 18 ++++++------------ gluon/custom_import.py | 3 +++ gluon/html.py | 3 ++- gluon/languages.py | 8 +++----- gluon/main.py | 5 +++-- gluon/rocket.py | 4 ++-- gluon/streamer.py | 8 ++++++-- gluon/tests/__init__.py | 2 +- gluon/tests/test_languages.py | 6 ++---- 10 files changed, 31 insertions(+), 32 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 7c28fcbb..b6bbda79 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native +from gluon._compat import builtin, PY2, unicodeT, to_native, basestring from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 @@ -151,7 +151,7 @@ def LOAD(c=None, f='index', args=None, vars=None, "infinity" or "continuous" are accepted to reload indefinitely the component """ - from html import TAG, DIV, URL, SCRIPT, XML + from gluon.html import TAG, DIV, URL, SCRIPT, XML if args is None: args = [] vars = Storage(vars or {}) @@ -430,7 +430,7 @@ def build_environment(request, response, session, store_current=True): __builtins__ = mybuiltin() elif is_pypy: # apply the same hack to pypy too __builtins__ = mybuiltin() - else: + elif PY2: __builtins__['__import__'] = builtin.__import__ # WHY? environment['request'] = request environment['response'] = response diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index cae5ff18..0752e063 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -6,15 +6,10 @@ from __future__ import print_function import re import urllib -from cgi import escape -from gluon._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes +from gluon._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape +from ast import parse as ast_parse +import ast -try: - from ast import parse as ast_parse - import ast -except ImportError: # python 2.5 - from compiler import parse - import compiler.ast as ast """ TODO: next version should use MathJax @@ -950,12 +945,11 @@ def render(text, if protolinks == "default": protolinks = protolinks_simple pp = '\n' if pretty_print else '' - if isinstance(text, unicodeT): - text = text.encode('utf8') - text = str(text or '') + text = to_native(text) + if not (isinstance(text, str)): + text = str(text or '') text = regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), text) text = text.replace('\x05', '').replace('\r\n', '\n') # concatenate strings separeted by \\n - if URL is not None: text = replace_at_urls(text, URL) diff --git a/gluon/custom_import.py b/gluon/custom_import.py index ad055153..8e848626 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -104,6 +104,9 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): finally: if import_tb: import_tb = None + elif not(PY2) and level < 0: + # FIXME PY3 why level is < 0? + level = 0 return NATIVE_IMPORTER(name, globals, locals, fromlist, level) diff --git a/gluon/html.py b/gluon/html.py index 642f097c..f2fb7b9b 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -974,7 +974,8 @@ class DIV(XmlComponent): """ str(COMPONENT) returns COMPONENT.xml() """ - return self.xml() + # In PY3 __str__ cannot return bytes (TypeError: __str__ returned non-string (type bytes)) + return to_native(self.xml()) def flatten(self, render=None): """ diff --git a/gluon/languages.py b/gluon/languages.py index b40cabd3..6b4748d8 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -27,9 +27,7 @@ from gluon.utf8 import Utf8 from gluon.fileutils import listdir from gluon.cfs import getcfs from gluon.html import XML, xmlescape -if PY2: - # FIXME PY3 - from gluon.contrib.markmin.markmin2html import render, markmin_escape +from gluon.contrib.markmin.markmin2html import render, markmin_escape __all__ = ['translator', 'findT', 'update_all_languages'] @@ -761,10 +759,10 @@ class translator(object): symbols = (symbols,) symbols = tuple( value if isinstance(value, NUMBERS) - else xmlescape(value).translate(ttab_in) + else to_native(xmlescape(value)).translate(ttab_in) for value in symbols) message = self.params_substitution(message, symbols) - return XML(message.translate(ttab_out)) + return to_native(XML(message.translate(ttab_out)).xml()) def M(self, message, symbols={}, language=None, lazy=None, filter=None, ftag=None, ns=None): diff --git a/gluon/main.py b/gluon/main.py index d7a977c9..e438f4ba 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -298,6 +298,7 @@ def wsgibase(environ, responder): env.web2py_version = web2py_version #env.update(global_settings) static_file = False + http_response = None try: try: try: @@ -439,8 +440,8 @@ def wsgibase(environ, responder): gluon.debug.dbg.do_debug(mainpyfile=request.folder) serve_controller(request, response, session) - - except HTTP as http_response: + except HTTP as hr: + http_response = hr if static_file: return http_response.to(responder, env=env) diff --git a/gluon/rocket.py b/gluon/rocket.py index 51460532..199f1004 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -11,7 +11,7 @@ import errno import socket import logging import platform -from gluon._compat import iteritems +from gluon._compat import iteritems, to_bytes # Define Constants VERSION = '1.2.6' @@ -1772,7 +1772,7 @@ class WSGIWorker(Worker): if self.chunked: self.conn.sendall(b('%x\r\n%s\r\n' % (len(data), data))) else: - self.conn.sendall(data) + self.conn.sendall(to_bytes(data)) except socket.timeout: self.closeConnection = True except socket.error: diff --git a/gluon/streamer.py b/gluon/streamer.py index 1095b592..dd7b78b2 100644 --- a/gluon/streamer.py +++ b/gluon/streamer.py @@ -17,6 +17,7 @@ import re import errno from gluon.http import HTTP from gluon.contenttype import contenttype +from gluon._compat import PY2 regex_start_range = re.compile('\d+(?=\-)') @@ -53,8 +54,11 @@ def stream_file_or_304_or_206( # if error_message is None: # error_message = rewrite.THREAD_LOCAL.routes.error_message % 'invalid request' try: - open = file # this makes no sense but without it GAE cannot open files - fp = open(static_file,'rb') + if PY2: + open_f = file # this makes no sense but without it GAE cannot open files + else: + open_f = open + fp = open_f(static_file,'rb') except IOError as e: if e[0] == errno.EISDIR: raise HTTP(403, error_message, web2py_error='file is a directory') diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 8a43f01c..4c879ad3 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -19,10 +19,10 @@ from .test_utils import * from .test_serializers import * from .test_languages import * from .test_compileapp import * +from .test_appadmin import * if sys.version[:3] == '2.7': from .test_is_url import * - from .test_appadmin import * from .test_scheduler import * from .test_web import * from .test_old_doctests import * diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index 0df6f21b..acc621ea 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -106,10 +106,8 @@ class TestTranslations(unittest.TestCase): '1 quark') self.assertEqual(str(T('%s %%{quark[0]}', 2)), '2 quarks') - if PY2: - # FIXME PY3 markmin is not supported yet - self.assertEqual(str(T.M('**Hello World**')), - 'Hello World') + self.assertEqual(str(T.M('**Hello World**')), + 'Hello World') T.force('it') self.assertEqual(str(T('Hello World')), 'Salve Mondo') From 6bb255286a5cad4507bc0414fcad634d6d19f82e Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 15 Jun 2016 20:26:09 +0200 Subject: [PATCH 011/436] repeats via cronline expression --- gluon/scheduler.py | 293 ++++++++++++++++++++++++---- gluon/tests/test_scheduler.py | 351 +++++++++++++++++++++++++++++++++- 2 files changed, 603 insertions(+), 41 deletions(-) diff --git a/gluon/scheduler.py b/gluon/scheduler.py index c2c9bd70..0f2a87ee 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -9,9 +9,9 @@ Background processes made simple --------------------------------- """ from __future__ import print_function -from gluon._compat import Queue, long, iteritems import os +import re import time import multiprocessing import sys @@ -24,13 +24,13 @@ import logging import optparse import tempfile import types - +from functools import reduce from json import loads, dumps -from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB +from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_EMPTY_OR from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB from gluon.utils import web2py_uuid +from gluon._compat import Queue, long, iteritems from gluon.storage import Storage -from functools import reduce USAGE = """ ## Example @@ -71,23 +71,6 @@ http://127.0.0.1:8000/myapp/appadmin/select/db?query=db.scheduler_run.id>0 ## view workers http://127.0.0.1:8000/myapp/appadmin/select/db?query=db.scheduler_worker.id>0 -## To install the scheduler as a permanent daemon on Linux (w/ Upstart), put -## the following into /etc/init/web2py-scheduler.conf: -## (This assumes your web2py instance is installed in 's home directory, -## running as , with app , on network interface eth0.) - -description "web2py task scheduler" -start on (local-filesystems and net-device-up IFACE=eth0) -stop on shutdown -respawn limit 8 60 # Give up if restart occurs 8 times in 60 seconds. -exec sudo -u python /home//web2py/web2py.py -K -respawn - -## You can then start/stop/restart/check status of the daemon with: -sudo start web2py-scheduler -sudo stop web2py-scheduler -sudo restart web2py-scheduler -sudo status web2py-scheduler """ path = os.getcwd() @@ -160,7 +143,7 @@ class TaskReport(object): class JobGraph(object): - """Experimental: dependencies amongs tasks""" + """Experimental: dependencies amongs tasks.""" def __init__(self, db, job_name): self.job_name = job_name or 'job_0' @@ -216,9 +199,216 @@ class JobGraph(object): db.rollback() return None + +class CronParser(object): + + def __init__(self, cronline, base=None): + self.cronline = cronline + self.sched = base or datetime.datetime.now() + self.task = None + + @staticmethod + def _rangetolist(s, period='min'): + retval = [] + if s.startswith('*'): + if period == 'min': + s = s.replace('*', '0-59', 1) + elif period == 'hr': + s = s.replace('*', '0-23', 1) + elif period == 'dom': + s = s.replace('*', '1-31', 1) + elif period == 'mon': + s = s.replace('*', '1-12', 1) + elif period == 'dow': + s = s.replace('*', '0-6', 1) + m = re.compile(r'(\d+)-(\d+)/(\d+)') + match = m.match(s) + if match: + min_, max_ = int(match.group(1)), int(match.group(2)) + 1 + step_ = int(match.group(3)) + else: + m = re.compile(r'(\d+)/(\d+)') + ranges_max = {'min': 59, 'hr': 23, 'mon': 12, 'dom': 31, 'dow': 7} + match = m.match(s) + if match: + min_, max_ = int(match.group(1)), ranges_max[period] + 1 + step_ = int(match.group(2)) + if match: + for i in range(min_, max_, step_): + retval.append(i) + return retval + + @staticmethod + def _sanitycheck(values, period): + if period == 'min': + check = all(0 <= i <= 59 for i in values) + elif period == 'hr': + check = all(0 <= i <= 23 for i in values) + elif period == 'dom': + check = all(1 <= i <= 31 or i == 'l' for i in values) + elif period == 'mon': + check = all(1 <= i <= 12 for i in values) + elif period == 'dow': + check = all(0 <= i <= 7 for i in values) + return check + + def _parse(self): + line = self.cronline.lower() + task = {} + if line.startswith('@yearly'): + line = line.replace('@yearly', '0 0 1 1 *') + elif line.startswith('@annually'): + line = line.replace('@annually', '0 0 1 1 *') + elif line.startswith('@monthly'): + line = line.replace('@monthly', '0 0 1 * *') + elif line.startswith('@weekly'): + line = line.replace('@weekly', '0 0 * * 0') + elif line.startswith('@daily'): + line = line.replace('@daily', '0 0 * * *') + elif line.startswith('@midnight'): + line = line.replace('@midnight', '0 0 * * *') + elif line.startswith('@hourly'): + line = line.replace('@hourly', '0 * * * *') + params = line.strip().split() + if len(params) < 5: + raise ValueError('Invalid cron line (too short)') + elif len(params) > 5: + raise ValueError('Invalid cron line (too long)') + daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, + 'fri': 5, 'sat': 6} + monthsofyear = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, + 'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, + 'nov': 11, 'dec': 12, 'l': 'l'} + for (s, i) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']): + if s not in [None, '*']: + task[i] = [] + vals = s.split(',') + for val in vals: + if i == 'dow': + refdict = daysofweek + elif i == 'mon': + refdict = monthsofyear + if i in ('dow', 'mon') and '-' in val and '/' not in val: + isnum = val.split('-')[0].isdigit() + if isnum: + val = '%s/1' % val + else: + val = '-'.join([str(refdict[v]) + for v in val.split('-')]) + if val != '-1' and '-' in val and '/' not in val: + val = '%s/1' % val + if '/' in val: + task[i] += self._rangetolist(val, i) + elif val.isdigit() or val == '-1': + task[i].append(int(val)) + elif i in ('dow', 'mon'): + if val in refdict: + task[i].append(refdict[val]) + elif i == 'dom' and val == 'l': + task[i].append(val) + if not task[i]: + raise ValueError('Invalid cron value (%s)' % s) + if not self._sanitycheck(task[i], i): + raise ValueError('Invalid cron value (%s)' % s) + task[i] = sorted(task[i]) + self.task = task + + @staticmethod + def _get_next_dow(sched, task): + task_dow = [a % 7 for a in task['dow']] + while sched.isoweekday() % 7 not in task_dow: + sched += datetime.timedelta(days=1) + return sched + + @staticmethod + def _get_next_dom(sched, task): + if task['dom'] == ['l']: + last_feb = 29 if sched.year % 4 == 0 else 28 + lastdayofmonth = [ + 31, last_feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + ] + task_dom = [lastdayofmonth[sched.month - 1]] + else: + task_dom = task['dom'] + while sched.day not in task_dom: + sched += datetime.timedelta(days=1) + return sched + + @staticmethod + def _get_next_mon(sched, task): + while sched.month not in task['mon']: + if sched.month < 12: + sched = sched.replace(month=sched.month + 1) + else: + sched = sched.replace(month=1, year=sched.year + 1) + return sched + + @staticmethod + def _getnext_hhmm(sched, task, add_to=True): + if add_to: + sched += datetime.timedelta(minutes=1) + if 'min' in task: + while sched.minute not in task['min']: + sched += datetime.timedelta(minutes=1) + if 'hr' in task and sched.hour not in task['hr']: + while sched.hour not in task['hr']: + sched += datetime.timedelta(hours=1) + return sched + + def _getnext_date(self, sched, task): + if 'dow' in task and 'dom' in task: + dow = self._get_next_dow(sched, task) + dom = self._get_next_dom(sched, task) + sched = min(dow, dom) + elif 'dow' in task: + sched = self._get_next_dow(sched, task) + elif 'dom' in task: + sched = self._get_next_dom(sched, task) + if 'mon' in task: + sched = self._get_next_mon(sched, task) + return sched.replace(hour=0, minute=0) + + def get_next(self): + """Get next date according to specs.""" + if not self.task: + self._parse() + task = self.task + sched = self.sched + x = 0 + while x < 1000: # avoid potential max recursions + x += 1 + try: + next_date = self._getnext_date(sched, task) + except (ValueError, OverflowError) as e: + raise ValueError('Invalid cron expression (%s)' % e) + if next_date.date() > self.sched.date(): + # we rolled date, check for valid hhmm + sched = self._getnext_hhmm(next_date, task, False) + break + else: + # same date, get next hhmm + sched_time = self._getnext_hhmm(sched, task, True) + if sched_time.date() > sched.date(): + # we rolled date again :( + sched = sched_time + else: + sched = sched_time + break + else: + raise ValueError('Potential bug found, please submit your ' + 'cron expression to the authors') + self.sched = sched + return sched + + def __iter__(self): + """Support iteration.""" + return self + + __next__ = next = get_next + # the two functions below deal with simplejson decoding as unicode, esp for the dict decode # and subsequent usage as function Keyword arguments unicode variable names won't work! -# borrowed from http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python +# borrowed from http://stackoverflow.com/questions/956867/ def _decode_list(lst): @@ -321,7 +511,7 @@ def executor(queue, task, out): class MetaScheduler(threading.Thread): - """Base class documenting scheduler's base methods""" + """Base class documenting scheduler's base methods.""" def __init__(self): threading.Thread.__init__(self) @@ -377,7 +567,7 @@ class MetaScheduler(threading.Thread): while not out.empty(): tout += out.get() if tout: - logger.debug(' partial output: "%s"' % str(tout)) + logger.debug(' partial output: "%s"', str(tout)) if CLEAROUT in tout: task_output = tout[ tout.rfind(CLEAROUT) + len(CLEAROUT):] @@ -428,7 +618,7 @@ class MetaScheduler(threading.Thread): self.have_heartbeat = False def terminate_process(self): - """Terminates any running tasks (internal use only)""" + """Terminate any running tasks (internal use only)""" try: self.process.terminate() except: @@ -468,6 +658,22 @@ TASK_STATUS = (QUEUED, RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED, EXPIRED) RUN_STATUS = (RUNNING, COMPLETED, FAILED, TIMEOUT, STOPPED) WORKER_STATUS = (ACTIVE, PICK, DISABLED, TERMINATE, KILL, STOP_TASK) +class IS_CRONLINE(object): + """ + Validates cronline + """ + def __init__(self, error_message=None): + self.error_message = error_message + + def __call__(self, value): + recur = CronParser(value, datetime.datetime.now()) + try: + recur.get_next() + return (value, None) + except (KeyError, ValueError) as e: + if not self.error_message: + return (value, e) + return (value, self.error_message) class TYPE(object): """ @@ -612,7 +818,10 @@ class Scheduler(MetaScheduler): Field('period', 'integer', default=60, comment='seconds', requires=IS_INT_IN_RANGE(0, None)), Field('prevent_drift', 'boolean', default=False, - comment='Cron-like start_times between runs'), + comment='Exact start_times between runs'), + Field('cronline', default=None, + comment='Discard "period", use this cron expr instead', + requires=IS_EMPTY_OR(IS_CRONLINE())), Field('timeout', 'integer', default=60, comment='seconds', requires=IS_INT_IN_RANGE(1, None)), Field('sync_output', 'integer', default=0, @@ -665,14 +874,6 @@ class Scheduler(MetaScheduler): if migrate is not False: db.commit() - @staticmethod - def total_seconds(td): - """Backport for py2.6.""" - if hasattr(td, 'total_seconds'): - return td.total_seconds() - else: - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10.0 ** 6 - def loop(self, worker_name=None): """Main loop. @@ -694,7 +895,7 @@ class Scheduler(MetaScheduler): signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) try: self.start_heartbeats() - while True and self.have_heartbeat: + while self.have_heartbeat: if self.w_stats.status == DISABLED: logger.debug('Someone stopped me, sleeping until better' ' times come (%s)', self.w_stats.sleep) @@ -798,7 +999,10 @@ class Scheduler(MetaScheduler): logger.info('nothing to do') return None times_run = task.times_run + 1 - if not task.prevent_drift: + if task.cronline: + cron_recur = CronParser(task.cronline, now.replace(second=0)) + next_run_time = cron_recur.get_next() + elif not task.prevent_drift: next_run_time = task.last_run_time + datetime.timedelta( seconds=task.period ) @@ -806,7 +1010,7 @@ class Scheduler(MetaScheduler): # calc next_run_time based on available slots # see #1191 next_run_time = task.start_time - secondspassed = self.total_seconds(now - next_run_time) + secondspassed = (now - next_run_time).total_seconds() steps = secondspassed // task.period + 1 next_run_time += datetime.timedelta(seconds=task.period * steps) @@ -1275,13 +1479,22 @@ class Scheduler(MetaScheduler): tuuid = 'uuid' in kwargs and kwargs.pop('uuid') or web2py_uuid() tname = 'task_name' in kwargs and kwargs.pop('task_name') or function immediate = 'immediate' in kwargs and kwargs.pop('immediate') or None - rtn = self.db.scheduler_task.validate_and_insert( + cronline = kwargs.get('cronline') + kwargs.update( function_name=function, task_name=tname, args=targs, vars=tvars, uuid=tuuid, - **kwargs) + ) + if cronline: + try: + start_time = kwargs.get('start_time', self.now) + next_run_time = CronParser(cronline, start_time).get_next() + kwargs.update(start_time=start_time, next_run_time=next_run_time) + except: + pass + rtn = self.db.scheduler_task.validate_and_insert(**kwargs) if not rtn.errors: rtn.uuid = tuuid if immediate: diff --git a/gluon/tests/test_scheduler.py b/gluon/tests/test_scheduler.py index c1b9616e..29cd877a 100644 --- a/gluon/tests/test_scheduler.py +++ b/gluon/tests/test_scheduler.py @@ -15,7 +15,7 @@ fix_sys_path(__file__) from gluon.storage import Storage from gluon.languages import translator -from gluon.scheduler import JobGraph, Scheduler +from gluon.scheduler import JobGraph, Scheduler, CronParser from gluon.dal import DAL @@ -51,6 +51,355 @@ class BaseTestScheduler(unittest.TestCase): pass +class CronParserTest(unittest.TestCase): + + def testMinute(self): + # minute asterisk + base = datetime.datetime(2010, 1, 23, 12, 18) + itr = CronParser('*/1 * * * *', base) + n1 = itr.get_next() # 19 + self.assertEqual(base.year, n1.year) + self.assertEqual(base.month, n1.month) + self.assertEqual(base.day, n1.day) + self.assertEqual(base.hour, n1.hour) + self.assertEqual(base.minute, n1.minute - 1) + for i in range(39): # ~ 58 + itr.get_next() + n2 = itr.get_next() + self.assertEqual(n2.minute, 59) + n3 = itr.get_next() + self.assertEqual(n3.minute, 0) + self.assertEqual(n3.hour, 13) + + itr = CronParser('*/5 * * * *', base) + n4 = itr.get_next() + self.assertEqual(n4.minute, 20) + for i in range(6): + itr.get_next() + n5 = itr.get_next() + self.assertEqual(n5.minute, 55) + n6 = itr.get_next() + self.assertEqual(n6.minute, 0) + self.assertEqual(n6.hour, 13) + + base = datetime.datetime(2010, 1, 23, 12, 18) + itr = CronParser('4/34 * * * *', base) + n7 = itr.get_next() + self.assertEqual(n7.minute, 38) + self.assertEqual(n7.hour, 12) + n8 = itr.get_next() + self.assertEqual(n8.minute, 4) + self.assertEqual(n8.hour, 13) + + def testHour(self): + base = datetime.datetime(2010, 1, 24, 12, 2) + itr = CronParser('0 */3 * * *', base) + n1 = itr.get_next() + self.assertEqual(n1.hour, 15) + self.assertEqual(n1.minute, 0) + for i in range(2): + itr.get_next() + n2 = itr.get_next() + self.assertEqual(n2.hour, 0) + self.assertEqual(n2.day, 25) + + def testDay(self): + base = datetime.datetime(2010, 2, 24, 12, 9) + itr = CronParser('0 0 */3 * *', base) + n1 = itr.get_next() + # 1 4 7 10 13 16 19 22 25 28 + self.assertEqual(n1.day, 25) + n2 = itr.get_next() + self.assertEqual(n2.day, 28) + n3 = itr.get_next() + self.assertEqual(n3.day, 1) + self.assertEqual(n3.month, 3) + + # test leap year + base = datetime.datetime(1996, 2, 27) + itr = CronParser('0 0 * * *', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 28) + self.assertEqual(n1.month, 2) + n2 = itr.get_next() + self.assertEqual(n2.day, 29) + self.assertEqual(n2.month, 2) + + base2 = datetime.datetime(2000, 2, 27) + itr2 = CronParser('0 0 * * *', base2) + n3 = itr2.get_next() + self.assertEqual(n3.day, 28) + self.assertEqual(n3.month, 2) + n4 = itr2.get_next() + self.assertEqual(n4.day, 29) + self.assertEqual(n4.month, 2) + + def testWeekDay(self): + base = datetime.datetime(2010, 2, 25) + itr = CronParser('0 0 * * sat', base) + n1 = itr.get_next() + self.assertEqual(n1.isoweekday(), 6) + self.assertEqual(n1.day, 27) + n2 = itr.get_next() + self.assertEqual(n2.isoweekday(), 6) + self.assertEqual(n2.day, 6) + self.assertEqual(n2.month, 3) + + base = datetime.datetime(2010, 1, 25) + itr = CronParser('0 0 1 * wed', base) + n1 = itr.get_next() + self.assertEqual(n1.month, 1) + self.assertEqual(n1.day, 27) + self.assertEqual(n1.year, 2010) + n2 = itr.get_next() + self.assertEqual(n2.month, 2) + self.assertEqual(n2.day, 1) + self.assertEqual(n2.year, 2010) + n3 = itr.get_next() + self.assertEqual(n3.month, 2) + self.assertEqual(n3.day, 3) + self.assertEqual(n3.year, 2010) + + def testMonth(self): + base = datetime.datetime(2010, 1, 25) + itr = CronParser('0 0 1 * *', base) + n1 = itr.get_next() + self.assertEqual(n1.month, 2) + self.assertEqual(n1.day, 1) + n2 = itr.get_next() + self.assertEqual(n2.month, 3) + self.assertEqual(n2.day, 1) + for i in range(8): + itr.get_next() + n3 = itr.get_next() + self.assertEqual(n3.month, 12) + self.assertEqual(n3.year, 2010) + n4 = itr.get_next() + self.assertEqual(n4.month, 1) + self.assertEqual(n4.year, 2011) + + base = datetime.datetime(2010, 1, 25) + itr = CronParser('0 0 1 */4 *', base) + n1 = itr.get_next() + self.assertEqual(n1.month, 5) + self.assertEqual(n1.day, 1) + + base = datetime.datetime(2010, 1, 25) + itr = CronParser('0 0 1 1-3 *', base) + n1 = itr.get_next() + self.assertEqual(n1.month, 2) + self.assertEqual(n1.day, 1) + n2 = itr.get_next() + self.assertEqual(n2.month, 3) + self.assertEqual(n2.day, 1) + n3 = itr.get_next() + self.assertEqual(n3.month, 1) + self.assertEqual(n3.day, 1) + + def testSundayToThursdayWithAlphaConversion(self): + base = datetime.datetime(2010, 8, 25, 15, 56) + itr = CronParser("30 22 * * sun-thu", base) + n1 = itr.get_next() + self.assertEqual(base.year, n1.year) + self.assertEqual(base.month, n1.month) + self.assertEqual(base.day, n1.day) + self.assertEqual(22, n1.hour) + self.assertEqual(30, n1.minute) + + def testISOWeekday(self): + base = datetime.datetime(2010, 2, 25) + itr = CronParser('0 0 * * 7', base) + n1 = itr.get_next() + self.assertEqual(n1.isoweekday(), 7) + self.assertEqual(n1.day, 28) + n2 = itr.get_next() + self.assertEqual(n2.isoweekday(), 7) + self.assertEqual(n2.day, 7) + self.assertEqual(n2.month, 3) + base = datetime.datetime(2010, 2, 22) + itr = CronParser('0 0 * * */2', base) + n1 = itr.get_next() + self.assertEqual(n1.isoweekday(), 2) + self.assertEqual(n1.day, 23) + n2 = itr.get_next() + self.assertEqual(n2.isoweekday(), 4) + self.assertEqual(n2.day, 25) + + def testBug2(self): + + base = datetime.datetime(2012, 1, 1, 0, 0) + itr = CronParser('0 * * 3 *', base) + n1 = itr.get_next() + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.month, 3) + self.assertEqual(n1.day, base.day) + self.assertEqual(n1.hour, base.hour) + self.assertEqual(n1.minute, base.minute) + + n2 = itr.get_next() + self.assertEqual(n2.year, base.year) + self.assertEqual(n2.month, 3) + self.assertEqual(n2.day, base.day) + self.assertEqual(n2.hour, base.hour + 1) + self.assertEqual(n2.minute, base.minute) + + n3 = itr.get_next() + self.assertEqual(n3.year, base.year) + self.assertEqual(n3.month, 3) + self.assertEqual(n3.day, base.day) + self.assertEqual(n3.hour, base.hour + 2) + self.assertEqual(n3.minute, base.minute) + + def testBug3(self): + base = datetime.datetime(2013, 3, 1, 12, 17, 34, 257877) + c = CronParser('00 03 16,30 * *', base) + + n1 = c.get_next() + self.assertEqual(n1.month, 3) + self.assertEqual(n1.day, 16) + + n2 = c.get_next() + self.assertEqual(n2.month, 3) + self.assertEqual(n2.day, 30) + + n3 = c.get_next() + self.assertEqual(n3.month, 4) + self.assertEqual(n3.day, 16) + + def test_rangeGenerator(self): + base = datetime.datetime(2013, 3, 4, 0, 0) + itr = CronParser('1-9/2 0 1 * *', base) + n1 = itr.get_next() + n2 = itr.get_next() + n3 = itr.get_next() + n4 = itr.get_next() + n5 = itr.get_next() + self.assertEqual(n1.minute, 1) + self.assertEqual(n2.minute, 3) + self.assertEqual(n3.minute, 5) + self.assertEqual(n4.minute, 7) + self.assertEqual(n5.minute, 9) + + def test_iterGenerator(self): + base = datetime.datetime(2013, 3, 4, 0, 0) + itr = CronParser('1-9/2 0 1 * *', base) + x = 0 + for n in itr: + x += 1 + if x > 4: + break + self.assertEqual(n.minute, 9) + + def test_invalidcron(self): + base = datetime.datetime(2013, 3, 4, 0, 0) + itr = CronParser('5 4 31 2 *', base) + self.assertRaises(ValueError, itr.get_next) + itr = CronParser('* * 5-1 * *', base) + self.assertRaises(ValueError, itr.get_next) + itr = CronParser('* * * janu-jun *', base) + self.assertRaises(KeyError, itr.get_next) + itr = CronParser('* * * * * *', base) + self.assertRaises(ValueError, itr.get_next) + itr = CronParser('* * * *', base) + self.assertRaises(ValueError, itr.get_next) + + def testLastDayOfMonth(self): + base = datetime.datetime(2015, 9, 4) + itr = CronParser('0 0 L * *', base) + n1 = itr.get_next() + self.assertEqual(n1.month, 9) + self.assertEqual(n1.day, 30) + n2 = itr.get_next() + self.assertEqual(n2.month, 10) + self.assertEqual(n2.day, 31) + n3 = itr.get_next() + self.assertEqual(n3.month, 11) + self.assertEqual(n3.day, 30) + n4 = itr.get_next() + self.assertEqual(n4.month, 12) + self.assertEqual(n4.day, 31) + + base = datetime.datetime(1996, 2, 27) + itr = CronParser('0 0 L * *', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 29) + self.assertEqual(n1.month, 2) + n2 = itr.get_next() + self.assertEqual(n2.day, 31) + self.assertEqual(n2.month, 3) + + def testSpecialExpr(self): + base = datetime.datetime(2000, 1, 1) + itr = CronParser('@yearly', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 1) + self.assertEqual(n1.month, 1) + self.assertEqual(n1.year, base.year + 1) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + + itr = CronParser('@annually', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 1) + self.assertEqual(n1.month, 1) + self.assertEqual(n1.year, base.year + 1) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + + itr = CronParser('@monthly', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 1) + self.assertEqual(n1.month, base.month + 1) + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + + itr = CronParser('@weekly', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 2) + self.assertEqual(n1.month, base.month) + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + n2 = itr.get_next() + self.assertEqual(n2.day, 9) + self.assertEqual(n2.month, base.month) + self.assertEqual(n2.year, base.year) + self.assertEqual(n2.hour, 0) + self.assertEqual(n2.minute, 0) + n3 = itr.get_next() + self.assertEqual(n3.day, 16) + self.assertEqual(n3.month, base.month) + self.assertEqual(n3.year, base.year) + self.assertEqual(n3.hour, 0) + self.assertEqual(n3.minute, 0) + + itr = CronParser('@daily', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 2) + self.assertEqual(n1.month, base.month) + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + + itr = CronParser('@midnight', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 2) + self.assertEqual(n1.month, base.month) + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.hour, 0) + self.assertEqual(n1.minute, 0) + + itr = CronParser('@hourly', base) + n1 = itr.get_next() + self.assertEqual(n1.day, 1) + self.assertEqual(n1.month, base.month) + self.assertEqual(n1.year, base.year) + self.assertEqual(n1.hour, 1) + self.assertEqual(n1.minute, 0) + + + class TestsForJobGraph(BaseTestScheduler): def testJobGraph(self): From 45a376eee904cf986f01a82e2fd41fed37ec1ed6 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Jun 2016 08:12:01 -0500 Subject: [PATCH 012/436] added extra_mssql_models.py, thanks Kyle Flanagan --- gluon/packages/dal | 2 +- scripts/extract_mssql_models.py | 328 ++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 scripts/extract_mssql_models.py diff --git a/gluon/packages/dal b/gluon/packages/dal index f44ce95f..902f222e 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit f44ce95f685d1fad484fefd22c4105914b1de56e +Subproject commit 902f222eb89451cb41bb20362a27a3410a0fdbb3 diff --git a/scripts/extract_mssql_models.py b/scripts/extract_mssql_models.py new file mode 100644 index 00000000..e6fd807e --- /dev/null +++ b/scripts/extract_mssql_models.py @@ -0,0 +1,328 @@ +"""Create web2py model (python code) to represent MS SQL Server tables. +Features: +* Uses ANSI Standard INFORMATION_SCHEMA (might work with other RDBMS) +* Detects legacy "keyed" tables (not having an "id" PK) +* Handles 'funny' column names. web2py requires all column names be valid python identifiers. This script uses rname +* for column names that have spaces or are otherwise invalid python identifiers. +* Connects directly to running databases, no need to do a SQL dump +* Handles notnull, unique and referential constraints +* Detects most common datatypes and default values +* Supports running from the command line as well as from an IDE's debug menu. See the COMMAND_LINE_MODE constant below +* for more info. + +Requirements: +* Needs pyodbc python connector + +Created by Kyle Flanagan. Based on a script by Mariano Reingart which was +based on a script to "generate schemas from dbs" (mysql) by Alexandre Andrade +""" + +_author__ = "Kyle Flanagan " + +HELP = """ +USAGE: extract_mssql_models db host port user passwd +Call with SQL Server database connection parameters, +web2py model will be printed on standard output. +EXAMPLE: python extract_mssql_models.py mydb localhost 3306 kflanaga pass +or +python extract_mssql_models.py mydb localhost 3306 kflanaga pass > db_model.py +""" + +# Config options +DEBUG = False # print debug messages to STDERR +SCHEMA = 'dbo' +COMMAND_LINE_MODE = True # running from command prompt. Disable to specify variables and use in IDE +# Only specify values below if not running from command line +DB = None +HOST = None +USER = None +PASSWD = None +PORT = None + +# Constant for Field keyword parameter order (and filter): +KWARGS = ('type', 'length', 'default', 'required', 'ondelete', + 'notnull', 'unique', 'label', 'comment', 'rname') + +import sys +import re +# This is from pydal/helpers/regex.py as of 2016-06-16 +# Use this to recognize if a field name need to have an rname representation +REGEX_VALID_TB_FLD = re.compile(r'^[^\d_][_0-9a-zA-Z]*\Z') +# For replacing invalid characters in field names +INVALID_CHARS = re.compile(r'[^a-zA-Z0-9_]') + + +def get_valid_column_name(field): + """Return a valid column name that follows Python's rules for identifiers, which is what web2py requires for column + names. Replaces invalid characters with underscores and leading digits with their associated English word.""" + if not REGEX_VALID_TB_FLD.match(field): + # If the first character is a digit, replace it with its word counterpart + if re.match(r'^[0-9]', field): + numbers = ['Zero', 'One', 'Two', 'Three', 'Four', + 'Five', 'Six', 'Seven', 'Eight', 'Nine'] + field = numbers[int(field[0])] + field[1:] + + field = INVALID_CHARS.sub('_', field) + return field + + +def query(conn, sql, *args): + "Execute a SQL query and return rows as a list of dicts" + cur = conn.cursor() + ret = [] + try: + if DEBUG: print >> sys.stderr, "QUERY: ", sql % args + cur.execute(sql % args) + for row in cur: + dic = {} + for i, value in enumerate(row): + field = cur.description[i][0] + dic[field] = value + if DEBUG: print >> sys.stderr, "RET: ", dic + ret.append(dic) + return ret + finally: + cur.close() + + +def get_tables(conn, schema=SCHEMA): + "List table names in a given schema" + rows = query(conn, """SELECT table_name FROM information_schema.tables + WHERE table_schema = '%s' + ORDER BY table_name""", schema) + return [row['table_name'] for row in rows] + + +def get_fields(conn, table): + "Retrieve field list for a given table" + if DEBUG: print >> sys.stderr, "Processing TABLE", table + rows = query(conn, """ + SELECT column_name, data_type, + is_nullable, + character_maximum_length, + numeric_precision, numeric_precision_radix, numeric_scale, + column_default + FROM information_schema.columns + WHERE table_name='%s' + ORDER BY ordinal_position""", table) + return rows + + +def define_field(conn, table, field, pks): + "Determine field type, default value, references, etc." + f = {} + ref = references(conn, table, field['column_name']) + if ref: + f.update(ref) + elif field['column_default'] and \ + field['column_default'].startswith("nextval") and \ + field['column_name'] in pks: + f['type'] = "'id'" + elif field['data_type'].startswith('character'): + f['type'] = "'string'" + if field['character_maximum_length']: + f['length'] = field['character_maximum_length'] + elif field['data_type'] in ('text', 'ntext'): + f['type'] = "'text'" + elif field['data_type'] in ('boolean', 'bit'): + f['type'] = "'boolean'" + elif field['data_type'] in ('tinyint', 'smallint', 'bigint', 'int'): + f['type'] = "'integer'" + elif field['data_type'] in ('real', 'float'): + f['type'] = "'double'" + elif field['data_type'] in ('datetime', 'datetime2', 'smalldatetime'): + f['type'] = "'datetime'" + elif field['data_type'] in ('timestamp',): + f['type'] = "'datetime'" + f['default'] = "request.now" + f['update'] = "request.now" + elif field['data_type'] in ('date',): + f['type'] = "'date'" + elif field['data_type'] in ('time',): + f['type'] = "'time'" + elif field['data_type'] in ('numeric', 'money', 'smallmoney', 'decimal'): + f['type'] = "'decimal'" + f['precision'] = field['numeric_precision'] + f['scale'] = field['numeric_scale'] or 0 + elif field['data_type'] in ('binary', 'varbinary', 'image'): + f['type'] = "'blob'" + elif field['data_type'] in ('point', 'lseg', 'polygon', 'unknown', 'USER-DEFINED', 'sql_variant'): + f['type'] = "" # unsupported? + elif field['data_type'] in ('varchar', 'char', 'nchar', 'nvarchar', 'uniqueidentifer'): + f['type'] = "'string'" + else: + raise RuntimeError("Data Type not supported: %s " % str(field)) + + try: + if field['column_default']: + if field['column_default'] == "now()": + d = "request.now" + elif field['column_default'] == "true": + d = "True" + elif field['column_default'] == "false": + d = "False" + else: + d = repr(eval(field['column_default'])) + f['default'] = str(d) + except (ValueError, SyntaxError): + pass + except Exception, e: + raise RuntimeError("Default unsupported '%s'" % field['column_default']) + + if not field['is_nullable']: + f['notnull'] = "True" + + # For field names that are not valid python identifiers, we need to add a reference to their actual name + # in the back end database + if not REGEX_VALID_TB_FLD.match(field['column_name']): + f['rname'] = "'[%s]'" % field['column_name'] + + return f + + +def is_unique(conn, table, field): + "Find unique columns (incomplete support)" + rows = query(conn, """ + SELECT c.column_name + FROM information_schema.table_constraints t + INNER JOIN information_schema.constraint_column_usage c + ON (t.CONSTRAINT_CATALOG = c.CONSTRAINT_CATALOG + AND t.CONSTRAINT_NAME = c.CONSTRAINT_NAME + AND t.CONSTRAINT_SCHEMA = c.CONSTRAINT_SCHEMA + AND t.TABLE_CATALOG = c.TABLE_CATALOG + AND t.TABLE_NAME = c.TABLE_NAME + AND t.TABLE_SCHEMA = c.TABLE_SCHEMA) + WHERE t.table_name='%s' + AND c.column_name='%s' + AND t.constraint_type='UNIQUE' + ;""", table, field['column_name']) + return rows and True or False + + +def primarykeys(conn, table): + "Find primary keys" + rows = query(conn, """ + SELECT c.column_name + FROM information_schema.table_constraints t + INNER JOIN information_schema.constraint_column_usage c + ON (t.CONSTRAINT_CATALOG = c.CONSTRAINT_CATALOG + AND t.CONSTRAINT_NAME = c.CONSTRAINT_NAME + AND t.CONSTRAINT_SCHEMA = c.CONSTRAINT_SCHEMA + AND t.TABLE_CATALOG = c.TABLE_CATALOG + AND t.TABLE_NAME = c.TABLE_NAME + AND t.TABLE_SCHEMA = c.TABLE_SCHEMA) + WHERE t.table_name='%s' + AND t.constraint_type='PRIMARY KEY' + ;""", table) + return [row['column_name'] for row in rows] + + +def references(conn, table, field): + "Find a FK (fails if multiple)" + rows1 = query(conn, """ + SELECT k.table_name, k.column_name, k.constraint_name, + r.update_rule, r.delete_rule, k.ordinal_position + FROM information_schema.key_column_usage k + INNER JOIN information_schema.referential_constraints r + ON (k.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG + AND k.CONSTRAINT_NAME = r.CONSTRAINT_NAME + AND k.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA) + INNER JOIN information_schema.table_constraints t + ON (r.CONSTRAINT_CATALOG = t.CONSTRAINT_CATALOG + AND r.CONSTRAINT_NAME = t.CONSTRAINT_NAME + AND r.CONSTRAINT_SCHEMA = t.CONSTRAINT_SCHEMA) + + WHERE k.table_name='%s' + AND k.column_name='%s' + AND t.constraint_type='FOREIGN KEY' + ;""", table, field) + if len(rows1) == 1: + rows2 = query(conn, """ + SELECT table_name, column_name, * + FROM information_schema.constraint_column_usage + WHERE constraint_name='%s' + """, rows1[0]['constraint_name']) + row = None + if len(rows2) > 1: + row = rows2[int(rows1[0]['ordinal_position']) - 1] + keyed = True + if len(rows2) == 1: + row = rows2[0] + keyed = False + if row: + if keyed: # THIS IS BAD, DON'T MIX "id" and primarykey!!! + ref = {'type': "'reference %s.%s'" % (row['table_name'], + row['column_name'])} + else: + ref = {'type': "'reference %s'" % (row['table_name'],)} + if rows1[0]['delete_rule'] != "NO ACTION": + ref['ondelete'] = repr(rows1[0]['delete_rule']) + return ref + elif rows2: + raise RuntimeError("Unsupported foreign key reference: %s" % + str(rows2)) + + elif rows1: + raise RuntimeError("Unsupported referential constraint: %s" % + str(rows1)) + + +def define_table(conn, table): + "Output single table definition" + fields = get_fields(conn, table) + pks = primarykeys(conn, table) + print "db.define_table('%s'," % (table,) + for field in fields: + fname = field['column_name'] + fdef = define_field(conn, table, field, pks) + if fname not in pks and is_unique(conn, table, field): + fdef['unique'] = "True" + if fdef['type'] == "'id'" and fname in pks: + pks.pop(pks.index(fname)) + print " Field('%s', %s)," % (get_valid_column_name(fname), + ', '.join(["%s=%s" % (k, fdef[k]) for k in KWARGS + if k in fdef and fdef[k]])) + if pks: + print " primarykey=[%s]," % ", ".join(["'%s'" % pk for pk in pks]) + print " migrate=migrate)" + print + + +def define_db(conn, db, host, port, user, passwd): + "Output database definition (model)" + dal = 'db = DAL("mssql4://%s:%s@%s:%s/%s", pool_size=10, decode_credentials=True)' + print dal % ( + user.replace('@', '%40').replace(':', '%3A'), passwd.replace('@', '%40').replace(':', '%3A'), host, port, db) + print + print "migrate = False" + print + for table in get_tables(conn): + define_table(conn, table) + + +if __name__ == "__main__": + # Parse arguments from command line: + if len(sys.argv) < 6 and COMMAND_LINE_MODE: + print HELP + else: + # Parse arguments from command line: + if COMMAND_LINE_MODE: + db, host, port, user, passwd = sys.argv[1:6] + else: + db = DB + host = HOST + user = USER + passwd = PASSWD + port = PORT + + # Make the database connection (change driver if required) + import pyodbc + # cnn = pyodbc.connect(database=db, host=host, port=port, + # user=user, password=passwd, + # ) + cnn = pyodbc.connect( + r'DRIVER={{SQL Server Native Client 11.0}};SERVER={server};PORT={port};DATABASE={db};UID={user};PWD={passwd}'.format( + server=host, port=port, db=db, user=user, passwd=passwd) + ) + # Start model code generation: + define_db(cnn, db, host, port, user, passwd) From e33dd015166e8960b424b4cbfef78dc2e4aec2bc Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Jun 2016 08:50:56 -0500 Subject: [PATCH 013/436] DAL v16.06.09 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 902f222e..a184b706 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 902f222eb89451cb41bb20362a27a3410a0fdbb3 +Subproject commit a184b706ed63f28f44584cea4eb207e5bcd879cc From 0dbd2ea6e5d3fec68608facbcecd065c907e9bb0 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Jun 2016 10:46:46 -0500 Subject: [PATCH 014/436] added quote_template --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index a184b706..d13e82fc 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit a184b706ed63f28f44584cea4eb207e5bcd879cc +Subproject commit d13e82fcb470a93d8e4f8e78cfcd5199b3916d07 From d9c7953147bd78bd2e3ebf546b989538ae983c8e Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 19 Jun 2016 11:44:19 +0200 Subject: [PATCH 015/436] updated user_agent_parser to 1.7.8, fix webclient --- applications/admin/controllers/default.py | 23 +- gluon/_compat.py | 2 + gluon/contrib/user_agent_parser.py | 862 ++++++++++++---------- gluon/contrib/webclient.py | 28 +- 4 files changed, 513 insertions(+), 402 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index bed4e863..fea3c3f7 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -23,7 +23,7 @@ try: if git.__version__ < '0.3.1': raise ImportError("Your version of git is %s. Upgrade to 0.3.1 or better." % git.__version__) have_git = True -except ImportError, e: +except ImportError as e: have_git = False GIT_MISSING = 'Requires gitpython module, but not installed or incompatible version: %s' % e @@ -123,6 +123,7 @@ def index(): redirect(send) elif failed_login_count() >= allowed_number_of_attempts: time.sleep(2 ** allowed_number_of_attempts) + print('4033') raise HTTP(403) elif request.vars.password: if verify_password(request.vars.password[:1024]): @@ -262,7 +263,7 @@ def site(): new_repo = git.Repo.clone_from(form_update.vars.url, target) session.flash = T('new application "%s" imported', form_update.vars.name) - except git.GitCommandError, err: + except git.GitCommandError as err: session.flash = T('Invalid git repository specified.') redirect(URL(r=request)) @@ -272,7 +273,7 @@ def site(): f = urllib.urlopen(form_update.vars.url) if f.code == 404: raise Exception("404 file not found") - except Exception, e: + except Exception as e: session.flash = \ DIV(T('Unable to download app because:'), PRE(repr(e))) redirect(URL(r=request)) @@ -347,7 +348,7 @@ def pack(): else: fname = 'web2py.app.%s.compiled.w2p' % app filename = app_pack_compiled(app, request, raise_ex=True) - except Exception, e: + except Exception as e: filename = None if filename: @@ -421,7 +422,7 @@ def pack_custom(): fname = 'web2py.app.%s.w2p' % app try: filename = app_pack(app, request, raise_ex=True, filenames=files) - except Exception, e: + except Exception as e: filename = None if filename: response.headers['Content-Type'] = 'application/w2p' @@ -732,7 +733,7 @@ def edit(): try: code = request.vars.data.rstrip().replace('\r\n', '\n') + '\n' compile(code, path, "exec", _ast.PyCF_ONLY_AST) - except Exception, e: + except Exception as e: # offset calculation is only used for textarea (start/stop) start = sum([len(line) + 1 for l, line in enumerate(request.vars.data.split("\n")) @@ -757,11 +758,11 @@ def edit(): # Lets try to reload the modules try: mopath = '.'.join(request.args[2:])[:-3] - exec 'import applications.%s.modules.%s' % ( - request.args[0], mopath) + exec('import applications.%s.modules.%s' % ( + request.args[0], mopath)) reload(sys.modules['applications.%s.modules.%s' % (request.args[0], mopath)]) - except Exception, e: + except Exception as e: response.flash = DIV( T('failed to reload module because:'), PRE(repr(e))) @@ -1468,7 +1469,7 @@ def create_file(): redirect(URL('edit', args=[os.path.join(request.vars.location, filename)], vars=vars)) - except Exception, e: + except Exception as e: if not isinstance(e, HTTP): session.flash = T('cannot create file') @@ -1661,7 +1662,7 @@ def errors(): pickel=error, causer=error_causer, last_line=last_line, hash=hash, ticket=fn.ticket_id) - except AttributeError, e: + except AttributeError as e: tk_db(tk_table.id == fn.id).delete() tk_db.commit() diff --git a/gluon/_compat.py b/gluon/_compat.py index 0baea6bb..44baefc3 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -30,6 +30,7 @@ if PY2: from string import maketrans from types import ClassType import cgi + import cookielib reduce = reduce hashlib_md5 = hashlib.md5 iterkeys = lambda d: d.iterkeys() @@ -94,6 +95,7 @@ else: from email.charset import Charset, add_charset, QP as charset_QP from urllib.request import FancyURLopener, urlopen from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode + from http import cookiejar as cookielib import html hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) iterkeys = lambda d: iter(d.keys()) diff --git a/gluon/contrib/user_agent_parser.py b/gluon/contrib/user_agent_parser.py index 5af9d32b..bba1e436 100644 --- a/gluon/contrib/user_agent_parser.py +++ b/gluon/contrib/user_agent_parser.py @@ -1,19 +1,14 @@ """ Extract client information from http user agent The module does not try to detect all capabilities of browser in current form (it can easily be extended though). -Aim is - * fast +Tries to + * be fast * very easy to extend * reliable enough for practical purposes - * and assist python web apps to detect clients. - -Taken from http://pypi.python.org/pypi/httpagentparser (MIT license) -Modified my Ross Peoples for web2py to better support iPhone and iPad. -Modified by Angelo Compagnucci to better support a wide ringe of mobile devices. -Now it supports: tablet device (is_tablet), BlackBerry, BlackBerry PlayBook, Android Tablets, Windows Mobile, -Symbian. + * assist python web apps to detect clients. """ -import sys + +__version__ = '1.7.8' class DetectorsHub(dict): @@ -32,24 +27,11 @@ class DetectorsHub(dict): else: self[detector.info_type].append(detector) - def reorderByPrefs(self, detectors, prefs): - if prefs is None: - return [] - elif prefs == []: - return detectors - else: - prefs.insert(0, '') - - def key_name(d): - return d.name in prefs and prefs.index(d.name) or sys.maxint - return sorted(detectors, key=key_name) - def __iter__(self): return iter(self._known_types) def registerDetectors(self): - detectors = [v() for v in globals().values() - if DetectorBase in getattr(v, '__mro__', [])] + detectors = [v() for v in globals().values() if DetectorBase in getattr(v, '__mro__', [])] for d in detectors: if d.can_register: self.register(d) @@ -57,17 +39,17 @@ class DetectorsHub(dict): class DetectorBase(object): name = "" # "to perform match in DetectorsHub object" - info_type = '' # override me - result_key = '' # override me + info_type = "override me" + result_key = "override me" order = 10 # 0 is highest - look_for = [] # list of words to look for + look_for = "string to look for" skip_if_found = [] # strings if present stop processin can_register = False - is_mobile = False - is_tablet = False - prefs = dict() # dict(info_type = [name1, name2], ..) - version_splitters = ["/", " "] + version_markers = [("/", " ")] + allow_space_in_version = False _suggested_detectors = None + platform = None + bot = False def __init__(self): if not self.name: @@ -75,54 +57,65 @@ class DetectorBase(object): self.can_register = (self.__class__.__dict__.get('can_register', True)) def detect(self, agent, result): - if agent and self.checkWords(agent): + # -> True/None + word = self.checkWords(agent) + if word: result[self.info_type] = dict(name=self.name) - is_mobile = self.is_mobile - is_tablet = self.is_tablet - if is_mobile: - result['is_mobile'] = is_mobile - if is_tablet: - result['is_tablet'] = is_tablet - - version = self.getVersion(agent) + result['bot'] = self.bot + version = self.getVersion(agent, word) if version: result[self.info_type]['version'] = version - + if self.platform: + result['platform'] = {'name': self.platform, 'version': version} return True - return False def checkWords(self, agent): + # -> True/None for w in self.skip_if_found: if w in agent: return False - for w in self.look_for: - if not w in agent: - return False - return True + if isinstance(self.look_for, (tuple, list)): + for word in self.look_for: + if word in agent: + return word + elif self.look_for in agent: + return self.look_for - # This works only for the first element of look_for - # If you want a different behaviour, you have to - # override this method - def getVersion(self, agent): - # -> version string /None - vs = self.version_splitters - return agent.partition(self.look_for[0] + vs[0])[2].partition(vs[1])[0].strip() + def getVersion(self, agent, word): + """ + => version string /None + """ + version_markers = self.version_markers if \ + isinstance(self.version_markers[0], (list, tuple)) else [self.version_markers] + version_part = agent.split(word, 1)[-1] + for start, end in version_markers: + if version_part.startswith(start) and end in version_part: + version = version_part[1:] + if end: # end could be empty string + version = version.split(end)[0] + if not self.allow_space_in_version: + version = version.split()[0] + return version class OS(DetectorBase): info_type = "os" can_register = False - version_splitters = [";", " "] + version_markers = [";", " "] + allow_space_in_version = True + platform = None class Dist(DetectorBase): info_type = "dist" can_register = False + platform = None class Flavor(DetectorBase): info_type = "flavor" can_register = False + platform = None class Browser(DetectorBase): @@ -130,293 +123,536 @@ class Browser(DetectorBase): can_register = False -class Macintosh(OS): - look_for = ['Macintosh'] - prefs = dict(dist=None) - - def getVersion(self, agent): - pass - - class Firefox(Browser): - look_for = ["Firefox"] + look_for = "Firefox" + version_markers = [('/', '')] + skip_if_found = ["SeaMonkey", "web/snippet"] + + +class SeaMonkey(Browser): + look_for = "SeaMonkey" + version_markers = [('/', '')] class Konqueror(Browser): - look_for = ["Konqueror"] - version_splitters = ["/", ";"] + look_for = "Konqueror" + version_markers = ["/", ";"] + + +class OperaMobile(Browser): + look_for = "Opera Mobi" + name = "Opera Mobile" + + def getVersion(self, agent, word): + try: + look_for = "Version" + return agent.split(look_for)[1][1:].split(' ')[0] + except IndexError: + look_for = "Opera" + return agent.split(look_for)[1][1:].split(' ')[0] class Opera(Browser): - look_for = ["Opera"] + look_for = "Opera" - def getVersion(self, agent): - return agent.partition(self.look_for[0])[2][1:].partition(' ')[0] + def getVersion(self, agent, word): + try: + look_for = "Version" + return agent.split(look_for)[1][1:].split(' ')[0] + except IndexError: + look_for = "Opera" + version = agent.split(look_for)[1][1:].split(' ')[0] + return version.split('(')[0] + + +class OperaNew(Browser): + """ + Opera after version 15 + """ + name = "Opera" + look_for = "OPR" + version_markers = [('/', '')] class Netscape(Browser): - look_for = ["Netscape"] + look_for = "Netscape" + version_markers = [("/", '')] + + +class Trident(Browser): + look_for = "Trident" + skip_if_found = ["MSIE", "Opera"] + name = "Microsoft Internet Explorer" + version_markers = ["/", ";"] + trident_to_ie_versions = { + '4.0': '8.0', + '5.0': '9.0', + '6.0': '10.0', + '7.0': '11.0', + } + + def getVersion(self, agent, word): + return self.trident_to_ie_versions.get(super(Trident, self).getVersion(agent, word)) class MSIE(Browser): - look_for = ["MSIE"] + look_for = "MSIE" skip_if_found = ["Opera"] name = "Microsoft Internet Explorer" - version_splitters = [" ", ";"] + version_markers = [" ", ";"] +class MSEdge(Browser): + look_for = "Edge" + skip_if_found = ["MSIE"] + version_markers = ["/", ""] class Galeon(Browser): - look_for = ["Galeon"] + look_for = "Galeon" + + +class WOSBrowser(Browser): + look_for = "wOSBrowser" + + def getVersion(self, agent, word): + pass class Safari(Browser): - look_for = ["Safari"] - skip_if_found = ["Chrome", "OmniWeb", "Mobile", "iPad", 'Android'] + look_for = "Safari" + skip_if_found = ["Edge"] - def getVersion(self, agent): + def checkWords(self, agent): + unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android"] + if self.look_for in agent: + for word in unless_list: + if word in agent: + return False + return self.look_for + + def getVersion(self, agent, word): if "Version/" in agent: - return agent.partition('Version/')[2].partition(' ')[0].strip() + return agent.split('Version/')[-1].split(' ')[0].strip() + if "Safari/" in agent: + return agent.split('Safari/')[-1].split(' ')[0].strip() + else: + return agent.split('Safari ')[-1].split(' ')[0].strip() # Mobile Safari +class GoogleBot(Browser): + # https://support.google.com/webmasters/answer/1061943 + look_for = ["Googlebot", "Googlebot-News", "Googlebot-Image", + "Googlebot-Video", "Googlebot-Mobile", "Mediapartners-Google", + "Mediapartners", "AdsBot-Google", "web/snippet"] + bot = True + version_markers = [('/', ';'), ('/', ' ')] -class SafariTablet(Browser): - name = "Safari" - look_for = ['Safari', 'Android'] - skip_if_found = ["Chrome", "OmniWeb", "Mobile", "iPad"] - is_mobile = True - is_tablet = True +class GoogleFeedFetcher(Browser): + look_for = "Feedfetcher-Google" + bot = True - def getVersion(self, agent): - if "Version/" in agent: - return agent.partition('Version/')[2].partition(' ')[0].strip() - - -class SafariMobile(Browser): - name = "Safari" - look_for = ["Safari", "Mobile"] - is_mobile = True - - def getVersion(self, agent): - if "Version/" in agent: - return agent.partition('Version/')[2].partition(' ')[0].strip() - - -class SafariNokia(Browser): - name = "Safari" - look_for = ["Safari", "SymbianOS"] - is_mobile = True - - def getVersion(self, agent): + def get_version(self, agent): pass +class RunscopeRadar(Browser): + look_for = "runscope-radar" + bot = True -class SafariiPad(Browser): - name = "Safari" - look_for = ["Safari", "iPad"] - skip_if_found = ["Chrome", "OmniWeb"] - is_mobile = True - is_tablet = True +class GoogleAppEngine(Browser): + look_for = "AppEngine-Google" + bot = True - def getVersion(self, agent): - if "Version/" in agent: - return agent.partition('Version/')[2].partition(' ')[0].strip() + def get_version(self, agent): + pass + +class GoogleApps(Browser): + look_for = "GoogleApps script" + bot = True + + def get_version(self, agent): + pass + +class TwitterBot(Browser): + look_for = "Twitterbot" + bot = True + +class MJ12Bot(Browser): + look_for = "MJ12bot" + bot = True + +class YandexBot(Browser): + # http://help.yandex.com/search/robots/agent.xml + look_for = "Yandex" + bot = True + + def getVersion(self, agent, word): + return agent[agent.index('Yandex'):].split('/')[-1].split(')')[0].strip() + +class BingBot(Browser): + look_for = "bingbot" + version_markers = ["/", ";"] + bot = True + + +class BaiduBot(Browser): + # http://help.baidu.com/question?prod_en=master&class=1&id=1000973 + look_for = ["Baiduspider", "Baiduspider-image", "Baiduspider-video", + "Baiduspider-news", "Baiduspider-favo", "Baiduspider-cpro", + "Baiduspider-ads"] + bot = True + version_markers = ('/', ';') + + +class LinkedInBot(Browser): + look_for = "LinkedInBot" + bot = True + +class ArchiveDotOrgBot(Browser): + look_for = "archive.org_bot" + bot = True + +class YoudaoBot(Browser): + look_for = "YoudaoBot" + bot = True + +class YoudaoBotImage(Browser): + look_for = "YodaoBot-Image" + bot = True + +class RogerBot(Browser): + look_for = "rogerbot" + bot = True + +class TweetmemeBot(Browser): + look_for = "TweetmemeBot" + bot = True + +class WebshotBot(Browser): + look_for = "WebshotBot" + bot = True + +class SensikaBot(Browser): + look_for = "SensikaBot" + bot = True + +class YesupBot(Browser): + look_for = "YesupBot" + bot = True + +class DotBot(Browser): + look_for = "DotBot" + bot = True + +class PhantomJS(Browser): + look_for = "Browser/Phantom" + bot = True + +class FacebookExternalHit(Browser): + look_for = 'facebookexternalhit' + bot = True + + +class NokiaOvi(Browser): + look_for = "S40OviBrowser" + +class UCBrowser(Browser): + look_for = "UCBrowser" + +class BrowserNG(Browser): + look_for = "BrowserNG" + +class Dolfin(Browser): + look_for = 'Dolfin' + +class NetFront(Browser): + look_for = 'NetFront' + +class Jasmine(Browser): + look_for = 'Jasmine' + +class Openwave(Browser): + look_for = 'Openwave' + +class UPBrowser(Browser): + look_for = 'UP.Browser' + +class OneBrowser(Browser): + look_for = 'OneBrowser' + +class ObigoInternetBrowser(Browser): + look_for = 'ObigoInternetBrowser' + +class TelecaBrowser(Browser): + look_for = 'TelecaBrowser' + +class MAUI(Browser): + look_for = 'Browser/MAUI' + + def getVersion(self, agent, word): + version = agent.split("Release/")[-1][:10] + return version + + +class NintendoBrowser(Browser): + look_for = 'NintendoBrowser' + + +class AndroidBrowser(Browser): + look_for = "Android" + skip_if_found = ['Chrome', 'Windows Phone'] + + # http://decadecity.net/blog/2013/11/21/android-browser-versions + def getVersion(self, agent, word): + pass class Linux(OS): - look_for = ["Linux"] - prefs = dict(dist=["Ubuntu", "Android", "Debian"], flavor=None) + look_for = 'Linux' + platform = 'Linux' - def getVersion(self, agent): + def getVersion(self, agent, word): pass -class BlackBerry(OS): - look_for = ['BlackBerry'] - prefs = dict(flavor=['PlayBook']) - is_mobile = True +class Blackberry(OS): + look_for = 'BlackBerry' + platform = 'BlackBerry' - # Manual check for tablet - def checkWords(self, agent): - if 'BlackBerry' in agent or 'PlayBook' in agent: - return True - return False - - def getVersion(self, agent): + def getVersion(self, agent, word): pass -class PlayBook(Flavor): - look_for = ['PlayBook'] - is_mobile = True - is_tablet = True +class BlackberryPlaybook(Dist): + look_for = 'PlayBook' + platform = 'BlackBerry' - def getVersion(self, agent): - return agent.partition('Tablet OS')[2].partition(';')[0].strip() + def getVersion(self, agent, word): + pass + + +class WindowsPhone(OS): + name = "Windows Phone" + platform = 'Windows' + look_for = ["Windows Phone OS", "Windows Phone"] + version_markers = [(" ", ";"), (" ", ")")] + + +class iOS(OS): + look_for = ('iPhone', 'iPad') + skip_if_found = ['like iPhone'] + + +class iPhone(Dist): + look_for = 'iPhone' + platform = 'iOS' + skip_if_found = ['like iPhone'] + + def getVersion(self, agent, word): + version_end_chars = [' '] + if not "iPhone OS" in agent: + return None + part = agent.split('iPhone OS')[-1].strip() + for c in version_end_chars: + if c in part: + version = part.split(c)[0] + return version.replace('_', '.') + return None + + +class IPad(Dist): + look_for = 'iPad;' + platform = 'iOS' + + def getVersion(self, agent, word): + version_end_chars = [' '] + if not "CPU OS " in agent: + return None + part = agent.split('CPU OS ')[-1].strip() + for c in version_end_chars: + if c in part: + version = part.split(c)[0] + return version.replace('_', '.') + return None class Macintosh(OS): - look_for = ['Macintosh'] - prefs = dict(dist=None, flavor=['MacOS']) + look_for = 'Macintosh' - def getVersion(self, agent): + def getVersion(self, agent, word): pass class MacOS(Flavor): - look_for = ['Mac OS'] - prefs = dict(browser=['Safari', 'SafariMobile', 'SafariIpad', - 'Firefox', 'Opera', "Microsoft Internet Explorer"]) + look_for = 'Mac OS' + platform = 'Mac OS' + skip_if_found = ['iPhone', 'iPad'] - def getVersion(self, agent): + def getVersion(self, agent, word): version_end_chars = [';', ')'] - part = agent.partition('Mac OS')[2].strip() + part = agent.split('Mac OS')[-1].strip() for c in version_end_chars: if c in part: - version = part.partition(c)[0] - break - return version.replace('_', '.') + version = part.split(c)[0] + return version.replace('_', '.') + return '' + + +class Windows(Dist): + look_for = 'Windows' + platform = 'Windows' class Windows(OS): - look_for = ['Windows', 'NT'] - prefs = dict(browser=["Microsoft Internet Explorer", 'Firefox'], - dist=['WindowsMobile'], flavor=None) + look_for = 'Windows' + platform = 'Windows' + skip_if_found = ["Windows Phone"] + win_versions = { + "NT 10.0": "10", + "NT 6.3": "8.1", + "NT 6.2": "8", + "NT 6.1": "7", + "NT 6.0": "Vista", + "NT 5.2": "Server 2003 / XP x64", + "NT 5.1": "XP", + "NT 5.01": "2000 SP1", + "NT 5.0": "2000", + "98; Win 9x 4.90": "Me" + } - def getVersion(self, agent): - v = agent.partition('NT') - return v[1] + ' ' + v[2].replace(')', ';').partition(';')[0].strip() - - -class WindowsMobile(Dist): - name = 'Phone' - look_for = ['Windows', 'Phone'] - is_mobile = True - - def getVersion(self, agent): - return agent.partition('Windows Phone')[2].replace(')', '').partition(';')[0].strip() + def getVersion(self, agent, word): + v = agent.split('Windows')[-1].split(';')[0].strip() + if ')' in v: + v = v.split(')')[0] + v = self.win_versions.get(v, v) + return v class Ubuntu(Dist): - look_for = ['Ubuntu'] - version_splitters = ["/", " "] - prefs = dict(browser=['Firefox']) + look_for = 'Ubuntu' + version_markers = ["/", " "] class Debian(Dist): - look_for = ['Debian'] - version_splitters = ["/", " "] - prefs = dict(browser=['Firefox']) + look_for = 'Debian' + version_markers = ["/", " "] class Chrome(Browser): - look_for = ['Chrome'] - version_splitters = ["/", " "] + look_for = "Chrome" + version_markers = ["/", " "] + skip_if_found = ["OPR", "Edge"] + + def getVersion(self, agent, word): + part = agent.split(word + self.version_markers[0])[-1] + version = part.split(self.version_markers[1])[0] + if '+' in version: + version = part.split('+')[0] + return version.strip() + + +class ChromeiOS(Browser): + look_for = "CriOS" + version_markers = ["/", " "] class ChromeOS(OS): - look_for = ['CrOS'] - version_splitters = [" ", ")"] - prefs = dict(browser=['Chrome']) + look_for = "CrOS" + platform = ' ChromeOS' + version_markers = [" ", " "] - def getVersion(self, agent): - vs = self.version_splitters - return agent.partition(self.look_for[0] + vs[0])[2].partition(vs[1])[0].partition(" ")[2].strip() + def getVersion(self, agent, word): + version_markers = self.version_markers + if word + '+' in agent: + version_markers = ['+', '+'] + return agent.split(word + version_markers[0])[-1].split(version_markers[1])[1].strip()[:-1] class Android(Dist): - look_for = ['Android'] - prefs = dict(browser=['SafariTablet', 'SafariMobile']) - is_mobile = True + look_for = 'Android' + platform = 'Android' + skip_if_found = ['Windows Phone'] - def getVersion(self, agent): - return agent.partition('Android')[2].partition(';')[0].strip() + def getVersion(self, agent, word): + return agent.split(word)[-1].split(';')[0].strip() -class SymbianOS(OS): - look_for = ['SymbianOS'] - prefs = dict(dist=['Series'], browser=['Safari', 'Opera']) - is_mobile = True - version_splitters = ['/', '; '] +class WebOS(Dist): + look_for = 'hpwOS' + + def getVersion(self, agent, word): + return agent.split('hpwOS/')[-1].split(';')[0].strip() -class Series(Flavor): - look_for = ['SymbianOS', 'Series'] - version_splitters = ['/', ';'] +class NokiaS40(OS): + look_for = 'Series40' + platform = 'Nokia S40' - def getVersion(self, agent): - return agent.partition('Series')[2].partition(' ')[0].replace('/', ' ') + def getVersion(self, agent, word): + pass -class BrowserNG(Browser): - look_for = ['BrowserNG'] - version_splitters = ['/', ';'] +class Symbian(OS): + look_for = ['Symbian', 'SymbianOS'] + platform = 'Symbian' -class iPhone(Dist): - look_for = ['iPhone'] - is_mobile = True - prefs = dict(browser=['SafariMobile']) - - def getVersion(self, agent): - version_end_chars = ['like', ';', ')'] - if (not 'CPU iPhone OS' in agent) and (not 'CPU OS' in agent): - return 'X' - part = agent.partition('OS')[2].strip() - for c in version_end_chars: - if c in part: - version = 'iOS ' + part.partition(c)[0].strip() - break - return version.replace('_', '.') +class PlayStation(OS): + look_for = ['PlayStation', 'PLAYSTATION'] + platform = 'PlayStation' + version_markers = [" ", ")"] -class iPad(Dist): - look_for = ['iPad'] - is_mobile = True - is_tablet = True +class prefs: # experimental + os = dict( + Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])), + BlackBerry=dict(dist=[BlackberryPlaybook]), + Macintosh=dict(flavor=[MacOS]), + Windows=dict(browser=[MSIE, Firefox]), + ChromeOS=dict(browser=[Chrome]), + Debian=dict(browser=[Firefox]) + ) + dist = dict( + Ubuntu=dict(browser=[Firefox]), + Android=dict(browser=[Safari]), + IPhone=dict(browser=[Safari]), + IPad=dict(browser=[Safari]), + ) + flavor = dict( + MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE]) + ) - def getVersion(self, agent): - version_end_chars = ['like', ';', ')'] - if not 'OS' in agent: - return '' - part = agent.partition('OS')[2].strip() - for c in version_end_chars: - if c in part: - version = 'iOS ' + part.partition(c)[0].strip() - break - return version.replace('_', '.') detectorshub = DetectorsHub() -def detect(agent): - result = dict() - prefs = dict() - result['is_mobile'] = False - result['is_tablet'] = False +def detect(agent, fill_none=False): + """ + fill_none: if name/version is not detected respective key is still added to the result with value None + """ + result = dict(platform=dict(name=None, version=None)) + _suggested_detectors = [] + for info_type in detectorshub: - detectors = detectorshub[info_type] - _d_prefs = prefs.get(info_type, []) - detectors = detectorshub.reorderByPrefs(detectors, _d_prefs) - try: - for detector in detectors: - if detector.detect(agent, result): - prefs = detector.prefs - break - except Exception, ex: - result['exception'] = ex - # hack to address https://code.google.com/p/web2py/issues/detail?id=1755 - if not 'browser' in result: - result['browser'] = {'name':'IE11'} + detectors = _suggested_detectors or detectorshub[info_type] + for detector in detectors: + try: + detector.detect(agent, result) + except Exception as _err: + pass + + if fill_none: + attrs_d = {'name': None, 'version': None} + for key in ('os', 'browser'): + if key not in result: + result[key] = attrs_d + else: + for k, v in attrs_d.items(): + result[k] = v + return result -class Result(dict): - def __missing__(self, k): - return "" - - def simple_detect(agent): """ - -> (os, browser, is_mobile) # tuple of strings + -> (os, browser) # tuple of strings """ result = detect(agent) os_list = [] @@ -428,142 +664,12 @@ def simple_detect(agent): os_list.append(result['os']['name']) os = os_list and " ".join(os_list) or "Unknown OS" - os_version = os_list and ('flavor' in result and result['flavor'] and result['flavor'].get( - 'version')) or ('dist' in result and result['dist'] and result['dist'].get('version')) \ - or ('os' in result and result['os'] and result['os'].get('version')) or "" - browser = 'browser' in result and result['browser'][ - 'name'] or 'Unknown Browser' - browser_version = 'browser' in result \ - and result['browser'].get('version') or "" + os_version = os_list and (result.get('flavor') and result['flavor'].get('version')) or \ + (result.get('dist') and result['dist'].get('version')) or (result.get('os') and result['os'].get('version')) or "" + browser = 'browser' in result and result['browser'].get('name') or 'Unknown Browser' + browser_version = 'browser' in result and result['browser'].get('version') or "" if browser_version: browser = " ".join((browser, browser_version)) if os_version: os = " ".join((os, os_version)) - #is_mobile = ('dist' in result and result.dist.is_mobile) or ('os' in result and result.os.is_mobile) or False - return os, browser, result['is_mobile'] - - -if __name__ == '__main__': - import time - import unittest - - data = ( - - - ( - 'Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 Nokia6120c/3.83; Profile/MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413 (KHTML, like Gecko) Safari/413', - ('Series SymbianOS 60 3.1', 'Safari', True), - {'is_mobile': True, 'is_tablet': False, 'flavor': {'name': 'Series', 'version': '60 3.1'}, 'os': {'name': 'SymbianOS', 'version': '9.2'}, 'browser': {'name': 'Safari'}},), - ( - 'Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124', - ('Series SymbianOS 60 5.0', 'BrowserNG 7.1.18124', True), - {'is_mobile': True, 'is_tablet': False, 'flavor': {'name': 'Series', 'version': '60 5.0'}, 'os': {'name': 'SymbianOS', 'version': '9.4'}, 'browser': {'name': 'BrowserNG', 'version': '7.1.18124'}},), - ( - 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Windows Phone 6.5.3.5)', - ('Phone Windows 6.5.3.5', 'Microsoft Internet Explorer 6.0', True), - {'is_mobile': True, 'is_tablet': False, 'dist': {'name': 'Phone', 'version': '6.5.3.5'}, 'os': {'name': 'Windows', 'version': 'NT 5.1'}, 'browser': {'name': 'Microsoft Internet Explorer', 'version': '6.0'}},), - ( - 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/0.0.1 Safari/534.8+', - ('PlayBook BlackBerry 1.0.0', 'Safari 0.0.1', True), - {'is_mobile': True, 'is_tablet': True, 'flavor': {'name': 'PlayBook', 'version': '1.0.0'}, 'os': {'name': 'BlackBerry'}, 'browser': {'name': 'Safari', 'version': '0.0.1'}},), - ( - 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en-US) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.246 Mobile Safari/534.1+', - ('BlackBerry', 'Safari 6.0.0.246', True), - {'is_mobile': True, 'is_tablet': False, 'os': {'name': 'BlackBerry'}, 'browser': {'name': 'Safari', 'version': '6.0.0.246'}},), - ( - 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.600 Mobile Safari/534.8+', - ('BlackBerry', 'Safari 6.0.0.600', True), - {'is_mobile': True, 'is_tablet': False, 'os': {'name': 'BlackBerry'}, 'browser': {'name': 'Safari', 'version': '6.0.0.600'}},), - ( - 'Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5', - ('MacOS iPad X', 'Safari 5.0.2', True), - {'is_mobile': True, 'is_tablet': True, 'flavor': {'version': 'X', 'name': 'MacOS'}, 'dist': {'version': 'iOS 4.2.1', 'name': 'iPad'}, 'browser': {'name': 'Safari', 'version': '5.0.2'}},), - ( - 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20060127 Netscape/8.1', - ('Windows NT 5.1', 'Netscape 8.1', False), - {'is_mobile': False, 'is_tablet': False, 'os': {'name': 'Windows', 'version': 'NT 5.1'}, 'browser': {'name': 'Netscape', 'version': '8.1'}},), - ( - 'Mozilla/5.0 (Linux; U; Android 3.0.1; en-us; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13', - ('Android Linux 3.0.1', 'Safari 4.0', True), - {'is_mobile': True, 'is_tablet': True, 'dist': {'version': '3.0.1', 'name': 'Android'}, 'os': {'name': 'Linux'}, 'browser': {'version': '4.0', 'name': 'Safari'}},), - ( - 'Mozilla/5.0 (Linux; U; Android 2.3.7; it-it; Dream/Sapphire Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1', - ('Android Linux 2.3.7', 'Safari 4.0', True), - {'is_mobile': True, 'is_tablet': False, 'dist': {'version': '2.3.7', 'name': 'Android'}, 'os': {'name': 'Linux'}, 'browser': {'version': '4.0', 'name': 'Safari'}},), - ( - 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-GB; rv:1.9.0.10) Gecko/2009042315 Firefox/3.0.10', - ('MacOS Macintosh X 10.5', 'Firefox 3.0.10', False), - {'is_mobile': False, 'is_tablet': False, 'flavor': {'version': 'X 10.5', 'name': 'MacOS'}, 'os': {'name': 'Macintosh'}, 'browser': {'version': '3.0.10', 'name': 'Firefox'}},), - ( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24,gzip(gfe)', - ('MacOS Macintosh X 10.6.6', 'Chrome 11.0.696.3', False), - {'is_mobile': False, 'is_tablet': False, 'flavor': {'version': 'X 10.6.6', 'name': 'MacOS'}, 'os': {'name': 'Macintosh'}, 'browser': {'version': '11.0.696.3', 'name': 'Chrome'}},), - ( - 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100308 Ubuntu/10.04 (lucid) Firefox/3.6 GTB7.1', - ('Ubuntu Linux 10.04', 'Firefox 3.6', False), - {'is_mobile': False, 'is_tablet': False, 'dist': {'version': '10.04', 'name': 'Ubuntu'}, 'os': {'name': 'Linux'}, 'browser': {'version': '3.6', 'name': 'Firefox'}},), - ( - 'Mozilla/5.0 (Linux; U; Android 2.2.1; fr-ch; A43 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1', - ('Android Linux 2.2.1', 'Safari 4.0', True), - {'is_mobile': True, 'is_tablet': False, 'dist': {'version': '2.2.1', 'name': 'Android'}, 'os': {'name': 'Linux'}, 'browser': {'version': '4.0', 'name': 'Safari'}},), - ( - 'Mozilla/5.0 (Linux; U; Android 2.3.4; it-it; LG-P990 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2', - ('Android Linux 2.3.4', 'Safari 4.0', True), - {'is_mobile': True, 'is_tablet': False, 'dist': {'version': '2.3.4', 'name': 'Android'}, 'os': {'name': 'Linux'}, 'browser': {'version': '4.0', 'name': 'Safari'}},), - ( - 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3', - ('MacOS iPhone X', 'Safari 3.0', True), - {'is_mobile': True, 'is_tablet': False, 'flavor': {'version': 'X', 'name': 'MacOS'}, 'dist': {'version': 'X', 'name': 'iPhone'}, 'browser': {'version': '3.0', 'name': 'Safari'}},), - ( - 'Mozilla/5.0 (X11; CrOS i686 0.0.0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.27 Safari/534.24,gzip(gfe)', - ('ChromeOS 0.0.0', 'Chrome 11.0.696.27', False), - {'is_mobile': False, 'is_tablet': False, 'os': {'name': 'ChromeOS', 'version': '0.0.0'}, 'browser': {'name': 'Chrome', 'version': '11.0.696.27'}},), - ( - 'Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1) Opera 7.02 [en]', - ('Windows NT 5.1', 'Opera 7.02', False), - {'is_mobile': False, 'is_tablet': False, 'os': {'name': 'Windows', 'version': 'NT 5.1'}, 'browser': {'name': 'Opera', 'version': '7.02'}},), - ('Opera/9.80 (X11; Linux i686; U; en) Presto/2.9.168 Version/11.50', - ('Linux', 'Opera 9.80', False), - {'is_mobile': False, 'is_tablet': False, 'os': {'name': 'Linux'}, 'browser': {'name': 'Opera', 'version': '9.80'}},), - ) - - class TestHAP(unittest.TestCase): - def setUp(self): - self.harass_repeat = 100 - self.data = data - - def test_simple_detect(self): - for agent, simple_res, res in data: - self.assertEqual(simple_detect(agent), simple_res) - - def test_detect(self): - for agent, simple_res, res in data: - self.assertEqual(detect(agent), res) - - def test_harass(self): - then = time.time() - for agent, simple_res, res in data * self.harass_repeat: - detect(agent) - time_taken = time.time() - then - no_of_tests = len(self.data) * self.harass_repeat - print "\nTime taken for %s detecttions: %s" \ - % (no_of_tests, time_taken) - print "Time taken for single detecttion: ", \ - time_taken / (len(self.data) * self.harass_repeat) - - unittest.main() - - -class mobilize(object): - - def __init__(self, func): - self.func = func - - def __call__(self): - from gluon import current - user_agent = current.request.user_agent() - if user_agent.is_mobile: - items = current.response.view.split('.') - items.insert(-1, 'mobile') - current.response.view = '.'.join(items) - return self.func() + return os, browser diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index 3beca75c..4dc4cdec 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -16,11 +16,9 @@ mostly for testing purposes Some examples at the bottom. """ from __future__ import print_function +from gluon._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes import re import time -import urllib -import urllib2 -import cookielib DEFAULT_HEADERS = { @@ -85,10 +83,10 @@ class WebClient(object): # copy headers from dict to list of key,value headers_list = [] - for key, value in self.default_headers.iteritems(): + for key, value in iteritems(self.default_headers): if not key in headers: headers[key] = value - for key, value in headers.iteritems(): + for key, value in iteritems(headers): if isinstance(value, (list, tuple)): for v in value: headers_list.append((key, v)) @@ -96,7 +94,7 @@ class WebClient(object): headers_list.append((key, value)) # move cookies to headers - for key, value in cookies.iteritems(): + for key, value in iteritems(cookies): headers_list.append(('Cookie', '%s=%s' % (key, value))) # add headers to request @@ -120,24 +118,25 @@ class WebClient(object): data['_formkey'] = self.forms[data['_formname']] # time the POST request - data = urllib.urlencode(data, doseq=True) + data = urlencode(data, doseq=True) else: self.method = 'GET' if method=='auto' else method data = None t0 = time.time() - self.response = opener.open(self.url, data) + self.response = opener.open(self.url, to_bytes(data)) self.time = time.time() - t0 - except urllib2.HTTPError as error: + except urllib2.HTTPError as er: + error = er # catch HTTP errors self.time = time.time() - t0 - self.response = error + self.response = er if hasattr(self.response, 'getcode'): self.status = self.response.getcode() else:#python2.5 self.status = None - self.text = self.response.read() + self.text = to_native(self.response.read()) self.headers = dict(self.response.headers) # treat web2py tickets as special types of errors @@ -147,6 +146,9 @@ class WebClient(object): else: raise error + if 'Set-Cookie' in self.headers: #PY3 + self.headers['set-cookie'] = self.headers['Set-Cookie'] + # parse headers into cookies self.cookies = {} if 'set-cookie' in self.headers: @@ -156,7 +158,7 @@ class WebClient(object): # check is a new session id has been issued, symptom of broken session if self.session_regex is not None: - for cookie, value in self.cookies.iteritems(): + for cookie, value in iteritems(self.cookies): match = self.session_regex.match(cookie) if match: name = match.group('name') @@ -166,7 +168,7 @@ class WebClient(object): # find all forms and formkeys in page self.forms = {} - for match in FORM_REGEX.finditer(self.text): + for match in FORM_REGEX.finditer(to_native(self.text)): self.forms[match.group('formname')] = match.group('formkey') # log this request From 476db873352a0f87ae9bbca43c79817f872309c1 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 21 Jun 2016 21:09:24 +0200 Subject: [PATCH 016/436] updated pysimplesoap to current master --- gluon/contrib/pysimplesoap/__init__.py | 2 +- gluon/contrib/pysimplesoap/c14n.py | 433 ++++++++++++++ gluon/contrib/pysimplesoap/client.py | 738 +++++++++++++++--------- gluon/contrib/pysimplesoap/helpers.py | 383 +++++++++--- gluon/contrib/pysimplesoap/plugins.py | 39 ++ gluon/contrib/pysimplesoap/server.py | 42 +- gluon/contrib/pysimplesoap/simplexml.py | 108 +++- gluon/contrib/pysimplesoap/transport.py | 30 +- gluon/contrib/pysimplesoap/wsse.py | 215 +++++++ gluon/contrib/pysimplesoap/xmlsec.py | 220 +++++++ 10 files changed, 1817 insertions(+), 393 deletions(-) mode change 100755 => 100644 gluon/contrib/pysimplesoap/__init__.py create mode 100644 gluon/contrib/pysimplesoap/c14n.py create mode 100644 gluon/contrib/pysimplesoap/plugins.py mode change 100755 => 100644 gluon/contrib/pysimplesoap/server.py mode change 100755 => 100644 gluon/contrib/pysimplesoap/simplexml.py create mode 100644 gluon/contrib/pysimplesoap/wsse.py create mode 100644 gluon/contrib/pysimplesoap/xmlsec.py diff --git a/gluon/contrib/pysimplesoap/__init__.py b/gluon/contrib/pysimplesoap/__init__.py old mode 100755 new mode 100644 index 28bfee12..f62376dc --- a/gluon/contrib/pysimplesoap/__init__.py +++ b/gluon/contrib/pysimplesoap/__init__.py @@ -8,7 +8,7 @@ __author__ = "Mariano Reingart" __author_email__ = "reingart@gmail.com" __copyright__ = "Copyright (C) 2013 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.11" +__version__ = "1.16" TIMEOUT = 60 diff --git a/gluon/contrib/pysimplesoap/c14n.py b/gluon/contrib/pysimplesoap/c14n.py new file mode 100644 index 00000000..5749e49c --- /dev/null +++ b/gluon/contrib/pysimplesoap/c14n.py @@ -0,0 +1,433 @@ +#! /usr/bin/env python +'''XML Canonicalization + +Patches Applied to xml.dom.ext.c14n: + http://sourceforge.net/projects/pyxml/ + + [ 1444526 ] c14n.py: http://www.w3.org/TR/xml-exc-c14n/ fix + -- includes [ 829905 ] c14n.py fix for bug #825115, + Date Submitted: 2003-10-24 23:43 + -- include dependent namespace declarations declared in ancestor nodes + (checking attributes and tags), + -- handle InclusiveNamespaces PrefixList parameter + +This module generates canonical XML of a document or element. + http://www.w3.org/TR/2001/REC-xml-c14n-20010315 +and includes a prototype of exclusive canonicalization + http://www.w3.org/Signature/Drafts/xml-exc-c14n + +Requires PyXML 0.7.0 or later. + +Known issues if using Ft.Lib.pDomlette: + 1. Unicode + 2. does not white space normalize attributes of type NMTOKEN and ID? + 3. seems to be include "\n" after importing external entities? + +Note, this version processes a DOM tree, and consequently it processes +namespace nodes as attributes, not from a node's namespace axis. This +permits simple document and element canonicalization without +XPath. When XPath is used, the XPath result node list is passed and used to +determine if the node is in the XPath result list, but little else. + +Authors: + "Joseph M. Reagle Jr." + "Rich Salz" + +$Date: 2006-03-30 23:47:16 +0000 (Thu, 30 Mar 2006) $ by $Author: boverhof $ +''' + +_copyright = '''Copyright 2001, Zolera Systems Inc. All Rights Reserved. +Copyright 2001, MIT. All Rights Reserved. + +Distributed under the terms of: + Python 2.0 License or later. + http://www.python.org/2.0.1/license.html +or + W3C Software License + http://www.w3.org/Consortium/Legal/copyright-software-19980720 +''' + +import string +from xml.dom import Node +try: + from xml.ns import XMLNS +except: + class XMLNS: + BASE = "http://www.w3.org/2000/xmlns/" + XML = "http://www.w3.org/XML/1998/namespace" +try: + import cStringIO + StringIO = cStringIO +except ImportError: + import StringIO + +_attrs = lambda E: (E.attributes and E.attributes.values()) or [] +_children = lambda E: E.childNodes or [] +_IN_XML_NS = lambda n: n.name.startswith("xmlns") +_inclusive = lambda n: n.unsuppressedPrefixes == None + + +# Does a document/PI has lesser/greater document order than the +# first element? +_LesserElement, _Element, _GreaterElement = range(3) + +def _sorter(n1,n2): + '''_sorter(n1,n2) -> int + Sorting predicate for non-NS attributes.''' + + i = cmp(n1.namespaceURI, n2.namespaceURI) + if i: return i + return cmp(n1.localName, n2.localName) + + +def _sorter_ns(n1,n2): + '''_sorter_ns((n,v),(n,v)) -> int + "(an empty namespace URI is lexicographically least)."''' + + if n1[0] == 'xmlns': return -1 + if n2[0] == 'xmlns': return 1 + return cmp(n1[0], n2[0]) + +def _utilized(n, node, other_attrs, unsuppressedPrefixes): + '''_utilized(n, node, other_attrs, unsuppressedPrefixes) -> boolean + Return true if that nodespace is utilized within the node''' + if n.startswith('xmlns:'): + n = n[6:] + elif n.startswith('xmlns'): + n = n[5:] + if (n=="" and node.prefix in ["#default", None]) or \ + n == node.prefix or n in unsuppressedPrefixes: + return 1 + for attr in other_attrs: + if n == attr.prefix: return 1 + # For exclusive need to look at attributes + if unsuppressedPrefixes is not None: + for attr in _attrs(node): + if n == attr.prefix: return 1 + + return 0 + + +def _inclusiveNamespacePrefixes(node, context, unsuppressedPrefixes): + '''http://www.w3.org/TR/xml-exc-c14n/ + InclusiveNamespaces PrefixList parameter, which lists namespace prefixes that + are handled in the manner described by the Canonical XML Recommendation''' + inclusive = [] + if node.prefix: + usedPrefixes = ['xmlns:%s' %node.prefix] + else: + usedPrefixes = ['xmlns'] + + for a in _attrs(node): + if a.nodeName.startswith('xmlns') or not a.prefix: continue + usedPrefixes.append('xmlns:%s' %a.prefix) + + unused_namespace_dict = {} + for attr in context: + n = attr.nodeName + if n in unsuppressedPrefixes: + inclusive.append(attr) + elif n.startswith('xmlns:') and n[6:] in unsuppressedPrefixes: + inclusive.append(attr) + elif n.startswith('xmlns') and n[5:] in unsuppressedPrefixes: + inclusive.append(attr) + elif attr.nodeName in usedPrefixes: + inclusive.append(attr) + elif n.startswith('xmlns:'): + unused_namespace_dict[n] = attr.value + + return inclusive, unused_namespace_dict + +#_in_subset = lambda subset, node: not subset or node in subset +_in_subset = lambda subset, node: subset is None or node in subset # rich's tweak + + +class _implementation: + '''Implementation class for C14N. This accompanies a node during it's + processing and includes the parameters and processing state.''' + + # Handler for each node type; populated during module instantiation. + handlers = {} + + def __init__(self, node, write, **kw): + '''Create and run the implementation.''' + self.write = write + self.subset = kw.get('subset') + self.comments = kw.get('comments', 0) + self.unsuppressedPrefixes = kw.get('unsuppressedPrefixes') + nsdict = kw.get('nsdict', { 'xml': XMLNS.XML, 'xmlns': XMLNS.BASE }) + + # Processing state. + self.state = (nsdict, {'xml':''}, {}, {}) #0422 + + if node.nodeType == Node.DOCUMENT_NODE: + self._do_document(node) + elif node.nodeType == Node.ELEMENT_NODE: + self.documentOrder = _Element # At document element + if not _inclusive(self): + inherited,unused = _inclusiveNamespacePrefixes(node, self._inherit_context(node), + self.unsuppressedPrefixes) + self._do_element(node, inherited, unused=unused) + else: + inherited = self._inherit_context(node) + self._do_element(node, inherited) + elif node.nodeType == Node.DOCUMENT_TYPE_NODE: + pass + else: + raise TypeError(str(node)) + + + def _inherit_context(self, node): + '''_inherit_context(self, node) -> list + Scan ancestors of attribute and namespace context. Used only + for single element node canonicalization, not for subset + canonicalization.''' + + # Collect the initial list of xml:foo attributes. + xmlattrs = filter(_IN_XML_NS, _attrs(node)) + + # Walk up and get all xml:XXX attributes we inherit. + inherited, parent = [], node.parentNode + while parent and parent.nodeType == Node.ELEMENT_NODE: + for a in filter(_IN_XML_NS, _attrs(parent)): + n = a.localName + if n not in xmlattrs: + xmlattrs.append(n) + inherited.append(a) + parent = parent.parentNode + return inherited + + + def _do_document(self, node): + '''_do_document(self, node) -> None + Process a document node. documentOrder holds whether the document + element has been encountered such that PIs/comments can be written + as specified.''' + + self.documentOrder = _LesserElement + for child in node.childNodes: + if child.nodeType == Node.ELEMENT_NODE: + self.documentOrder = _Element # At document element + self._do_element(child) + self.documentOrder = _GreaterElement # After document element + elif child.nodeType == Node.PROCESSING_INSTRUCTION_NODE: + self._do_pi(child) + elif child.nodeType == Node.COMMENT_NODE: + self._do_comment(child) + elif child.nodeType == Node.DOCUMENT_TYPE_NODE: + pass + else: + raise TypeError(str(child)) + handlers[Node.DOCUMENT_NODE] = _do_document + + + def _do_text(self, node): + '''_do_text(self, node) -> None + Process a text or CDATA node. Render various special characters + as their C14N entity representations.''' + if not _in_subset(self.subset, node): return + s = string.replace(node.data, "&", "&") + s = string.replace(s, "<", "<") + s = string.replace(s, ">", ">") + s = string.replace(s, "\015", " ") + if s: self.write(s) + handlers[Node.TEXT_NODE] = _do_text + handlers[Node.CDATA_SECTION_NODE] = _do_text + + + def _do_pi(self, node): + '''_do_pi(self, node) -> None + Process a PI node. Render a leading or trailing #xA if the + document order of the PI is greater or lesser (respectively) + than the document element. + ''' + if not _in_subset(self.subset, node): return + W = self.write + if self.documentOrder == _GreaterElement: W('\n') + W('') + if self.documentOrder == _LesserElement: W('\n') + handlers[Node.PROCESSING_INSTRUCTION_NODE] = _do_pi + + + def _do_comment(self, node): + '''_do_comment(self, node) -> None + Process a comment node. Render a leading or trailing #xA if the + document order of the comment is greater or lesser (respectively) + than the document element. + ''' + if not _in_subset(self.subset, node): return + if self.comments: + W = self.write + if self.documentOrder == _GreaterElement: W('\n') + W('') + if self.documentOrder == _LesserElement: W('\n') + handlers[Node.COMMENT_NODE] = _do_comment + + + def _do_attr(self, n, value): + ''''_do_attr(self, node) -> None + Process an attribute.''' + + W = self.write + W(' ') + W(n) + W('="') + s = string.replace(value, "&", "&") + s = string.replace(s, "<", "<") + s = string.replace(s, '"', '"') + s = string.replace(s, '\011', ' ') + s = string.replace(s, '\012', ' ') + s = string.replace(s, '\015', ' ') + W(s) + W('"') + + + def _do_element(self, node, initial_other_attrs = [], unused = None): + '''_do_element(self, node, initial_other_attrs = [], unused = {}) -> None + Process an element (and its children).''' + + # Get state (from the stack) make local copies. + # ns_parent -- NS declarations in parent + # ns_rendered -- NS nodes rendered by ancestors + # ns_local -- NS declarations relevant to this element + # xml_attrs -- Attributes in XML namespace from parent + # xml_attrs_local -- Local attributes in XML namespace. + # ns_unused_inherited -- not rendered namespaces, used for exclusive + ns_parent, ns_rendered, xml_attrs = \ + self.state[0], self.state[1].copy(), self.state[2].copy() #0422 + + ns_unused_inherited = unused + if unused is None: + ns_unused_inherited = self.state[3].copy() + + ns_local = ns_parent.copy() + inclusive = _inclusive(self) + xml_attrs_local = {} + + # Divide attributes into NS, XML, and others. + other_attrs = [] + in_subset = _in_subset(self.subset, node) + for a in initial_other_attrs + _attrs(node): + if a.namespaceURI == XMLNS.BASE: + n = a.nodeName + if n == "xmlns:": n = "xmlns" # DOM bug workaround + ns_local[n] = a.nodeValue + elif a.namespaceURI == XMLNS.XML: + if inclusive or (in_subset and _in_subset(self.subset, a)): #020925 Test to see if attribute node in subset + xml_attrs_local[a.nodeName] = a #0426 + else: + if _in_subset(self.subset, a): #020925 Test to see if attribute node in subset + other_attrs.append(a) + +# # TODO: exclusive, might need to define xmlns:prefix here +# if not inclusive and a.prefix is not None and not ns_rendered.has_key('xmlns:%s' %a.prefix): +# ns_local['xmlns:%s' %a.prefix] = ?? + + #add local xml:foo attributes to ancestor's xml:foo attributes + xml_attrs.update(xml_attrs_local) + + # Render the node + W, name = self.write, None + if in_subset: + name = node.nodeName + if not inclusive: + if node.prefix is not None: + prefix = 'xmlns:%s' %node.prefix + else: + prefix = 'xmlns' + + if not ns_rendered.has_key(prefix) and not ns_local.has_key(prefix): + if not ns_unused_inherited.has_key(prefix): + raise RuntimeError(\ + 'For exclusive c14n, unable to map prefix "%s" in %s' %( + prefix, node)) + + ns_local[prefix] = ns_unused_inherited[prefix] + del ns_unused_inherited[prefix] + + W('<') + W(name) + + # Create list of NS attributes to render. + ns_to_render = [] + for n,v in ns_local.items(): + + # If default namespace is XMLNS.BASE or empty, + # and if an ancestor was the same + if n == "xmlns" and v in [ XMLNS.BASE, '' ] \ + and ns_rendered.get('xmlns') in [ XMLNS.BASE, '', None ]: + continue + + # "omit namespace node with local name xml, which defines + # the xml prefix, if its string value is + # http://www.w3.org/XML/1998/namespace." + if n in ["xmlns:xml", "xml"] \ + and v in [ 'http://www.w3.org/XML/1998/namespace' ]: + continue + + + # If not previously rendered + # and it's inclusive or utilized + if (n,v) not in ns_rendered.items(): + if inclusive or _utilized(n, node, other_attrs, self.unsuppressedPrefixes): + ns_to_render.append((n, v)) + elif not inclusive: + ns_unused_inherited[n] = v + + # Sort and render the ns, marking what was rendered. + ns_to_render.sort(_sorter_ns) + for n,v in ns_to_render: + self._do_attr(n, v) + ns_rendered[n]=v #0417 + + # If exclusive or the parent is in the subset, add the local xml attributes + # Else, add all local and ancestor xml attributes + # Sort and render the attributes. + if not inclusive or _in_subset(self.subset,node.parentNode): #0426 + other_attrs.extend(xml_attrs_local.values()) + else: + other_attrs.extend(xml_attrs.values()) + other_attrs.sort(_sorter) + for a in other_attrs: + self._do_attr(a.nodeName, a.value) + W('>') + + # Push state, recurse, pop state. + state, self.state = self.state, (ns_local, ns_rendered, xml_attrs, ns_unused_inherited) + for c in _children(node): + _implementation.handlers[c.nodeType](self, c) + self.state = state + + if name: W('' % name) + handlers[Node.ELEMENT_NODE] = _do_element + + +def Canonicalize(node, output=None, **kw): + '''Canonicalize(node, output=None, **kw) -> UTF-8 + + Canonicalize a DOM document/element node and all descendents. + Return the text; if output is specified then output.write will + be called to output the text and None will be returned + Keyword parameters: + nsdict: a dictionary of prefix:uri namespace entries + assumed to exist in the surrounding context + comments: keep comments if non-zero (default is 0) + subset: Canonical XML subsetting resulting from XPath + (default is []) + unsuppressedPrefixes: do exclusive C14N, and this specifies the + prefixes that should be inherited. + ''' + if output: + apply(_implementation, (node, output.write), kw) + else: + s = StringIO.StringIO() + apply(_implementation, (node, s.write), kw) + return s.getvalue() diff --git a/gluon/contrib/pysimplesoap/client.py b/gluon/contrib/pysimplesoap/client.py index 88300eba..2518bcab 100755 --- a/gluon/contrib/pysimplesoap/client.py +++ b/gluon/contrib/pysimplesoap/client.py @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -21,28 +21,30 @@ try: import cPickle as pickle except ImportError: import pickle +import copy import hashlib import logging import os import tempfile +import warnings from . import __author__, __copyright__, __license__, __version__, TIMEOUT -from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict +from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, Struct from .transport import get_http_wrapper, set_http_wrapper, get_Http # Utility functions used throughout wsdl_parse, moved aside for readability -from .helpers import fetch, sort_dict, make_key, process_element, \ +from .helpers import Alias, fetch, sort_dict, make_key, process_element, \ postprocess_element, get_message, preprocess_schema, \ get_local_name, get_namespace_prefix, TYPE_MAP, urlsplit - +from .wsse import UsernameToken log = logging.getLogger(__name__) - class SoapFault(RuntimeError): - def __init__(self, faultcode, faultstring): + def __init__(self, faultcode, faultstring, detail=None): self.faultcode = faultcode self.faultstring = faultstring - RuntimeError.__init__(self, faultcode, faultstring) + self.detail = detail + RuntimeError.__init__(self, faultcode, faultstring, detail) def __unicode__(self): return '%s: %s' % (self.faultcode, self.faultstring) @@ -54,8 +56,9 @@ class SoapFault(RuntimeError): return self.__unicode__().encode('ascii', 'ignore') def __repr__(self): - return "SoapFault(%s, %s)" % (repr(self.faultcode), - repr(self.faultstring)) + return "SoapFault(faultcode = %s, faultstring %s, detail = %s)" % (repr(self.faultcode), + repr(self.faultstring), + repr(self.detail)) # soap protocol specification & namespace @@ -76,30 +79,38 @@ class SoapClient(object): sessions=False, soap_server=None, timeout=TIMEOUT, http_headers=None, trace=False, username=None, password=None, + key_file=None, plugins=None, strict=True, ): """ :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} """ self.certssl = cert - self.keyssl = None + self.keyssl = key_file self.location = location # server location (url) self.action = action # SOAP base action self.namespace = namespace # message self.exceptions = exceptions # lanzar execpiones? (Soap Faults) self.xml_request = self.xml_response = '' self.http_headers = http_headers or {} + self.plugins = plugins or [] + self.strict = strict # extract the base directory / url for wsdl relative imports: if wsdl and wsdl_basedir == '': # parse the wsdl url, strip the scheme and filename url_scheme, netloc, path, query, fragment = urlsplit(wsdl) wsdl_basedir = os.path.dirname(netloc + path) - + self.wsdl_basedir = wsdl_basedir - + # shortcut to print all debugging info and sent / received xml messages if trace: - logging.basicConfig(level=logging.DEBUG) - + if trace is True: + level = logging.DEBUG # default logging level + else: + level = trace # use the provided level + logging.basicConfig(level=level) + log.setLevel(level) + if not soap_ns and not ns: self.__soap_ns = 'soap' # 1.1 elif not soap_ns and ns: @@ -112,7 +123,7 @@ class SoapClient(object): # SOAP Header support self.__headers = {} # general headers - self.__call_headers = None # OrderedDict to be marshalled for RPC Call + self.__call_headers = None # Struct to be marshalled for RPC Call # check if the Certification Authority Cert is a string and store it if cacert and cacert.startswith('-----BEGIN CERTIFICATE-----'): @@ -130,7 +141,10 @@ class SoapClient(object): if username and password: if hasattr(self.http, 'add_credentials'): self.http.add_credentials(username, password) - + if cert and key_file: + if hasattr(self.http, 'add_certificate'): + self.http.add_certificate(key=key_file, cert=cert, domain='') + # namespace prefix, None to use xmlns attribute or False to not use it: self.__ns = ns @@ -149,11 +163,7 @@ class SoapClient(object): self.__xml = """ <%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" xmlns:%(ns)s="%(namespace)s"> <%(soap_ns)s:Header/> -<%(soap_ns)s:Body> - <%(ns)s:%(method)s> - - -""" +<%(soap_ns)s:Body><%(ns)s:%(method)s>""" # parse wsdl url self.services = wsdl and self.wsdl_parse(wsdl, cache=cache) @@ -162,7 +172,7 @@ class SoapClient(object): def __getattr__(self, attr): """Return a pseudo-method that can be called""" if not self.services: # not using WSDL? - return lambda self=self, *args, **kwargs: self.call(attr, *args, **kwargs) + return lambda *args, **kwargs: self.call(attr, *args, **kwargs) else: # using WSDL: return lambda *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) @@ -175,12 +185,13 @@ class SoapClient(object): """ #TODO: method != input_message # Basic SOAP request: + soap_uri = soap_namespaces[self.__soap_ns] xml = self.__xml % dict(method=method, # method tag name namespace=self.namespace, # method ns uri ns=self.__ns, # method ns prefix soap_ns=self.__soap_ns, # soap prefix & uri - soap_uri=soap_namespaces[self.__soap_ns]) - request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace, + soap_uri=soap_uri) + request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace, prefix=self.__ns) request_headers = kwargs.pop('headers', None) @@ -191,18 +202,21 @@ class SoapClient(object): else: parameters = args if parameters and isinstance(parameters[0], SimpleXMLElement): + body = request('Body', ns=list(soap_namespaces.values()),) + # remove default body parameter (method name) + delattr(body, method) # merge xmlelement parameter ("raw" - already marshalled) - if parameters[0].children() is not None: - for param in parameters[0].children(): - getattr(request, method).import_node(param) - for k,v in parameters[0].attributes().items(): - getattr(request, method)[k] = v + body.import_node(parameters[0]) elif parameters: # marshall parameters: use_ns = None if (self.__soap_server == "jetty" or self.qualified is False) else True for k, v in parameters: # dict: tag=valor - getattr(request, method).marshall(k, v, ns=use_ns) - elif not self.__soap_server in ('oracle',) or self.__soap_server in ('jbossas6',): + if hasattr(v, "namespaces") and use_ns: + ns = v.namespaces.get(None, True) + else: + ns = use_ns + getattr(request, method).marshall(k, v, ns=ns) + elif self.__soap_server in ('jbossas6',): # JBossAS-6 requires no empty method parameters! delattr(request("Body", ns=list(soap_namespaces.values()),), method) @@ -210,15 +224,12 @@ class SoapClient(object): if self.__headers and not self.services: self.__call_headers = dict([(k, v) for k, v in self.__headers.items() if not k.startswith('wsse:')]) - # always extract WS Security header and send it - if 'wsse:Security' in self.__headers: - #TODO: namespaces too hardwired, clean-up... - header = request('Header', ns=list(soap_namespaces.values()),) - k = 'wsse:Security' - v = self.__headers[k] - header.marshall(k, v, ns=False, add_children_ns=False) - header(k)['xmlns:wsse'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' - # + # always extract WS Security header and send it (backward compatible) + if 'wsse:Security' in self.__headers and not self.plugins: + warnings.warn("Replace wsse:Security with UsernameToken plugin", + DeprecationWarning) + self.plugins.append(UsernameToken()) + if self.__call_headers: header = request('Header', ns=list(soap_namespaces.values()),) for k, v in self.__call_headers.items(): @@ -236,18 +247,46 @@ class SoapClient(object): for subheader in request_headers.children(): header.import_node(subheader) + # do pre-processing using plugins (i.e. WSSE signing) + for plugin in self.plugins: + plugin.preprocess(self, request, method, args, kwargs, + self.__headers, soap_uri) + self.xml_request = request.as_xml() self.xml_response = self.send(method, self.xml_request) response = SimpleXMLElement(self.xml_response, namespace=self.namespace, jetty=self.__soap_server in ('jetty',)) if self.exceptions and response("Fault", ns=list(soap_namespaces.values()), error=False): - raise SoapFault(unicode(response.faultcode), unicode(response.faultstring)) + detailXml = response("detail", ns=list(soap_namespaces.values()), error=False) + detail = None + + if detailXml and detailXml.children(): + if self.services is not None: + operation = self.get_operation(method) + fault_name = detailXml.children()[0].get_name() + # if fault not defined in WSDL, it could be an axis or other + # standard type (i.e. "hostname"), try to convert it to string + fault = operation['faults'].get(fault_name) or unicode + detail = detailXml.children()[0].unmarshall(fault, strict=False) + else: + detail = repr(detailXml.children()) + + raise SoapFault(unicode(response.faultcode), + unicode(response.faultstring), + detail) + + # do post-processing using plugins (i.e. WSSE signature verification) + for plugin in self.plugins: + plugin.postprocess(self, response, method, args, kwargs, + self.__headers, soap_uri) + return response def send(self, method, xml): """Send SOAP request using HTTP""" if self.location == 'test': return # location = '%s' % self.location #?op=%s" % (self.location, method) + http_method = str('POST') location = str(self.location) if self.services: @@ -258,15 +297,25 @@ class SoapClient(object): headers = { 'Content-type': 'text/xml; charset="UTF-8"', 'Content-length': str(len(xml)), - 'SOAPAction': '"%s"' % soap_action } + + if self.action is not None: + headers['SOAPAction'] = soap_action + headers.update(self.http_headers) log.info("POST %s" % location) log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()])) log.debug(xml) + if sys.version < '3': + # Ensure http_method, location and all headers are binary to prevent + # UnicodeError inside httplib.HTTPConnection._send_output. + + # httplib in python3 do the same inside itself, don't need to convert it here + headers = dict((str(k), str(v)) for k, v in headers.items()) + response, content = self.http.request( - location, 'POST', body=xml, headers=headers) + location, http_method, body=xml, headers=headers) self.response = response self.content = content @@ -298,6 +347,10 @@ class SoapClient(object): return operation def wsdl_call(self, method, *args, **kwargs): + """Pre and post process SOAP call, input and output parameters using WSDL""" + return self.wsdl_call_with_args(method, args, kwargs) + + def wsdl_call_with_args(self, method, args, kwargs): """Pre and post process SOAP call, input and output parameters using WSDL""" soap_uri = soap_namespaces[self.__soap_ns] operation = self.get_operation(method) @@ -308,23 +361,23 @@ class SoapClient(object): header = operation.get('header') if 'action' in operation: self.action = operation['action'] - + if 'namespace' in operation: self.namespace = operation['namespace'] or '' - self.qualified = operation['qualified'] + self.qualified = operation['qualified'] # construct header and parameters if header: self.__call_headers = sort_dict(header, self.__headers) - method, params = self.wsdl_call_get_params(method, input, *args, **kwargs) + method, params = self.wsdl_call_get_params(method, input, args, kwargs) # call remote procedure response = self.call(method, *params) # parse results: - resp = response('Body', ns=soap_uri).children().unmarshall(output) + resp = response('Body', ns=soap_uri).children().unmarshall(output, strict=self.strict) return resp and list(resp.values())[0] # pass Response tag children - def wsdl_call_get_params(self, method, input, *args, **kwargs): + def wsdl_call_get_params(self, method, input, args, kwargs): """Build params from input and args/kwargs""" params = inputname = inputargs = None all_args = {} @@ -338,10 +391,9 @@ class SoapClient(object): for idx, arg in enumerate(args): key = list(inputargs.keys())[idx] if isinstance(arg, dict): - if key in arg: - d[key] = arg[key] - else: - raise KeyError('Unhandled key %s. use client.help(method)') + if key not in arg: + raise KeyError('Unhandled key %s. use client.help(method)' % key) + d[key] = arg[key] else: d[key] = arg all_args.update({inputname: d}) @@ -352,7 +404,17 @@ class SoapClient(object): valid, errors, warnings = self.wsdl_validate_params(input, all_args) if not valid: raise ValueError('Invalid Args Structure. Errors: %s' % errors) - params = list(sort_dict(input, all_args).values())[0].items() + # sort and filter parameters according to wsdl input structure + tree = sort_dict(input, all_args) + root = list(tree.values())[0] + params = [] + # make a params tuple list suitable for self.call(method, *params) + for k, v in root.items(): + # fix referenced namespaces as info is lost when calling call + root_ns = root.namespaces[k] + if not root.references[k] and isinstance(v, Struct): + v.namespaces[None] = root_ns + params.append((k, v)) # TODO: check style and document attributes if self.__soap_server in ('axis', ): # use the operation name @@ -368,7 +430,7 @@ class SoapClient(object): return (method, params) def wsdl_validate_params(self, struct, value): - """Validate the arguments (actual values) for the parameters structure. + """Validate the arguments (actual values) for the parameters structure. Fail for any invalid arguments or type mismatches.""" errors = [] warnings = [] @@ -378,15 +440,15 @@ class SoapClient(object): if type(struct) == type(value): typematch = True if not isinstance(struct, dict) and isinstance(value, dict): - typematch = True # struct can be an OrderedDict + typematch = True # struct can be a dict or derived (Struct) else: typematch = False if struct == str: struct = unicode # fix for py2 vs py3 string handling - + if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys(): - if not type(value) == struct: + if not type(value) == struct and value is not None: try: struct(value) # attempt to cast input to parameter type except: @@ -459,6 +521,313 @@ class SoapClient(object): headers, ) + soap_ns_uris = { + 'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11', + 'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12', + } + wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/' + xsd_uri = 'http://www.w3.org/2001/XMLSchema' + xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance' + + def _url_to_xml_tree(self, url, cache, force_download): + """Unmarshall the WSDL at the given url into a tree of SimpleXMLElement nodes""" + # Open uri and read xml: + xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir, self.http_headers) + # Parse WSDL XML: + wsdl = SimpleXMLElement(xml, namespace=self.wsdl_uri) + + # Extract useful data: + self.namespace = "" + self.documentation = unicode(wsdl('documentation', error=False)) or '' + + # some wsdl are split down in several files, join them: + imported_wsdls = {} + for element in wsdl.children() or []: + if element.get_local_name() in ('import'): + wsdl_namespace = element['namespace'] + wsdl_location = element['location'] + if wsdl_location is None: + log.warning('WSDL location not provided for %s!' % wsdl_namespace) + continue + if wsdl_location in imported_wsdls: + log.warning('WSDL %s already imported!' % wsdl_location) + continue + imported_wsdls[wsdl_location] = wsdl_namespace + log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location)) + # Open uri and read xml: + xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir, self.http_headers) + # Parse imported XML schema (recursively): + imported_wsdl = SimpleXMLElement(xml, namespace=self.xsd_uri) + # merge the imported wsdl into the main document: + wsdl.import_node(imported_wsdl) + # warning: do not process schemas to avoid infinite recursion! + + return wsdl + + def _xml_tree_to_services(self, wsdl, cache, force_download): + """Convert SimpleXMLElement tree representation of the WSDL into pythonic objects""" + # detect soap prefix and uri (xmlns attributes of ) + xsd_ns = None + soap_uris = {} + for k, v in wsdl[:]: + if v in self.soap_ns_uris and k.startswith('xmlns:'): + soap_uris[get_local_name(k)] = v + if v == self.xsd_uri and k.startswith('xmlns:'): + xsd_ns = get_local_name(k) + + elements = {} # element: type def + messages = {} # message: element + port_types = {} # port_type_name: port_type + bindings = {} # binding_name: binding + services = {} # service_name: service + + # check axis2 namespace at schema types attributes (europa.eu checkVat) + if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values(): + # get the sub-namespace in the first schema element (see issue 8) + if wsdl('types', error=False): + schema = wsdl.types('schema', ns=self.xsd_uri) + attrs = dict(schema[:]) + self.namespace = attrs.get('targetNamespace', self.namespace) + if not self.namespace or self.namespace == "urn:DefaultNamespace": + self.namespace = wsdl['targetNamespace'] or self.namespace + + imported_schemas = {} + global_namespaces = {None: self.namespace} + + # process current wsdl schema (if any, or many if imported): + # + # + # + # + # ... + # or + # + # + # + # + # + + for types in wsdl('types', error=False) or []: + # avoid issue if schema is not given in the main WSDL file + schemas = types('schema', ns=self.xsd_uri, error=False) + for schema in schemas or []: + preprocess_schema(schema, imported_schemas, elements, self.xsd_uri, + self.__soap_server, self.http, cache, + force_download, self.wsdl_basedir, + global_namespaces=global_namespaces) + + # 2nd phase: alias, postdefined elements, extend bases, convert lists + postprocess_element(elements, []) + + for message in wsdl.message: + for part in message('part', error=False) or []: + element = {} + element_name = part['element'] + if not element_name: + # some implementations (axis) uses type instead + element_name = part['type'] + type_ns = get_namespace_prefix(element_name) + type_uri = part.get_namespace_uri(type_ns) + part_name = part['name'] or None + if type_uri == self.xsd_uri: + element_name = get_local_name(element_name) + fn = REVERSE_TYPE_MAP.get(element_name, None) + element = {part_name: fn} + # emulate a true Element (complexType) for rpc style + if (message['name'], part_name) not in messages: + od = Struct() + od.namespaces[None] = type_uri + messages[(message['name'], part_name)] = {message['name']: od} + else: + od = messages[(message['name'], part_name)].values()[0] + od.namespaces[part_name] = type_uri + od.references[part_name] = False + od.update(element) + else: + element_name = get_local_name(element_name) + fn = elements.get(make_key(element_name, 'element', type_uri)) + if not fn: + # some axis servers uses complexType for part messages (rpc) + fn = elements.get(make_key(element_name, 'complexType', type_uri)) + od = Struct() + od[part_name] = fn + od.namespaces[None] = type_uri + od.namespaces[part_name] = type_uri + od.references[part_name] = False + element = {message['name']: od} + else: + element = {element_name: fn} + messages[(message['name'], part_name)] = element + + for port_type_node in wsdl.portType: + port_type_name = port_type_node['name'] + port_type = port_types[port_type_name] = {} + operations = port_type['operations'] = {} + + for operation_node in port_type_node.operation: + op_name = operation_node['name'] + op = operations[op_name] = {} + op['style'] = operation_node['style'] + op['parameter_order'] = (operation_node['parameterOrder'] or "").split(" ") + op['documentation'] = unicode(operation_node('documentation', error=False)) or '' + + if operation_node('input', error=False): + op['input_msg'] = get_local_name(operation_node.input['message']) + ns = get_namespace_prefix(operation_node.input['message']) + op['namespace'] = operation_node.get_namespace_uri(ns) + + if operation_node('output', error=False): + op['output_msg'] = get_local_name(operation_node.output['message']) + + #Get all fault message types this operation may return + fault_msgs = op['fault_msgs'] = {} + faults = operation_node('fault', error=False) + if faults is not None: + for fault in operation_node('fault', error=False): + fault_msgs[fault['name']] = get_local_name(fault['message']) + + for binding_node in wsdl.binding: + port_type_name = get_local_name(binding_node['type']) + if port_type_name not in port_types: + # Invalid port type + continue + port_type = port_types[port_type_name] + binding_name = binding_node['name'] + soap_binding = binding_node('binding', ns=list(soap_uris.values()), error=False) + transport = soap_binding and soap_binding['transport'] or None + style = soap_binding and soap_binding['style'] or None # rpc + + binding = bindings[binding_name] = { + 'name': binding_name, + 'operations': copy.deepcopy(port_type['operations']), + 'port_type_name': port_type_name, + 'transport': transport, + 'style': style, + } + + for operation_node in binding_node.operation: + op_name = operation_node['name'] + op_op = operation_node('operation', ns=list(soap_uris.values()), error=False) + action = op_op and op_op['soapAction'] + + op = binding['operations'].setdefault(op_name, {}) + op['name'] = op_name + op['style'] = op.get('style', style) + if action is not None: + op['action'] = action + + # input and/or output can be not present! + input = operation_node('input', error=False) + body = input and input('body', ns=list(soap_uris.values()), error=False) + parts_input_body = body and body['parts'] or None + + # parse optional header messages (some implementations use more than one!) + parts_input_headers = [] + headers = input and input('header', ns=list(soap_uris.values()), error=False) + for header in headers or []: + hdr = {'message': header['message'], 'part': header['part']} + parts_input_headers.append(hdr) + + if 'input_msg' in op: + headers = {} # base header message structure + for input_header in parts_input_headers: + header_msg = get_local_name(input_header.get('message')) + header_part = get_local_name(input_header.get('part')) + # warning: some implementations use a separate message! + hdr = get_message(messages, header_msg or op['input_msg'], header_part) + if hdr: + headers.update(hdr) + else: + pass # not enough info to search the header message: + op['input'] = get_message(messages, op['input_msg'], parts_input_body, op['parameter_order']) + op['header'] = headers + + try: + element = list(op['input'].values())[0] + ns_uri = element.namespaces[None] + qualified = element.qualified + except (AttributeError, KeyError) as e: + # TODO: fix if no parameters parsed or "variants" + ns_uri = op['namespace'] + qualified = None + if ns_uri: + op['namespace'] = ns_uri + op['qualified'] = qualified + + # Remove temporary property + del op['input_msg'] + + else: + op['input'] = None + op['header'] = None + + output = operation_node('output', error=False) + body = output and output('body', ns=list(soap_uris.values()), error=False) + parts_output_body = body and body['parts'] or None + if 'output_msg' in op: + op['output'] = get_message(messages, op['output_msg'], parts_output_body) + # Remove temporary property + del op['output_msg'] + else: + op['output'] = None + + if 'fault_msgs' in op: + faults = op['faults'] = {} + for msg in op['fault_msgs'].values(): + msg_obj = get_message(messages, msg, parts_output_body) + tag_name = list(msg_obj)[0] + faults[tag_name] = msg_obj + + # useless? never used + parts_output_headers = [] + headers = output and output('header', ns=list(soap_uris.values()), error=False) + for header in headers or []: + hdr = {'message': header['message'], 'part': header['part']} + parts_output_headers.append(hdr) + + + + + for service in wsdl("service", error=False) or []: + service_name = service['name'] + if not service_name: + continue # empty service? + + serv = services.setdefault(service_name, {}) + ports = serv['ports'] = {} + serv['documentation'] = service['documentation'] or '' + for port in service.port: + binding_name = get_local_name(port['binding']) + + if not binding_name in bindings: + continue # unknown binding + + binding = ports[port['name']] = copy.deepcopy(bindings[binding_name]) + address = port('address', ns=list(soap_uris.values()), error=False) + location = address and address['location'] or None + soap_uri = address and soap_uris.get(address.get_prefix()) + soap_ver = soap_uri and self.soap_ns_uris.get(soap_uri) + + binding.update({ + 'location': location, + 'service_name': service_name, + 'soap_uri': soap_uri, + 'soap_ver': soap_ver, + }) + + # create an default service if none is given in the wsdl: + if not services: + services[''] = {'ports': {'': None}} + + elements = list(e for e in elements.values() if type(e) is type) + sorted(e for e in elements.values() if not(type(e) is type)) + e = None + self.elements = [] + for element in elements: + if e!= element: self.elements.append(element) + e = element + + return services + def wsdl_parse(self, url, cache=False): """Parse Web Service Description v1.1""" @@ -477,7 +846,6 @@ class SoapClient(object): f.close() # sanity check: if pkl['version'][:-1] != __version__.split(' ')[0][:-1] or pkl['url'] != url: - import warnings warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning) log.debug('Version: %s %s' % (pkl['version'], __version__)) log.debug('URL: %s %s' % (pkl['url'], url)) @@ -487,211 +855,11 @@ class SoapClient(object): self.documentation = pkl['documentation'] return pkl['services'] - soap_ns = { - 'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11', - 'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12', - } - wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/' - xsd_uri = 'http://www.w3.org/2001/XMLSchema' - xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance' - # always return an unicode object: REVERSE_TYPE_MAP['string'] = str - # Open uri and read xml: - xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir) - # Parse WSDL XML: - wsdl = SimpleXMLElement(xml, namespace=wsdl_uri) - - # Extract useful data: - self.namespace = "" - self.documentation = unicode(wsdl('documentation', error=False)) or '' - - # some wsdl are splitted down in several files, join them: - imported_wsdls = {} - for element in wsdl.children() or []: - if element.get_local_name() in ('import'): - wsdl_namespace = element['namespace'] - wsdl_location = element['location'] - if wsdl_location is None: - log.warning('WSDL location not provided for %s!' % wsdl_namespace) - continue - if wsdl_location in imported_wsdls: - log.warning('WSDL %s already imported!' % wsdl_location) - continue - imported_wsdls[wsdl_location] = wsdl_namespace - log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location)) - # Open uri and read xml: - xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir) - # Parse imported XML schema (recursively): - imported_wsdl = SimpleXMLElement(xml, namespace=xsd_uri) - # merge the imported wsdl into the main document: - wsdl.import_node(imported_wsdl) - # warning: do not process schemas to avoid infinite recursion! - - - # detect soap prefix and uri (xmlns attributes of ) - xsd_ns = None - soap_uris = {} - for k, v in wsdl[:]: - if v in soap_ns and k.startswith('xmlns:'): - soap_uris[get_local_name(k)] = v - if v == xsd_uri and k.startswith('xmlns:'): - xsd_ns = get_local_name(k) - - services = {} - bindings = {} # binding_name: binding - operations = {} # operation_name: operation - port_type_bindings = {} # port_type_name: binding - messages = {} # message: element - elements = {} # element: type def - - for service in wsdl.service: - service_name = service['name'] - if not service_name: - continue # empty service? - serv = services.setdefault(service_name, {'ports': {}}) - serv['documentation'] = service['documentation'] or '' - for port in service.port: - binding_name = get_local_name(port['binding']) - operations[binding_name] = {} - address = port('address', ns=list(soap_uris.values()), error=False) - location = address and address['location'] or None - soap_uri = address and soap_uris.get(address.get_prefix()) - soap_ver = soap_uri and soap_ns.get(soap_uri) - bindings[binding_name] = {'name': binding_name, - 'service_name': service_name, - 'location': location, - 'soap_uri': soap_uri, - 'soap_ver': soap_ver, } - serv['ports'][port['name']] = bindings[binding_name] - - for binding in wsdl.binding: - binding_name = binding['name'] - soap_binding = binding('binding', ns=list(soap_uris.values()), error=False) - transport = soap_binding and soap_binding['transport'] or None - port_type_name = get_local_name(binding['type']) - bindings[binding_name].update({ - 'port_type_name': port_type_name, - 'transport': transport, 'operations': {}, - }) - if port_type_name not in port_type_bindings: - port_type_bindings[port_type_name] = [] - port_type_bindings[port_type_name].append(bindings[binding_name]) - for operation in binding.operation: - op_name = operation['name'] - op = operation('operation', ns=list(soap_uris.values()), error=False) - action = op and op['soapAction'] - d = operations[binding_name].setdefault(op_name, {}) - bindings[binding_name]['operations'][op_name] = d - d.update({'name': op_name}) - d['parts'] = {} - # input and/or ouput can be not present! - input = operation('input', error=False) - body = input and input('body', ns=list(soap_uris.values()), error=False) - d['parts']['input_body'] = body and body['parts'] or None - output = operation('output', error=False) - body = output and output('body', ns=list(soap_uris.values()), error=False) - d['parts']['output_body'] = body and body['parts'] or None - header = input and input('header', ns=list(soap_uris.values()), error=False) - d['parts']['input_header'] = header and {'message': header['message'], 'part': header['part']} or None - header = output and output('header', ns=list(soap_uris.values()), error=False) - d['parts']['output_header'] = header and {'message': header['message'], 'part': header['part']} or None - if action: - d['action'] = action - - # check axis2 namespace at schema types attributes (europa.eu checkVat) - if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values(): - # get the sub-namespace in the first schema element (see issue 8) - if wsdl('types', error=False): - schema = wsdl.types('schema', ns=xsd_uri) - attrs = dict(schema[:]) - self.namespace = attrs.get('targetNamespace', self.namespace) - if not self.namespace or self.namespace == "urn:DefaultNamespace": - self.namespace = wsdl['targetNamespace'] or self.namespace - - imported_schemas = {} - global_namespaces = {None: self.namespace} - - # process current wsdl schema (if any): - if wsdl('types', error=False): - for schema in wsdl.types('schema', ns=xsd_uri): - preprocess_schema(schema, imported_schemas, elements, xsd_uri, - self.__soap_server, self.http, cache, - force_download, self.wsdl_basedir, - global_namespaces=global_namespaces) - - # 2nd phase: alias, postdefined elements, extend bases, convert lists - postprocess_element(elements, []) - - for message in wsdl.message: - for part in message('part', error=False) or []: - element = {} - element_name = part['element'] - if not element_name: - # some implementations (axis) uses type instead - element_name = part['type'] - type_ns = get_namespace_prefix(element_name) - type_uri = wsdl.get_namespace_uri(type_ns) - if type_uri == xsd_uri: - element_name = get_local_name(element_name) - fn = REVERSE_TYPE_MAP.get(element_name, None) - element = {part['name']: fn} - # emulate a true Element (complexType) - list(messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values())[0].update(element) - else: - element_name = get_local_name(element_name) - fn = elements.get(make_key(element_name, 'element', type_uri)) - if not fn: - # some axis servers uses complexType for part messages - fn = elements.get(make_key(element_name, 'complexType', type_uri)) - element = {message['name']: {part['name']: fn}} - else: - element = {element_name: fn} - messages[(message['name'], part['name'])] = element - - for port_type in wsdl.portType: - port_type_name = port_type['name'] - - for binding in port_type_bindings.get(port_type_name, []): - for operation in port_type.operation: - op_name = operation['name'] - op = operations[binding['name']][op_name] - op['documentation'] = unicode(operation('documentation', error=False)) or '' - if binding['soap_ver']: - #TODO: separe operation_binding from operation (non SOAP?) - if operation('input', error=False): - input_msg = get_local_name(operation.input['message']) - input_header = op['parts'].get('input_header') - if input_header: - header_msg = get_local_name(input_header.get('message')) - header_part = get_local_name(input_header.get('part')) - # warning: some implementations use a separate message! - header = get_message(messages, header_msg or input_msg, header_part) - else: - header = None # not enought info to search the header message: - op['input'] = get_message(messages, input_msg, op['parts'].get('input_body')) - op['header'] = header - try: - element = list(op['input'].values())[0] - ns_uri = element.namespace - qualified = element.qualified - except AttributeError: - # TODO: fix if no parameters parsed or "variants" - ns = get_namespace_prefix(operation.input['message']) - ns_uri = operation.get_namespace_uri(ns) - qualified = None - if ns_uri: - op['namespace'] = ns_uri - op['qualified'] = qualified - else: - op['input'] = None - op['header'] = None - if operation('output', error=False): - output_msg = get_local_name(operation.output['message']) - op['output'] = get_message(messages, output_msg, op['parts'].get('output_body')) - else: - op['output'] = None + wsdl = self._url_to_xml_tree(url, cache, force_download) + services = self._xml_tree_to_services(wsdl, cache, force_download) # dump the full service/port/operation map #log.debug(pprint.pformat(services)) @@ -722,6 +890,56 @@ class SoapClient(object): log.debug('removing %s' % self.cacert) os.unlink(self.cacert) + def __repr__(self): + s = 'SOAP CLIENT' + s += '\n ELEMENTS' + for e in self.elements: + if isinstance(e, type): + e = e.__name__ + elif isinstance(e, Alias): + e = e.xml_type + elif isinstance(e, Struct) and e.key[1]=='element': + e = repr(e) + else: + continue + s += '\n %s' % e + for service in self.services: + s += '\n SERVICE (%s)' % service + ports = self.services[service]['ports'] + for port in ports: + port = ports[port] + if port['soap_ver'] == None: continue + s += '\n PORT (%s)' % port['name'] + s += '\n Location: %s' % port['location'] + s += '\n Soap ver: %s' % port['soap_ver'] + s += '\n Soap URI: %s' % port['soap_uri'] + s += '\n OPERATIONS' + operations = port['operations'] + for operation in sorted(operations): + operation = self.get_operation(operation) + input = operation.get('input') + input = input and input.values() and list(input.values())[0] + input_str = '' + if isinstance(input, dict): + if 'parameters' not in input or input['parameters']!=None: + for k, v in input.items(): + if isinstance(v, type): + v = v.__name__ + elif isinstance(v, Alias): + v = v.xml_type + elif isinstance(v, Struct): + v = v.key[0] + input_str += '%s: %s, ' % (k, v) + output = operation.get('output') + if output: + output = list(operation['output'].values())[0] + s += '\n %s(%s)' % ( + operation['name'], + input_str[:-2] + ) + s += '\n > %s' % output + + return s def parse_proxy(proxy_str): """Parses proxy address user:pass@host:port into a dict suitable for httplib2""" diff --git a/gluon/contrib/pysimplesoap/helpers.py b/gluon/contrib/pysimplesoap/helpers.py index b0e1af7d..b6e6ce04 100644 --- a/gluon/contrib/pysimplesoap/helpers.py +++ b/gluon/contrib/pysimplesoap/helpers.py @@ -1,12 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation; either version 3, or (at your option) any later -# version. +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) any +# later version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -38,7 +38,7 @@ from . import __author__, __copyright__, __license__, __version__ log = logging.getLogger(__name__) -def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): +def fetch(url, http, cache=False, force_download=False, wsdl_basedir='', headers={}): """Download a document from a URL, save it locally if cache enabled""" # check / append a valid schema if not given: @@ -46,12 +46,13 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): if not url_scheme in ('http', 'https', 'file'): for scheme in ('http', 'https', 'file'): try: + path = os.path.normpath(os.path.join(wsdl_basedir, url)) if not url.startswith("/") and scheme in ('http', 'https'): - tmp_url = "%s://%s" % (scheme, os.path.join(wsdl_basedir, url)) + tmp_url = "%s://%s" % (scheme, path) else: - tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url)) + tmp_url = "%s:%s" % (scheme, path) log.debug('Scheme not found, trying %s' % scheme) - return fetch(tmp_url, http, cache, force_download, wsdl_basedir) + return fetch(tmp_url, http, cache, force_download, wsdl_basedir, headers) except Exception as e: log.error(e) raise RuntimeError('No scheme given for url: %s' % url) @@ -72,7 +73,7 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): xml = f.read() else: log.info('GET %s using %s' % (url, http._wrapper_version)) - response, xml = http.request(url, 'GET', None, {}) + response, xml = http.request(url, 'GET', None, headers) if cache: log.info('Writing file %s' % filename) if not os.path.isdir(cache): @@ -86,7 +87,7 @@ def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): def sort_dict(od, d): """Sort parameters (same order as xsd:sequence)""" if isinstance(od, dict): - ret = OrderedDict() + ret = Struct() for k in od.keys(): v = d.get(k) # don't append null tags! @@ -96,8 +97,9 @@ def sort_dict(od, d): elif isinstance(v, list): v = [sort_dict(od[k][0], v1) for v1 in v] ret[k] = v - if hasattr(od, 'namespace'): - ret.namespace = od.namespace + if hasattr(od, 'namespaces'): + ret.namespaces.update(od.namespaces) + ret.references.update(od.references) ret.qualified = od.qualified return ret else: @@ -116,15 +118,21 @@ def make_key(element_name, element_type, namespace): return (element_name, eltype, namespace) -def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace, qualified=None, - soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'): - """Parse and define simple element types""" +def process_element(elements, element_name, node, element_type, xsd_uri, + dialect, namespace, qualified=None, + soapenc_uri='http://schemas.xmlsoap.org/soap/encoding/', + struct=None): + """Parse and define simple element types as Struct objects""" log.debug('Processing element %s %s' % (element_name, element_type)) + + # iterate over inner tags of the element definition: for tag in node: + + # sanity checks (skip superfluous xml tags, resolve aliases, etc.): if tag.get_local_name() in ('annotation', 'documentation'): continue - elif tag.get_local_name() in ('element', 'restriction'): + elif tag.get_local_name() in ('element', 'restriction', 'list'): log.debug('%s has no children! %s' % (element_name, tag)) children = tag # element "alias"? alias = True @@ -134,11 +142,21 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect else: log.debug('%s has no children! %s' % (element_name, tag)) continue # TODO: abstract? - d = OrderedDict() - d.namespace = namespace - d.qualified = qualified + + # check if extending a previous processed element ("extension"): + new_struct = struct is None + if new_struct: + struct = Struct() + struct.namespaces[None] = namespace # set the default namespace + struct.qualified = qualified + + # iterate over the element's components (sub-elements): for e in children: + + # extract type information from xml attributes / children: t = e['type'] + if not t: + t = e['itemType'] # xs:list if not t: t = e['base'] # complexContent (extension)! if not t: @@ -147,37 +165,58 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect # "anonymous" elements had no type attribute but children if e['name'] and e.children(): # create a type name to process the children - t = "%s_%s" % (element_name, e['name']) + t = "%s_%s" % (element_name, e['name']) c = e.children() et = c.get_local_name() c = c.children() - process_element(elements, t, c, et, xsd_uri, dialect, namespace, qualified) + process_element(elements, t, c, et, xsd_uri, dialect, + namespace, qualified) else: t = 'anyType' # no type given! + + # extract namespace uri and type from xml attribute: t = t.split(":") if len(t) > 1: ns, type_name = t else: ns, type_name = None, t[0] - if element_name == type_name and not alias and len(children) > 1: - continue # abort to prevent infinite recursion uri = ns and e.get_namespace_uri(ns) or xsd_uri + + # look for the conversion function (python type) if uri in (xsd_uri, soapenc_uri) and type_name != 'Array': # look for the type, None == any fn = REVERSE_TYPE_MAP.get(type_name, None) - elif uri == soapenc_uri and type_name == 'Array': + if tag.get_local_name() == 'list': + # simple list type (values separated by spaces) + fn = lambda s: [fn(v) for v in s.split(" ")] + elif (uri == soapenc_uri and type_name == 'Array'): # arrays of simple types (look at the attribute tags): fn = [] for a in e.children(): for k, v in a[:]: if k.endswith(":arrayType"): type_name = v + fn_namespace = None if ":" in type_name: - type_name = type_name[type_name.index(":")+1:] + fn_uri, type_name = type_name.split(":") + fn_namespace = e.get_namespace_uri(fn_uri) if "[]" in type_name: - type_name = type_name[:type_name.index("[]")] - fn.append(REVERSE_TYPE_MAP.get(type_name, None)) + type_name = type_name[:type_name.index("[]")] + # get the scalar conversion function (if any) + fn_array = REVERSE_TYPE_MAP.get(type_name, None) + if fn_array is None and type_name != "anyType" and fn_namespace: + # get the complext element: + ref_type = "complexType" + key = make_key(type_name, ref_type, fn_namespace) + fn_complex = elements.setdefault(key, Struct(key)) + # create an indirect struct {type_name: ...}: + fn_array = Struct(key) + fn_array[type_name] = fn_complex + fn_array.namespaces[None] = fn_namespace # set the default namespace + fn_array.qualified = qualified + fn.append(fn_array) else: + # not a simple python type / conversion function not available fn = None if not fn: @@ -189,87 +228,149 @@ def process_element(elements, element_name, node, element_type, xsd_uri, dialect for k, v in e[:]: if k.startswith("xmlns:"): # get the namespace uri from the element - fn_namespace = v - fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict()) + fn_namespace = v + # create and store an empty python element (dict) filled later + if not e['ref']: + ref_type = "complexType" + else: + ref_type = "element" + key = make_key(type_name, ref_type, fn_namespace) + fn = elements.setdefault(key, Struct(key)) if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'): # it's an array... TODO: compound arrays? and check ns uri! - if isinstance(fn, OrderedDict): - if len(children) > 1 and dialect in ('jetty',): + if isinstance(fn, Struct): + if len(children) > 1 or (dialect in ('jetty', )): # Jetty style support # {'ClassName': [{'attr1': val1, 'attr2': val2}] fn.array = True else: - # .NET style support (backward compatibility) - # [{'ClassName': {'attr1': val1, 'attr2': val2}] - d.array = True + # .NET style now matches Jetty style + # {'ClassName': [{'attr1': val1, 'attr2': val2}] + #fn.array = True + #struct.array = True + fn = [fn] else: - if dialect in ('jetty',): - # scalar support [{'attr1': [val1]}] + if len(children) > 1 or dialect in ('jetty',): + # Jetty style support + # scalar array support {'attr1': [val1]} fn = [fn] else: - d.array = True + # Jetty.NET style support (backward compatibility) + # scalar array support [{'attr1': val1}] + struct.array = True + # store the sub-element python type (function) in the element dict if (e['name'] is not None and not alias) or e['ref']: e_name = e['name'] or type_name # for refs, use the type name - d[e_name] = fn + struct[e_name] = fn + struct.references[e_name] = e['ref'] + struct.namespaces[e_name] = namespace # set the element namespace else: log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name)) - d[None] = fn + # use None to point this is a complex element reference + struct.refers_to = fn if e is not None and e.get_local_name() == 'extension' and e.children(): + # extend base element (if ComplexContent only!): + if isinstance(fn, Struct) and fn.refers_to: + base_struct = fn.refers_to + else: + # TODO: check if this actually works for SimpleContent + base_struct = None # extend base element: - process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace, qualified) - elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d) + process_element(elements, element_name, e.children(), + element_type, xsd_uri, dialect, namespace, + qualified, struct=base_struct) + + # add the processed element to the main dictionary (if not extension): + if new_struct: + key = make_key(element_name, element_type, namespace) + elements.setdefault(key, Struct(key)).update(struct) def postprocess_element(elements, processed): - """Fix unresolved references (elements referenced before its definition, thanks .net)""" - + """Fix unresolved references""" + #elements variable contains all eelements and complexTypes defined in http://www.w3.org/2001/XMLSchema + + # (elements referenced before its definition, thanks .net) # avoid already processed elements: if elements in processed: return processed.append(elements) - + for k, v in elements.items(): - if isinstance(v, OrderedDict): + if isinstance(v, Struct): if v != elements: # TODO: fix recursive elements - postprocess_element(v, processed) - if None in v and v[None]: # extension base? - if isinstance(v[None], dict): - for i, kk in enumerate(v[None]): - # extend base -keep orginal order- - if v[None] is not None: - elements[k].insert(kk, v[None][kk], i) - del v[None] + try: + postprocess_element(v, processed) + except RuntimeError as e: # maximum recursion depth exceeded + warnings.warn(unicode(e), RuntimeWarning) + if v.refers_to: # extension base? + if isinstance(v.refers_to, dict): + extend_element(v, v.refers_to) + # clean the reference: + v.refers_to = None else: # "alias", just replace - log.debug('Replacing %s = %s' % (k, v[None])) - elements[k] = v[None] - #break + ##log.debug('Replacing %s = %s' % (k, v.refers_to)) + elements[k] = v.refers_to if v.array: elements[k] = [v] # convert arrays to python lists if isinstance(v, list): for n in v: # recurse list - if isinstance(n, (OrderedDict, list)): + if isinstance(n, (Struct, list)): #if n != elements: # TODO: fix recursive elements postprocess_element(n, processed) +def extend_element(element, base): + ''' Recursively extend the elemnet if it has an extension base.''' + ''' Recursion is needed if the extension base itself extends another element.''' + if isinstance(base, dict): + for i, kk in enumerate(base): + # extend base -keep orignal order- + if isinstance(base, Struct): + element.insert(kk, base[kk], i) + # update namespace (avoid ArrayOfKeyValueOfanyTypeanyType) + if isinstance(base, Struct) and base.namespaces and kk: + element.namespaces[kk] = base.namespaces[kk] + element.references[kk] = base.references[kk] + if base.refers_to: + extend_element(element, base.refers_to) -def get_message(messages, message_name, part_name): +def get_message(messages, message_name, part_name, parameter_order=None): if part_name: # get the specific part of the message: return messages.get((message_name, part_name)) else: # get the first part for the specified message: + parts = {} for (message_name_key, part_name_key), message in messages.items(): if message_name_key == message_name: - return message + parts[part_name_key] = message + if len(parts)>1: + # merge (sorted by parameter_order for rpc style) + new_msg = None + for part_name_key in parameter_order: + part = parts.get(part_name_key) + if not part: + log.error('Part %s not found for %s' % (part_name_key, message_name)) + elif not new_msg: + new_msg = part.copy() + else: + new_msg[message_name].update(part[message_name]) + return new_msg + elif parts: + return list(parts.values())[0] + #return parts.values()[0] + get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s) get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None) -def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False): +def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, + http, cache, force_download, wsdl_basedir, + global_namespaces=None, qualified=False): """Find schema elements and complex types""" from .simplexml import SimpleXMLElement # here to avoid recursive imports @@ -290,7 +391,7 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http for ns in local_namespaces.values(): if ns not in global_namespaces: global_namespaces[ns] = 'ns%s' % len(global_namespaces) - + for element in schema.children() or []: if element.get_local_name() in ('import', 'include',): schema_namespace = element['namespace'] @@ -306,9 +407,15 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http # Open uri and read xml: xml = fetch(schema_location, http, cache, force_download, wsdl_basedir) + # recalculate base path for relative schema locations + path = os.path.normpath(os.path.join(wsdl_basedir, schema_location)) + path = os.path.dirname(path) + # Parse imported XML schema (recursively): imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) - preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces, qualified) + preprocess_schema(imported_schema, imported_schemas, elements, + xsd_uri, dialect, http, cache, force_download, + path, global_namespaces, qualified) element_type = element.get_local_name() if element_type in ('element', 'complexType', "simpleType"): @@ -319,7 +426,9 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http if element.get_local_name() == 'complexType': children = element.children() elif element.get_local_name() == 'simpleType': - children = element('restriction', ns=xsd_uri) + children = element('restriction', ns=xsd_uri, error=False) + if not children: + children = element.children() # xs:list elif element.get_local_name() == 'element' and element['type']: children = element else: @@ -329,7 +438,8 @@ def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http elif element.get_local_name() == 'element': children = element if children: - process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace, qualified) + process_element(elements, element_name, children, element_type, + xsd_uri, dialect, namespace, qualified) # simplexml utilities: @@ -347,10 +457,28 @@ def datetime_u(s): return _strptime(s, fmt) except ValueError: try: - # strip utc offset - if s[-3] == ":" and s[-6] in (' ', '-', '+'): - warnings.warn('removing unsupported UTC offset', RuntimeWarning) - s = s[:-6] + # strip zulu timezone suffix or utc offset + if s[-1] == "Z" or (s[-3] == ":" and s[-6] in (' ', '-', '+')): + try: + import iso8601 + return iso8601.parse_date(s) + except ImportError: + pass + + try: + import isodate + return isodate.parse_datetime(s) + except ImportError: + pass + + try: + import dateutil.parser + return dateutil.parser.parse(s) + except ImportError: + pass + + warnings.warn('removing unsupported "Z" suffix or UTC offset. Install `iso8601`, `isodate` or `python-dateutil` package to support it', RuntimeWarning) + s = s[:-1] if s[-1] == "Z" else s[:-6] # parse microseconds try: return _strptime(s, fmt + ".%f") @@ -363,6 +491,7 @@ def datetime_u(s): s = s[:s.index(".")] return _strptime(s, fmt) + datetime_m = lambda dt: dt.isoformat() date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date() date_m = lambda d: d.strftime("%Y-%m-%d") @@ -370,7 +499,8 @@ time_u = lambda s: _strptime(s, "%H:%M:%S").time() time_m = lambda d: d.strftime("%H%M%S") bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s] bool_m = lambda s: {False: 'false', True: 'true'}[s] - +decimal_m = lambda d: '{0:f}'.format(d) +float_m = lambda f: '{0:.10f}'.format(f) # aliases: class Alias(object): @@ -383,6 +513,31 @@ class Alias(object): def __repr__(self): return "" % (self.xml_type, self.py_type) + def __eq__(self, other): + return isinstance(other, Alias) and self.xml_type == other.xml_type + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + if isinstance(other, Alias): return self.xml_type > other.xml_type + if isinstance(other, Struct): return False + return True + + def __lt__(self, other): + if isinstance(other, Alias): return self.xml_type < other.xml_type + if isinstance(other, Struct): return True + return False + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __le__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __hash__(self): + return hash(self.xml_type) + if sys.version > '3': long = Alias(int, 'long') byte = Alias(str, 'byte') @@ -392,8 +547,10 @@ integer = Alias(long, 'integer') DateTime = datetime.datetime Date = datetime.date Time = datetime.time +duration = Alias(str, 'duration') +any_uri = Alias(str, 'anyURI') -# Define convertion function (python type): xml schema type +# Define conversion function (python type): xml schema type TYPE_MAP = { unicode: 'string', bool: 'boolean', @@ -407,15 +564,22 @@ TYPE_MAP = { Decimal: 'decimal', datetime.datetime: 'dateTime', datetime.date: 'date', + datetime.time: 'time', + duration: 'duration', + any_uri: 'anyURI', } TYPE_MARSHAL_FN = { datetime.datetime: datetime_m, datetime.date: date_m, - bool: bool_m + datetime.time: time_m, + float: float_m, + Decimal: decimal_m, + bool: bool_m, } TYPE_UNMARSHAL_FN = { datetime.datetime: datetime_u, datetime.date: date_u, + datetime.time: time_u, bool: bool_u, str: unicode, } @@ -424,19 +588,27 @@ REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()]) REVERSE_TYPE_MAP.update({ 'base64Binary': str, + 'unsignedByte': byte, + 'unsignedInt': int, + 'unsignedLong': long, + 'unsignedShort': short }) # insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors) if str not in TYPE_MAP: - TYPE_MAP[str] = 'string' + TYPE_MAP[str] = 'string' -class OrderedDict(dict): - """Minimal ordered dictionary for xsd:sequences""" - def __init__(self): +class Struct(dict): + """Minimal ordered dictionary to represent elements (i.e. xsd:sequences)""" + + def __init__(self, key=None): + self.key = key self.__keys = [] self.array = False - self.namespace = None + self.namespaces = {} # key: element, value: namespace URI + self.references = {} # key: element, value: reference name + self.refers_to = None # "symbolic linked" struct self.qualified = None def __setitem__(self, key, value): @@ -464,26 +636,71 @@ class OrderedDict(dict): return [(key, self[key]) for key in self.__keys] def update(self, other): + if isinstance(other, Struct) and other.key: + self.key = other.key for k, v in other.items(): self[k] = v # do not change if we are an array but the other is not: - if isinstance(other, OrderedDict) and not self.array: + if isinstance(other, Struct) and not self.array: self.array = other.array - if isinstance(other, OrderedDict) and not self.namespace: - self.namespace = other.namespace + if isinstance(other, Struct): + # TODO: check replacing default ns is a regression + self.namespaces.update(other.namespaces) + self.references.update(other.references) self.qualified = other.qualified + self.refers_to = other.refers_to def copy(self): "Make a duplicate" - new = OrderedDict() + new = Struct(self.key) new.update(self) return new + def __eq__(self, other): + return isinstance(other, Struct) and self.key == other.key and self.key != None + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) > (other.key[2], other.key[0], other.key[1]) + return True + + def __lt__(self, other): + if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) < (other.key[2], other.key[0], other.key[1]) + return False + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __le__(self, other): + return self.__gt__(other) or self.__eq__(other) + + def __hash__(self): + return hash(self.key) + def __str__(self): return "%s" % dict.__str__(self) def __repr__(self): - s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()]) - if self.array and False: - s = "[%s]" % s + if not self.key: return str(self.keys()) + s = '%s' % self.key[0] + if self.keys(): + s += ' {' + for k, t in self.items(): + is_list = False + if isinstance(t, list): + is_list = True + t = t[0] + if isinstance(t, type): + t = t.__name__ + pass + elif isinstance(t, Alias): + t = t.xml_type + elif isinstance(t, Struct): + t = t.key[0] + if is_list: + t = [t] + s += '%s: %s, ' % (k, t) + s = s[:-2]+'}' return s diff --git a/gluon/contrib/pysimplesoap/plugins.py b/gluon/contrib/pysimplesoap/plugins.py new file mode 100644 index 00000000..b08daaa8 --- /dev/null +++ b/gluon/contrib/pysimplesoap/plugins.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + +"""Pythonic simple SOAP Client plugins""" + + +from __future__ import unicode_literals +import sys +if sys.version > '3': + basestring = unicode = str + +import datetime +from decimal import Decimal +import os +import logging +import hashlib +import warnings + + +from . import __author__, __copyright__, __license__, __version__ + + +class WSSE: + + def preprocess(self, request): + header = request('Header') + + + def postprocess(response): + return response diff --git a/gluon/contrib/pysimplesoap/server.py b/gluon/contrib/pysimplesoap/server.py old mode 100755 new mode 100644 index b3175936..4f92b506 --- a/gluon/contrib/pysimplesoap/server.py +++ b/gluon/contrib/pysimplesoap/server.py @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -39,6 +39,13 @@ log = logging.getLogger(__name__) NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"') +class SoapFault(Exception): + def __init__(self, faultcode=None, faultstring=None, detail=None): + self.faultcode = faultcode or self.__class__.__name__ + self.faultstring = faultstring or '' + self.detail = detail + + class SoapDispatcher(object): """Simple Dispatcher for SOAP Server""" @@ -115,6 +122,9 @@ class SoapDispatcher(object): def register_function(self, name, fn, returns=None, args=None, doc=None): self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "") + def response_element_name(self, method): + return '%sResponse' % method + def dispatch(self, xml, action=None, fault=None): """Receive and process SOAP call, returns the xml""" # a dict can be sent in fault to expose it to the caller @@ -137,7 +147,8 @@ class SoapDispatcher(object): # detect soap prefix and uri (xmlns attributes of Envelope) for k, v in request[:]: if v in ("http://schemas.xmlsoap.org/soap/envelope/", - "http://www.w3.org/2003/05/soap-env",): + "http://www.w3.org/2003/05/soap-env", + "http://www.w3.org/2003/05/soap-envelope",): soap_ns = request.attributes()[k].localName soap_uri = request.attributes()[k].value @@ -184,13 +195,20 @@ class SoapDispatcher(object): ret = function(**args) log.debug('dispathed method returns: %s', ret) + except SoapFault as e: + fault.update({ + 'faultcode': "%s.%s" % (soap_fault_code, e.faultcode), + 'faultstring': e.faultstring, + 'detail': e.detail + }) + except Exception: # This shouldn't be one huge try/except import sys etype, evalue, etb = sys.exc_info() log.error(traceback.format_exc()) if self.debug: - detail = ''.join(traceback.format_exception(etype, evalue, etb)) - detail += '\n\nXML REQUEST\n\n' + xml + detail = u''.join(traceback.format_exception(etype, evalue, etb)) + detail += u'\n\nXML REQUEST\n\n' + xml.decode('UTF-8') else: detail = None fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), @@ -235,7 +253,7 @@ class SoapDispatcher(object): body.marshall("%s:Fault" % soap_ns, fault, ns=False) else: # return normal value - res = body.add_child("%sResponse" % name, ns=self.namespace) + res = body.add_child(self.response_element_name(name), ns=self.namespace) if not prefix: res['xmlns'] = self.namespace # add target namespace @@ -251,7 +269,7 @@ class SoapDispatcher(object): "%s vs %s" % (str(returns_types), str(ret))) if not complex_type or not types_ok: # backward compatibility for scalar and simple types - res.marshall(returns_types.keys()[0], ret, ) + res.marshall(list(returns_types.keys())[0], ret, ) else: # new style for complex classes for k, v in ret.items(): @@ -367,7 +385,7 @@ class SoapDispatcher(object): parse_element(n, v.items(), complex=True) t = "tns:%s" % n else: - raise TypeError("unknonw type v for marshalling" % str(v)) + raise TypeError("unknonw type %s for marshalling" % str(v)) e.add_attribute('type', t) parse_element("%s" % method, args and args.items()) @@ -452,7 +470,13 @@ class SOAPHandler(BaseHTTPRequestHandler): def do_POST(self): """SOAP POST gateway""" - request = self.rfile.read(int(self.headers.getheader('content-length'))) + request = self.rfile.read(int(self.headers.get('content-length'))) + # convert xml request to unicode (according to request headers) + if sys.version < '3': + encoding = self.headers.getparam("charset") + else: + encoding = self.headers.get_param("charset") + request = request.decode(encoding) fault = {} # execute the method response = self.server.dispatcher.dispatch(request, fault=fault) @@ -576,7 +600,7 @@ if __name__ == "__main__": namespace="http://example.com/sample.wsdl", soap_ns='soap', trace=True, - ns=False + ns="ns0", ) p = {'a': 1, 'b': 2} c = [{'d': '1.20'}, {'d': '2.01'}] diff --git a/gluon/contrib/pysimplesoap/simplexml.py b/gluon/contrib/pysimplesoap/simplexml.py old mode 100755 new mode 100644 index 9a23497a..e20ef6cd --- a/gluon/contrib/pysimplesoap/simplexml.py +++ b/gluon/contrib/pysimplesoap/simplexml.py @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -28,7 +28,7 @@ from . import __author__, __copyright__, __license__, __version__ # Utility functions used for marshalling, moved aside for readability from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \ - REVERSE_TYPE_MAP, OrderedDict, Date, Decimal + REVERSE_TYPE_MAP, Struct, Date, Decimal log = logging.getLogger(__name__) @@ -79,7 +79,10 @@ class SimpleXMLElement(object): element = self.__document.createElementNS(self.__ns, name) # don't append null tags! if text is not None: - element.appendChild(self.__document.createTextNode(text)) + if isinstance(text, xml.dom.minidom.CDATASection): + element.appendChild(self.__document.createCDATASection(text.data)) + else: + element.appendChild(self.__document.createTextNode(text)) self._element.appendChild(element) return SimpleXMLElement( elements=[element], @@ -117,10 +120,15 @@ class SimpleXMLElement(object): else: return self.__document.toprettyxml(encoding='UTF-8') - def __repr__(self): - """Return the XML representation of this tag""" - # NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc - return self._element.toxml('UTF-8') + if sys.version > '3': + def __repr__(self): + """Return the XML representation of this tag""" + return self._element.toxml() + else: + def __repr__(self): + """Return the XML representation of this tag""" + # NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc + return self._element.toxml('UTF-8') def get_name(self): """Return the tag name of this node""" @@ -182,6 +190,10 @@ class SimpleXMLElement(object): for k, v in value.items(): self.add_attribute(k, v) + def __delitem__(self, item): + "Remove an attribute" + self._element.removeAttribute(item) + def __call__(self, tag=None, ns=None, children=False, root=False, error=True, ): """Search (even in child nodes) and return a child tag by name""" @@ -284,17 +296,17 @@ class SimpleXMLElement(object): def __unicode__(self): """Returns the unicode text nodes of the current element""" - if self._element.childNodes: - rc = "" - for node in self._element.childNodes: - if node.nodeType == node.TEXT_NODE: - rc = rc + node.data - return rc - return '' + rc = '' + for node in self._element.childNodes: + if node.nodeType == node.TEXT_NODE or node.nodeType == node.CDATA_SECTION_NODE: + rc = rc + node.data + return rc - def __str__(self): - """Returns the str text nodes of the current element""" - return self.__unicode__() + if sys.version > '3': + __str__ = __unicode__ + else: + def __str__(self): + return self.__unicode__().encode('utf-8') def __int__(self): """Returns the integer value of the current element""" @@ -313,7 +325,7 @@ class SimpleXMLElement(object): #import pdb; pdb.set_trace() """Convert to python values the current serialized xml element""" - # types is a dict of {tag name: convertion function} + # types is a dict of {tag name: conversion function} # strict=False to use default type conversion if not specified # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]} # expected xml:

12

holachau @@ -329,7 +341,7 @@ class SimpleXMLElement(object): if ref_node['id'] == href: node = ref_node ref_name_type = ref_node['xsi:type'].split(":")[1] - break + break try: if isinstance(types, dict): @@ -341,12 +353,29 @@ class SimpleXMLElement(object): else: fn = types except (KeyError, ) as e: + xmlns = node['xmlns'] or node.get_namespace_uri(node.get_prefix()) if 'xsi:type' in node.attributes().keys(): xsd_type = node['xsi:type'].split(":")[1] try: - fn = REVERSE_TYPE_MAP[xsd_type] + # get fn type from SOAP-ENC:arrayType="xsd:string[28]" + if xsd_type == 'Array': + array_type = [k for k,v in node[:] if 'arrayType' in k][0] + xsd_type = node[array_type].split(":")[1] + if "[" in xsd_type: + xsd_type = xsd_type[:xsd_type.index("[")] + fn = [REVERSE_TYPE_MAP[xsd_type]] + else: + fn = REVERSE_TYPE_MAP[xsd_type] except: fn = None # ignore multirefs! + elif xmlns == "http://www.w3.org/2001/XMLSchema": + # self-defined schema, return the SimpleXMLElement + # TODO: parse to python types if + fn = None + elif None in types: + # , return the SimpleXMLElement + # TODO: check position of None if inside + fn = None elif strict: raise TypeError("Tag: %s invalid (type not found)" % (name,)) else: @@ -356,7 +385,9 @@ class SimpleXMLElement(object): if isinstance(fn, list): # append to existing list (if any) - unnested dict arrays - value = d.setdefault(name, []) - children = node.children() + # If the node has no children then the node itself might + # have multiple occurrences: + children = node.children() or node # TODO: check if this was really needed (get first child only) ##if len(fn[0]) == 1 and children: ## children = children() @@ -365,14 +396,15 @@ class SimpleXMLElement(object): for child in (children or []): tmp_dict = child.unmarshall(fn[0], strict) value.extend(tmp_dict.values()) - elif (self.__jetty and len(fn[0]) > 1): - # Jetty array style support [{k, v}] + #elif (self.__jetty and len(fn[0]) > 1): + elif (len(fn[0]) > 1): + # Jetty and now all dialects use array style support [{k, v}] for parent in node: tmp_dict = {} # unmarshall each value & mix for child in (node.children() or []): tmp_dict.update(child.unmarshall(fn[0], strict)) value.append(tmp_dict) - else: # .Net / Java + else: # len(fn[0]) == 0 for child in (children or []): value.append(child.unmarshall(fn[0], strict)) @@ -446,9 +478,12 @@ class SimpleXMLElement(object): for k, v in value.items(): if not add_children_ns: ns = False - else: + elif hasattr(value, 'namespaces'): # for children, use the wsdl element target namespace: - ns = getattr(value, 'namespace', None) + ns = value.namespaces.get(k) + else: + # simple type + ns = None child.marshall(k, v, add_comments=add_comments, ns=ns) elif isinstance(value, tuple): # serialize tuple (value) child = add_child and self.add_child(name, ns=ns) or self @@ -456,15 +491,23 @@ class SimpleXMLElement(object): ns = False for k, v in value: getattr(self, name).marshall(k, v, add_comments=add_comments, ns=ns) - elif isinstance(value, list): # serialize lists + elif isinstance(value, list): # serialize lists name: [value1, value2] + # list elements should be a dict with one element: + # 'vats': [{'vat': {'vat_amount': 50, 'vat_percent': 5}}, {...}] + # or an array of complex types directly (a.k.a. jetty dialect) + # 'vat': [{'vat_amount': 100, 'vat_percent': 21.0}, {...}] child = self.add_child(name, ns=ns) if not add_children_ns: ns = False if add_comments: child.add_comment("Repetitive array of:") - for t in value: + for i, t in enumerate(value): child.marshall(name, t, False, add_comments=add_comments, ns=ns) - elif isinstance(value, basestring): # do not convert strings or unicodes + # "jetty" arrays: add new base node (if not last) -see abobe- + # TODO: this could be an issue for some arrays of single values + if isinstance(t, dict) and len(t) > 1 and i < len(value) - 1: + child = self.add_child(name, ns=ns) + elif isinstance(value, (xml.dom.minidom.CDATASection, basestring)): # do not convert strings or unicodes self.add_child(name, value, ns=ns) elif value is None: # sent a empty tag? self.add_child(name, ns=ns) @@ -480,3 +523,10 @@ class SimpleXMLElement(object): def import_node(self, other): x = self.__document.importNode(other._element, True) # deep copy self._element.appendChild(x) + + def write_c14n(self, output=None, exclusive=True): + "Generate the canonical version of the XML node" + from . import c14n + xml = c14n.Canonicalize(self._element, output, + unsuppressedPrefixes=[] if exclusive else None) + return xml diff --git a/gluon/contrib/pysimplesoap/transport.py b/gluon/contrib/pysimplesoap/transport.py index 87806c06..9c27fc56 100644 --- a/gluon/contrib/pysimplesoap/transport.py +++ b/gluon/contrib/pysimplesoap/transport.py @@ -6,7 +6,7 @@ # version. # # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. @@ -14,6 +14,7 @@ import logging +import ssl import sys try: import urllib2 @@ -23,7 +24,7 @@ except ImportError: from http.cookiejar import CookieJar from . import __author__, __copyright__, __license__, __version__, TIMEOUT -from .simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict +from .simplexml import SimpleXMLElement, TYPE_MAP, Struct log = logging.getLogger(__name__) @@ -83,14 +84,14 @@ else: _wrapper_name = 'httplib2' def __init__(self, timeout, proxy=None, cacert=None, sessions=False): - ##httplib2.debuglevel=4 +# httplib2.debuglevel=4 kwargs = {} if proxy: import socks kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) log.info("using proxy %s" % proxy) - # set optional parameters according supported httplib2 version + # set optional parameters according to supported httplib2 version if httplib2.__version__ >= '0.3.0': kwargs['timeout'] = timeout if httplib2.__version__ >= '0.7.0': @@ -121,12 +122,21 @@ class urllib2Transport(TransportBase): raise RuntimeError('proxy is not supported with urllib2 transport') if cacert: raise RuntimeError('cacert is not support with urllib2 transport') + + handlers = [] - self.request_opener = urllib2.urlopen + if ((sys.version_info[0] == 2 and sys.version_info >= (2,7,9)) or + (sys.version_info[0] == 3 and sys.version_info >= (3,2,0))): + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(urllib2.HTTPSHandler(context=context)) + if sessions: - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) - self.request_opener = opener.open - + handlers.append(urllib2.HTTPCookieProcessor(CookieJar())) + + opener = urllib2.build_opener(*handlers) + self.request_opener = opener.open self._timeout = timeout def request(self, url, method="GET", body=None, headers={}): @@ -142,10 +152,8 @@ class urllib2Transport(TransportBase): _http_connectors['urllib2'] = urllib2Transport _http_facilities.setdefault('sessions', []).append('urllib2') -import sys if sys.version_info >= (2, 6): _http_facilities.setdefault('timeout', []).append('urllib2') -del sys # # pycurl support. @@ -191,7 +199,7 @@ else: c.setopt(c.CAINFO, self.cacert) c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) c.setopt(pycurl.SSL_VERIFYHOST, self.cacert and 2 or 0) - c.setopt(pycurl.CONNECTTIMEOUT, self.timeout / 6) + c.setopt(pycurl.CONNECTTIMEOUT, self.timeout) c.setopt(pycurl.TIMEOUT, self.timeout) if method == 'POST': c.setopt(pycurl.POST, 1) diff --git a/gluon/contrib/pysimplesoap/wsse.py b/gluon/contrib/pysimplesoap/wsse.py new file mode 100644 index 00000000..58044cf5 --- /dev/null +++ b/gluon/contrib/pysimplesoap/wsse.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + +"""Pythonic simple SOAP Client plugins for WebService Security extensions""" + + +from __future__ import unicode_literals +import sys +if sys.version > '3': + basestring = unicode = str + +import datetime +from decimal import Decimal +import os +import logging +import hashlib +import warnings + +from . import __author__, __copyright__, __license__, __version__ +from .simplexml import SimpleXMLElement + +import random +import string +from hashlib import sha1 + +def randombytes(N): + return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(N)) + +# Namespaces: + +WSSE_URI = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' +WSU_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" +XMLDSIG_URI = "http://www.w3.org/2000/09/xmldsig#" +X509v3_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" +Base64Binary_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" +PasswordDigest_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest" + + +class UsernameToken: + "WebService Security extension to add a basic credentials to xml request" + + def __init__(self, username="", password=""): + self.token = { + 'wsse:UsernameToken': { + 'wsse:Username': username, + 'wsse:Password': password, + } + } + + def preprocess(self, client, request, method, args, kwargs, headers, soap_uri): + "Add basic credentials to outgoing message" + # always extract WS Security header and send it + header = request('Header', ns=soap_uri, ) + k = 'wsse:Security' + # for backward compatibility, use header if given: + if k in headers: + self.token = headers[k] + # convert the token to xml + header.marshall(k, self.token, ns=False, add_children_ns=False) + header(k)['xmlns:wsse'] = WSSE_URI + # + + def postprocess(self, client, response, method, args, kwargs, headers, soap_uri): + "Analyze incoming credentials" + # TODO: add some password validation callback? + pass + +class UsernameDigestToken(UsernameToken): + """ + WebService Security extension to add a http digest credentials to xml request + drift -> time difference from the server in seconds, needed for 'Created' header + """ + + def __init__(self, username="", password="", drift=0): + self.username = username + self.password = password + self.drift = datetime.timedelta(seconds=drift) + + def preprocess(self, client, request, method, args, kwargs, headers, soap_uri): + header = request('Header', ns=soap_uri, ) + wsse = header.add_child('wsse:Security', ns=False) + wsse['xmlns:wsse'] = WSSE_URI + wsse['xmlns:wsu'] = WSU_URI + + usertoken = wsse.add_child('wsse:UsernameToken', ns=False) + usertoken.add_child('wsse:Username', self.username, ns=False) + + created = (datetime.datetime.utcnow() + self.drift).isoformat() + 'Z' + usertoken.add_child('wsu:Created', created, ns=False) + + nonce = randombytes(16) + wssenonce = usertoken.add_child('wsse:Nonce', nonce.encode('base64')[:-1], ns=False) + wssenonce['EncodingType'] = Base64Binary_URI + + sha1obj = sha1() + sha1obj.update(nonce + created + self.password) + digest = sha1obj.digest() + password = usertoken.add_child('wsse:Password', digest.encode('base64')[:-1], ns=False) + password['Type'] = PasswordDigest_URI + + +BIN_TOKEN_TMPL = """ + + +%(certificate)s + + %(signed_info)s + %(signature_value)s + + + + + + + +""" + +class BinaryTokenSignature: + "WebService Security extension to add a basic signature to xml request" + + def __init__(self, certificate="", private_key="", password=None, cacert=None): + # read the X509v3 certificate (PEM) + self.certificate = ''.join([line for line in open(certificate) + if not line.startswith("---")]) + self.private_key = private_key + self.password = password + self.cacert = cacert + + def preprocess(self, client, request, method, args, kwargs, headers, soap_uri): + "Sign the outgoing SOAP request" + # get xml elements: + body = request('Body', ns=soap_uri, ) + header = request('Header', ns=soap_uri, ) + # prepare body xml attributes to be signed (reference) + body['wsu:Id'] = "id-14" + body['xmlns:wsu'] = WSU_URI + # workaround: copy namespaces so lxml can parse the xml to be signed + for attr, value in request[:]: + if attr.startswith("xmlns"): + body[attr] = value + # use the internal tag xml representation (not the full xml document) + ref_xml = repr(body) + # sign using RSA-SHA1 (XML Security) + from . import xmlsec + vars = xmlsec.rsa_sign(ref_xml, "#id-14", + self.private_key, self.password) + vars['certificate'] = self.certificate + # generate the xml (filling the placeholders) + wsse = SimpleXMLElement(BIN_TOKEN_TMPL % vars) + header.import_node(wsse) + + def postprocess(self, client, response, method, args, kwargs, headers, soap_uri): + "Verify the signature of the incoming response" + from . import xmlsec + # get xml elements: + body = response('Body', ns=soap_uri, ) + header = response('Header', ns=soap_uri, ) + wsse = header("Security", ns=WSSE_URI) + cert = wsse("BinarySecurityToken", ns=WSSE_URI) + # check that the cert (binary token) is coming in the correct format: + self.__check(cert["EncodingType"], Base64Binary_URI) + self.__check(cert["ValueType"], X509v3_URI) + # extract the certificate (in DER to avoid new line & padding issues!) + cert_der = str(cert).decode("base64") + public_key = xmlsec.x509_extract_rsa_public_key(cert_der, binary=True) + # validate the certificate using the certification authority: + if not self.cacert: + warnings.warn("No CA provided, WSSE not validating certificate") + elif not xmlsec.x509_verify(self.cacert, cert_der, binary=True): + raise RuntimeError("WSSE certificate validation failed") + # check body xml attributes was signed correctly (reference) + self.__check(body['xmlns:wsu'], WSU_URI) + ref_uri = body['wsu:Id'] + signature = wsse("Signature", ns=XMLDSIG_URI) + signed_info = signature("SignedInfo", ns=XMLDSIG_URI) + signature_value = signature("SignatureValue", ns=XMLDSIG_URI) + # TODO: these sanity checks should be moved to xmlsec? + self.__check(signed_info("Reference", ns=XMLDSIG_URI)['URI'], "#" + ref_uri) + self.__check(signed_info("SignatureMethod", ns=XMLDSIG_URI)['Algorithm'], + XMLDSIG_URI + "rsa-sha1") + self.__check(signed_info("Reference", ns=XMLDSIG_URI)("DigestMethod", ns=XMLDSIG_URI)['Algorithm'], + XMLDSIG_URI + "sha1") + # TODO: check KeyInfo uses the correct SecurityTokenReference + # workaround: copy namespaces so lxml can parse the xml to be signed + for attr, value in response[:]: + if attr.startswith("xmlns"): + body[attr] = value + # use the internal tag xml representation (not the full xml document) + ref_xml = xmlsec.canonicalize(repr(body)) + # verify the signed hash + computed_hash = xmlsec.sha1_hash_digest(ref_xml) + digest_value = str(signed_info("Reference", ns=XMLDSIG_URI)("DigestValue", ns=XMLDSIG_URI)) + if computed_hash != digest_value: + raise RuntimeError("WSSE SHA1 hash digests mismatch") + # workaround: prepare the signed info (assure the parent ns is present) + signed_info['xmlns'] = XMLDSIG_URI + xml = repr(signed_info) + # verify the signature using RSA-SHA1 (XML Security) + ok = xmlsec.rsa_verify(xml, str(signature_value), public_key) + if not ok: + raise RuntimeError("WSSE RSA-SHA1 signature verification failed") + # TODO: remove any unsigned part from the xml? + + def __check(self, value, expected, msg="WSSE sanity check failed"): + if value != expected: + raise RuntimeError(msg) diff --git a/gluon/contrib/pysimplesoap/xmlsec.py b/gluon/contrib/pysimplesoap/xmlsec.py new file mode 100644 index 00000000..2f96df7e --- /dev/null +++ b/gluon/contrib/pysimplesoap/xmlsec.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation; either version 3, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + +"""Pythonic XML Security Library implementation""" +from __future__ import print_function +import base64 +import hashlib +import os +from cStringIO import StringIO +from M2Crypto import BIO, EVP, RSA, X509, m2 + +# if lxml is not installed, use c14n.py native implementation +try: + import lxml.etree +except ImportError: + lxml = None + +# Features: +# * Uses M2Crypto and lxml (libxml2) but it is independent from libxmlsec1 +# * Sign, Verify, Encrypt & Decrypt XML documents + +# Enveloping templates ("by reference": signature is parent): +SIGN_REF_TMPL = """ + + + + + + + + + %(digest_value)s + + +""" +SIGNED_TMPL = """ + + +%(signed_info)s +%(signature_value)s +%(key_info)s +%(ref_xml)s + +""" + +# Enveloped templates (signature is child, the reference is the root object): +SIGN_ENV_TMPL = """ + + + + + + + + + + %(digest_value)s + + +""" +SIGNATURE_TMPL = """ +%(signed_info)s +%(signature_value)s +%(key_info)s +""" + +KEY_INFO_RSA_TMPL = """ + + + + %(modulus)s + %(exponent)s + + + +""" + +KEY_INFO_X509_TMPL = """ + + + + %(issuer_name)s + %(serial_number)s + + + +""" + +def canonicalize(xml, c14n_exc=True): + "Return the canonical (c14n) form of the xml document for hashing" + # UTF8, normalization of line feeds/spaces, quoting, attribute ordering... + output = StringIO() + if lxml is not None: + # use faster libxml2 / lxml canonicalization function if available + et = lxml.etree.parse(StringIO(xml)) + et.write_c14n(output, exclusive=c14n_exc) + else: + # use pure-python implementation: c14n.py (avoid recursive import) + from .simplexml import SimpleXMLElement + SimpleXMLElement(xml).write_c14n(output, exclusive=c14n_exc) + return output.getvalue() + + +def sha1_hash_digest(payload): + "Create a SHA1 hash and return the base64 string" + return base64.b64encode(hashlib.sha1(payload).digest()) + + +def rsa_sign(xml, ref_uri, private_key, password=None, cert=None, c14n_exc=True, + sign_template=SIGN_REF_TMPL, key_info_template=KEY_INFO_RSA_TMPL): + "Sign an XML document usign RSA (templates: enveloped -ref- or enveloping)" + + # normalize the referenced xml (to compute the SHA1 hash) + ref_xml = canonicalize(xml, c14n_exc) + # create the signed xml normalized (with the referenced uri and hash value) + signed_info = sign_template % {'ref_uri': ref_uri, + 'digest_value': sha1_hash_digest(ref_xml)} + signed_info = canonicalize(signed_info, c14n_exc) + # Sign the SHA1 digest of the signed xml using RSA cipher + pkey = RSA.load_key(private_key, lambda *args, **kwargs: password) + signature = pkey.sign(hashlib.sha1(signed_info).digest()) + # build the mapping (placeholders) to create the final xml signed message + return { + 'ref_xml': ref_xml, 'ref_uri': ref_uri, + 'signed_info': signed_info, + 'signature_value': base64.b64encode(signature), + 'key_info': key_info(pkey, cert, key_info_template), + } + + +def rsa_verify(xml, signature, key, c14n_exc=True): + "Verify a XML document signature usign RSA-SHA1, return True if valid" + + # load the public key (from buffer or filename) + if key.startswith("-----BEGIN PUBLIC KEY-----"): + bio = BIO.MemoryBuffer(key) + rsa = RSA.load_pub_key_bio(bio) + else: + rsa = RSA.load_pub_key(certificate) + # create the digital envelope + pubkey = EVP.PKey() + pubkey.assign_rsa(rsa) + # do the cryptographic validation (using the default sha1 hash digest) + pubkey.reset_context(md='sha1') + pubkey.verify_init() + # normalize and feed the signed xml to be verified + pubkey.verify_update(canonicalize(xml, c14n_exc)) + ret = pubkey.verify_final(base64.b64decode(signature)) + return ret == 1 + + +def key_info(pkey, cert, key_info_template): + "Convert private key (PEM) to XML Signature format (RSAKeyValue/X509Data)" + exponent = base64.b64encode(pkey.e[4:]) + modulus = m2.bn_to_hex(m2.mpi_to_bn(pkey.n)).decode("hex").encode("base64") + x509 = x509_parse_cert(cert) if cert else None + return key_info_template % { + 'modulus': modulus, + 'exponent': exponent, + 'issuer_name': x509.get_issuer().as_text() if x509 else "", + 'serial_number': x509.get_serial_number() if x509 else "", + } + + +# Miscellaneous certificate utility functions: + + +def x509_parse_cert(cert, binary=False): + "Create a X509 certificate from binary DER, plain text PEM or filename" + if binary: + bio = BIO.MemoryBuffer(cert) + x509 = X509.load_cert_bio(bio, X509.FORMAT_DER) + elif cert.startswith("-----BEGIN CERTIFICATE-----"): + bio = BIO.MemoryBuffer(cert) + x509 = X509.load_cert_bio(bio, X509.FORMAT_PEM) + else: + x509 = X509.load_cert(cert, 1) + return x509 + + +def x509_extract_rsa_public_key(cert, binary=False): + "Return the public key (PEM format) from a X509 certificate" + x509 = x509_parse_cert(cert, binary) + return x509.get_pubkey().get_rsa().as_pem() + + +def x509_verify(cacert, cert, binary=False): + "Validate the certificate's authenticity using a certification authority" + ca = x509_parse_cert(cacert) + crt = x509_parse_cert(cert, binary) + return crt.verify(ca.get_pubkey()) + + +if __name__ == "__main__": + # basic test of enveloping signature (the reference is a part of the xml) + sample_xml = """data""" + output = canonicalize(sample_xml) + print (output) + vars = rsa_sign(sample_xml, '#object', "no_encriptada.key", "password") + print (SIGNED_TMPL % vars) + + # basic test of enveloped signature (the reference is the document itself) + sample_xml = """data%s""" + vars = rsa_sign(sample_xml % "", '', "no_encriptada.key", "password", + sign_template=SIGN_ENV_TMPL, c14n_exc=False) + print (sample_xml % (SIGNATURE_TMPL % vars)) + + # basic signature verification: + public_key = x509_extract_rsa_public_key(open("zunimercado.crt").read()) + assert rsa_verify(vars['signed_info'], vars['signature_value'], public_key, + c14n_exc=False) From 3270d39596aee072050a8c67c25302c564157fd9 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 21 Jun 2016 19:47:56 +0200 Subject: [PATCH 017/436] py3 fixed http.to --- applications/admin/controllers/appadmin.py | 7 ++++--- applications/admin/controllers/default.py | 12 ++++++++---- applications/admin/views/default/ticket.html | 2 +- applications/admin/views/default/ticket.load | 2 +- applications/admin/views/layout.html | 2 +- applications/examples/controllers/appadmin.py | 7 ++++--- applications/examples/controllers/soap_examples.py | 2 +- applications/examples/models/markmin.py | 2 +- applications/welcome/controllers/appadmin.py | 3 ++- gluon/compileapp.py | 7 ++++++- gluon/http.py | 4 ++-- 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 2f780749..3060945c 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -10,6 +10,7 @@ import datetime import copy import gluon.contenttype import gluon.fileutils +from gluon._compat import iteritems try: import pygraphviz as pgv @@ -267,7 +268,7 @@ def select(): else: rows = db(query, ignore_common_filters=True).select( *fields, limitby=(start, stop)) - except Exception, e: + except Exception as e: import traceback tb = traceback.format_exc() (rows, nrows) = ([], 0) @@ -286,7 +287,7 @@ def select(): import_csv(db[request.vars.table], request.vars.csvfile.file) response.flash = T('data uploaded') - except Exception, e: + except Exception as e: response.flash = DIV(T('unable to parse csv file'), PRE(str(e))) # end handle upload csv @@ -454,7 +455,7 @@ def ccache(): except (KeyError, ZeroDivisionError): ram['ratio'] = 0 - for key, value in cache.ram.storage.iteritems(): + for key, value in iteritems(cache.ram.storage): if hp: ram['bytes'] += hp.iso(value[1]).size ram['objects'] += hp.iso(value[1]).count diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index fea3c3f7..b28c7626 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -15,6 +15,7 @@ from gluon.utils import web2py_uuid from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob +from gluon._compat import iteritems, PY2 import shutil import platform @@ -81,7 +82,10 @@ def safe_open(a, b): def close(self): pass return tmp() - return open(a, b) + if PY2 or 'b' in b: + return open(a, b) + else: + return open(a, b, encoding="utf8") def safe_read(a, b='r'): @@ -314,7 +318,7 @@ def site(): if FILTER_APPS: apps = [f for f in apps if f in FILTER_APPS] - apps = sorted(apps, lambda a, b: cmp(a.upper(), b.upper())) + apps = sorted(apps, key=lambda a: a.upper()) myplatform = platform.python_version() return dict(app=None, apps=apps, myversion=myversion, myplatform=myplatform, form_create=form_create, form_update=form_update) @@ -1152,7 +1156,7 @@ def design(): # Get all languages langpath = os.path.join(apath(app, r=request), 'languages') languages = dict([(lang, info) for lang, info - in read_possible_languages(langpath).iteritems() + in iteritems(read_possible_languages(langpath)) if info[2] != 0]) # info[2] is langfile_mtime: # get only existed files @@ -1288,7 +1292,7 @@ def plugin(): # Get all languages languages = sorted([lang + '.py' for lang, info in - T.get_possible_languages_info().iteritems() + iteritems(T.get_possible_languages_info()) if info[2] != 0]) # info[2] is langfile_mtime: # get only existed files diff --git a/applications/admin/views/default/ticket.html b/applications/admin/views/default/ticket.html index 2b9a73b1..eb26c849 100644 --- a/applications/admin/views/default/ticket.html +++ b/applications/admin/views/default/ticket.html @@ -122,7 +122,7 @@ {{=XML(snapshot.get('response','no response available in snapshot'))}} -{{except Exception, e:}} +{{except Exception as e:}} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} diff --git a/applications/admin/views/default/ticket.load b/applications/admin/views/default/ticket.load index 381eee1d..9a42bba3 100644 --- a/applications/admin/views/default/ticket.load +++ b/applications/admin/views/default/ticket.load @@ -117,7 +117,7 @@
session
{{=BEAUTIFY(snapshot['session'])}}
response
{{=BEAUTIFY(snapshot['response'])}}
-{{except Exception, e:}} +{{except Exception as e:}} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} diff --git a/applications/admin/views/layout.html b/applications/admin/views/layout.html index 748f8f6e..5203dfc6 100644 --- a/applications/admin/views/layout.html +++ b/applications/admin/views/layout.html @@ -61,7 +61,7 @@ {{if hasattr(T,'get_possible_languages_info'):}} - {{=T('Admin language')}} diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 2f780749..3060945c 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -10,6 +10,7 @@ import datetime import copy import gluon.contenttype import gluon.fileutils +from gluon._compat import iteritems try: import pygraphviz as pgv @@ -267,7 +268,7 @@ def select(): else: rows = db(query, ignore_common_filters=True).select( *fields, limitby=(start, stop)) - except Exception, e: + except Exception as e: import traceback tb = traceback.format_exc() (rows, nrows) = ([], 0) @@ -286,7 +287,7 @@ def select(): import_csv(db[request.vars.table], request.vars.csvfile.file) response.flash = T('data uploaded') - except Exception, e: + except Exception as e: response.flash = DIV(T('unable to parse csv file'), PRE(str(e))) # end handle upload csv @@ -454,7 +455,7 @@ def ccache(): except (KeyError, ZeroDivisionError): ram['ratio'] = 0 - for key, value in cache.ram.storage.iteritems(): + for key, value in iteritems(cache.ram.storage): if hp: ram['bytes'] += hp.iso(value[1]).size ram['objects'] += hp.iso(value[1]).count diff --git a/applications/examples/controllers/soap_examples.py b/applications/examples/controllers/soap_examples.py index 7840105e..2d091e80 100644 --- a/applications/examples/controllers/soap_examples.py +++ b/applications/examples/controllers/soap_examples.py @@ -44,7 +44,7 @@ def test_soap_sub(): try: ret = client.SubIntegers(a=3, b=2) result = ret['SubResult'] - except SoapFault, sf: + except SoapFault as sf: result = sf response.view = "soap_examples/generic.html" return dict(xml_request=client.xml_request, diff --git a/applications/examples/models/markmin.py b/applications/examples/models/markmin.py index 3376bac6..0370df95 100644 --- a/applications/examples/models/markmin.py +++ b/applications/examples/models/markmin.py @@ -28,7 +28,7 @@ def get_content(b=None, try: openedfile = openfile() - except Exception, IOError: + except (Exception, IOError): l = 'en' openedfile = openfile() diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index ddb02548..3060945c 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -10,6 +10,7 @@ import datetime import copy import gluon.contenttype import gluon.fileutils +from gluon._compat import iteritems try: import pygraphviz as pgv @@ -454,7 +455,7 @@ def ccache(): except (KeyError, ZeroDivisionError): ram['ratio'] = 0 - for key, value in cache.ram.storage.iteritems(): + for key, value in iteritems(cache.ram.storage): if hp: ram['bytes'] += hp.iso(value[1]).size ram['objects'] += hp.iso(value[1]).count diff --git a/gluon/compileapp.py b/gluon/compileapp.py index b6bbda79..1a55c3bc 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native, basestring +from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 @@ -395,6 +395,11 @@ _base_environment_['SQLField'] = Field # for backward compatibility _base_environment_['SQLFORM'] = SQLFORM _base_environment_['SQLTABLE'] = SQLTABLE _base_environment_['LOAD'] = LOAD +# For an easier PY3 migration +_base_environment_['PY2'] = PY2 +_base_environment_['to_native'] = to_native +_base_environment_['to_bytes'] = to_bytes +_base_environment_['iteritems'] = iteritems def build_environment(request, response, session, store_current=True): """ diff --git a/gluon/http.py b/gluon/http.py index a0273866..e34fb8f0 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -110,7 +110,7 @@ class HTTP(Exception): if status[:1] == '4': if not body: body = status - if isinstance(body, str): + if isinstance(body, (str, bytes, bytearray)): headers['Content-Length'] = len(body) rheaders = [] for k, v in iteritems(headers): @@ -121,7 +121,7 @@ class HTTP(Exception): responder(status, rheaders) if env.get('request_method', '') == 'HEAD': return [''] - elif isinstance(body, str): + elif isinstance(body, (str, bytes, bytearray)): return [body] elif hasattr(body, '__iter__'): return body From 3bb0299b297543b12f49b176e5ddbcccfc67c8d8 Mon Sep 17 00:00:00 2001 From: zvolsky Date: Wed, 22 Jun 2016 12:19:42 +0200 Subject: [PATCH 018/436] cs translation: removed unwanted '@markmin' literal from translated strings --- applications/admin/languages/cs.py | 1 + applications/welcome/languages/cs.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/admin/languages/cs.py b/applications/admin/languages/cs.py index d6282119..0edeb8e2 100644 --- a/applications/admin/languages/cs.py +++ b/applications/admin/languages/cs.py @@ -700,6 +700,7 @@ 'Welcome to web2py': 'Vitejte ve Web2py aplikaci.', 'Welcome to web2py!': 'VĆ­tejte ve Web2py aplikaci.', 'Which called the function %s located in the file %s': 'kterĆ” zavolala funkci %s v souboru (kontrolĆ©ru) %s.', +'Working...': 'Pracuji...', 'WSGI reference name': 'jmĆ©no WSGI reference', 'YES': 'ANO', 'Yes': 'Ano', diff --git a/applications/welcome/languages/cs.py b/applications/welcome/languages/cs.py index 1cefd617..c2169745 100644 --- a/applications/welcome/languages/cs.py +++ b/applications/welcome/languages/cs.py @@ -14,9 +14,9 @@ '(requires internet access)': '(vyžaduje připojenĆ­ k internetu)', '(requires internet access, experimental)': '(vyžaduje internetovĆ© připojenĆ­, experimentĆ”lnĆ­)', '(something like "it-it")': '(například "cs-cz")', -'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '@markmin\x01(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', -'@markmin\x01An error occured, please [[reload %s]] the page': '@markmin\x01DoÅ”lo k chybě, prosĆ­m [[obnovte strĆ”nku %s]]', -'@markmin\x01Searching: **%s** %%{file}': '@markmin\x01HledĆ”nĆ­: **%s** %%{soubor}', +'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', +'@markmin\x01An error occured, please [[reload %s]] the page': 'DoÅ”lo k chybě, prosĆ­m [[obnovte strĆ”nku %s]]', +'@markmin\x01Searching: **%s** %%{file}': 'HledĆ”nĆ­: **%s** %%{soubor}', 'About': 'O programu', 'About application': 'O aplikaci', 'Access Control': 'ŘízenĆ­ přístupu', From 61795bc65ec90a84e6f00ac3f255aab4275b5dae Mon Sep 17 00:00:00 2001 From: ilvalle Date: Wed, 22 Jun 2016 18:39:30 +0200 Subject: [PATCH 019/436] enabled test_web.py in PY3 --- applications/welcome/private/appconfig.ini | 5 +++-- gluon/contrib/webclient.py | 8 ++++---- gluon/tests/__init__.py | 2 +- gluon/tests/test_web.py | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/applications/welcome/private/appconfig.ini b/applications/welcome/private/appconfig.ini index f0813547..99109407 100644 --- a/applications/welcome/private/appconfig.ini +++ b/applications/welcome/private/appconfig.ini @@ -14,7 +14,8 @@ names = localhost:*, 127.0.0.1:*, *:*, * [db] uri = sqlite://storage.sqlite migrate = true -pool_size = 10 ; ignored for sqlite +; ignored for sqlite +pool_size = 10 ; smtp address and credentials [smtp] @@ -27,4 +28,4 @@ ssl = true ; form styling [forms] formstyle = bootstrap3_inline -separator = \ No newline at end of file +separator = diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index 4dc4cdec..54444ec0 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -137,7 +137,10 @@ class WebClient(object): self.status = None self.text = to_native(self.response.read()) - self.headers = dict(self.response.headers) + # In PY3 self.response.headers are case sensitive + self.headers = dict() + for h in self.response.headers: + self.headers[h.lower()] = self.response.headers[h] # treat web2py tickets as special types of errors if error is not None: @@ -146,9 +149,6 @@ class WebClient(object): else: raise error - if 'Set-Cookie' in self.headers: #PY3 - self.headers['set-cookie'] = self.headers['Set-Cookie'] - # parse headers into cookies self.cookies = {} if 'set-cookie' in self.headers: diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 4c879ad3..6b564235 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -20,9 +20,9 @@ from .test_serializers import * from .test_languages import * from .test_compileapp import * from .test_appadmin import * +from .test_web import * if sys.version[:3] == '2.7': from .test_is_url import * from .test_scheduler import * - from .test_web import * from .test_old_doctests import * diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index f6e41752..f7e9ddf3 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -16,7 +16,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon.contrib.webclient import WebClient -from gluon._compat import urllib2 +from gluon._compat import urllib2, PY2 webserverprocess = None @@ -110,6 +110,7 @@ class TestWeb(LiveTest): assert('expires' in s.headers) assert(s.headers['cache-control'].startswith('max-age')) + @unittest.skipIf(not(PY2), 'skip PY3 testSoap') def testSoap(self): # test soap server implementation from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault From 8aecaf451436c014ac32dfe2535c96a7e6f4221f Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 24 Jun 2016 15:02:28 +0200 Subject: [PATCH 020/436] PY3 fixes and added tests for gluon/admin.py --- .travis.yml | 8 +++--- applications/admin/controllers/debug.py | 7 +++-- applications/admin/controllers/default.py | 24 ++++++++--------- applications/admin/controllers/webservices.py | 2 +- applications/admin/controllers/wizard.py | 2 +- .../admin/views/default.mobile/ticket.html | 2 +- .../admin/views/default.mobile/ticket.load | 2 +- applications/admin/views/default/about.html | 2 +- applications/admin/views/default/design.html | 8 +++--- .../admin/views/default/manage_students.html | 4 +-- gluon/admin.py | 8 +++--- gluon/compileapp.py | 5 +++- gluon/contrib/markmin/markmin2html.py | 2 +- gluon/fileutils.py | 5 +++- gluon/html.py | 3 ++- gluon/rocket.py | 26 +++--------------- gluon/tests/test_compileapp.py | 27 ++++++++++++++++++- gluon/widget.py | 6 ++--- web2py.py | 1 + 19 files changed, 75 insertions(+), 69 deletions(-) diff --git a/.travis.yml b/.travis.yml index a55f3cf9..be3d0356 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,12 +25,12 @@ install: virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install --download-cache $HOME/.pip-cache pycrypto pg8000 pymysql; fi; + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000 pymysql; fi; - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; before_script: - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi; - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache codecov; fi + - pip install --download-cache $HOME/.pip-cache coverage + - pip install --download-cache $HOME/.pip-cache codecov - mysql -e 'create database pydal;' - psql -c 'create database pydal;' -U postgres - psql -c 'create extension postgis;' -U postgres -d pydal; @@ -41,7 +41,7 @@ script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --ru after_success: - coverage combine; - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then codecov; fi + - codecov notifications: email: true diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 7176e09c..06ba3b83 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -1,6 +1,5 @@ import os import sys -import cStringIO import gluon.contrib.shell import gluon.dal import gluon.html @@ -122,7 +121,7 @@ def execute(): output = web_debugger.do_exec(command) if output is None: output = "" - except Exception, e: + except Exception as e: output = T("Exception %s") % str(e) k = len(session['debug_commands:' + app]) - 1 return '[%i] %s%s\n' % (k + 1, command, output) @@ -212,7 +211,7 @@ def toggle_breakpoint(): ok = True else: response.flash = T("Unable to determine the line number!") - except Exception, e: + except Exception as e: session.flash = str(e) return response.json({'ok': ok, 'lineno': lineno}) @@ -233,6 +232,6 @@ def list_breakpoints(): if filename == bp_filename: breakpoints.append(bp_lineno) ok = True - except Exception, e: + except Exception as e: session.flash = str(e) return response.json({'ok': ok, 'breakpoints': breakpoints}) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index b28c7626..359a5bc8 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -15,7 +15,7 @@ from gluon.utils import web2py_uuid from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob -from gluon._compat import iteritems, PY2 +from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes import shutil import platform @@ -127,7 +127,6 @@ def index(): redirect(send) elif failed_login_count() >= allowed_number_of_attempts: time.sleep(2 ** allowed_number_of_attempts) - print('4033') raise HTTP(403) elif request.vars.password: if verify_password(request.vars.password[:1024]): @@ -274,7 +273,7 @@ def site(): elif form_update.vars.url: # fetch an application via URL or file upload try: - f = urllib.urlopen(form_update.vars.url) + f = urlopen(form_update.vars.url) if f.code == 404: raise Exception("404 file not found") except Exception as e: @@ -387,7 +386,7 @@ def pack_exe(app, base, filenames=None): # Download latest web2py_win and open it with zipfile download_url = 'http://www.web2py.com/examples/static/web2py_win.zip' out = StringIO() - out.write(urllib.urlopen(download_url).read()) + out.write(urlopen(download_url).read()) web2py_win = zipfile.ZipFile(out, mode='a') # Write routes.py with the application as default routes = u'# -*- coding: utf-8 -*-\nrouters = dict(BASE=dict(default_application="%s"))' % app @@ -858,7 +857,7 @@ def todolist(): for f in listfiles(app, d): matches = [] filename = apath(os.path.join(app, d, f), r=request) - with open(filename, 'r') as f_s: + with safe_open(filename, 'r') as f_s: src = f_s.read() for m in regex.finditer(src): start = m.start() @@ -1575,7 +1574,6 @@ def errors(): """ Error handler """ import operator import os - import pickle import hashlib app = get_app() @@ -1605,7 +1603,7 @@ def errors(): if not os.path.isfile(fullpath): continue try: - fullpath_file = open(fullpath, 'r') + fullpath_file = safe_open(fullpath, 'rb') try: error = pickle.load(fullpath_file) finally: @@ -1615,7 +1613,7 @@ def errors(): except EOFError: continue - hash = hashlib.md5(error['traceback']).hexdigest() + hash = hashlib.md5(to_bytes(error['traceback'])).hexdigest() if hash in delete_hashes: os.unlink(fullpath) @@ -1710,7 +1708,7 @@ def get_ticket_storage(app): private_folder = apath('%s/private' % app, r=request) ticket_file = os.path.join(private_folder, 'ticket_storage.txt') if os.path.exists(ticket_file): - db_string = open(ticket_file).read() + db_string = safe_open(ticket_file).read() db_string = db_string.strip().replace('\r', '').replace('\n', '') elif is_gae: # use Datastore as fallback if there is no ticket_file @@ -1965,9 +1963,9 @@ def plugins(): from serializers import loads_json if not session.plugins: try: - rawlist = urllib.urlopen("http://www.web2pyslices.com/" + - "public/api.json/action/list/content/Package?package" + - "_type=plugin&search_index=false").read() + rawlist = urlopen("http://www.web2pyslices.com/" + + "public/api.json/action/list/content/Package?package" + + "_type=plugin&search_index=false").read() session.plugins = loads_json(rawlist) except: response.flash = T('Unable to download the list of plugins') @@ -1993,7 +1991,7 @@ def install_plugin(): source.split("web2py.plugin.")[-1].split(".w2p")[0] else: filename = "web2py.plugin.%s.w2p" % cleanpath(plugin) - if plugin_install(app, urllib.urlopen(source), + if plugin_install(app, urlopen(source), request, filename): session.flash = T('New plugin installed: %s', filename) else: diff --git a/applications/admin/controllers/webservices.py b/applications/admin/controllers/webservices.py index a043aab5..eb760df2 100644 --- a/applications/admin/controllers/webservices.py +++ b/applications/admin/controllers/webservices.py @@ -101,7 +101,7 @@ def attach_debugger(host='localhost', port=6000, authkey='secret password'): gluon.debug.qdb_debugger = qdb.Qdb(gluon.debug.qdb_connection) gluon.debug.dbg = gluon.debug.qdb_debugger # welcome message (this should be displayed on the frontend) - print 'debugger connected to', gluon.debug.qdb_listener.last_accepted + print('debugger connected to', gluon.debug.qdb_listener.last_accepted) return True # connection successful! diff --git a/applications/admin/controllers/wizard.py b/applications/admin/controllers/wizard.py index 065729ff..f4511b96 100644 --- a/applications/admin/controllers/wizard.py +++ b/applications/admin/controllers/wizard.py @@ -516,7 +516,7 @@ def create(options): plugin_name = 'web2py.plugin.' + plugin + '.w2p' stream = urllib.urlopen(PLUGINS_APP + '/static/' + plugin_name) plugin_install(app, stream, request, plugin_name) - except Exception, e: + except Exception as e: session.flash = T("unable to download plugin: %s" % plugin) ### write configuration file into newapp/models/0.py diff --git a/applications/admin/views/default.mobile/ticket.html b/applications/admin/views/default.mobile/ticket.html index f9e08ed3..44484537 100644 --- a/applications/admin/views/default.mobile/ticket.html +++ b/applications/admin/views/default.mobile/ticket.html @@ -118,7 +118,7 @@
session
{{=BEAUTIFY(snapshot['session'])}}
response
{{=BEAUTIFY(snapshot['response'])}}
-{{except Exception, e:}} +{{except Exception as e:}} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} diff --git a/applications/admin/views/default.mobile/ticket.load b/applications/admin/views/default.mobile/ticket.load index 381eee1d..9a42bba3 100644 --- a/applications/admin/views/default.mobile/ticket.load +++ b/applications/admin/views/default.mobile/ticket.load @@ -117,7 +117,7 @@
session
{{=BEAUTIFY(snapshot['session'])}}
response
{{=BEAUTIFY(snapshot['response'])}}
-{{except Exception, e:}} +{{except Exception as e:}} {{import traceback;tb=traceback.format_exc().replace("\n","\\n") }} diff --git a/applications/admin/views/default/about.html b/applications/admin/views/default/about.html index 1fe839e7..1e15b821 100644 --- a/applications/admin/views/default/about.html +++ b/applications/admin/views/default/about.html @@ -23,4 +23,4 @@ jQuery(document).ready(function() { jQuery.plot(jQuery("#placeholder"), [ {{=progress}} ]); }) - \ No newline at end of file + diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index 173f0053..82ad7bd4 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -67,7 +67,6 @@ def deletefile(arglist, vars={}): **{'_data-placement':'right', '_data-original-title':T('Delete this file (you will be asked to confirm deletion)')}) }} - {{block sectionclass}}design{{end}} @@ -120,7 +119,7 @@ def deletefile(arglist, vars={}): {{=peekfile('models',m, dict(id=id))}} - {{if len(defines[m]):}}{{=T("defines tables")}} {{pass}}{{=XML(', '.join([B(table).xml() for table in defines[m]]))}} + {{if len(defines[m]):}}{{=T("defines tables")}} {{pass}}{{=XML(b', '.join([B(table).xml() for table in defines[m]]))}} {{pass}} @@ -133,7 +132,6 @@ def deletefile(arglist, vars={}): - {{ controller_functions=[] @@ -168,7 +166,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi {{=peekfile('controllers',c, dict(id=id))}} - {{if functions[c]:}}{{=T("exposes")}}{{pass}} {{=XML(', '.join([A(f,_href=URL(a=app,c=c[:-3],f=f)).xml() for f in functions[c]]))}} + {{if functions[c]:}}{{=T("exposes")}}{{pass}} {{=XML(b', '.join([A(f,_href=URL(a=app,c=c[:-3],f=f)).xml() for f in functions[c]]))}} {{pass}} @@ -208,7 +206,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi {{if c in extend:}}{{=T("extends")}} {{=extend[c]}} {{pass}} - {{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}} + {{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(b', '.join([B(f).xml() for f in include[c]]))}} {{pass}} diff --git a/applications/admin/views/default/manage_students.html b/applications/admin/views/default/manage_students.html index d4e827d7..0885ff76 100644 --- a/applications/admin/views/default/manage_students.html +++ b/applications/admin/views/default/manage_students.html @@ -150,7 +150,7 @@ elif request.args(0)=='edit': input = tr[1][0] help = tr[2][0] new_form_flds.append(label) - print label + print(label) new_form_flds.append(input) new_form_flds.append(help) pass @@ -160,4 +160,4 @@ pass }}

{{=T('Manage Admin Users/Students')}}

-{{=grid}} \ No newline at end of file +{{=grid}} diff --git a/gluon/admin.py b/gluon/admin.py index 61aa56e8..8a2f9277 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -14,7 +14,6 @@ import os import sys import traceback import zipfile -import urllib from shutil import rmtree from gluon.utils import web2py_uuid from gluon.fileutils import w2p_pack, w2p_unpack, w2p_pack_plugin, w2p_unpack_plugin @@ -23,7 +22,7 @@ from gluon.fileutils import read_file, write_file, parse_version from gluon.restricted import RestrictedError from gluon.settings import global_settings from gluon.cache import CacheOnDisk - +from gluon._compat import urlopen, to_native if not global_settings.web2py_runtime_gae: import site @@ -338,8 +337,7 @@ def check_new_version(myversion, version_url): """ try: - from urllib import urlopen - version = urlopen(version_url).read() + version = to_native(urlopen(version_url).read()) pversion = parse_version(version) pmyversion = parse_version(myversion) except IOError: @@ -423,7 +421,7 @@ def upgrade(request, url='http://web2py.com'): full_url = url + '/examples/static/web2py_%s.zip' % version_type filename = abspath('web2py_%s_downloaded.zip' % version_type) try: - write_file(filename, urllib.urlopen(full_url).read(), 'wb') + write_file(filename, urlopen(full_url).read(), 'wb') except Exception as e: return False, e try: diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 1a55c3bc..5bedc04e 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring +from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 @@ -400,6 +400,9 @@ _base_environment_['PY2'] = PY2 _base_environment_['to_native'] = to_native _base_environment_['to_bytes'] = to_bytes _base_environment_['iteritems'] = iteritems +_base_environment_['reduce'] = reduce +_base_environment_['xrange'] = xrange + def build_environment(request, response, session, store_current=True): """ diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 0752e063..fe9b374d 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -6,7 +6,7 @@ from __future__ import print_function import re import urllib -from gluon._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape +from gluon._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape, xrange from ast import parse as ast_parse import ast diff --git a/gluon/fileutils.py b/gluon/fileutils.py index e9d869b3..5ccdfaac 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -115,7 +115,10 @@ def write_file(filename, value, mode='w'): """Writes to filename, making sure to close the file explicitly on exit. """ - f = open(filename, mode) + if PY2: + f = open(filename, mode) + else: + f = open(filename, mode, encoding="utf8") try: return f.write(value) finally: diff --git a/gluon/html.py b/gluon/html.py index f2fb7b9b..5ddf5dd4 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -2826,7 +2826,8 @@ class MARKMIN(XmlComponent): return to_bytes(html) if not self.kwargs else to_bytes(DIV(XML(html), **self.kwargs).xml()) def __str__(self): - return self.xml() + # In PY3 __str__ cannot return bytes (TypeError: __str__ returned non-string (type bytes)) + return to_native(self.xml()) def ASSIGNJS(**kargs): diff --git a/gluon/rocket.py b/gluon/rocket.py index 199f1004..83261a36 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -11,7 +11,7 @@ import errno import socket import logging import platform -from gluon._compat import iteritems, to_bytes +from gluon._compat import iteritems, to_bytes, StringIO, urllib_unquote # Define Constants VERSION = '1.2.6' @@ -182,13 +182,6 @@ class Connection(object): # Import System Modules import socket -try: - from io import StringIO -except ImportError: - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO # Import Package Modules # package imports removed in monolithic build @@ -1179,19 +1172,6 @@ from wsgiref.headers import Headers from threading import Thread from datetime import datetime -try: - from urllib import unquote -except ImportError: - from urllib.parse import unquote - -try: - from io import StringIO -except ImportError: - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO - try: from ssl import SSLError except ImportError: @@ -1436,7 +1416,7 @@ class Worker(Thread): req[k] = "" if k == 'path': req['path'] = r'%2F'.join( - [unquote(x) for x in re_SLASH.split(v)]) + [urllib_unquote(x) for x in re_SLASH.split(v)]) self.protocol = req['protocol'] return req @@ -1471,7 +1451,7 @@ class Worker(Thread): if '?' in path: path, query_string = path.split('?', 1) - path = r'%2F'.join([unquote(x) for x in re_SLASH.split(path)]) + path = r'%2F'.join([urllib_unquote(x) for x in re_SLASH.split(path)]) req.update(path=path, query_string=query_string, diff --git a/gluon/tests/test_compileapp.py b/gluon/tests/test_compileapp.py index 2e740e06..956c28a4 100644 --- a/gluon/tests/test_compileapp.py +++ b/gluon/tests/test_compileapp.py @@ -10,8 +10,12 @@ fix_sys_path(__file__) from gluon.compileapp import compile_application, remove_compiled_application from gluon.fileutils import w2p_pack, w2p_unpack -import os +from gluon.globals import Request +from gluon.admin import app_compile, app_create, app_cleanup, check_new_version, app_uninstall +from gluon.main import global_settings +import os, shutil +WEB2PY_VERSION_URL = "http://web2py.com/examples/default/version" class TestPack(unittest.TestCase): """ Tests the compileapp.py module """ @@ -30,6 +34,27 @@ class TestPack(unittest.TestCase): w2p_unpack(test_path, unpack_path) return + def test_admin_compile(self): + #apps = ['welcome', 'admin', 'examples'] + request = Request(env={}) + request.application = 'a' + request.controller = 'c' + request.function = 'f' + request.folder = 'applications/admin' + apps = ['welcome'] + for appname in apps: + appname_path = os.path.join(os.getcwd(), 'applications', appname) + self.assertEqual(app_compile(appname_path, request), None) + # remove any existing test_app + new_app = 'test_app_%s' % (appname) + if(os.path.exists('applications/%s' % (new_app))): + shutil.rmtree('applications/%s' % (new_app)) + self.assertEqual(app_create(new_app, request), True) + self.assertEqual(os.path.exists('applications/test_app_%s/controllers/default.py' % (appname)), True) + self.assertEqual(app_cleanup(new_app, request), True) + self.assertEqual(app_uninstall(new_app, request), True) + self.assertNotEqual(check_new_version(global_settings.web2py_version, WEB2PY_VERSION_URL), -1) + return if __name__ == '__main__': unittest.main() diff --git a/gluon/widget.py b/gluon/widget.py index b641bb50..1bbdb830 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -13,7 +13,7 @@ from __future__ import print_function import datetime import sys -from gluon._compat import StringIO, thread +from gluon._compat import StringIO, thread, xrange import time import threading import os @@ -87,7 +87,7 @@ class IO(object): def __init__(self): """ """ - self.buffer = cStringIO.StringIO() + self.buffer = StringIO() def write(self, data): """ """ @@ -968,7 +968,7 @@ def console(): run_system_tests(options) if options.quiet: - capture = cStringIO.StringIO() + capture = StringIO() sys.stdout = capture logger.setLevel(logging.CRITICAL + 1) else: diff --git a/web2py.py b/web2py.py index 516427f7..ced8ff22 100755 --- a/web2py.py +++ b/web2py.py @@ -28,5 +28,6 @@ if __name__ == '__main__': import coverage coverage.process_startup() except: + print('Coverage is not available') pass gluon.widget.start(cron=True) From abf8d9fb27b286d0d07e496b28a619534022f7c5 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 25 Jun 2016 13:27:35 +0200 Subject: [PATCH 021/436] fix compiled app in py3 --- gluon/admin.py | 2 -- gluon/compileapp.py | 14 ++++++-------- gluon/fileutils.py | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/gluon/admin.py b/gluon/admin.py index 8a2f9277..8c70c348 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -59,8 +59,6 @@ def app_pack(app, request, raise_ex=False, filenames=None): w2p_pack(filename, apath(app, request), filenames=filenames) return filename except Exception as e: - import traceback - print(traceback.format_exc()) if raise_ex: raise return False diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 5bedc04e..0ee9c18c 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -44,18 +44,15 @@ from functools import reduce logger = logging.getLogger("web2py") from gluon import rewrite from gluon.custom_import import custom_import_install - -try: - import py_compile -except: - logger.warning('unable to import py_compile') +import py_compile is_pypy = settings.global_settings.is_pypy is_gae = settings.global_settings.web2py_runtime_gae is_jython = settings.global_settings.is_jython - pjoin = os.path.join +marshal_header_size = 8 if PY2 else 12 + TEST_CODE = \ r""" def _TEST(): @@ -456,7 +453,8 @@ def save_pyc(filename): """ Bytecode compiles the file `filename` """ - py_compile.compile(filename) + cfile = "%sc" % filename + py_compile.compile(filename, cfile=cfile) def read_pyc(filename): @@ -470,7 +468,7 @@ def read_pyc(filename): data = read_file(filename, 'rb') if not is_gae and data[:4] != imp.get_magic(): raise SystemError('compiled code is incompatible') - return marshal.loads(data[8:]) + return marshal.loads(data[marshal_header_size:]) def compile_views(folder, skip_failed_views=False): diff --git a/gluon/fileutils.py b/gluon/fileutils.py index 5ccdfaac..27fe8a8d 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -97,14 +97,19 @@ def parse_version(version): return version_tuple -def read_file(filename, mode='r'): - """Returns content from filename, making sure to close the file explicitly - on exit. - """ +def open_file(filename, mode): if PY2 or 'b' in mode: f = open(filename, mode) else: f = open(filename, mode, encoding="utf8") + return f + + +def read_file(filename, mode='r'): + """Returns content from filename, making sure to close the file explicitly + on exit. + """ + f = open_file(filename, mode) try: return f.read() finally: @@ -115,10 +120,7 @@ def write_file(filename, value, mode='w'): """Writes to filename, making sure to close the file explicitly on exit. """ - if PY2: - f = open(filename, mode) - else: - f = open(filename, mode, encoding="utf8") + f = open_file(filename, mode) try: return f.write(value) finally: From d429554c5b2de99c87e4606ae905fed995aa6026 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 25 Jun 2016 13:40:12 +0200 Subject: [PATCH 022/436] fix travis, pip 8.1.2 no longer supports --download-cache option --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index be3d0356..9f276d6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,8 +29,8 @@ install: - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; before_script: - - pip install --download-cache $HOME/.pip-cache coverage - - pip install --download-cache $HOME/.pip-cache codecov + - pip install coverage + - pip install codecov - mysql -e 'create database pydal;' - psql -c 'create database pydal;' -U postgres - psql -c 'create extension postgis;' -U postgres -d pydal; From f343fab528fb37c09b0cc0ee16734f6b8f69366d Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 25 Jun 2016 17:36:37 +0200 Subject: [PATCH 023/436] py3 fixes for admin app --- applications/admin/controllers/debug.py | 4 ++-- applications/admin/views/default/edit_js.html | 4 ++-- gluon/debug.py | 2 +- gluon/html.py | 7 ++++--- gluon/main.py | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 06ba3b83..8a08dc83 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -1,12 +1,12 @@ import os import sys -import gluon.contrib.shell +#import gluon.contrib.shell import gluon.dal import gluon.html import gluon.validators import code -import thread from gluon.debug import communicate, web_debugger, qdb_debugger +from gluon._compat import thread import pydoc diff --git a/applications/admin/views/default/edit_js.html b/applications/admin/views/default/edit_js.html index a10f39d4..75aa74a5 100644 --- a/applications/admin/views/default/edit_js.html +++ b/applications/admin/views/default/edit_js.html @@ -25,11 +25,11 @@

{{if functions:}} - {{=B(T('exposes:'))}} {{=XML(', '.join([A(f,_target="_blank", _href=URL(a=app,c=controller,f=f)).xml() for f in functions]))}} + {{=B(T('exposes:'))}} {{=XML(b', '.join([A(f,_target="_blank", _href=URL(a=app,c=controller,f=f)).xml() for f in functions]))}} {{if editviewlinks:}}
{{=B(T('edit views:'))}} - {{=XML(', '.join([v.xml() for v in editviewlinks]))}} + {{=XML(b', '.join([v.xml() for v in editviewlinks]))}} {{pass}} {{pass}} {{if edit_controller:}} diff --git a/gluon/debug.py b/gluon/debug.py index 12db5a93..a4432590 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -13,8 +13,8 @@ Debugger support classes import logging import pdb -import Queue import sys +from gluon._compat import Queue logger = logging.getLogger("web2py") diff --git a/gluon/html.py b/gluon/html.py index 5ddf5dd4..183bb4e0 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -596,17 +596,18 @@ class XML(XmlComponent): for A, IMG and BlockQuote). The key is the tag; the value is a list of allowed attributes. """ - if sanitize: text = sanitizer.sanitize(text, permitted_tags, allowed_attributes) if isinstance(text, unicodeT): text = to_native(text.encode('utf8', 'xmlcharrefreplace')) + elif isinstance(text, bytes): + text = to_native(text) elif not isinstance(text, str): text = str(text) - self.text = to_bytes(text) + self.text = text def xml(self): - return self.text + return to_bytes(self.text) def __str__(self): return self.text diff --git a/gluon/main.py b/gluon/main.py index e438f4ba..81f04b90 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -482,7 +482,7 @@ def wsgibase(environ, responder): if request.ajax: if response.flash: http_response.headers['web2py-component-flash'] = \ - urllib2.quote(xmlescape(response.flash).replace('\n', '')) + urllib2.quote(xmlescape(response.flash).replace(b'\n', b'')) if response.js: http_response.headers['web2py-component-command'] = \ urllib2.quote(response.js.replace('\n', '')) From f8f471f51f6efaf1c1e0f36bdfd395f68e6f854e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Jun 2016 00:30:25 -0500 Subject: [PATCH 024/436] new cheatsheet --- .../examples/static/web2py_cheatsheet.pdf | Bin 146645 -> 126651 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/applications/examples/static/web2py_cheatsheet.pdf b/applications/examples/static/web2py_cheatsheet.pdf index 8985166c8841de0a4dc56c24802ed9c7d8fed484..74b44ad5271871bfd4f9988b829ce6f151ea6aff 100644 GIT binary patch delta 125770 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

AB<1g9wUjla85a~qxy0`+9 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 Ze6iFDAvkh$P99Efb^slngpwrSzW@^7t-b&N delta 145850 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

q{f+<|UJ@yeljKaq8QHh7g$L8293 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 Date: Sun, 26 Jun 2016 17:47:12 +0200 Subject: [PATCH 025/436] p3 fixes in applications --- applications/admin/controllers/default.py | 9 ++++----- applications/admin/controllers/pythonanywhere.py | 5 +---- applications/admin/controllers/shell.py | 2 +- applications/admin/controllers/webservices.py | 6 +----- applications/examples/models/markmin.py | 3 ++- gluon/contrib/imageutils.py | 6 +++--- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 359a5bc8..6c46d971 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -15,7 +15,7 @@ from gluon.utils import web2py_uuid from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob -from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes +from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native import shutil import platform @@ -382,7 +382,6 @@ def pack_plugin(): def pack_exe(app, base, filenames=None): import urllib import zipfile - from cStringIO import StringIO # Download latest web2py_win and open it with zipfile download_url = 'http://www.web2py.com/examples/static/web2py_win.zip' out = StringIO() @@ -1742,9 +1741,9 @@ def make_link(path): for key in editable.keys(): check_extension = folder.endswith("%s/%s" % (app, key)) if ext.lower() == editable[key] and check_extension: - return A('"' + tryFile + '"', - _href=URL(r=request, - f='edit/%s/%s/%s' % (app, key, filename))).xml() + return to_native(A('"' + tryFile + '"', + _href=URL(r=request, + f='edit/%s/%s/%s' % (app, key, filename))).xml()) return '' diff --git a/applications/admin/controllers/pythonanywhere.py b/applications/admin/controllers/pythonanywhere.py index e10e4020..33dfda6f 100644 --- a/applications/admin/controllers/pythonanywhere.py +++ b/applications/admin/controllers/pythonanywhere.py @@ -4,10 +4,7 @@ import os import re import gzip import tarfile -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO +from gluon._compat import StringIO from xmlrpclib import ProtocolError from gluon.contrib.simplejsonrpc import ServerProxy diff --git a/applications/admin/controllers/shell.py b/applications/admin/controllers/shell.py index 3a835c7d..e25bbdd6 100644 --- a/applications/admin/controllers/shell.py +++ b/applications/admin/controllers/shell.py @@ -1,10 +1,10 @@ import sys -import cStringIO import gluon.contrib.shell import code import thread import cgi from gluon.shell import env +from gluon._compat import StringIO if DEMO_MODE or MULTI_USER_MODE: session.flash = T('disabled in demo mode') diff --git a/applications/admin/controllers/webservices.py b/applications/admin/controllers/webservices.py index eb760df2..4f5a352c 100644 --- a/applications/admin/controllers/webservices.py +++ b/applications/admin/controllers/webservices.py @@ -7,15 +7,11 @@ import platform import time import base64 import os -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO +from gluon._compat import StringIO service = Service(globals()) - @service.jsonrpc def login(): "dummy function to test credentials" diff --git a/applications/examples/models/markmin.py b/applications/examples/models/markmin.py index 0370df95..11f9f2ae 100644 --- a/applications/examples/models/markmin.py +++ b/applications/examples/models/markmin.py @@ -1,4 +1,5 @@ import gluon.template +from gluon.fileutils import open_file markmin_dict = dict( code_python=lambda code: str(CODE(code)), @@ -24,7 +25,7 @@ def get_content(b=None, import os path = os.path.join( request.folder, 'private', 'content', l, c, f, b + '.' + format) - return open(path) + return open_file(path, mode='r') try: openedfile = openfile() diff --git a/gluon/contrib/imageutils.py b/gluon/contrib/imageutils.py index ef7343f0..28cf10e0 100644 --- a/gluon/contrib/imageutils.py +++ b/gluon/contrib/imageutils.py @@ -36,11 +36,11 @@ class RESIZE(object): if isinstance(value, str) and len(value) == 0: return (value, None) from PIL import Image - import cStringIO + from io import BytesIO try: img = Image.open(value.file) img.thumbnail((self.nx, self.ny), Image.ANTIALIAS) - s = cStringIO.StringIO() + s = BytesIO() if self.padding: background = Image.new('RGBA', (self.nx, self.ny), (255, 255, 255, 0)) background.paste( @@ -48,7 +48,7 @@ class RESIZE(object): ((self.nx - img.size[0]) / 2, (self.ny - img.size[1]) / 2)) background.save(s, 'JPEG', quality=self.quality) else: - img.save(s, 'JPEG', queality=self.quality) + img.save(s, 'JPEG', quality=self.quality) s.seek(0) value.file = s except: From 4cdcf8eae0ea7b4a1248d56d8273f854e600c50c Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 26 Jun 2016 18:16:56 +0200 Subject: [PATCH 026/436] Since py2.7 compile() supports Win and Mac newlines. Also input in 'exec' mode does not have to end in a newline anymore. --- gluon/restricted.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gluon/restricted.py b/gluon/restricted.py index 278b3a47..c67bcbf4 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -199,11 +199,7 @@ class RestrictedError(Exception): def compile2(code, layer): - """ - The ``+'\\n'`` is necessary else compile fails when code ends in a comment. - """ - - return compile(code.rstrip().replace('\r\n', '\n') + '\n', layer, 'exec') + return compile(code.rstrip(), layer, 'exec') def restricted(code, environment=None, layer='Unknown'): From 955b30a871209797bf2182bca33e4d5c797623b9 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 21:36:59 +0200 Subject: [PATCH 027/436] fixes #1364 --- applications/welcome/static/js/web2py-bootstrap3.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/welcome/static/js/web2py-bootstrap3.js b/applications/welcome/static/js/web2py-bootstrap3.js index f988cc20..91f3d8ff 100644 --- a/applications/welcome/static/js/web2py-bootstrap3.js +++ b/applications/welcome/static/js/web2py-bootstrap3.js @@ -11,7 +11,7 @@ $('form input[type="submit"]:not([class^="btn"]), form input[type="button"]:not([class^="btn"])', target).addClass('btn btn-default'); /* javascript for PasswordWidget*/ $('input[type=password][data-w2p_entropy]', target).each(function() { - web2py.validate_entropy($(this)); + $.web2py.validate_entropy($(this)); }); /* javascript for ListWidget*/ $('ul.w2p_list', target).each(function() { From a96f137e034c0238f832f8bc6bf34a52d8973e66 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 21:43:31 +0200 Subject: [PATCH 028/436] fixes #1355 --- gluon/contrib/redis_cache.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index 0163abb5..05aa53fc 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -85,7 +85,7 @@ def RedisCache(redis_conn=None, debug=False, with_lock=False, fail_gracefully=Fa try: instance_name = 'redis_instance_' + current.request.application if not hasattr(RedisCache, instance_name): - setattr(RedisCache, instance_name, + setattr(RedisCache, instance_name, RedisClient(redis_conn=redis_conn, debug=debug, with_lock=with_lock, fail_gracefully=fail_gracefully)) return getattr(RedisCache, instance_name) @@ -137,6 +137,10 @@ class RedisClient(object): value = None ttl = 0 try: + if f is None: + # delete and never look back + self.r_server.delete(newKey) + return None # is there a value obj = self.r_server.get(newKey) # what's its ttl @@ -149,9 +153,6 @@ class RedisClient(object): if self.debug: self.r_server.incr('web2py_cache_statistics:hit_total') value = pickle.loads(obj) - elif f is None: - # delete and never look back - self.r_server.delete(newKey) else: # naive distributed locking if with_lock: From 496646650910cd83e77139ae1c4f3735c2389827 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 21:46:07 +0200 Subject: [PATCH 029/436] fixes #1354 --- gluon/contrib/login_methods/ldap_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index e19010f3..89ee56ec 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -532,7 +532,7 @@ def ldap_auth(server='ldap', 'There is no username or email for %s!' % username) raise # if old pydal version, assume this is a relational database which can do joins - db_can_join = db.can_join() if hasattr(db, 'can_join') else True + db_can_join = db.can_join() if hasattr(db._adapter, 'can_join') else True if db_can_join: db_group_search = \ db((db.auth_membership.user_id == db_user_id) & From 5e0a53f4c2bb349906d959f6d25cbf8dfee784c0 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 21:49:23 +0200 Subject: [PATCH 030/436] fixes #1347 --- gluon/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 0f2a87ee..1b9a478e 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -833,7 +833,7 @@ class Scheduler(MetaScheduler): Field('assigned_worker_name', default='', writable=False), on_define=self.set_requirements, migrate=self.__get_migrate('scheduler_task', migrate), - format='%(task_name)s') + format='(%(id)s) %(task_name)s') db.define_table( 'scheduler_run', From 0708dd36e72dfc11022d37dbc214e51413126234 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 22:00:57 +0200 Subject: [PATCH 031/436] fixes #1331 (just rocket, really) --- gluon/rocket.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gluon/rocket.py b/gluon/rocket.py index 83261a36..c41ac32d 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -1184,17 +1184,17 @@ except ImportError: # Define Constants re_SLASH = re.compile('%2F', re.IGNORECASE) re_REQUEST_LINE = re.compile(r"""^ -(?POPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) # Request Method -\ # (single space) +(?POPTIONS|GET|HEAD|POST|PUT|DELETE|PATCH|TRACE|CONNECT) # Req Method +\ # single space ( - (?P[^:/]+) # Scheme + (?P[^:/]+) # Scheme (://) # - (?P[^/]+) # Host + (?P[^/]+) # Host )? # -(?P(\*|/[^ \?]*)) # Path -(\? (?P[^ ]*))? # Query String -\ # (single space) -(?PHTTPS?/1\.[01]) # Protocol +(?P(\*|/[^ \?]*)) # Path +(\? (?P[^ ]*))? # Query String +\ # single space +(?PHTTPS?/1\.[01]) # Protocol $ """, re.X) LOG_LINE = '%(client_ip)s - "%(request_line)s" - %(status)s %(size)s' From 595e37c2def48f98198f47a9620fdbcfbf93b109 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 23:22:37 +0200 Subject: [PATCH 032/436] removed web shell --- CHANGELOG | 8 ++ applications/admin/controllers/debug.py | 1 - applications/admin/controllers/shell.py | 51 -------- .../admin/views/default.mobile/design.html | 1 - applications/admin/views/default/design.html | 1 - applications/admin/views/shell/index.html | 117 ------------------ 6 files changed, 8 insertions(+), 171 deletions(-) delete mode 100644 applications/admin/controllers/shell.py delete mode 100644 applications/admin/views/shell/index.html diff --git a/CHANGELOG b/CHANGELOG index c0df8ac7..208a41a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ + +## 2.15.x +- web2py does not support python 2.6 anymore +- py3.5 syntax compatible (see #1353 for details) +- dropped web shell from admin +- scheduler new feature: you can now specify intervals with cron + + ## 2.14.6 - Increased test coverage (thanks Richard) diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 8a08dc83..4f04ab09 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -1,6 +1,5 @@ import os import sys -#import gluon.contrib.shell import gluon.dal import gluon.html import gluon.validators diff --git a/applications/admin/controllers/shell.py b/applications/admin/controllers/shell.py deleted file mode 100644 index 3a835c7d..00000000 --- a/applications/admin/controllers/shell.py +++ /dev/null @@ -1,51 +0,0 @@ -import sys -import cStringIO -import gluon.contrib.shell -import code -import thread -import cgi -from gluon.shell import env - -if DEMO_MODE or MULTI_USER_MODE: - session.flash = T('disabled in demo mode') - redirect(URL('default', 'site')) - -FE = 10 ** 9 - - -def index(): - app = request.args(0) or 'admin' - reset() - return dict(app=app) - - -def callback(): - app = request.args[0] - command = request.vars.statement - escape = command[:1] != '!' - history = session['history:' + app] = session.get( - 'history:' + app, gluon.contrib.shell.History()) - if not escape: - command = command[1:] - if command == '%reset': - reset() - return '*** reset ***' - elif command[0] == '%': - try: - command = session['commands:' + app][int(command[1:])] - except ValueError: - return '' - session['commands:' + app].append(command) - environ = env(app, True, extra_request=dict(is_https=request.is_https)) - output = gluon.contrib.shell.run(history, command, environ) - k = len(session['commands:' + app]) - 1 - #output = PRE(output) - #return TABLE(TR('In[%i]:'%k,PRE(command)),TR('Out[%i]:'%k,output)) - return cgi.escape('In [%i] : %s%s\n' % (k + 1, command, output)) - - -def reset(): - app = request.args(0) or 'admin' - session['commands:' + app] = [] - session['history:' + app] = gluon.contrib.shell.History() - return 'done' diff --git a/applications/admin/views/default.mobile/design.html b/applications/admin/views/default.mobile/design.html index bcdd3c37..5ee4011e 100644 --- a/applications/admin/views/default.mobile/design.html +++ b/applications/admin/views/default.mobile/design.html @@ -98,7 +98,6 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi

{{if not controllers:}}

{{=T("There are no controllers")}}

{{else:}}
- {{=button(URL(r=request,c='shell',f='index',args=app), T("shell"))}} {{=button(URL('test',args=app), T("test"))}} {{=button(URL('edit',args=[app,'cron','crontab']), T("crontab"))}}
diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index 82ad7bd4..25ec819b 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -149,7 +149,6 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
{{if not controllers:}}

{{=T("There are no controllers")}}

{{else:}}
- {{=button(URL(r=request,c='shell',f='index',args=app), T("shell"))}} {{=button(URL('test',args=app), T("test"))}} {{=button(URL('edit',args=[app,'cron','crontab']), T("crontab"))}}
diff --git a/applications/admin/views/shell/index.html b/applications/admin/views/shell/index.html deleted file mode 100644 index c90da324..00000000 --- a/applications/admin/views/shell/index.html +++ /dev/null @@ -1,117 +0,0 @@ -{{extend 'layout.html'}} -{{block sectionclass}}shell{{end}} - -
-
-
- -
-
- -
- -
-
-
    -
  • Using the shell may lock the database to other users of this app.
  • -
  • Each db statement is automatically committed.
  • -
  • Creating new tables dynamically is not allowed.
  • -
  • Models are automatically imported in the shell.
  • -
-
-
-
- - - - From 9c8db3f65a82d87b1d654f8258a5a945fd9b95af Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 28 Jun 2016 23:52:28 +0200 Subject: [PATCH 033/436] corner case - fixes #1363 --- gluon/sanitizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index c1dd2cf0..20e9e205 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -129,7 +129,7 @@ class XssCleaner(HTMLParser): def handle_endtag(self, tag): bracketed = '' % tag - self.in_disallowed.pop() + self.in_disallowed and self.in_disallowed.pop() if tag not in self.permitted_tags: if (not self.strip_disallowed): self.result += xssescape(bracketed) From f87c3e260cb1fd7ac56775886f66c402acfc3343 Mon Sep 17 00:00:00 2001 From: Alex Artigues Date: Wed, 29 Jun 2016 20:54:13 -0400 Subject: [PATCH 034/436] Fix next redirect if only one / exists --- gluon/tools.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 7bfa4b7f..23fba4b4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1918,11 +1918,13 @@ class Auth(object): if isinstance(next, (list, tuple)): next = next[0] if next and self.settings.prevent_open_redirect_attacks: - # Prevent an attacker from adding an arbitrary url after the - # _next variable in the request. - items = next.split('/') - if '//' in next and items[2] != current.request.env.http_host: - next = None + # Prevent an attacker from adding an arbitrary url after the _next variable in the request. + # Browsers will fix a single / so check multiple things just in case + items = filter(None, next.split('/')) + has_url = any(x in next for x in ['//', ':', 'ftp', 'http', 'rss', 'xml']) + if has_url and len(items) > 1: + if items[1] != current.request.env.http_host: + next = None return next def _get_user_id(self): From d95acb68971ec9096f1d685de0bbc26d7922645b Mon Sep 17 00:00:00 2001 From: Th3R3p0 Date: Thu, 30 Jun 2016 17:24:47 -0400 Subject: [PATCH 035/436] =?UTF-8?q?Fixed=20open=20redirect=20security=20vu?= =?UTF-8?q?lnerability.=20The=20previous=20filter=20searched=20for=20two?= =?UTF-8?q?=20forward=20slashes=20"//"=20in=20the=20"=5Fnext=E2=80=9D=20pa?= =?UTF-8?q?rameter=20and=20if=20the=20two=20forward=20slashes=20were=20fou?= =?UTF-8?q?nd=20it=20would=20check=20the=20URI=20and=20determine=20if=20th?= =?UTF-8?q?e=20hostname=20matched=20the=20hostname=20of=20the=20web=20serv?= =?UTF-8?q?er.=20If=20not,=20it=20would=20change=20the=20next=20variable?= =?UTF-8?q?=20to=20the=20None.=20However,=20browsers=20don't=20require=20t?= =?UTF-8?q?wo=20forward=20slashes.=20As=20a=20feature,=20browsers=20accept?= =?UTF-8?q?=20typos=20such=20as=20http:google.com=20or=20http:/google.com?= =?UTF-8?q?=20and=20redirect=20to=20http://google.com.=20This=20can=20be?= =?UTF-8?q?=20used=20to=20leverage=20an=20open=20redirect=20attack=20even?= =?UTF-8?q?=20with=20the=20current=20filter.=20This=20commit=20fixes=20the?= =?UTF-8?q?=20open=20redirect=20vulnerability=20in=20the=20=5Fnext=20get?= =?UTF-8?q?=20parameter.=20Thanks=20to=20jnbrex=20for=20helping=20debug/wr?= =?UTF-8?q?ite=20the=20patch=20for=20this=20vulnerability.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gluon/tools.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 7bfa4b7f..98e6bc02 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1921,8 +1921,11 @@ class Auth(object): # Prevent an attacker from adding an arbitrary url after the # _next variable in the request. items = next.split('/') - if '//' in next and items[2] != current.request.env.http_host: - next = None + if next: + if next[0] != '/': + if '://' not in next or next.split('://')[1].split('/')[0] != current.request.env.http_host: + if ':' in next.split('/')[0]: + next = None return next def _get_user_id(self): From 0c4d254a9c3221c029be9bf2062c164de1cd06c4 Mon Sep 17 00:00:00 2001 From: Jason Bohrer Date: Fri, 1 Jul 2016 13:26:23 -0400 Subject: [PATCH 036/436] Changed tuple to list The comparison between parts[1:3] and ('', host) would return false because a list and a tuple were being compared. --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 9345f24c..998368fd 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1930,7 +1930,7 @@ class Auth(object): parts = next.split('/') if not ':' in parts[0]: return next - elif len(parts)>2 and parts[0].endswith(':') and parts[1:3]==('', host): + elif len(parts)>2 and parts[0].endswith(':') and parts[1:3]==['', host]: return next return None From 61c9b7991e02975ba79e25147e5dda472e0fcfa9 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Mon, 4 Jul 2016 18:33:34 +0200 Subject: [PATCH 037/436] py35 fix cacheOnDisk on win --- gluon/cache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/cache.py b/gluon/cache.py index 814b5bc1..8a4b0a85 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -40,7 +40,7 @@ try: except ImportError: have_settings = False -from gluon._compat import pickle, thread +from gluon._compat import pickle, thread, to_bytes, to_native try: import psutil @@ -304,13 +304,13 @@ class CacheOnDisk(CacheAbstract): Windows doesn't allow \ / : * ? "< > | in filenames. To go around this encode the keys with base32. """ - return base64.b32encode(key) + return to_native(base64.b32encode(to_bytes(key))) def key_filter_out_windows(key): """ We need to decode the keys so regex based removal works. """ - return base64.b32decode(key) + return to_native(base64.b32decode(to_bytes(key))) self.key_filter_in = key_filter_in_windows self.key_filter_out = key_filter_out_windows From c5bd7ca25d1c42ff797cf6ce71d52bf7955365c2 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 5 Jul 2016 08:35:42 -0500 Subject: [PATCH 038/436] look for dal in packages by default --- gluon/__init__.py | 4 ++-- gluon/contrib/hypermedia.py | 6 ++---- gluon/dal.py | 1 - gluon/packages/dal | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/gluon/__init__.py b/gluon/__init__.py index 9ff79614..c0e306f2 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -16,8 +16,8 @@ __all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE import os import sys try: - sys.path.append(os.path.join( - os.path.dirname(os.path.abspath(__file__)), "packages", "dal")) + sys.path.insert(1,os.path.join( + os.path.dirname(os.path.abspath(__file__)), "packages", "dal")) import pydal sys.modules['pydal'] = pydal except ImportError: diff --git a/gluon/contrib/hypermedia.py b/gluon/contrib/hypermedia.py index e6901f1a..03aec2ff 100644 --- a/gluon/contrib/hypermedia.py +++ b/gluon/contrib/hypermedia.py @@ -26,8 +26,7 @@ def api(): 'DELETE':{'query':None}, }, } - return Collection(db).process(request,response,policies) - + return Collection(db).process(request, response, policies) """ __all__ = ['Collection'] @@ -226,8 +225,7 @@ class Collection(object): id = row.id for name in ('slug','fullname','title','name'): if name in row: - href = URL(args=(tablename,id,IS_SLUG.urlify(row[name] or '')), - scheme=True) + href = URL(args=(tablename,id),scheme=True) break else: href = URL(args=(tablename,id),scheme=True) diff --git a/gluon/dal.py b/gluon/dal.py index b1414515..d73720cf 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -15,7 +15,6 @@ from pydal import Field from pydal.objects import Row, Rows, Table, Query, Set, Expression from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon - def _default_validators(db, field): """ Field type validation, using web2py's validators mechanism. diff --git a/gluon/packages/dal b/gluon/packages/dal index dba8fa82..dbcb240f 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit dba8fa826b58764f1e4f6190db924294f8436f30 +Subproject commit dbcb240fe5be53ddaf9b5e935caeb8b5718ad90c From 5e94c11e4075c069ca4f46bc2f9c55f413d4f62a Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 3 Jul 2016 20:18:18 +0200 Subject: [PATCH 039/436] Enabled py35 on appveyor --- appveyor.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 49fe13c3..80e86c5d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,11 @@ environment: matrix: - PYTHON: "C:/Python27" COVERAGE_PROCESS_START: gluon/tests/coverage.ini + PYTHON_ARCH: "64" + + - PYTHON: "C:/Python35" + COVERAGE_PROCESS_START: gluon/tests/coverage.ini + PYTHON_ARCH: "64" clone_depth: 50 @@ -16,10 +21,14 @@ install: - python get-pip.py - pip install codecov - git submodule update --init --recursive + - pip install pycrypto + # Check that we have the expected version and architecture for Python + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" test_script: - python web2py.py --run_system_tests --with_coverage after_test: - coverage combine - - codecov \ No newline at end of file + - codecov From 967b2e27a7fe7f2595677300b29178044ab215fb Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 5 Jul 2016 19:44:00 +0200 Subject: [PATCH 040/436] added global_settings.is_py2 --- gluon/_compat.py | 5 ----- gluon/cache.py | 4 ++-- gluon/html.py | 6 ++---- gluon/settings.py | 3 +++ 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index 44baefc3..1c3421c5 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -45,11 +45,6 @@ if PY2: unichr = unichr unicodeT = unicode - def implements_iterator(cls): - cls.next = cls.__next__ - del cls.__next__ - return cls - def implements_bool(cls): cls.__nonzero__ = cls.__bool__ del cls.__bool__ diff --git a/gluon/cache.py b/gluon/cache.py index 8a4b0a85..474910bb 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -40,7 +40,7 @@ try: except ImportError: have_settings = False -from gluon._compat import pickle, thread, to_bytes, to_native +from gluon._compat import pickle, thread, to_bytes, to_native, hashlib_md5 try: import psutil @@ -624,7 +624,7 @@ class Cache(object): cache_key.append(current.request.env.query_string) if lang_: cache_key.append(current.T.accepted_language) - cache_key = hashlib.md5('__'.join(cache_key)).hexdigest() + cache_key = hashlib_md5('__'.join(cache_key)).hexdigest() if prefix: cache_key = prefix + cache_key try: diff --git a/gluon/html.py b/gluon/html.py index 183bb4e0..11f9653e 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -21,7 +21,7 @@ import base64 from gluon import sanitizer, decoder import itertools from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, \ - to_native, to_unicode, _local_html_escape, basestring, urlencode + to_native, to_unicode, _local_html_escape, basestring, urlencode, implements_bool import marshal from gluon.storage import Storage @@ -671,7 +671,7 @@ def XML_pickle(data): return XML_unpickle, (marshal.dumps(str(data)),) copyreg.pickle(XML, XML_pickle, XML_unpickle) - +@implements_bool class DIV(XmlComponent): """ HTML helper, for easy generating and manipulating a DOM structure. @@ -824,8 +824,6 @@ class DIV(XmlComponent): """ return True - __nonzero__ = __bool__ - def _fixup(self): """ Handling of provided components. diff --git a/gluon/settings.py b/gluon/settings.py index 48fee6d4..5e2af7e0 100644 --- a/gluon/settings.py +++ b/gluon/settings.py @@ -10,6 +10,7 @@ import os import sys import platform from gluon.storage import Storage +from gluon._compat import PY2 global_settings = Storage() settings = global_settings # legacy compatibility @@ -40,3 +41,5 @@ global_settings.is_jython = \ global_settings.is_source = os.path.exists(os.path.join( global_settings.gluon_parent, 'web2py.py')) + +global_settings.is_py2 = PY2 From d067897cef064b33c0fa2ca457f794d6bd6b9f72 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 7 Jul 2016 01:47:59 -0500 Subject: [PATCH 041/436] json(indent=...) parameter --- gluon/globals.py | 4 ++-- gluon/packages/dal | 2 +- gluon/serializers.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 25727560..2cd7ddcf 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -644,10 +644,10 @@ class Response(Storage): 'attachment; filename="%s"' % download_filename.replace('"', '\"') return self.stream(stream, chunk_size=chunk_size, request=request) - def json(self, data, default=None): + def json(self, data, default=None, indent=None): if 'Content-Type' not in self.headers: self.headers['Content-Type'] = 'application/json' - return json(data, default=default or custom_json) + return json(data, default=default or custom_json, indent=indent) def xmlrpc(self, request, methods): from gluon.xmlrpc import handler diff --git a/gluon/packages/dal b/gluon/packages/dal index dbcb240f..0a700393 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit dbcb240fe5be53ddaf9b5e935caeb8b5718ad90c +Subproject commit 0a700393ab7d480000e2b489b4b1dc7e2a57fd06 diff --git a/gluon/serializers.py b/gluon/serializers.py index 559eb725..d53740d7 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -119,8 +119,8 @@ def xml(value, encoding='UTF-8', key='document', quote=True): return ('' % encoding) + str(xml_rec(value, key, quote)) -def json(value, default=custom_json): - value = json_parser.dumps(value, default=default) +def json(value, default=custom_json, indent=None): + value = json_parser.dumps(value, default=default, sort_keys=True, indent=indent) # replace JavaScript incompatible spacing # http://timelessrepo.com/json-isnt-a-javascript-subset # PY3 FIXME From bd47b7f80a5f720440b53ef9af66dbebde46977e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Sun, 10 Jul 2016 12:30:22 -0400 Subject: [PATCH 042/436] .att() -> .prop() for setting autocomplete off --- applications/welcome/static/js/web2py.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index f3d14b29..2bedd1c5 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -201,7 +201,7 @@ showsTime: true, timeFormat: '24' }); - $(this).attr('autocomplete', 'off'); + $(this).prop('autocomplete', 'off'); $(this).data('w2p_datetime', 1); $(this).trigger('click'); } @@ -218,7 +218,7 @@ showsTime: false }); $(this).data('w2p_date', 1); - $(this).attr('autocomplete', 'off'); + $(this).prop('autocomplete', 'off'); $(this).trigger('click'); } }); @@ -227,7 +227,7 @@ if (web2py.isUndefined(active)) { $(this).timeEntry({ spinnerImage: '' - }).attr('autocomplete', 'off'); + }).prop('autocomplete', 'off'); $(this).data('w2p_time', 1); } }); From 350dc4ed8b3dd08a49c39da1851ff4f7f5b531cd Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 11 Jul 2016 03:03:55 -0500 Subject: [PATCH 043/436] partially addressed scheduler in widget --- gluon/packages/dal | 2 +- gluon/widget.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 0a700393..2e3f2bac 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 0a700393ab7d480000e2b489b4b1dc7e2a57fd06 +Subproject commit 2e3f2bac3132f6c338c002b22f57f8435d6e2399 diff --git a/gluon/widget.py b/gluon/widget.py index 1bbdb830..8aeb9b4c 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -321,9 +321,6 @@ class web2pyDialog(object): def update_schedulers(self, start=False): applications_folder = os.path.join(self.options.folder, 'applications') apps = [] - ##FIXME - can't start scheduler in the correct dir from Tk - if self.options.folder: - return available_apps = [ arq for arq in os.listdir(applications_folder) if os.path.exists(os.path.join(applications_folder, arq, 'models', 'scheduler.py')) @@ -339,6 +336,7 @@ class web2pyDialog(object): # reset the menu self.schedmenu.delete(0, len(available_apps)) + for arq in available_apps: if arq not in self.scheduler_processes: item = lambda u = arq: self.try_start_scheduler(u) From 64df136e697616b55193f98119e7e314ea074925 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 11 Jul 2016 03:15:31 -0500 Subject: [PATCH 044/436] minor import fix --- gluon/sqlhtml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 6bf336b4..edadb7c2 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -27,7 +27,7 @@ from gluon.html import FORM, INPUT, LABEL, OPTION, SELECT, COL, COLGROUP from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE, SCRIPT from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY from pydal.base import DEFAULT -from pydal.objects import Table, Row, Expression, Field, Set +from pydal.objects import Table, Row, Expression, Field, Set, Rows from pydal.adapters.base import CALLABLETYPES from pydal.helpers.methods import smart_query, bar_encode, _repr_ref from pydal.helpers.classes import Reference, SQLCustomType From 4a3ccfdeab0670d42050a1b9709ec7355a13ada8 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 11 Jul 2016 03:20:38 -0500 Subject: [PATCH 045/436] fixed issue #1362, == in args --- gluon/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/rewrite.py b/gluon/rewrite.py index 1939695c..40fb9adb 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -81,7 +81,7 @@ def _router_default(): # pathological backtracking from nested patterns. # file_match = r'([-+=@$%\w]|(?<=[-+=@$%\w])[./])*$', # legal static subpath - args_match=r'([\w@ -]|(?<=[\w@ -])[.=])*$', # legal arg in args + args_match=r'([\w@ -=]|(?<=[\w@ -=])[.])*$', # legal arg in args ) return router From 584327553c48e70d699387ab2b0065610539385f Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 11 Jul 2016 03:30:48 -0500 Subject: [PATCH 046/436] fixed Link to Chrome extension that auto-opens error tickets #613 --- .../private/content/en/default/documentation/community.markmin | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/examples/private/content/en/default/documentation/community.markmin b/applications/examples/private/content/en/default/documentation/community.markmin index 43551da1..ac0af6b3 100644 --- a/applications/examples/private/content/en/default/documentation/community.markmin +++ b/applications/examples/private/content/en/default/documentation/community.markmin @@ -5,3 +5,4 @@ - Apostila em português [[Download http://dl.dropbox.com/u/830444/apostila_web2py_basico.pdf popup]] e [[Online http://web2pybrasil.appspot.com/init/plugin_wiki/page/curso-web2py-000 popup]] - [[Sitio en español http://www.web2py.com.ar popup]] - [[Documentación en español http://www.web2py.com.ar/examples/default/docs popup]] +- [[Chrome extension to display tickets https://github.com/agarden/web2py-devmode-chrome]] \ No newline at end of file From f9927f4f3d78c50fd98979a0fbe82a6b9f0f1cd3 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 15 Jul 2016 20:34:27 +0200 Subject: [PATCH 047/436] fix local import for contrib modules --- gluon/custom_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/custom_import.py b/gluon/custom_import.py index 8e848626..ef6c7ab2 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -62,6 +62,9 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): except: # there is no current.request (should never happen) base_importer = NATIVE_IMPORTER + if not(PY2) and level < 0: + level = 0 + # if not relative and not from applications: if hasattr(current, 'request') \ and level <= 0 \ @@ -104,9 +107,6 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): finally: if import_tb: import_tb = None - elif not(PY2) and level < 0: - # FIXME PY3 why level is < 0? - level = 0 return NATIVE_IMPORTER(name, globals, locals, fromlist, level) From 73399fdee62aca7107d917e34bd4133784c9f682 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 15 Jul 2016 20:35:54 +0200 Subject: [PATCH 048/436] fix py3 html --- gluon/globals.py | 4 ++-- gluon/sqlhtml.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 25727560..6d090dd2 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -453,9 +453,9 @@ class Response(Storage): for meta in iteritems((self.meta or {})): k, v = meta if isinstance(v, dict): - s += '\n' + s += '\n' else: - s += '\n' % (k, xmlescape(v)) + s += '\n' % (k, to_native(xmlescape(v))) self.write(s, escape=False) def include_files(self, extensions=None): diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 6bf336b4..c0354508 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -19,7 +19,7 @@ import urllib import re import os -from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT +from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native from gluon.http import HTTP, redirect from gluon.html import XmlComponent, truncate_string from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG @@ -1395,8 +1395,8 @@ class SQLFORM(FORM): self['hidden']['id'] = record[table._id.name] (begin, end) = self._xml() - self.custom.begin = XML("<%s %s>" % (self.tag, begin)) - self.custom.end = XML("%s" % (end, self.tag)) + self.custom.begin = XML("<%s %s>" % (self.tag, to_native(begin))) + self.custom.end = XML("%s" % (to_native(end), self.tag)) table = self.createform(xfields) self.components = [table] From 665e14186ee6b1b7cb7a68b51f8142c0c1711fca Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 17 Jul 2016 04:18:56 -0500 Subject: [PATCH 049/436] fixed issue with sessions in db on GAE --- applications/examples/static/css/stupid.css | 2 +- gluon/globals.py | 37 +++++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/applications/examples/static/css/stupid.css b/applications/examples/static/css/stupid.css index 35d6fa52..08f42938 100644 --- a/applications/examples/static/css/stupid.css +++ b/applications/examples/static/css/stupid.css @@ -317,7 +317,7 @@ a:not(.btn):not(.noeffect):after { .accordion>input {display:none} .accordion>input:checked ~ *:not(label) { max-height: 1000px !important; - overflow:visible !important; + overflow:hidden !important; -webkit-transition: max-height .3s ease-in; transition: max-height .3s ease-in; } diff --git a/gluon/globals.py b/gluon/globals.py index ae8b2ac2..b1a501e3 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1192,28 +1192,29 @@ class Session(Storage): def _try_store_in_file(self, request, response): try: - if (not response.session_id or self._forget - or self._unchanged(response)): + if (not response.session_id or + not response.session_filename or + self._forget + or self._unchanged(response)): # self.clear_session_cookies() - self.save_session_id_cookie() return False - if response.session_new or not response.session_file: - # Tests if the session sub-folder exists, if not, create it - session_folder = os.path.dirname(response.session_filename) - if not os.path.exists(session_folder): - os.mkdir(session_folder) - response.session_file = recfile.open(response.session_filename, 'wb') - portalocker.lock(response.session_file, portalocker.LOCK_EX) - response.session_locked = True - if response.session_file: - session_pickled = response.session_pickled or pickle.dumps(self, pickle.HIGHEST_PROTOCOL) - response.session_file.write(session_pickled) - response.session_file.truncate() + else: + if response.session_new or not response.session_file: + # Tests if the session sub-folder exists, if not, create it + session_folder = os.path.dirname(response.session_filename) + if not os.path.exists(session_folder): + os.mkdir(session_folder) + response.session_file = recfile.open(response.session_filename, 'wb') + portalocker.lock(response.session_file, portalocker.LOCK_EX) + response.session_locked = True + if response.session_file: + session_pickled = response.session_pickled or pickle.dumps(self, pickle.HIGHEST_PROTOCOL) + response.session_file.write(session_pickled) + response.session_file.truncate() + return True finally: self._close(response) - - self.save_session_id_cookie() - return True + self.save_session_id_cookie() def _unlock(self, response): if response and response.session_file and response.session_locked: From 7aa617a68f48fd338f9cfe3f94843ea6318d36e0 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 17 Jul 2016 15:12:04 -0500 Subject: [PATCH 050/436] fixed regex (test) --- applications/admin/private/hosts.deny | 1 + gluon/rewrite.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/admin/private/hosts.deny b/applications/admin/private/hosts.deny index e69de29b..ea4e5b60 100644 --- a/applications/admin/private/hosts.deny +++ b/applications/admin/private/hosts.deny @@ -0,0 +1 @@ +127.0.0.1 1 1468786268 diff --git a/gluon/rewrite.py b/gluon/rewrite.py index 40fb9adb..fc14557a 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -81,7 +81,7 @@ def _router_default(): # pathological backtracking from nested patterns. # file_match = r'([-+=@$%\w]|(?<=[-+=@$%\w])[./])*$', # legal static subpath - args_match=r'([\w@ -=]|(?<=[\w@ -=])[.])*$', # legal arg in args + args_match=r'([\w@ =-]|(?<=[\w@ -])[.])*$', ) return router From a0720ce01e94612a359d37aeceaa4b4951ccda06 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 18 Jul 2016 21:36:56 +0200 Subject: [PATCH 051/436] feature parity (cron recurrence) with scheduler --- gluon/contrib/redis_scheduler.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/gluon/contrib/redis_scheduler.py b/gluon/contrib/redis_scheduler.py index f7380aec..6a3f00fc 100644 --- a/gluon/contrib/redis_scheduler.py +++ b/gluon/contrib/redis_scheduler.py @@ -8,6 +8,7 @@ Scheduler with redis backend --------------------------------- """ +from __future__ import print_function import os import time @@ -475,7 +476,10 @@ class RScheduler(Scheduler): logger.info('nothing to do') return None times_run = task.times_run + 1 - if not task.prevent_drift: + if task.cronline: + cron_recur = CronParser(task.cronline, now.replace(second=0)) + next_run_time = cron_recur.get_next() + elif not task.prevent_drift: next_run_time = task.last_run_time + datetime.timedelta( seconds=task.period ) @@ -711,13 +715,20 @@ class RScheduler(Scheduler): tuuid = 'uuid' in kwargs and kwargs.pop('uuid') or web2py_uuid() tname = 'task_name' in kwargs and kwargs.pop('task_name') or function immediate = 'immediate' in kwargs and kwargs.pop('immediate') or None - rtn = self.db.scheduler_task.validate_and_insert( - function_name=function, + cronline = kwargs.get('cronline') + kwargs.update(function_name=function, task_name=tname, args=targs, vars=tvars, - uuid=tuuid, - **kwargs) + uuid=tuuid) + if cronline: + try: + start_time = kwargs.get('start_time', self.now) + next_run_time = CronParser(cronline, start_time).get_next() + kwargs.update(start_time=start_time, next_run_time=next_run_time) + except: + pass + rtn = self.db.scheduler_task.validate_and_insert(**kwargs) if not rtn.errors: rtn.uuid = tuuid if immediate: From cae10a68c01b3702cf4b5a4f974b8e9b618485a1 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 18 Jul 2016 23:28:18 +0200 Subject: [PATCH 052/436] fixed most of py3 warnings, output is much cleaner this way --- appveyor.yml | 1 + gluon/tests/test_recfile.py | 13 ++-- gluon/tests/test_router.py | 126 +++++++++++---------------------- gluon/tests/test_storage.py | 4 +- gluon/tests/test_tools.py | 27 ++++--- gluon/tests/test_validators.py | 21 +++--- gluon/tools.py | 3 +- gluon/utils.py | 6 +- 8 files changed, 88 insertions(+), 113 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 80e86c5d..12635602 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,6 +22,7 @@ install: - pip install codecov - git submodule update --init --recursive - pip install pycrypto + - pip install pypiwin32 # Check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" diff --git a/gluon/tests/test_recfile.py b/gluon/tests/test_recfile.py index 07b9b886..a2d711d3 100644 --- a/gluon/tests/test_recfile.py +++ b/gluon/tests/test_recfile.py @@ -29,7 +29,8 @@ class TestRecfile(unittest.TestCase): filename = os.path.join('tests', str(uuid.uuid4()) + '.test') with recfile.open(filename, "w") as g: g.write(teststring) - self.assertEqual(recfile.open(filename, "r").read(), teststring) + with recfile.open(filename, "r") as f: + self.assertEqual(f.read(), teststring) is_there = recfile.exists(filename) self.assertTrue(is_there) recfile.remove(filename) @@ -40,7 +41,8 @@ class TestRecfile(unittest.TestCase): filename = str(uuid.uuid4()) + '.test' with recfile.open(filename, "w", path='tests') as g: g.write(teststring) - self.assertEqual(recfile.open(filename, "r", path='tests').read(), teststring) + with recfile.open(filename, "r", path='tests') as f: + self.assertEqual(f.read(), teststring) is_there = recfile.exists(filename, path='tests') self.assertTrue(is_there) recfile.remove(filename, path='tests') @@ -51,7 +53,8 @@ class TestRecfile(unittest.TestCase): filename = os.path.join('tests', str(uuid.uuid4()), str(uuid.uuid4()) + '.test') with recfile.open(filename, "w") as g: g.write(teststring) - self.assertEqual(recfile.open(filename, "r").read(), teststring) + with recfile.open(filename, "r") as f: + self.assertEqual(f.read(), teststring) is_there = recfile.exists(filename) self.assertTrue(is_there) recfile.remove(filename) @@ -63,7 +66,9 @@ class TestRecfile(unittest.TestCase): with open(filename, 'w') as g: g.write('this file exists') self.assertTrue(recfile.exists(filename)) - self.assertTrue(hasattr(recfile.open(filename, "r"), 'read')) + r = recfile.open(filename, "r") + self.assertTrue(hasattr(r, 'read')) + r.close() recfile.remove(filename, path='tests') self.assertFalse(recfile.exists(filename)) self.assertRaises(IOError, recfile.remove, filename) diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index 0cf51a83..edec86cd 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -21,7 +21,7 @@ from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from gluon._compat import to_bytes, to_native +from gluon._compat import to_bytes, to_native, PY2 logger = None oldcwd = None @@ -93,6 +93,11 @@ def tearDownModule(): class TestRouter(unittest.TestCase): """ Tests the routers logic from gluon.rewrite """ + def myassertRaisesRegex(self, *args, **kwargs): + if PY2: + return getattr(self, 'assertRaisesRegexp')(*args, **kwargs) + return getattr(self, 'assertRaisesRegex')(*args, **kwargs) + def test_router_syntax(self): """ Test router syntax error """ level = logger.getEffectiveLevel() @@ -102,16 +107,13 @@ class TestRouter(unittest.TestCase): SyntaxError, load, rdict=dict(BASE=dict(badkey="value"))) self.assertRaises(SyntaxError, load, rdict=dict( BASE=dict(), app=dict(default_application="name"))) - try: - # 2.7+ only - self.assertRaisesRegexp(SyntaxError, "invalid syntax", - load, data='x:y') - self.assertRaisesRegexp(SyntaxError, "unknown key", - load, rdict=dict(BASE=dict(badkey="value"))) - self.assertRaisesRegexp(SyntaxError, "BASE-only key", - load, rdict=dict(BASE=dict(), app=dict(default_application="name"))) - except AttributeError: - pass + + self.myassertRaisesRegex(SyntaxError, "invalid syntax", + load, data='x:y') + self.myassertRaisesRegex(SyntaxError, "unknown key", + load, rdict=dict(BASE=dict(badkey="value"))) + self.myassertRaisesRegex(SyntaxError, "BASE-only key", + load, rdict=dict(BASE=dict(), app=dict(default_application="name"))) logger.setLevel(level) def test_router_null(self): @@ -135,11 +137,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(filter_url('http://domain.com/welcome/static/path/to/static').replace('/', os.sep), norm_root("%s/applications/welcome/static/path/to/static" % root)) self.assertRaises(HTTP, filter_url, 'http://domain.com/welcome/static/bad/path/to/st~tic') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*invalid static file", filter_url, 'http://domain.com/welcome/static/bad/path/to/st~tic') - except AttributeError: - pass + self.myassertRaisesRegex(HTTP, "400.*invalid static file", filter_url, 'http://domain.com/welcome/static/bad/path/to/st~tic') # outgoing self.assertEqual( filter_url('http://domain.com/init/default/index', out=True), '/') @@ -287,15 +285,10 @@ class TestRouter(unittest.TestCase): # incoming self.assertRaises(HTTP, filter_url, 'http://domain.com') self.assertRaises(HTTP, filter_url, 'http://domain.com/appadmin') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*invalid application", - filter_url, 'http://domain.com') - self.assertRaisesRegexp(HTTP, "400.*invalid application", + self.myassertRaisesRegex(HTTP, "400.*invalid application", + filter_url, 'http://domain.com') + self.myassertRaisesRegex(HTTP, "400.*invalid application", filter_url, 'http://domain.com/appadmin') - except AttributeError: - pass - routers = dict( BASE=dict(default_application='welcome', applications=None), ) @@ -324,12 +317,7 @@ class TestRouter(unittest.TestCase): self.assertEqual( filter_url('http://domain.com/'), '/welcome/default/index') self.assertRaises(HTTP, filter_url, 'http://domain.com/appadmin') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') - except AttributeError: - pass - + self.myassertRaisesRegex(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') routers = dict( BASE=dict(default_application='welcome', applications=None), welcome=dict(controllers=None), @@ -359,11 +347,7 @@ class TestRouter(unittest.TestCase): self.assertEqual( filter_url('http://domain.com/'), '/welcome/default/index') self.assertRaises(HTTP, filter_url, 'http://domain.com/appadmin') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') - except AttributeError: - pass + self.myassertRaisesRegex(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') routers = dict( BASE=dict(default_application='welcome', applications=None), @@ -391,13 +375,9 @@ class TestRouter(unittest.TestCase): # incoming self.assertRaises(HTTP, filter_url, 'http://domain.com') self.assertRaises(HTTP, filter_url, 'http://domain.com/appadmin') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*invalid controller", - filter_url, 'http://domain.com') - self.assertRaisesRegexp(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') - except AttributeError: - pass + self.myassertRaisesRegex(HTTP, "400.*invalid controller", + filter_url, 'http://domain.com') + self.myassertRaisesRegex(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') routers = dict( BASE=dict(default_application='welcome', applications=None), @@ -425,13 +405,9 @@ class TestRouter(unittest.TestCase): # incoming self.assertRaises(HTTP, filter_url, 'http://domain.com') self.assertRaises(HTTP, filter_url, 'http://domain.com/appadmin') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, "400.*invalid function", - filter_url, 'http://domain.com') - self.assertRaisesRegexp(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') - except AttributeError: - pass + self.myassertRaisesRegex(HTTP, "400.*invalid function", + filter_url, 'http://domain.com') + self.myassertRaisesRegex(HTTP, "400.*unknown application: 'appadmin'", filter_url, 'http://domain.com/appadmin') def test_router_app(self): """ Tests the doctest router app resolution""" @@ -474,21 +450,13 @@ class TestRouter(unittest.TestCase): filter_url('http://domain.com/goodapp', app=True), 'goodapp') self.assertRaises( HTTP, filter_url, 'http://domain.com/bad!app', app=True) - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, '400.*invalid application', + self.myassertRaisesRegex(HTTP, '400.*invalid application', filter_url, 'http://domain.com/bad!app') - except AttributeError: - pass routers['BASE']['domains']['domain3.com'] = 'app3' self.assertRaises(SyntaxError, load, rdict=routers) - try: - # 2.7+ only - self.assertRaisesRegexp( + self.myassertRaisesRegex( SyntaxError, "unknown.*app3", load, rdict=routers) - except AttributeError: - pass def test_router_domains_fs(self): ''' @@ -632,11 +600,8 @@ class TestRouter(unittest.TestCase): self.assertEqual(filter_url('http://domain2.com/app3/c3a/f3', domain=('app2b', None), out=True), "/app3/c3a/f3") self.assertRaises(SyntaxError, filter_url, 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) - try: - # 2.7+ only - self.assertRaisesRegexp(SyntaxError, 'cross-domain conflict', filter_url, 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) - except AttributeError: - pass + self.myassertRaisesRegex(SyntaxError, 'cross-domain conflict', filter_url, + 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) self.assertEqual(filter_url('http://domain1.com/app1/c1/f1', domain=( 'app2b', None), host='domain2.com', out=True), "/app1") @@ -752,11 +717,8 @@ class TestRouter(unittest.TestCase): self.assertRaises(SyntaxError, filter_url, 'http://domain2.com/app3/c3a/f3', domain=('app2b', None), out=True) self.assertRaises(SyntaxError, filter_url, 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) - try: - # 2.7+ only - self.assertRaisesRegexp(SyntaxError, 'cross-domain conflict', filter_url, 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) - except AttributeError: - pass + self.myassertRaisesRegex(SyntaxError, 'cross-domain conflict', filter_url, + 'http://domain1.com/app1/c1/f1', domain=('app2b', None), out=True) self.assertEqual(filter_url('http://domain1.com/app1/c1/f1', domain=( 'app2b', None), host='domain2.com', out=True), "/app1") @@ -789,28 +751,20 @@ class TestRouter(unittest.TestCase): HTTP, filter_url, 'http://domain.com/ctl/fcn.bad!ext') self.assertRaises( HTTP, filter_url, 'http://domain.com/ctl/fcn/bad!arg') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, '400.*invalid controller', filter_url, 'http://domain.com/init/bad!ctl') - self.assertRaisesRegexp(HTTP, '400.*invalid function', filter_url, - 'http://domain.com/init/ctlr/bad!fcn') - self.assertRaisesRegexp(HTTP, '400.*invalid extension', filter_url, - 'http://domain.com/init/ctlr/fcn.bad!ext') - self.assertRaisesRegexp(HTTP, '400.*invalid arg', filter_url, - 'http://domain.com/appc/init/fcn/bad!arg') - except AttributeError: - pass + self.myassertRaisesRegex(HTTP, '400.*invalid controller', filter_url, 'http://domain.com/init/bad!ctl') + self.myassertRaisesRegex(HTTP, '400.*invalid function', filter_url, + 'http://domain.com/init/ctlr/bad!fcn') + self.myassertRaisesRegex(HTTP, '400.*invalid extension', filter_url, + 'http://domain.com/init/ctlr/fcn.bad!ext') + self.myassertRaisesRegex(HTTP, '400.*invalid arg', filter_url, + 'http://domain.com/appc/init/fcn/bad!arg') self.assertEqual(filter_url('http://domain.com/welcome/default/fcn_1'), "/welcome/default/fcn_1") self.assertRaises( HTTP, filter_url, 'http://domain.com/welcome/default/fcn-1') - try: - # 2.7+ only - self.assertRaisesRegexp(HTTP, '400.*invalid function', filter_url, + self.myassertRaisesRegex(HTTP, '400.*invalid function', filter_url, 'http://domain.com/welcome/default/fcn-1') - except AttributeError: - pass def test_router_out(self): ''' @@ -1047,7 +1001,7 @@ class TestRouter(unittest.TestCase): from gluon.globals import current current.response.static_version = None - + self.assertEqual(str(URL(a='init', c='default', f='a_b')), "/a_b") self.assertEqual(str(URL(a='app1', c='default', f='a_b')), "/app1/a-b") self.assertEqual(str(URL(a='app2', c='default', f='a_b')), "/app2/a_b") diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 9156ba6a..48adce2e 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -67,8 +67,8 @@ class TestStorage(unittest.TestCase): #self.assertRaises(KeyError, lambda x: s[x], 'd') # old Storage s.a = 1 s['a'] = None - self.assertEquals(s.a, None) - self.assertEquals(s['a'], None) + self.assertEqual(s.a, None) + self.assertEqual(s['a'], None) self.assertTrue('a' in s) def test_pickling(self): diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index ed1b6483..fa963144 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -22,6 +22,7 @@ from gluon.dal import DAL, Field from pydal.objects import Table from gluon import tools from gluon.tools import Auth, Mail, Recaptcha, Recaptcha2, prettydate, Expose +from gluon._compat import PY2 from gluon.globals import Request, Response, Session from gluon.storage import Storage from gluon.languages import translator @@ -232,7 +233,7 @@ class TestAuthJWT(unittest.TestCase): from gluon.tools import AuthJWT from gluon import current - + self.request = Request(env={}) self.request.application = 'a' self.request.controller = 'c' @@ -263,7 +264,7 @@ class TestAuthJWT(unittest.TestCase): self.token = self.jwtauth.jwt_token_manager() self.assertIsNotNone(self.token) - + def test_allows_jwt(self): import gluon.serializers self.request.vars.update(self.user_data) @@ -277,7 +278,7 @@ class TestAuthJWT(unittest.TestCase): def optional_auth(): self.assertEqual(self.user_data['username'], self.auth.user.username) optional_auth() - + @unittest.skipIf(IS_IMAP, "TODO: Imap raises 'Connection refused'") # class TestAuth(unittest.TestCase): # @@ -498,7 +499,15 @@ class TestAuthJWT(unittest.TestCase): # # impersonate_form = auth.impersonate(user_id=omer_id) # # self.assertTrue(auth.is_impersonating()) # # self.assertEqual(impersonate_form, 'test') + + class TestAuth(unittest.TestCase): + + def myassertRaisesRegex(self, *args, **kwargs): + if PY2: + return getattr(self, 'assertRaisesRegexp')(*args, **kwargs) + return getattr(self, 'assertRaisesRegex')(*args, **kwargs) + def setUp(self): self.request = Request(env={}) self.request.application = 'a' @@ -550,9 +559,9 @@ class TestAuth(unittest.TestCase): self.assertTrue(b'name="_formkey"' in html_form) for f in ['logout', 'verify_email', 'reset_password', 'change_password', 'profile', 'groups']: - self.assertRaisesRegexp(HTTP, "303*", getattr(self.auth, f)) + self.myassertRaisesRegex(HTTP, "303*", getattr(self.auth, f)) - self.assertRaisesRegexp(HTTP, "401*", self.auth.impersonate) + self.myassertRaisesRegex(HTTP, "401*", self.auth.impersonate) try: for t in ['t0_archive', 't0', 'auth_cas', 'auth_event', @@ -783,13 +792,13 @@ class TestAuth(unittest.TestCase): self.auth.login_user(self.db(self.db.auth_user.username == 'omer').select().first()) # bypass login_bare() # self.assertTrue(self.auth.is_logged_in()) # For developing test # self.assertFalse(self.auth.is_impersonating()) # For developing test - self.assertRaisesRegexp(HTTP, "403*", self.auth.impersonate, bart_id) + self.myassertRaisesRegex(HTTP, "403*", self.auth.impersonate, bart_id) self.auth.logout_bare() # Try impersonate a non existing user self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() # self.assertTrue(self.auth.is_logged_in()) # For developing test # self.assertFalse(self.auth.is_impersonating()) # For developing test - self.assertRaisesRegexp(HTTP, "401*", self.auth.impersonate, 1000) # user with id 1000 shouldn't exist + self.myassertRaisesRegex(HTTP, "401*", self.auth.impersonate, 1000) # user with id 1000 shouldn't exist # Try impersonate user with id = 0 or '0' when bart impersonating omer self.auth.impersonate(user_id=omer_id) self.assertTrue(self.auth.is_impersonating()) @@ -804,12 +813,12 @@ class TestAuth(unittest.TestCase): def test_not_authorized(self): self.current.request.ajax = 'facke_ajax_request' - self.assertRaisesRegexp(HTTP, "403*", self.auth.not_authorized) + self.myassertRaisesRegex(HTTP, "403*", self.auth.not_authorized) self.current.request.ajax = None self.assertEqual(self.auth.not_authorized(), self.auth.messages.access_denied) def test_allows_jwt(self): - self.assertRaisesRegexp(HTTP, "400*", self.auth.allows_jwt) + self.myassertRaisesRegex(HTTP, "400*", self.auth.allows_jwt) # TODO: def test_requires(self): # TODO: def test_requires_login(self): diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index e7a560bb..19a18e16 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- """Unit tests for http.py """ @@ -17,6 +17,11 @@ from gluon._compat import PY2, to_bytes class TestValidators(unittest.TestCase): + def myassertRegex(self, *args, **kwargs): + if PY2: + return getattr(self, 'assertRegexpMatches')(*args, **kwargs) + return getattr(self, 'assertRegex')(*args, **kwargs) + def test_MISC(self): """ Test miscelaneous utility functions and some general behavior guarantees """ from gluon.validators import translate, options_sorter, Validator, UTC @@ -791,20 +796,20 @@ class TestValidators(unittest.TestCase): def test_CRYPT(self): rtn = str(CRYPT(digest_alg='md5', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^md5\$.{16}\$.{32}$') + self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$') rtn = str(CRYPT(digest_alg='sha1', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^sha1\$.{16}\$.{40}$') + self.myassertRegex(rtn, r'^sha1\$.{16}\$.{40}$') rtn = str(CRYPT(digest_alg='sha256', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^sha256\$.{16}\$.{64}$') + self.myassertRegex(rtn, r'^sha256\$.{16}\$.{64}$') rtn = str(CRYPT(digest_alg='sha384', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^sha384\$.{16}\$.{96}$') + self.myassertRegex(rtn, r'^sha384\$.{16}\$.{96}$') rtn = str(CRYPT(digest_alg='sha512', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^sha512\$.{16}\$.{128}$') + self.myassertRegex(rtn, r'^sha512\$.{16}\$.{128}$') alg = 'pbkdf2(1000,20,sha512)' rtn = str(CRYPT(digest_alg=alg, salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^pbkdf2\(1000,20,sha512\)\$.{16}\$.{40}$') + self.myassertRegex(rtn, r'^pbkdf2\(1000,20,sha512\)\$.{16}\$.{40}$') rtn = str(CRYPT(digest_alg='md5', key='mykey', salt=True)('test')[0]) - self.assertRegexpMatches(rtn, r'^md5\$.{16}\$.{32}$') + self.myassertRegex(rtn, r'^md5\$.{16}\$.{32}$') a = str(CRYPT(digest_alg='sha1', salt=False)('test')[0]) self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a) self.assertEqual(CRYPT(digest_alg='sha1', salt=False)('test')[0], a[6:]) diff --git a/gluon/tools.py b/gluon/tools.py index 998368fd..d4ef18e6 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -6270,7 +6270,8 @@ class Expose(object): self.filenames = [f[dirname_len:] for f in allowed if not os.path.isdir(f)] if 'README' in self.filenames: - readme = open(os.path.join(filename, 'README')).read() + with open(os.path.join(filename, 'README')) as f: + readme = f.read() self.paragraph = MARKMIN(readme) else: self.paragraph = None diff --git a/gluon/utils.py b/gluon/utils.py index 540f4e1f..fc468a57 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -40,7 +40,7 @@ import hmac if hasattr(hashlib, "pbkdf2_hmac"): def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): hashfunc = hashfunc or sha1 - hmac = hashlib.pbkdf2_hmac(hashfunc().name, to_bytes(data), + hmac = hashlib.pbkdf2_hmac(hashfunc().name, to_bytes(data), to_bytes(salt), iterations, keylen) return binascii.hexlify(hmac) HAVE_PBKDF2 = True @@ -171,7 +171,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): key = pad(encryption_key)[:32] cipher, IV = AES_new(key) encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) - signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data).hexdigest()) + signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest()) return signature + b':' + encrypted_data @@ -184,7 +184,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None): hash_key = sha1(encryption_key).hexdigest() signature, encrypted_data = data.split(':', 1) encrypted_data = to_bytes(encrypted_data) - actual_signature = hmac.new(to_bytes(hash_key), encrypted_data).hexdigest() + actual_signature = hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest() if not compare(signature, actual_signature): return None key = pad(encryption_key)[:32] From ae6fdb21c9f4e1dd08d0032ed820ac5b4e4532da Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 19 Jul 2016 22:21:35 +0200 Subject: [PATCH 053/436] this crashes win and 3.5 --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 12635602..80e86c5d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,6 @@ install: - pip install codecov - git submodule update --init --recursive - pip install pycrypto - - pip install pypiwin32 # Check that we have the expected version and architecture for Python - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" From fe45dff9e417b8f1f39b9ac9b667a1df3cdf4467 Mon Sep 17 00:00:00 2001 From: Van Woods Date: Tue, 26 Jul 2016 18:16:10 -0700 Subject: [PATCH 054/436] minor grammatical fix --- applications/admin/views/wizard/step.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/admin/views/wizard/step.html b/applications/admin/views/wizard/step.html index 39513c6b..48abd6d5 100644 --- a/applications/admin/views/wizard/step.html +++ b/applications/admin/views/wizard/step.html @@ -64,9 +64,9 @@ {{if request.function!='step6':}}
{{=form.custom.begin}} - {{ for fieldname in form.table.fields: }} + {{ for fieldname in form.table.fields: }} {{if fieldname is not 'id':}} - + {{=form.custom.widget[fieldname]}} {{if fieldname=='layout_theme':}} @@ -129,7 +129,7 @@
  • You can create an app with a name that already exists.
  • If you do not have an email server set email server to "logging".
  • If you want to use Janrain Engage for login: 1) Sign up for a Janrain Engage account; 2) Register you hostname, domain, and obtain an api key; 3) Set Login Config above to "domain:api_key".
  • -
  • ATTENTION: you can use the wizard to download plugins BUT we cannot guarantee the stability or backward compatibility of plugins. Moreover plugins may conflict with each other. Anyway, we do recommend installing plugin "wiki" with adds CMS like capabilities to your app.
  • +
  • ATTENTION: you can use the wizard to download plugins BUT we cannot guarantee the stability or backward compatibility of plugins. Moreover plugins may conflict with each other. Anyway, we do recommend installing plugin "wiki" which adds CMS-like capabilities to your app.
  • {{elif request.function=='step2':}}
      From 3bc6769e287ef282fe6a3a3be6b8d83bb3eb3104 Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 28 Jul 2016 21:36:43 +0200 Subject: [PATCH 055/436] fixes #1406 --- gluon/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gluon/main.py b/gluon/main.py index 81f04b90..971c48a7 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -454,7 +454,8 @@ def wsgibase(environ, responder): # ################################################## # on success, try store session in database # ################################################## - session._try_store_in_db(request, response) + if not env.web2py_disable_session: + session._try_store_in_db(request, response) # ################################################## # on success, commit database @@ -471,8 +472,8 @@ def wsgibase(environ, responder): # if session not in db try store session on filesystem # this must be done after trying to commit database! # ################################################## - - session._try_store_in_cookie_or_file(request, response) + if not env.web2py_disable_session: + session._try_store_in_cookie_or_file(request, response) # Set header so client can distinguish component requests. if request.cid: From a6b96ba85b01a0b03ef0d6cd56c815e539de87a5 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 29 Jul 2016 07:12:29 -0500 Subject: [PATCH 056/436] minor change to fabfile --- applications/admin/private/hosts.deny | 1 - fabfile.py | 2 +- gluon/packages/dal | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/admin/private/hosts.deny b/applications/admin/private/hosts.deny index ea4e5b60..e69de29b 100644 --- a/applications/admin/private/hosts.deny +++ b/applications/admin/private/hosts.deny @@ -1 +0,0 @@ -127.0.0.1 1 1468786268 diff --git a/fabfile.py b/fabfile.py index 6e6fcdb4..b7bd41fc 100644 --- a/fabfile.py +++ b/fabfile.py @@ -124,7 +124,7 @@ def deploy(appname=None, all=False): if all=='all' or not backup: local('zip -r _update.zip * -x *~ -x .* -x \#* -x *.bak -x *.bak2') else: - local('zip -r _update.zip */*.py views/*.html views/*/*.html static/*') + local('zip -r _update.zip */*.py */*/*.py views/*.html views/*/*.html static/*') put('_update.zip','/tmp/_update.zip') try: diff --git a/gluon/packages/dal b/gluon/packages/dal index 2e3f2bac..9a5332e4 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 2e3f2bac3132f6c338c002b22f57f8435d6e2399 +Subproject commit 9a5332e45bef6516bf159b69e2cd0b355ec9a6ca From 6be3977fa76596b7235310aa4b26c424630ec69a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 29 Jul 2016 08:50:28 -0500 Subject: [PATCH 057/436] fixed issue #1409, py2 vs py3 --- gluon/sqlhtml.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 701b9872..a0f19d64 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -54,17 +54,27 @@ REGEX_ALIAS_MATCH = re.compile('^(.*) AS (.*)$') def add_class(a, b): return a + ' ' + b if a else b +def count_expected_args(f): + if hasattr(f,'func_code'): + # python 2 + n = f.func_code.co_argcount - len(f.func_defaults or []) + if getattr(f, 'im_self', None): + n -= 1 + elif hasattr(f, '__code__'): + # python 3 + n = f.__code__.co_argcount - len(f.__defaults__ or []) + if getattr(f, '__self__', None): + n -= 1 + else: + # doh! + n = 1 + return n def represent(field, value, record): f = field.represent if not callable(f): return str(value) - if hasattr(f,'func_code'): - n = f.__code__.co_argcount - len(f.__defaults__ or []) - if getattr(f, 'im_self', None): - n -= 1 - else: - n = 1 + n = count_expected_args(f) if n == 1: return f(value) elif n == 2: From 4519985dc255145e5fd7d827851ea8c2f615f563 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 29 Jul 2016 08:53:56 -0500 Subject: [PATCH 058/436] fixed indentation in webclient, issue #1396 --- gluon/contrib/webclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index 54444ec0..c5e7f3c7 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -154,7 +154,7 @@ class WebClient(object): if 'set-cookie' in self.headers: for item in self.headers['set-cookie'].split(','): key, value = item[:item.find(';')].split('=') - self.cookies[key.strip()] = value.strip() + self.cookies[key.strip()] = value.strip() # check is a new session id has been issued, symptom of broken session if self.session_regex is not None: From 35eaba1096f9d7a606cda92824e57f9791be183b Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 03:39:22 -0500 Subject: [PATCH 059/436] removed duplicated code, using pydal's _compat.py --- gluon/_compat.py | 172 -------------------------- gluon/admin.py | 2 +- gluon/cache.py | 2 +- gluon/compileapp.py | 2 +- gluon/contenttype.py | 2 +- gluon/contrib/appconfig.py | 2 +- gluon/contrib/autolinks.py | 2 +- gluon/contrib/markmin/markmin2html.py | 2 +- gluon/contrib/shell.py | 2 +- gluon/contrib/webclient.py | 2 +- gluon/custom_import.py | 2 +- gluon/debug.py | 2 +- gluon/fileutils.py | 2 +- gluon/globals.py | 4 +- gluon/highlight.py | 13 +- gluon/html.py | 6 +- gluon/http.py | 2 +- gluon/languages.py | 7 +- gluon/local_html_escape.py | 27 ++++ gluon/main.py | 2 +- gluon/packages/dal | 2 +- gluon/recfile.py | 2 +- gluon/restricted.py | 2 +- gluon/rewrite.py | 2 +- gluon/rocket.py | 2 +- gluon/scheduler.py | 2 +- gluon/serializers.py | 2 +- gluon/settings.py | 2 +- gluon/shell.py | 2 +- gluon/sqlhtml.py | 2 +- gluon/storage.py | 2 +- gluon/streamer.py | 2 +- gluon/template.py | 2 +- gluon/tests/test_contenttype.py | 2 +- gluon/tests/test_contribs.py | 2 +- gluon/tests/test_dal.py | 2 +- gluon/tests/test_globals.py | 2 +- gluon/tests/test_html.py | 2 +- gluon/tests/test_languages.py | 2 +- gluon/tests/test_router.py | 2 +- gluon/tests/test_routes.py | 2 +- gluon/tests/test_storage.py | 2 +- gluon/tests/test_template.py | 2 +- gluon/tests/test_tools.py | 2 +- gluon/tests/test_validators.py | 2 +- gluon/tests/test_web.py | 2 +- gluon/tools.py | 2 +- gluon/utf8.py | 2 +- gluon/utils.py | 2 +- gluon/validators.py | 2 +- gluon/widget.py | 2 +- 51 files changed, 87 insertions(+), 232 deletions(-) delete mode 100644 gluon/_compat.py create mode 100644 gluon/local_html_escape.py diff --git a/gluon/_compat.py b/gluon/_compat.py deleted file mode 100644 index 1c3421c5..00000000 --- a/gluon/_compat.py +++ /dev/null @@ -1,172 +0,0 @@ -import sys -import hashlib -import os - -PY2 = sys.version_info[0] == 2 - -_identity = lambda x: x - -if PY2: - import cPickle as pickle - from cStringIO import StringIO - import copy_reg as copyreg - from HTMLParser import HTMLParser - import urlparse - from htmlentitydefs import entitydefs, name2codepoint - import __builtin__ as builtin - import thread - import Cookie - import urllib2 - import Queue - import ConfigParser as configparser - from email.MIMEBase import MIMEBase - from email.Header import Header - from email import Encoders, Charset - from email.MIMEMultipart import MIMEMultipart - from email.MIMEText import MIMEText - from email.Charset import add_charset, QP as charset_QP - from urllib import FancyURLopener, urlencode, urlopen - from urllib import quote as urllib_quote, unquote as urllib_unquote - from string import maketrans - from types import ClassType - import cgi - import cookielib - reduce = reduce - hashlib_md5 = hashlib.md5 - iterkeys = lambda d: d.iterkeys() - itervalues = lambda d: d.itervalues() - iteritems = lambda d: d.iteritems() - integer_types = (int, long) - string_types = (str, unicode) - text_type = unicode - basestring = basestring - xrange = xrange - long = long - unichr = unichr - unicodeT = unicode - - def implements_bool(cls): - cls.__nonzero__ = cls.__bool__ - del cls.__bool__ - return cls - - def to_bytes(obj, charset='utf-8', errors='strict'): - if obj is None: - return None - if isinstance(obj, (bytes, bytearray, buffer)): - return bytes(obj) - if isinstance(obj, unicode): - return obj.encode(charset, errors) - raise TypeError('Expected bytes') - - def to_native(obj, charset='utf8', errors='strict'): - if obj is None or isinstance(obj, str): - return obj - return obj.encode(charset, errors) - - def _local_html_escape(data, quote=False): - s = cgi.escape(data, quote) - return s.replace("'", "'") if quote else s - -else: - import pickle - from io import StringIO - import copyreg - from functools import reduce - from html.parser import HTMLParser - from http import cookies as Cookie - from urllib import parse as urlparse - from urllib import request as urllib2 - from html.entities import entitydefs, name2codepoint - import builtins as builtin - import _thread as thread - import configparser - import queue as Queue - from email.mime.base import MIMEBase - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText - from email import encoders as Encoders - from email.header import Header - from email.charset import Charset, add_charset, QP as charset_QP - from urllib.request import FancyURLopener, urlopen - from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode - from http import cookiejar as cookielib - import html - hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) - iterkeys = lambda d: iter(d.keys()) - itervalues = lambda d: iter(d.values()) - iteritems = lambda d: iter(d.items()) - integer_types = (int,) - string_types = (str,) - text_type = str - basestring = str - xrange = range - long = int - unichr = chr - unicodeT = str - maketrans = str.maketrans - ClassType = type - - implements_iterator = _identity - implements_bool = _identity - - def to_bytes(obj, charset='utf-8', errors='strict'): - if obj is None: - return None - if isinstance(obj, (bytes, bytearray, memoryview)): - return bytes(obj) - if isinstance(obj, str): - return obj.encode(charset, errors) - raise TypeError('Expected bytes') - - def to_native(obj, charset='utf8', errors='strict'): - if obj is None or isinstance(obj, str): - return obj - return obj.decode(charset, errors) - - def _local_html_escape(s, quote=True): - """ - Works with bytes. - Replace special characters "&", "<" and ">" to HTML-safe sequences. - If the optional flag quote is true (the default), the quotation mark - characters, both double quote (") and single quote (') characters are also - translated. - """ - if isinstance(s, str): - return html.escape(s, quote=quote) - - s = s.replace(b"&", b"&") # Must be done first! - s = s.replace(b"<", b"<") - s = s.replace(b">", b">") - if quote: - s = s.replace(b'"', b""") - s = s.replace(b'\'', b"'") - return s - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - -def to_unicode(obj, charset='utf-8', errors='strict'): - if obj is None: - return None - if not isinstance(obj, bytes): - return text_type(obj) - return obj.decode(charset, errors) - - -# shortcuts -pjoin = os.path.join -exists = os.path.exists diff --git a/gluon/admin.py b/gluon/admin.py index 8c70c348..9a1a5ff8 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -22,7 +22,7 @@ from gluon.fileutils import read_file, write_file, parse_version from gluon.restricted import RestrictedError from gluon.settings import global_settings from gluon.cache import CacheOnDisk -from gluon._compat import urlopen, to_native +from pydal._compat import urlopen, to_native if not global_settings.web2py_runtime_gae: import site diff --git a/gluon/cache.py b/gluon/cache.py index 474910bb..8407ab15 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -40,7 +40,7 @@ try: except ImportError: have_settings = False -from gluon._compat import pickle, thread, to_bytes, to_native, hashlib_md5 +from pydal._compat import pickle, thread, to_bytes, to_native, hashlib_md5 try: import psutil diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 0ee9c18c..5d0dc3b6 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long +from pydal._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 7dd28f35..146ee2f4 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -19,7 +19,7 @@ Additions: - .pickle: application/python-pickle - .w2p': application/w2p """ -from gluon._compat import to_native +from pydal._compat import to_native __all__ = ['contenttype'] diff --git a/gluon/contrib/appconfig.py b/gluon/contrib/appconfig.py index 1160e08a..849ada00 100644 --- a/gluon/contrib/appconfig.py +++ b/gluon/contrib/appconfig.py @@ -29,7 +29,7 @@ is restarted (or reload=True is passed). """ import os import json -from gluon._compat import thread, configparser +from pydal._compat import thread, configparser from gluon.globals import current locker = thread.allocate_lock() diff --git a/gluon/contrib/autolinks.py b/gluon/contrib/autolinks.py index 4670c304..91d53a96 100644 --- a/gluon/contrib/autolinks.py +++ b/gluon/contrib/autolinks.py @@ -42,7 +42,7 @@ revision3.com viddler.com """ from __future__ import print_function -from gluon._compat import FancyURLopener, urllib_quote +from pydal._compat import FancyURLopener, urllib_quote import re import cgi diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index fe9b374d..ff092bac 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -6,7 +6,7 @@ from __future__ import print_function import re import urllib -from gluon._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape, xrange +from pydal._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape, xrange from ast import parse as ast_parse import ast diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index 638d3c81..6f43465a 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -29,7 +29,7 @@ An interactive, stateful AJAX shell that runs Python code on the server. """ from __future__ import print_function -from gluon._compat import ClassType, pickle, StringIO +from pydal._compat import ClassType, pickle, StringIO import logging import new import sys diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index c5e7f3c7..670e93d1 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -16,7 +16,7 @@ mostly for testing purposes Some examples at the bottom. """ from __future__ import print_function -from gluon._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes +from pydal._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes import re import time diff --git a/gluon/custom_import.py b/gluon/custom_import.py index ef6c7ab2..1840561c 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -8,7 +8,7 @@ Support for smart import syntax for web2py applications ------------------------------------------------------- """ -from gluon._compat import builtin, unicodeT, PY2, to_native +from pydal._compat import builtin, unicodeT, PY2, to_native import os import sys import threading diff --git a/gluon/debug.py b/gluon/debug.py index a4432590..dd89836e 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -14,7 +14,7 @@ Debugger support classes import logging import pdb import sys -from gluon._compat import Queue +from pydal._compat import Queue logger = logging.getLogger("web2py") diff --git a/gluon/fileutils.py b/gluon/fileutils.py index 27fe8a8d..07624dc4 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -21,7 +21,7 @@ import logging from gluon.http import HTTP from gzip import open as gzopen from gluon.recfile import generate -from gluon._compat import PY2 +from pydal._compat import PY2 __all__ = [ 'parse_version', diff --git a/gluon/globals.py b/gluon/globals.py index b1a501e3..62ce0764 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,7 @@ Contains the classes for the global used variables: - Session """ -from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT +from pydal._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -436,7 +436,7 @@ class Response(Storage): self._vars.update(b) self._view_environment.update(self._vars) if view: - from gluon._compat import StringIO + from pydal._compat import StringIO (obody, oview) = (self.body, self.view) (self.body, self.view) = (StringIO(), view) run_view_in(self._view_environment) diff --git a/gluon/highlight.py b/gluon/highlight.py index 56afc2ea..c0b823b5 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -7,7 +7,8 @@ | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ from __future__ import print_function -from gluon._compat import xrange, _local_html_escape +from pydal._compat import xrange, +from local_html_escape import local_html_escape import re __all__ = ['highlight'] @@ -62,7 +63,7 @@ class Highlighter(object): Callback for C specific highlighting. """ - value = _local_html_escape(match.group(), quote=False) + value = local_html_escape(match.group(), quote=False) self.change_style(token, style) self.output.append(value) @@ -76,7 +77,7 @@ class Highlighter(object): Callback for python specific highlighting. """ - value = _local_html_escape(match.group(), quote=False) + value = local_html_escape(match.group(), quote=False) if token == 'MULTILINESTRING': self.change_style(token, style) self.output.append(value) @@ -113,7 +114,7 @@ class Highlighter(object): Callback for HTML specific highlighting. """ - value = _local_html_escape(match.group(), quote=False) + value = local_html_escape(match.group(), quote=False) self.change_style(token, style) self.output.append(value) if token == 'GOTOPYTHON': @@ -291,13 +292,13 @@ def highlight( 'WEB2PY']: code = Highlighter(language, link, styles).highlight(code) else: - code = _local_html_escape(code, quote=False) + code = local_html_escape(code, quote=False) lines = code.split('\n') if counter is None: linenumbers = [''] * len(lines) elif isinstance(counter, str): - linenumbers = [_local_html_escape(counter, quote=False)] * len(lines) + linenumbers = [local_html_escape(counter, quote=False)] * len(lines) else: linenumbers = [str(i + counter) + '.' for i in xrange(len(lines))] diff --git a/gluon/html.py b/gluon/html.py index 11f9653e..f8fe625c 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -20,8 +20,8 @@ import urllib import base64 from gluon import sanitizer, decoder import itertools -from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, \ - to_native, to_unicode, _local_html_escape, basestring, urlencode, implements_bool +from pydal._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool +from local_html_escape import local_html_escape import marshal from gluon.storage import Storage @@ -129,7 +129,7 @@ def xmlescape(data, quote=True): # ... and do the escaping - data = _local_html_escape(data, quote) + data = local_html_escape(data, quote) return data diff --git a/gluon/http.py b/gluon/http.py index e34fb8f0..9cf17eec 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -11,7 +11,7 @@ HTTP statuses helpers """ import re -from gluon._compat import iteritems +from pydal._compat import iteritems __all__ = ['HTTP', 'redirect'] diff --git a/gluon/languages.py b/gluon/languages.py index 6b4748d8..cd60e352 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -18,9 +18,8 @@ import pkgutil import logging from cgi import escape from threading import RLock -from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, _local_html_escape, to_native, \ - pjoin - +from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin +from local_html_escape import local_html_escape from gluon.portalocker import read_locked, LockedFile from gluon.utf8 import Utf8 @@ -423,7 +422,7 @@ class lazyT(object): return len(str(self)) def xml(self): - return str(self) if self.M else _local_html_escape(str(self), quote=False) + return str(self) if self.M else local_html_escape(str(self), quote=False) def encode(self, *a, **b): return str(self).encode(*a, **b) diff --git a/gluon/local_html_escape.py b/gluon/local_html_escape.py new file mode 100644 index 00000000..a4345eef --- /dev/null +++ b/gluon/local_html_escape.py @@ -0,0 +1,27 @@ +import sys + +PY2 = sys.version_info[0] == 2 + +def local_html_escape(data, quote=False): + """ + Works with bytes. + Replace special characters "&", "<" and ">" to HTML-safe sequences. + If the optional flag quote is true (the default), the quotation mark + characters, both double quote (") and single quote (') characters are also + translated. + """ + if PY2: + import cgi + s = cgi.escape(data, quote) + return s.replace("'", "'") if quote else s + else: + import html + if isinstance(s, str): + return html.escape(s, quote=quote) + s = s.replace(b"&", b"&") # Must be done first! + s = s.replace(b"<", b"<") + s = s.replace(b">", b">") + if quote: + s = s.replace(b'"', b""") + s = s.replace(b'\'', b"'") + return s diff --git a/gluon/main.py b/gluon/main.py index 971c48a7..1549b3df 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -25,7 +25,7 @@ import socket import random import string -from gluon._compat import Cookie, urllib2 +from pydal._compat import Cookie, urllib2 #from thread import allocate_lock from gluon.fileutils import abspath, write_file diff --git a/gluon/packages/dal b/gluon/packages/dal index 9a5332e4..79ec4e99 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 9a5332e45bef6516bf159b69e2cd0b355ec9a6ca +Subproject commit 79ec4e99d09b768ca2dcc42ac32f408529e0745f diff --git a/gluon/recfile.py b/gluon/recfile.py index 51f81cf9..9881e7ac 100755 --- a/gluon/recfile.py +++ b/gluon/recfile.py @@ -10,7 +10,7 @@ Generates names for cache and session files -------------------------------------------- """ import os -from gluon._compat import builtin +from pydal._compat import builtin def generate(filename, depth=2, base=512): if os.path.sep in filename: diff --git a/gluon/restricted.py b/gluon/restricted.py index c67bcbf4..6f4493b2 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -10,7 +10,7 @@ Restricted environment to execute application's code """ import sys -from gluon._compat import pickle, ClassType +from pydal._compat import pickle, ClassType import traceback import types import os diff --git a/gluon/rewrite.py b/gluon/rewrite.py index fc14557a..bced6731 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -27,7 +27,7 @@ from gluon.storage import Storage, List from gluon.http import HTTP from gluon.fileutils import abspath, read_file from gluon.settings import global_settings -from gluon._compat import urllib_unquote, urllib_quote, iteritems, xrange +from pydal._compat import urllib_unquote, urllib_quote, iteritems, xrange isdir = os.path.isdir isfile = os.path.isfile diff --git a/gluon/rocket.py b/gluon/rocket.py index c41ac32d..c3eb5006 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -11,7 +11,7 @@ import errno import socket import logging import platform -from gluon._compat import iteritems, to_bytes, StringIO, urllib_unquote +from pydal._compat import iteritems, to_bytes, StringIO, urllib_unquote # Define Constants VERSION = '1.2.6' diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 1b9a478e..cc429591 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -29,7 +29,7 @@ from json import loads, dumps from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_EMPTY_OR from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB from gluon.utils import web2py_uuid -from gluon._compat import Queue, long, iteritems +from pydal._compat import Queue, long, iteritems from gluon.storage import Storage USAGE = """ diff --git a/gluon/serializers.py b/gluon/serializers.py index d53740d7..823c5b2b 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -10,7 +10,7 @@ from gluon.html import TAG, XmlComponent, xmlescape from gluon.languages import lazyT import gluon.contrib.rss2 as rss2 import json as json_parser -from gluon._compat import long, to_native, unicodeT +from pydal._compat import long, to_native, unicodeT have_yaml = True try: diff --git a/gluon/settings.py b/gluon/settings.py index 5e2af7e0..1ef8fcad 100644 --- a/gluon/settings.py +++ b/gluon/settings.py @@ -10,7 +10,7 @@ import os import sys import platform from gluon.storage import Storage -from gluon._compat import PY2 +from pydal._compat import PY2 global_settings = Storage() settings = global_settings # legacy compatibility diff --git a/gluon/shell.py b/gluon/shell.py index 95359c3a..29462491 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -30,7 +30,7 @@ from gluon.globals import Request, Response, Session from gluon.storage import Storage, List from gluon.admin import w2p_unpack from pydal.base import BaseAdapter -from gluon._compat import iteritems, ClassType +from pydal._compat import iteritems, ClassType logger = logging.getLogger("web2py") diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 9ddc34d6..3e8d8843 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -19,7 +19,7 @@ import urllib import re import os -from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native +from pydal._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native from gluon.http import HTTP, redirect from gluon.html import XmlComponent, truncate_string from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG diff --git a/gluon/storage.py b/gluon/storage.py index 2d8c3dfa..4bb12b5b 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -12,7 +12,7 @@ Provides: - Storage; like dictionary allowing also for `obj.foo` for `obj['foo']` """ -from gluon._compat import copyreg, pickle, PY2 +from pydal._compat import copyreg, pickle, PY2 import gluon.portalocker as portalocker __all__ = ['List', 'Storage', 'Settings', 'Messages', diff --git a/gluon/streamer.py b/gluon/streamer.py index dd7b78b2..c10c129c 100644 --- a/gluon/streamer.py +++ b/gluon/streamer.py @@ -17,7 +17,7 @@ import re import errno from gluon.http import HTTP from gluon.contenttype import contenttype -from gluon._compat import PY2 +from pydal._compat import PY2 regex_start_range = re.compile('\d+(?=\-)') diff --git a/gluon/template.py b/gluon/template.py index 2afc29ab..5ce69655 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -18,7 +18,7 @@ import os import cgi import logging from re import compile, sub, escape, DOTALL -from gluon._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native +from pydal._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native try: # have web2py diff --git a/gluon/tests/test_contenttype.py b/gluon/tests/test_contenttype.py index 1a476a62..ba73fd42 100644 --- a/gluon/tests/test_contenttype.py +++ b/gluon/tests/test_contenttype.py @@ -11,7 +11,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon.contenttype import contenttype -from gluon._compat import iteritems +from pydal._compat import iteritems class TestContentType(unittest.TestCase): diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py index 577056e4..012f1648 100644 --- a/gluon/tests/test_contribs.py +++ b/gluon/tests/test_contribs.py @@ -8,7 +8,7 @@ import os from .fix_path import fix_sys_path fix_sys_path(__file__) -from gluon._compat import to_bytes +from pydal._compat import to_bytes from gluon.storage import Storage from gluon.contrib import fpdf as fpdf from gluon.contrib import pyfpdf as pyfpdf diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 4c7e0b24..fe574a8a 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -34,7 +34,7 @@ class TestDALSubclass(unittest.TestCase): db.close() def testSerialization(self): - from gluon._compat import pickle + from pydal._compat import pickle db = DAL(check_reserved=['all']) db.define_table('t_a', Field('f_a')) db.t_a.insert(f_a='test') diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index d2336093..4ef0aa10 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -14,7 +14,7 @@ fix_sys_path(__file__) from gluon.globals import Request, Response, Session from gluon import URL -from gluon._compat import basestring +from pydal._compat import basestring def setup_clean_session(): request = Request(env={}) diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py index 9b98f2c2..18bc80f3 100644 --- a/gluon/tests/test_html.py +++ b/gluon/tests/test_html.py @@ -17,7 +17,7 @@ from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, from gluon.storage import Storage from gluon.html import XML_pickle, XML_unpickle from gluon.html import TAG_pickler, TAG_unpickler -from gluon._compat import xrange, PY2, to_native +from pydal._compat import xrange, PY2, to_native class TestBareHelpers(unittest.TestCase): diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index acc621ea..90fd3932 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -15,7 +15,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon import languages -from gluon._compat import PY2 +from pydal._compat import PY2 MP_WORKING = 0 try: diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index edec86cd..c42881f2 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -21,7 +21,7 @@ from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from gluon._compat import to_bytes, to_native, PY2 +from pydal._compat import to_bytes, to_native, PY2 logger = None oldcwd = None diff --git a/gluon/tests/test_routes.py b/gluon/tests/test_routes.py index 16bb8a09..1d2113f4 100644 --- a/gluon/tests/test_routes.py +++ b/gluon/tests/test_routes.py @@ -21,7 +21,7 @@ from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from gluon._compat import to_bytes +from pydal._compat import to_bytes logger = None oldcwd = None diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 48adce2e..7acae127 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -10,7 +10,7 @@ fix_sys_path(__file__) from gluon.storage import Storage, StorageList, List from gluon.http import HTTP -from gluon._compat import pickle +from pydal._compat import pickle class TestStorage(unittest.TestCase): diff --git a/gluon/tests/test_template.py b/gluon/tests/test_template.py index 47ac8d44..ee7d3f9b 100644 --- a/gluon/tests/test_template.py +++ b/gluon/tests/test_template.py @@ -66,7 +66,7 @@ class TestTemplate(unittest.TestCase): def testWithDummyFileSystem(self): from os.path import join as pjoin import contextlib - from gluon._compat import StringIO + from pydal._compat import StringIO from gluon.restricted import RestrictedError @contextlib.contextmanager diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index fa963144..a615063f 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -22,7 +22,7 @@ from gluon.dal import DAL, Field from pydal.objects import Table from gluon import tools from gluon.tools import Auth, Mail, Recaptcha, Recaptcha2, prettydate, Expose -from gluon._compat import PY2 +from pydal._compat import PY2 from gluon.globals import Request, Response, Session from gluon.storage import Storage from gluon.languages import translator diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 19a18e16..c77a5b2c 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -13,7 +13,7 @@ fix_sys_path(__file__) from gluon.validators import * -from gluon._compat import PY2, to_bytes +from pydal._compat import PY2, to_bytes class TestValidators(unittest.TestCase): diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index f7e9ddf3..89f42177 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -16,7 +16,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon.contrib.webclient import WebClient -from gluon._compat import urllib2, PY2 +from pydal._compat import urllib2, PY2 webserverprocess = None diff --git a/gluon/tools.py b/gluon/tools.py index d4ef18e6..d5178850 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -12,7 +12,7 @@ Auth, Mail, PluginManager and various utilities import base64 from functools import reduce -from gluon._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ +from pydal._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ MIMEText, Encoders, Charset, long, urllib_quote, iteritems, to_bytes, to_native, add_charset, \ charset_QP, basestring, unicodeT, to_unicode import datetime diff --git a/gluon/utf8.py b/gluon/utf8.py index 21fd12c4..f9672ee7 100644 --- a/gluon/utf8.py +++ b/gluon/utf8.py @@ -11,7 +11,7 @@ Utilities and class for UTF8 strings managing ---------------------------------------------- """ from __future__ import print_function -from gluon._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode, to_native +from pydal._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode, to_native __all__ = ['Utf8'] diff --git a/gluon/utils.py b/gluon/utils.py index fc468a57..5e611b9c 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -23,7 +23,7 @@ import logging import socket import base64 import zlib -from gluon._compat import basestring, pickle, PY2, xrange, to_bytes, to_native +from pydal._compat import basestring, pickle, PY2, xrange, to_bytes, to_native _struct_2_long_long = struct.Struct('=QQ') diff --git a/gluon/validators.py b/gluon/validators.py index 4181c226..2e9fd490 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,7 +21,7 @@ import urllib import struct import decimal import unicodedata -from gluon._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native +from pydal._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce diff --git a/gluon/widget.py b/gluon/widget.py index 8aeb9b4c..f801b001 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -13,7 +13,7 @@ from __future__ import print_function import datetime import sys -from gluon._compat import StringIO, thread, xrange +from pydal._compat import StringIO, thread, xrange import time import threading import os From cc1ff0207993a8b1c2746575dd9ca961f52a5916 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 03:46:51 -0500 Subject: [PATCH 060/436] only one portalocker --- gluon/cache.py | 2 +- gluon/globals.py | 2 +- gluon/languages.py | 7 +- gluon/newcron.py | 2 +- gluon/portalocker.py | 169 ------------------------------------------- gluon/storage.py | 2 +- 6 files changed, 8 insertions(+), 176 deletions(-) delete mode 100644 gluon/portalocker.py diff --git a/gluon/cache.py b/gluon/cache.py index 8407ab15..d9cfa061 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -30,7 +30,6 @@ import hashlib import datetime import tempfile from gluon import recfile -from gluon import portalocker from collections import defaultdict from collections import OrderedDict @@ -40,6 +39,7 @@ try: except ImportError: have_settings = False +from pydal import portalocker from pydal._compat import pickle, thread, to_bytes, to_native, hashlib_md5 try: diff --git a/gluon/globals.py b/gluon/globals.py index 62ce0764..b0c70357 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -28,7 +28,7 @@ from gluon import recfile from gluon.cache import CacheInRam from gluon.fileutils import copystream import hashlib -from gluon import portalocker +from pydal import portalocker from pickle import Pickler, MARK, DICT, EMPTY_DICT #from types import DictionaryType import datetime diff --git a/gluon/languages.py b/gluon/languages.py index cd60e352..dd3d912f 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -18,10 +18,11 @@ import pkgutil import logging from cgi import escape from threading import RLock -from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin -from local_html_escape import local_html_escape -from gluon.portalocker import read_locked, LockedFile from gluon.utf8 import Utf8 +from local_html_escape import local_html_escape + +from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin +from pydal.portalocker import read_locked, LockedFile from gluon.fileutils import listdir from gluon.cfs import getcfs diff --git a/gluon/newcron.py b/gluon/newcron.py index 9cf9291c..7cd8549f 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -19,7 +19,6 @@ import sched import re import datetime import platform -import portalocker import fileutils from functools import reduce try: @@ -27,6 +26,7 @@ try: except: import pickle from gluon.settings import global_settings +from pydal import portalocker logger = logging.getLogger("web2py.cron") _cron_stopping = False diff --git a/gluon/portalocker.py b/gluon/portalocker.py deleted file mode 100644 index 095c66b7..00000000 --- a/gluon/portalocker.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Cross-platform (posix/nt) API for flock-style file locking. - -Synopsis:: - - import portalocker - file = open(\"somefile\", \"r+\") - portalocker.lock(file, portalocker.LOCK_EX) - file.seek(12) - file.write(\"foo\") - file.close() - -If you know what you're doing, you may choose to:: - - portalocker.unlock(file) - -before closing the file, but why? - -Methods:: - - lock( file, flags ) - unlock( file ) - -Constants:: - - LOCK_EX - LOCK_SH - LOCK_NB - -I learned the win32 technique for locking files from sample code -provided by John Nielsen in the documentation -that accompanies the win32 modules. - -Author: Jonathan Feinberg -Version: $Id: portalocker.py,v 1.3 2001/05/29 18:47:55 Administrator Exp $ -""" - -import logging -import platform -logger = logging.getLogger("web2py") - -os_locking = None -try: - import google.appengine - os_locking = 'gae' -except: - try: - import fcntl - os_locking = 'posix' - except: - try: - import win32con - import pywintypes - import win32file - os_locking = 'windows' - except: - pass - -if os_locking == 'windows': - LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK - LOCK_SH = 0 # the default - LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY - - # is there any reason not to reuse the following structure? - - __overlapped = pywintypes.OVERLAPPED() - - def lock(file, flags): - hfile = win32file._get_osfhandle(file.fileno()) - win32file.LockFileEx(hfile, flags, 0, 0x7fff0000, __overlapped) - - def unlock(file): - hfile = win32file._get_osfhandle(file.fileno()) - win32file.UnlockFileEx(hfile, 0, 0x7fff0000, __overlapped) - - -elif os_locking == 'posix': - LOCK_EX = fcntl.LOCK_EX - LOCK_SH = fcntl.LOCK_SH - LOCK_NB = fcntl.LOCK_NB - - def lock(file, flags): - fcntl.flock(file.fileno(), flags) - - def unlock(file): - fcntl.flock(file.fileno(), fcntl.LOCK_UN) - - -else: - if platform.system() == 'Windows': - logger.error('no file locking, you must install the win32 extensions from: http://sourceforge.net/projects/pywin32/files/') - elif os_locking != 'gae': - logger.debug('no file locking, this will cause problems') - - LOCK_EX = None - LOCK_SH = None - LOCK_NB = None - - def lock(file, flags): - pass - - def unlock(file): - pass - - -class LockedFile(object): - def __init__(self, filename, mode='rb'): - self.filename = filename - self.mode = mode - self.file = None - if 'r' in mode: - self.file = open(filename, mode) - lock(self.file, LOCK_SH) - elif 'w' in mode or 'a' in mode: - self.file = open(filename, mode.replace('w', 'a')) - lock(self.file, LOCK_EX) - if not 'a' in mode: - self.file.seek(0) - self.file.truncate(0) - else: - raise RuntimeError("invalid LockedFile(...,mode)") - - def read(self, size=None): - return self.file.read() if size is None else self.file.read(size) - - def readline(self): - return self.file.readline() - - def readlines(self): - return self.file.readlines() - - def write(self, data): - self.file.write(data) - self.file.flush() - - def close(self): - if not self.file is None: - unlock(self.file) - self.file.close() - self.file = None - - def __del__(self): - if not self.file is None: - self.close() - - -def read_locked(filename): - fp = LockedFile(filename, 'rb') - data = fp.read() - fp.close() - return data - - -def write_locked(filename, data): - fp = LockedFile(filename, 'wb') - data = fp.write(data) - fp.close() - -if __name__ == '__main__': - import sys - f = LockedFile('test.txt', mode='wb') - f.write('test ok') - f.close() - f = LockedFile('test.txt', mode='rb') - sys.stdout.write(f.read()+'\n') - f.close() diff --git a/gluon/storage.py b/gluon/storage.py index 4bb12b5b..46f16e63 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -12,8 +12,8 @@ Provides: - Storage; like dictionary allowing also for `obj.foo` for `obj['foo']` """ +from pydal import portalocker from pydal._compat import copyreg, pickle, PY2 -import gluon.portalocker as portalocker __all__ = ['List', 'Storage', 'Settings', 'Messages', 'StorageList', 'load_storage', 'save_storage'] From eebf406d97c8cb23d7892c5f259cf31d9a1a1cb1 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:13:32 -0500 Subject: [PATCH 061/436] fixed import error --- gluon/cache.py | 2 +- gluon/globals.py | 2 +- gluon/languages.py | 2 +- gluon/newcron.py | 2 +- gluon/storage.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gluon/cache.py b/gluon/cache.py index d9cfa061..da38e5a0 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -39,7 +39,7 @@ try: except ImportError: have_settings = False -from pydal import portalocker +from pydal.contrib import portalocker from pydal._compat import pickle, thread, to_bytes, to_native, hashlib_md5 try: diff --git a/gluon/globals.py b/gluon/globals.py index b0c70357..ff0cbf52 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -28,7 +28,7 @@ from gluon import recfile from gluon.cache import CacheInRam from gluon.fileutils import copystream import hashlib -from pydal import portalocker +from pydal.contrib import portalocker from pickle import Pickler, MARK, DICT, EMPTY_DICT #from types import DictionaryType import datetime diff --git a/gluon/languages.py b/gluon/languages.py index dd3d912f..50cfa6d7 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -22,7 +22,7 @@ from gluon.utf8 import Utf8 from local_html_escape import local_html_escape from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin -from pydal.portalocker import read_locked, LockedFile +from pydal.contrib.portalocker import read_locked, LockedFile from gluon.fileutils import listdir from gluon.cfs import getcfs diff --git a/gluon/newcron.py b/gluon/newcron.py index 7cd8549f..0bd1d689 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -26,7 +26,7 @@ try: except: import pickle from gluon.settings import global_settings -from pydal import portalocker +from pydal.contrib import portalocker logger = logging.getLogger("web2py.cron") _cron_stopping = False diff --git a/gluon/storage.py b/gluon/storage.py index 46f16e63..dd5d78f2 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -12,7 +12,7 @@ Provides: - Storage; like dictionary allowing also for `obj.foo` for `obj['foo']` """ -from pydal import portalocker +from pydal.contrib import portalocker from pydal._compat import copyreg, pickle, PY2 __all__ = ['List', 'Storage', 'Settings', 'Messages', From 92a03f397d04bb163d4248aaef911a29fe1ccace Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:16:29 -0500 Subject: [PATCH 062/436] fixed another _compat import --- gluon/cfs.py | 2 +- gluon/sanitizer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/cfs.py b/gluon/cfs.py index b8bca354..8b8c695f 100644 --- a/gluon/cfs.py +++ b/gluon/cfs.py @@ -14,8 +14,8 @@ Note: """ from os import stat -from ._compat import thread from gluon.fileutils import read_file +from pydal._compat import thread cfs = {} # for speed-up cfs_lock = thread.allocate_lock() # and thread safety diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index 20e9e205..0aea74d7 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -10,7 +10,7 @@ Cross-site scripting (XSS) defense ----------------------------------- """ -from ._compat import HTMLParser, urlparse, entitydefs, basestring +from pydal._compat import HTMLParser, urlparse, entitydefs, basestring from cgi import escape from formatter import AbstractFormatter from xml.sax.saxutils import quoteattr From 68375a4ea7748c6fa864aff888e2d6e4c966b77e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:18:06 -0500 Subject: [PATCH 063/436] removed training comma --- gluon/highlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/highlight.py b/gluon/highlight.py index c0b823b5..9aef3557 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -7,7 +7,7 @@ | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ from __future__ import print_function -from pydal._compat import xrange, +from pydal._compat import xrange from local_html_escape import local_html_escape import re From cf12fea68b3773e34ae799fe6bc787dba8333d57 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:26:46 -0500 Subject: [PATCH 064/436] fixed more imports --- gluon/contrib/markmin/markmin2html.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index ff092bac..45f78dd1 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -6,7 +6,8 @@ from __future__ import print_function import re import urllib -from pydal._compat import maketrans, urllib_quote, unicodeT, _local_html_escape, to_bytes, to_native, _local_html_escape as escape, xrange +from pydal._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange +from gluon.local_html_escape import local_html_escape as escape from ast import parse as ast_parse import ast From 6543512983cf958aee7dcf31dd83b39a3a42c94e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:34:33 -0500 Subject: [PATCH 065/436] fixed another import error in appadmin --- applications/welcome/controllers/appadmin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 3060945c..6de48036 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -10,7 +10,7 @@ import datetime import copy import gluon.contenttype import gluon.fileutils -from gluon._compat import iteritems +from pydal._compat import iteritems try: import pygraphviz as pgv From 4bb261ec15c28096e607aceef1755483f2bbc183 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 04:41:51 -0500 Subject: [PATCH 066/436] fixed py3 import --- gluon/highlight.py | 2 +- gluon/html.py | 2 +- gluon/languages.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/highlight.py b/gluon/highlight.py index 9aef3557..91b4fa78 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -8,7 +8,7 @@ """ from __future__ import print_function from pydal._compat import xrange -from local_html_escape import local_html_escape +from gluon.local_html_escape import local_html_escape import re __all__ = ['highlight'] diff --git a/gluon/html.py b/gluon/html.py index f8fe625c..db3a7c15 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -21,7 +21,7 @@ import base64 from gluon import sanitizer, decoder import itertools from pydal._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool -from local_html_escape import local_html_escape +from gluon.local_html_escape import local_html_escape import marshal from gluon.storage import Storage diff --git a/gluon/languages.py b/gluon/languages.py index 50cfa6d7..0daf475d 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -19,7 +19,7 @@ import logging from cgi import escape from threading import RLock from gluon.utf8 import Utf8 -from local_html_escape import local_html_escape +from gluon.local_html_escape import local_html_escape from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin from pydal.contrib.portalocker import read_locked, LockedFile From a45bffc90843a876997f53df38b405e990dedf17 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 05:02:52 -0500 Subject: [PATCH 067/436] fixed admin imports --- applications/admin/controllers/default.py | 2 +- applications/admin/models/access.py | 2 +- applications/admin/private/hosts.deny | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 6c46d971..bdadcf2b 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -15,7 +15,7 @@ from gluon.utils import web2py_uuid from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob -from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native +from pydal._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native import shutil import platform diff --git a/applications/admin/models/access.py b/applications/admin/models/access.py index b94ce8d0..7e24ac58 100644 --- a/applications/admin/models/access.py +++ b/applications/admin/models/access.py @@ -1,10 +1,10 @@ import base64 import os import time -from gluon import portalocker from gluon.admin import apath from gluon.fileutils import read_file from gluon.utils import web2py_uuid +from pydal.contrib import portalocker # ########################################################### # ## make sure administrator is on localhost or https # ########################################################### diff --git a/applications/admin/private/hosts.deny b/applications/admin/private/hosts.deny index e69de29b..9447a26c 100644 --- a/applications/admin/private/hosts.deny +++ b/applications/admin/private/hosts.deny @@ -0,0 +1 @@ +127.0.0.1 1 1470045759 From 116a7ff006be84df97a58f529e086122e84b4ecc Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 05:09:39 -0500 Subject: [PATCH 068/436] removed unwanted hosts.deny --- applications/admin/private/hosts.deny | 1 - 1 file changed, 1 deletion(-) delete mode 100644 applications/admin/private/hosts.deny diff --git a/applications/admin/private/hosts.deny b/applications/admin/private/hosts.deny deleted file mode 100644 index 9447a26c..00000000 --- a/applications/admin/private/hosts.deny +++ /dev/null @@ -1 +0,0 @@ -127.0.0.1 1 1470045759 From 2e0630c48cc5ccd3b20960bdfd27712779122357 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 05:15:52 -0500 Subject: [PATCH 069/436] moved local_html_escape into utils, thanks Simone --- gluon/contrib/markmin/markmin2html.py | 2 +- gluon/highlight.py | 2 +- gluon/html.py | 2 +- gluon/languages.py | 2 +- gluon/local_html_escape.py | 27 --------------------------- gluon/utils.py | 25 +++++++++++++++++++++++++ 6 files changed, 29 insertions(+), 31 deletions(-) delete mode 100644 gluon/local_html_escape.py diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 45f78dd1..51622ce7 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -7,7 +7,7 @@ from __future__ import print_function import re import urllib from pydal._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange -from gluon.local_html_escape import local_html_escape as escape +from pydal.utils import local_html_escape as escape from ast import parse as ast_parse import ast diff --git a/gluon/highlight.py b/gluon/highlight.py index 91b4fa78..97b77990 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -8,7 +8,7 @@ """ from __future__ import print_function from pydal._compat import xrange -from gluon.local_html_escape import local_html_escape +from pydal.utils import local_html_escape import re __all__ = ['highlight'] diff --git a/gluon/html.py b/gluon/html.py index db3a7c15..bd693406 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -21,7 +21,7 @@ import base64 from gluon import sanitizer, decoder import itertools from pydal._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool -from gluon.local_html_escape import local_html_escape +from pydal.utils import local_html_escape import marshal from gluon.storage import Storage diff --git a/gluon/languages.py b/gluon/languages.py index 0daf475d..88e659a6 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -19,7 +19,7 @@ import logging from cgi import escape from threading import RLock from gluon.utf8 import Utf8 -from gluon.local_html_escape import local_html_escape +from pydal.utils import local_html_escape from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin from pydal.contrib.portalocker import read_locked, LockedFile diff --git a/gluon/local_html_escape.py b/gluon/local_html_escape.py deleted file mode 100644 index a4345eef..00000000 --- a/gluon/local_html_escape.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys - -PY2 = sys.version_info[0] == 2 - -def local_html_escape(data, quote=False): - """ - Works with bytes. - Replace special characters "&", "<" and ">" to HTML-safe sequences. - If the optional flag quote is true (the default), the quotation mark - characters, both double quote (") and single quote (') characters are also - translated. - """ - if PY2: - import cgi - s = cgi.escape(data, quote) - return s.replace("'", "'") if quote else s - else: - import html - if isinstance(s, str): - return html.escape(s, quote=quote) - s = s.replace(b"&", b"&") # Must be done first! - s = s.replace(b"<", b"<") - s = s.replace(b">", b">") - if quote: - s = s.replace(b'"', b""") - s = s.replace(b'\'', b"'") - return s diff --git a/gluon/utils.py b/gluon/utils.py index 5e611b9c..22e3cef4 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -359,3 +359,28 @@ def getipaddrinfo(host): and isinstance(addrinfo[4][0], basestring)] except socket.error: return [] + + +def local_html_escape(data, quote=False): + """ + Works with bytes. + Replace special characters "&", "<" and ">" to HTML-safe sequences. + If the optional flag quote is true (the default), the quotation mark + characters, both double quote (") and single quote (') characters are also + translated. + """ + if PY2: + import cgi + s = cgi.escape(data, quote) + return s.replace("'", "'") if quote else s + else: + import html + if isinstance(s, str): + return html.escape(s, quote=quote) + s = s.replace(b"&", b"&") # Must be done first! + s = s.replace(b"<", b"<") + s = s.replace(b">", b">") + if quote: + s = s.replace(b'"', b""") + s = s.replace(b'\'', b"'") + return s From 7440d382539183b0c4fdb25cd00d8a532163395a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 05:19:46 -0500 Subject: [PATCH 070/436] moved local_html_escape into utils, thanks Simone --- gluon/contrib/markmin/markmin2html.py | 2 +- gluon/highlight.py | 2 +- gluon/html.py | 2 +- gluon/languages.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 51622ce7..e64224d2 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -7,7 +7,7 @@ from __future__ import print_function import re import urllib from pydal._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange -from pydal.utils import local_html_escape as escape +from gluon.utils import local_html_escape as escape from ast import parse as ast_parse import ast diff --git a/gluon/highlight.py b/gluon/highlight.py index 97b77990..bf98e9b3 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -8,7 +8,7 @@ """ from __future__ import print_function from pydal._compat import xrange -from pydal.utils import local_html_escape +from gluon.utils import local_html_escape import re __all__ = ['highlight'] diff --git a/gluon/html.py b/gluon/html.py index bd693406..049ede93 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -21,7 +21,7 @@ import base64 from gluon import sanitizer, decoder import itertools from pydal._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool -from pydal.utils import local_html_escape +from gluon.utils import local_html_escape import marshal from gluon.storage import Storage diff --git a/gluon/languages.py b/gluon/languages.py index 88e659a6..528a9ceb 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -19,7 +19,7 @@ import logging from cgi import escape from threading import RLock from gluon.utf8 import Utf8 -from pydal.utils import local_html_escape +from gluon.utils import local_html_escape from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin from pydal.contrib.portalocker import read_locked, LockedFile From be5ddca28fffa7fa320b7d3a4aadc56bf6b7052d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 1 Aug 2016 05:23:40 -0500 Subject: [PATCH 071/436] fixed last_sql_parse for py3 --- gluon/utils.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/gluon/utils.py b/gluon/utils.py index 22e3cef4..79fd8604 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -371,16 +371,17 @@ def local_html_escape(data, quote=False): """ if PY2: import cgi - s = cgi.escape(data, quote) - return s.replace("'", "'") if quote else s + data = cgi.escape(data, quote) + return data.replace("'", "'") if quote else data else: import html - if isinstance(s, str): - return html.escape(s, quote=quote) - s = s.replace(b"&", b"&") # Must be done first! - s = s.replace(b"<", b"<") - s = s.replace(b">", b">") + if isinstance(data, str): + return html.escape(data, quote=quote) + data = data.replace(b"&", b"&") # Must be done first! + data = data.replace(b"<", b"<") + data = data.replace(b">", b">") if quote: - s = s.replace(b'"', b""") - s = s.replace(b'\'', b"'") - return s + data = data.replace(b'"', b""") + data = data.replace(b'\'', b"'") + return data + From 3fc57bd9add55d4e8e7729b339205cd66688a3f7 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 3 Aug 2016 03:22:24 -0500 Subject: [PATCH 072/436] improved fabfile --- fabfile.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fabfile.py b/fabfile.py index b7bd41fc..2e7db9e8 100644 --- a/fabfile.py +++ b/fabfile.py @@ -17,16 +17,14 @@ applications = '/home/www-data/web2py/applications' def create_user(username): """fab -H root@host create_user:username""" - password = getpass.getpass(name+' password for %s> ' % username) - run('useradd -m %s' % username) - run('usermod --password %s %s' % (crypt.crypt(password, 'salt'), username)) - run('mkdir -p ~%s/.ssh' % username) + password = getpass.getpass('password for %s> ' % username) + run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username)) run('cp /etc/sudoers /tmp/sudoers.new') - append('/tmp/sudoers.new', '%s ALL=NOPASSWD: ALL' % username, use_sudo=True) + append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True) run('visudo -c -f /tmp/sudoers.new') run('EDITOR="cp /tmp/sudoers.new" visudo') - uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes') local('ssh-copy-id %s' % env.hosts[0]) + uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes') def install_web2py(): """fab -H username@host install_web2py""" From 032ee427c6144576674c0f6943e1fed73462949b Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 7 Aug 2016 10:46:07 +0200 Subject: [PATCH 073/436] enabled tests/test_sqlhtml.py --- gluon/tests/__init__.py | 1 + gluon/tests/test_sqlhtml.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 6b564235..70d55565 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -21,6 +21,7 @@ from .test_languages import * from .test_compileapp import * from .test_appadmin import * from .test_web import * +from .test_sqlhtml import * if sys.version[:3] == '2.7': from .test_is_url import * diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index 8e00a197..3b89801b 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -280,7 +280,7 @@ class TestSQLFORM(unittest.TestCase): def test_SQLFORM(self): form = SQLFORM(self.db.auth_user) - self.assertEqual(form.xml(), '
      ') + self.assertEqual(form.xml(), b'
      ') # def test_assert_status(self): # pass @@ -300,7 +300,7 @@ class TestSQLFORM(unittest.TestCase): def test_factory(self): factory_form = SQLFORM.factory(Field('field_one', 'string', IS_NOT_EMPTY()), Field('field_two', 'string')) - self.assertEqual(factory_form.xml(), '
      ') + self.assertEqual(factory_form.xml(), b'
      ') # def test_build_query(self): # pass @@ -310,11 +310,11 @@ class TestSQLFORM(unittest.TestCase): def test_grid(self): grid_form = SQLFORM.grid(self.db.auth_user) - self.assertEqual(grid_form.xml(), '
      1 records found
      IdFirst nameLast nameE-mailUsername
      1BartSimpsonuser1@test.comuser1 View
      ') + self.assertEqual(grid_form.xml(), b'
      1 records found
      IdFirst nameLast nameE-mailUsername
      1BartSimpsonuser1@test.comuser1 View
      ') def test_smartgrid(self): smartgrid_form = SQLFORM.smartgrid(self.db.auth_user) - self.assertEqual(smartgrid_form.xml(), '
      1 records found
      ') + self.assertEqual(smartgrid_form.xml(), b'
      1 records found
      ') class TestSQLTABLE(unittest.TestCase): @@ -349,10 +349,10 @@ class TestSQLTABLE(unittest.TestCase): self.db.commit() - def test_SQLTABLE(self): - rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL) - sqltable = SQLTABLE(rows) - self.assertEqual(sqltable.xml(), '
      auth_user.idauth_user.first_nameauth_user.last_nameauth_user.emailauth_user.usernameauth_user.passwordauth_user.registration_keyauth_user.reset_password_keyauth_user.registration_id
      1BartSimpsonuser1@test.comuser1password_123NoneNone
      ') +# def test_SQLTABLE(self): +# rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL) +# sqltable = SQLTABLE(rows) +# self.assertEqual(sqltable.xml(), '
      auth_user.idauth_user.first_nameauth_user.last_nameauth_user.emailauth_user.usernameauth_user.passwordauth_user.registration_keyauth_user.reset_password_keyauth_user.registration_id
      1BartSimpsonuser1@test.comuser1password_123NoneNone
      ') # class TestExportClass(unittest.TestCase): From 461e29076867febced4fa2a11b0eebe4ad18220d Mon Sep 17 00:00:00 2001 From: ilvalle Date: Mon, 8 Aug 2016 20:50:50 +0200 Subject: [PATCH 074/436] added unittest for sqlform represent --- gluon/tests/test_sqlhtml.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index 3b89801b..39b3d8fc 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -265,7 +265,7 @@ class TestSQLFORM(unittest.TestCase): self.db = DAL(DEFAULT_URI, check_reserved=['all']) self.auth = Auth(self.db) self.auth.define_tables(username=True, signature=False) - self.db.define_table('t0', Field('tt'), self.auth.signature) + self.db.define_table('t0', Field('tt', default='web2py'), self.auth.signature) self.auth.enable_record_versioning(self.db) # Create a user self.db.auth_user.insert(first_name='Bart', @@ -282,6 +282,16 @@ class TestSQLFORM(unittest.TestCase): form = SQLFORM(self.db.auth_user) self.assertEqual(form.xml(), b'
      ') + def test_represent_SQLFORM(self): + self.db.t0.tt.represent = lambda value: value.capitalize() + self.db.t0.tt.writable = False + self.db.t0.tt.readable = True + form = SQLFORM(self.db.t0) + self.assertTrue(b'Web2py' in form.xml()) + self.db.t0.tt.represent = lambda value, row: value.capitalize() + form = SQLFORM(self.db.t0) + self.assertTrue(b'Web2py' in form.xml()) + # def test_assert_status(self): # pass From 0528a347b37140671496f20914c8ca735fd8e5c5 Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Sat, 13 Aug 2016 15:38:55 +0200 Subject: [PATCH 075/436] Updated pyDAL to 16.08 --- applications/admin/controllers/default.py | 2 +- applications/welcome/controllers/appadmin.py | 6 +- gluon/_compat.py | 157 +++++++++++++++++++ gluon/admin.py | 2 +- gluon/cache.py | 2 +- gluon/cfs.py | 2 +- gluon/compileapp.py | 2 +- gluon/contenttype.py | 2 +- gluon/contrib/appconfig.py | 2 +- gluon/contrib/autolinks.py | 2 +- gluon/contrib/markmin/markmin2html.py | 2 +- gluon/contrib/shell.py | 2 +- gluon/contrib/webclient.py | 2 +- gluon/custom_import.py | 2 +- gluon/debug.py | 2 +- gluon/fileutils.py | 2 +- gluon/globals.py | 6 +- gluon/highlight.py | 2 +- gluon/html.py | 10 +- gluon/http.py | 2 +- gluon/languages.py | 2 +- gluon/main.py | 2 +- gluon/packages/dal | 2 +- gluon/recfile.py | 2 +- gluon/restricted.py | 2 +- gluon/rewrite.py | 2 +- gluon/rocket.py | 2 +- gluon/sanitizer.py | 2 +- gluon/scheduler.py | 2 +- gluon/serializers.py | 2 +- gluon/settings.py | 4 +- gluon/shell.py | 2 +- gluon/sqlhtml.py | 2 +- gluon/storage.py | 2 +- gluon/streamer.py | 2 +- gluon/template.py | 2 +- gluon/tests/test_contenttype.py | 2 +- gluon/tests/test_contribs.py | 2 +- gluon/tests/test_dal.py | 2 +- gluon/tests/test_globals.py | 2 +- gluon/tests/test_html.py | 4 +- gluon/tests/test_languages.py | 2 +- gluon/tests/test_router.py | 2 +- gluon/tests/test_routes.py | 2 +- gluon/tests/test_storage.py | 2 +- gluon/tests/test_template.py | 2 +- gluon/tests/test_tools.py | 2 +- gluon/tests/test_validators.py | 2 +- gluon/tests/test_web.py | 2 +- gluon/tools.py | 12 +- gluon/utf8.py | 2 +- gluon/utils.py | 4 +- gluon/validators.py | 2 +- gluon/widget.py | 4 +- 54 files changed, 227 insertions(+), 70 deletions(-) create mode 100644 gluon/_compat.py diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index bdadcf2b..6c46d971 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -15,7 +15,7 @@ from gluon.utils import web2py_uuid from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob -from pydal._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native +from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native import shutil import platform diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 6de48036..0b8b227f 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -10,7 +10,7 @@ import datetime import copy import gluon.contenttype import gluon.fileutils -from pydal._compat import iteritems +from gluon._compat import iteritems try: import pygraphviz as pgv @@ -213,7 +213,7 @@ def select(): if is_imap: step = 3 - + stop = start + step table = None @@ -421,7 +421,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] diff --git a/gluon/_compat.py b/gluon/_compat.py new file mode 100644 index 00000000..f7b53dde --- /dev/null +++ b/gluon/_compat.py @@ -0,0 +1,157 @@ +import sys +import hashlib +import os + +PY2 = sys.version_info[0] == 2 + +_identity = lambda x: x + +if PY2: + import cPickle as pickle + from cStringIO import StringIO + import copy_reg as copyreg + from HTMLParser import HTMLParser + import urlparse + from htmlentitydefs import entitydefs, name2codepoint + import __builtin__ as builtin + import thread + import Cookie + import urllib2 + import Queue + import ConfigParser as configparser + from email.MIMEBase import MIMEBase + from email.Header import Header + from email import Encoders, Charset + from email.MIMEMultipart import MIMEMultipart + from email.MIMEText import MIMEText + from email.Charset import add_charset, QP as charset_QP + from urllib import FancyURLopener, urlencode, urlopen + from urllib import quote as urllib_quote, unquote as urllib_unquote + from string import maketrans + from types import ClassType + import cgi + import cookielib + BytesIO = StringIO + reduce = reduce + hashlib_md5 = hashlib.md5 + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() + integer_types = (int, long) + string_types = (str, unicode) + text_type = unicode + basestring = basestring + xrange = xrange + long = long + unichr = unichr + unicodeT = unicode + + def implements_bool(cls): + cls.__nonzero__ = cls.__bool__ + del cls.__bool__ + return cls + + def implements_iterator(cls): + cls.next = cls.__next__ + del cls.__next__ + return cls + + def to_bytes(obj, charset='utf-8', errors='strict'): + if obj is None: + return None + if isinstance(obj, (bytes, bytearray, buffer)): + return bytes(obj) + if isinstance(obj, unicode): + return obj.encode(charset, errors) + raise TypeError('Expected bytes') + + def to_native(obj, charset='utf8', errors='strict'): + if obj is None or isinstance(obj, str): + return obj + return obj.encode(charset, errors) + + +else: + import pickle + from io import StringIO, BytesIO + import copyreg + from functools import reduce + from html.parser import HTMLParser + from http import cookies as Cookie + from urllib import parse as urlparse + from urllib import request as urllib2 + from html.entities import entitydefs, name2codepoint + import builtins as builtin + import _thread as thread + import configparser + import queue as Queue + from email.mime.base import MIMEBase + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email import encoders as Encoders + from email.header import Header + from email.charset import Charset, add_charset, QP as charset_QP + from urllib.request import FancyURLopener, urlopen + from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode + from http import cookiejar as cookielib + import html # warning, this is the python3 module and not the web2py html module + hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) + integer_types = (int,) + string_types = (str,) + text_type = str + basestring = str + xrange = range + long = int + unichr = chr + unicodeT = str + maketrans = str.maketrans + ClassType = type + + implements_iterator = _identity + implements_bool = _identity + + def to_bytes(obj, charset='utf-8', errors='strict'): + if obj is None: + return None + if isinstance(obj, (bytes, bytearray, memoryview)): + return bytes(obj) + if isinstance(obj, str): + return obj.encode(charset, errors) + raise TypeError('Expected bytes') + + def to_native(obj, charset='utf8', errors='strict'): + if obj is None or isinstance(obj, str): + return obj + return obj.decode(charset, errors) + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +def to_unicode(obj, charset='utf-8', errors='strict'): + if obj is None: + return None + if not isinstance(obj, bytes): + return text_type(obj) + return obj.decode(charset, errors) + + +# shortcuts +pjoin = os.path.join +exists = os.path.exists diff --git a/gluon/admin.py b/gluon/admin.py index 9a1a5ff8..8c70c348 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -22,7 +22,7 @@ from gluon.fileutils import read_file, write_file, parse_version from gluon.restricted import RestrictedError from gluon.settings import global_settings from gluon.cache import CacheOnDisk -from pydal._compat import urlopen, to_native +from gluon._compat import urlopen, to_native if not global_settings.web2py_runtime_gae: import site diff --git a/gluon/cache.py b/gluon/cache.py index da38e5a0..5050d213 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -40,7 +40,7 @@ except ImportError: have_settings = False from pydal.contrib import portalocker -from pydal._compat import pickle, thread, to_bytes, to_native, hashlib_md5 +from gluon._compat import pickle, thread, to_bytes, to_native, hashlib_md5 try: import psutil diff --git a/gluon/cfs.py b/gluon/cfs.py index 8b8c695f..4925a3cd 100644 --- a/gluon/cfs.py +++ b/gluon/cfs.py @@ -15,7 +15,7 @@ Note: from os import stat from gluon.fileutils import read_file -from pydal._compat import thread +from gluon._compat import thread cfs = {} # for speed-up cfs_lock = thread.allocate_lock() # and thread safety diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 5d0dc3b6..0ee9c18c 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from pydal._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long +from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 146ee2f4..7dd28f35 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -19,7 +19,7 @@ Additions: - .pickle: application/python-pickle - .w2p': application/w2p """ -from pydal._compat import to_native +from gluon._compat import to_native __all__ = ['contenttype'] diff --git a/gluon/contrib/appconfig.py b/gluon/contrib/appconfig.py index 849ada00..1160e08a 100644 --- a/gluon/contrib/appconfig.py +++ b/gluon/contrib/appconfig.py @@ -29,7 +29,7 @@ is restarted (or reload=True is passed). """ import os import json -from pydal._compat import thread, configparser +from gluon._compat import thread, configparser from gluon.globals import current locker = thread.allocate_lock() diff --git a/gluon/contrib/autolinks.py b/gluon/contrib/autolinks.py index 91d53a96..4670c304 100644 --- a/gluon/contrib/autolinks.py +++ b/gluon/contrib/autolinks.py @@ -42,7 +42,7 @@ revision3.com viddler.com """ from __future__ import print_function -from pydal._compat import FancyURLopener, urllib_quote +from gluon._compat import FancyURLopener, urllib_quote import re import cgi diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index e64224d2..9492a6ca 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -6,7 +6,7 @@ from __future__ import print_function import re import urllib -from pydal._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange +from gluon._compat import maketrans, urllib_quote, unicodeT, to_bytes, to_native, xrange from gluon.utils import local_html_escape as escape from ast import parse as ast_parse import ast diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index 6f43465a..638d3c81 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -29,7 +29,7 @@ An interactive, stateful AJAX shell that runs Python code on the server. """ from __future__ import print_function -from pydal._compat import ClassType, pickle, StringIO +from gluon._compat import ClassType, pickle, StringIO import logging import new import sys diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index 670e93d1..c5e7f3c7 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -16,7 +16,7 @@ mostly for testing purposes Some examples at the bottom. """ from __future__ import print_function -from pydal._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes +from gluon._compat import urllib2, cookielib, iteritems, to_native, urlencode, to_bytes import re import time diff --git a/gluon/custom_import.py b/gluon/custom_import.py index 1840561c..ef6c7ab2 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -8,7 +8,7 @@ Support for smart import syntax for web2py applications ------------------------------------------------------- """ -from pydal._compat import builtin, unicodeT, PY2, to_native +from gluon._compat import builtin, unicodeT, PY2, to_native import os import sys import threading diff --git a/gluon/debug.py b/gluon/debug.py index dd89836e..a4432590 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -14,7 +14,7 @@ Debugger support classes import logging import pdb import sys -from pydal._compat import Queue +from gluon._compat import Queue logger = logging.getLogger("web2py") diff --git a/gluon/fileutils.py b/gluon/fileutils.py index 07624dc4..27fe8a8d 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -21,7 +21,7 @@ import logging from gluon.http import HTTP from gzip import open as gzopen from gluon.recfile import generate -from pydal._compat import PY2 +from gluon._compat import PY2 __all__ = [ 'parse_version', diff --git a/gluon/globals.py b/gluon/globals.py index ff0cbf52..41ac708e 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,7 @@ Contains the classes for the global used variables: - Session """ -from pydal._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT +from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -436,7 +436,7 @@ class Response(Storage): self._vars.update(b) self._view_environment.update(self._vars) if view: - from pydal._compat import StringIO + from gluon._compat import StringIO (obody, oview) = (self.body, self.view) (self.body, self.view) = (StringIO(), view) run_view_in(self._view_environment) @@ -1192,7 +1192,7 @@ class Session(Storage): def _try_store_in_file(self, request, response): try: - if (not response.session_id or + if (not response.session_id or not response.session_filename or self._forget or self._unchanged(response)): diff --git a/gluon/highlight.py b/gluon/highlight.py index bf98e9b3..d6faff87 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -7,7 +7,7 @@ | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ from __future__ import print_function -from pydal._compat import xrange +from gluon._compat import xrange from gluon.utils import local_html_escape import re diff --git a/gluon/html.py b/gluon/html.py index 049ede93..da8e2d6e 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -20,7 +20,7 @@ import urllib import base64 from gluon import sanitizer, decoder import itertools -from pydal._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool +from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool from gluon.utils import local_html_escape import marshal @@ -127,7 +127,7 @@ def xmlescape(data, quote=True): data=str(data) data = to_bytes(data, 'utf8', 'xmlcharrefreplace') - + # ... and do the escaping data = local_html_escape(data, quote) return data @@ -945,7 +945,7 @@ class DIV(XmlComponent): fa = b'' for name, value in attr: fa += (b' %s="%s"') % (to_bytes(name), xmlescape(value, True)) - + # get the xml for the inner components co = b''.join([xmlescape(component) for component in self.components]) return (fa, co) @@ -975,7 +975,7 @@ class DIV(XmlComponent): """ # In PY3 __str__ cannot return bytes (TypeError: __str__ returned non-string (type bytes)) return to_native(self.xml()) - + def flatten(self, render=None): """ Returns the text stored by the DIV object rendered by the render function @@ -1440,7 +1440,7 @@ class STYLE(DIV): def xml(self): (fa, co) = self._xml() - fa = to_native(fa) + fa = to_native(fa) # no escaping of subcomponents co = '\n'.join([str(component) for component in self.components]) diff --git a/gluon/http.py b/gluon/http.py index 9cf17eec..e34fb8f0 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -11,7 +11,7 @@ HTTP statuses helpers """ import re -from pydal._compat import iteritems +from gluon._compat import iteritems __all__ = ['HTTP', 'redirect'] diff --git a/gluon/languages.py b/gluon/languages.py index 528a9ceb..98fa4790 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -21,7 +21,7 @@ from threading import RLock from gluon.utf8 import Utf8 from gluon.utils import local_html_escape -from pydal._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin +from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin from pydal.contrib.portalocker import read_locked, LockedFile from gluon.fileutils import listdir diff --git a/gluon/main.py b/gluon/main.py index 1549b3df..971c48a7 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -25,7 +25,7 @@ import socket import random import string -from pydal._compat import Cookie, urllib2 +from gluon._compat import Cookie, urllib2 #from thread import allocate_lock from gluon.fileutils import abspath, write_file diff --git a/gluon/packages/dal b/gluon/packages/dal index 79ec4e99..77e0d3f3 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 79ec4e99d09b768ca2dcc42ac32f408529e0745f +Subproject commit 77e0d3f386e326a2016a7e2139900ce72d7ddada diff --git a/gluon/recfile.py b/gluon/recfile.py index 9881e7ac..51f81cf9 100755 --- a/gluon/recfile.py +++ b/gluon/recfile.py @@ -10,7 +10,7 @@ Generates names for cache and session files -------------------------------------------- """ import os -from pydal._compat import builtin +from gluon._compat import builtin def generate(filename, depth=2, base=512): if os.path.sep in filename: diff --git a/gluon/restricted.py b/gluon/restricted.py index 6f4493b2..c67bcbf4 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -10,7 +10,7 @@ Restricted environment to execute application's code """ import sys -from pydal._compat import pickle, ClassType +from gluon._compat import pickle, ClassType import traceback import types import os diff --git a/gluon/rewrite.py b/gluon/rewrite.py index bced6731..fc14557a 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -27,7 +27,7 @@ from gluon.storage import Storage, List from gluon.http import HTTP from gluon.fileutils import abspath, read_file from gluon.settings import global_settings -from pydal._compat import urllib_unquote, urllib_quote, iteritems, xrange +from gluon._compat import urllib_unquote, urllib_quote, iteritems, xrange isdir = os.path.isdir isfile = os.path.isfile diff --git a/gluon/rocket.py b/gluon/rocket.py index c3eb5006..c41ac32d 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -11,7 +11,7 @@ import errno import socket import logging import platform -from pydal._compat import iteritems, to_bytes, StringIO, urllib_unquote +from gluon._compat import iteritems, to_bytes, StringIO, urllib_unquote # Define Constants VERSION = '1.2.6' diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index 0aea74d7..dbf84eca 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -10,7 +10,7 @@ Cross-site scripting (XSS) defense ----------------------------------- """ -from pydal._compat import HTMLParser, urlparse, entitydefs, basestring +from gluon._compat import HTMLParser, urlparse, entitydefs, basestring from cgi import escape from formatter import AbstractFormatter from xml.sax.saxutils import quoteattr diff --git a/gluon/scheduler.py b/gluon/scheduler.py index cc429591..1b9a478e 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -29,7 +29,7 @@ from json import loads, dumps from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_EMPTY_OR from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB from gluon.utils import web2py_uuid -from pydal._compat import Queue, long, iteritems +from gluon._compat import Queue, long, iteritems from gluon.storage import Storage USAGE = """ diff --git a/gluon/serializers.py b/gluon/serializers.py index 823c5b2b..d53740d7 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -10,7 +10,7 @@ from gluon.html import TAG, XmlComponent, xmlescape from gluon.languages import lazyT import gluon.contrib.rss2 as rss2 import json as json_parser -from pydal._compat import long, to_native, unicodeT +from gluon._compat import long, to_native, unicodeT have_yaml = True try: diff --git a/gluon/settings.py b/gluon/settings.py index 1ef8fcad..cb46648e 100644 --- a/gluon/settings.py +++ b/gluon/settings.py @@ -10,7 +10,7 @@ import os import sys import platform from gluon.storage import Storage -from pydal._compat import PY2 +from gluon._compat import PY2 global_settings = Storage() settings = global_settings # legacy compatibility @@ -41,5 +41,5 @@ global_settings.is_jython = \ global_settings.is_source = os.path.exists(os.path.join( global_settings.gluon_parent, 'web2py.py')) - + global_settings.is_py2 = PY2 diff --git a/gluon/shell.py b/gluon/shell.py index 29462491..95359c3a 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -30,7 +30,7 @@ from gluon.globals import Request, Response, Session from gluon.storage import Storage, List from gluon.admin import w2p_unpack from pydal.base import BaseAdapter -from pydal._compat import iteritems, ClassType +from gluon._compat import iteritems, ClassType logger = logging.getLogger("web2py") diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 3e8d8843..9ddc34d6 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -19,7 +19,7 @@ import urllib import re import os -from pydal._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native +from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native from gluon.http import HTTP, redirect from gluon.html import XmlComponent, truncate_string from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG diff --git a/gluon/storage.py b/gluon/storage.py index dd5d78f2..818ff914 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -13,7 +13,7 @@ Provides: """ from pydal.contrib import portalocker -from pydal._compat import copyreg, pickle, PY2 +from gluon._compat import copyreg, pickle, PY2 __all__ = ['List', 'Storage', 'Settings', 'Messages', 'StorageList', 'load_storage', 'save_storage'] diff --git a/gluon/streamer.py b/gluon/streamer.py index c10c129c..dd7b78b2 100644 --- a/gluon/streamer.py +++ b/gluon/streamer.py @@ -17,7 +17,7 @@ import re import errno from gluon.http import HTTP from gluon.contenttype import contenttype -from pydal._compat import PY2 +from gluon._compat import PY2 regex_start_range = re.compile('\d+(?=\-)') diff --git a/gluon/template.py b/gluon/template.py index 5ce69655..2afc29ab 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -18,7 +18,7 @@ import os import cgi import logging from re import compile, sub, escape, DOTALL -from pydal._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native +from gluon._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native try: # have web2py diff --git a/gluon/tests/test_contenttype.py b/gluon/tests/test_contenttype.py index ba73fd42..1a476a62 100644 --- a/gluon/tests/test_contenttype.py +++ b/gluon/tests/test_contenttype.py @@ -11,7 +11,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon.contenttype import contenttype -from pydal._compat import iteritems +from gluon._compat import iteritems class TestContentType(unittest.TestCase): diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py index 012f1648..577056e4 100644 --- a/gluon/tests/test_contribs.py +++ b/gluon/tests/test_contribs.py @@ -8,7 +8,7 @@ import os from .fix_path import fix_sys_path fix_sys_path(__file__) -from pydal._compat import to_bytes +from gluon._compat import to_bytes from gluon.storage import Storage from gluon.contrib import fpdf as fpdf from gluon.contrib import pyfpdf as pyfpdf diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index fe574a8a..4c7e0b24 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -34,7 +34,7 @@ class TestDALSubclass(unittest.TestCase): db.close() def testSerialization(self): - from pydal._compat import pickle + from gluon._compat import pickle db = DAL(check_reserved=['all']) db.define_table('t_a', Field('f_a')) db.t_a.insert(f_a='test') diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index 4ef0aa10..d2336093 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -14,7 +14,7 @@ fix_sys_path(__file__) from gluon.globals import Request, Response, Session from gluon import URL -from pydal._compat import basestring +from gluon._compat import basestring def setup_clean_session(): request = Request(env={}) diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py index 18bc80f3..c861e511 100644 --- a/gluon/tests/test_html.py +++ b/gluon/tests/test_html.py @@ -17,7 +17,7 @@ from gluon.html import STYLE, TABLE, TR, TD, TAG, TBODY, THEAD, TEXTAREA, TFOOT, from gluon.storage import Storage from gluon.html import XML_pickle, XML_unpickle from gluon.html import TAG_pickler, TAG_unpickler -from pydal._compat import xrange, PY2, to_native +from gluon._compat import xrange, PY2, to_native class TestBareHelpers(unittest.TestCase): @@ -676,7 +676,7 @@ class TestBareHelpers(unittest.TestCase): # TODO: def test_markdown_serializer(self): # TODO: def test_markmin_serializer(self): - + @unittest.skipIf(not PY2, "Skipping Python 3.x tests for MARKMIN") def test_MARKMIN(self): # This test pass with python 2.7 but expected to fail under 2.6 diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index 90fd3932..acc621ea 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -15,7 +15,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon import languages -from pydal._compat import PY2 +from gluon._compat import PY2 MP_WORKING = 0 try: diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index c42881f2..edec86cd 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -21,7 +21,7 @@ from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from pydal._compat import to_bytes, to_native, PY2 +from gluon._compat import to_bytes, to_native, PY2 logger = None oldcwd = None diff --git a/gluon/tests/test_routes.py b/gluon/tests/test_routes.py index 1d2113f4..16bb8a09 100644 --- a/gluon/tests/test_routes.py +++ b/gluon/tests/test_routes.py @@ -21,7 +21,7 @@ from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from pydal._compat import to_bytes +from gluon._compat import to_bytes logger = None oldcwd = None diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 7acae127..48adce2e 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -10,7 +10,7 @@ fix_sys_path(__file__) from gluon.storage import Storage, StorageList, List from gluon.http import HTTP -from pydal._compat import pickle +from gluon._compat import pickle class TestStorage(unittest.TestCase): diff --git a/gluon/tests/test_template.py b/gluon/tests/test_template.py index ee7d3f9b..47ac8d44 100644 --- a/gluon/tests/test_template.py +++ b/gluon/tests/test_template.py @@ -66,7 +66,7 @@ class TestTemplate(unittest.TestCase): def testWithDummyFileSystem(self): from os.path import join as pjoin import contextlib - from pydal._compat import StringIO + from gluon._compat import StringIO from gluon.restricted import RestrictedError @contextlib.contextmanager diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index a615063f..fa963144 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -22,7 +22,7 @@ from gluon.dal import DAL, Field from pydal.objects import Table from gluon import tools from gluon.tools import Auth, Mail, Recaptcha, Recaptcha2, prettydate, Expose -from pydal._compat import PY2 +from gluon._compat import PY2 from gluon.globals import Request, Response, Session from gluon.storage import Storage from gluon.languages import translator diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index c77a5b2c..19a18e16 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -13,7 +13,7 @@ fix_sys_path(__file__) from gluon.validators import * -from pydal._compat import PY2, to_bytes +from gluon._compat import PY2, to_bytes class TestValidators(unittest.TestCase): diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index 89f42177..f7e9ddf3 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -16,7 +16,7 @@ from .fix_path import fix_sys_path fix_sys_path(__file__) from gluon.contrib.webclient import WebClient -from pydal._compat import urllib2, PY2 +from gluon._compat import urllib2, PY2 webserverprocess = None diff --git a/gluon/tools.py b/gluon/tools.py index d5178850..f88a01bc 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -12,7 +12,7 @@ Auth, Mail, PluginManager and various utilities import base64 from functools import reduce -from pydal._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ +from gluon._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ MIMEText, Encoders, Charset, long, urllib_quote, iteritems, to_bytes, to_native, add_charset, \ charset_QP, basestring, unicodeT, to_unicode import datetime @@ -1193,7 +1193,7 @@ class AuthJWT(object): return '%s$%s' % (request.now, auth.user_id) return "No auth info!" - + """ @@ -1246,7 +1246,7 @@ class AuthJWT(object): self.before_authorization = before_authorization self.max_header_length = max_header_length self.recvd_token = None - + @staticmethod def jwt_b64e(string): string = to_bytes(string) @@ -1362,7 +1362,7 @@ class AuthJWT(object): """ The part that issues (and refreshes) tokens. Used in a controller, given myjwt is the istantiated class, as - + @myjwt.allow_jwt(required=False, verify_expiration=False) def api_auth(): return myjwt.jwt_token_manager() @@ -1440,7 +1440,7 @@ class AuthJWT(object): self.recvd_token = token return token - + def allows_jwt(self, otherwise=None, required=True, verify_expiration=True, token_param='_token'): """ The decorator that takes care of injecting auth info in the decorated action. @@ -1832,7 +1832,7 @@ class Auth(object): # ## what happens after registration? settings = self.settings = Settings() - settings.update(Auth.default_settings) + settings.update(Auth.default_settings) host = self.select_host(request.env.http_host, host_names) settings.update( cas_domains=[host], diff --git a/gluon/utf8.py b/gluon/utf8.py index f9672ee7..21fd12c4 100644 --- a/gluon/utf8.py +++ b/gluon/utf8.py @@ -11,7 +11,7 @@ Utilities and class for UTF8 strings managing ---------------------------------------------- """ from __future__ import print_function -from pydal._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode, to_native +from gluon._compat import builtin as __builtin__, unicodeT, iteritems, to_unicode, to_native __all__ = ['Utf8'] diff --git a/gluon/utils.py b/gluon/utils.py index 79fd8604..8589cd2f 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -23,7 +23,7 @@ import logging import socket import base64 import zlib -from pydal._compat import basestring, pickle, PY2, xrange, to_bytes, to_native +from gluon._compat import basestring, pickle, PY2, xrange, to_bytes, to_native _struct_2_long_long = struct.Struct('=QQ') @@ -370,7 +370,7 @@ def local_html_escape(data, quote=False): translated. """ if PY2: - import cgi + import cgi data = cgi.escape(data, quote) return data.replace("'", "'") if quote else data else: diff --git a/gluon/validators.py b/gluon/validators.py index 2e9fd490..4181c226 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,7 +21,7 @@ import urllib import struct import decimal import unicodedata -from pydal._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native +from gluon._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce diff --git a/gluon/widget.py b/gluon/widget.py index f801b001..73692f9d 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -13,7 +13,7 @@ from __future__ import print_function import datetime import sys -from pydal._compat import StringIO, thread, xrange +from gluon._compat import StringIO, thread, xrange import time import threading import os @@ -935,7 +935,7 @@ def console(): sys.argv, other_args = sys.argv[:k], sys.argv[k + 1:] (options, args) = parser.parse_args() options.args = [options.run] + other_args - + copy_options = copy.deepcopy(options) copy_options.password = '******' global_settings.cmd_options = copy_options From 16b11c7ef26e07166e16bf4ee7b558d4f91a726a Mon Sep 17 00:00:00 2001 From: zvolsky Date: Tue, 16 Aug 2016 13:28:29 +0200 Subject: [PATCH 076/436] F redirect in admin/default/design (install of plugin) --- applications/admin/controllers/default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index bdadcf2b..95c7d842 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -1088,10 +1088,10 @@ def design(): else: session.flash = \ T('unable to create application "%s"', request.vars.filename) - redirect(URL(r=request)) + redirect(URL(r=request, args=app)) elif isinstance(request.vars.pluginfile, str): session.flash = T('plugin not specified') - redirect(URL(r=request)) + redirect(URL(r=request, args=app)) # If we have only pyc files it means that # we cannot design From 9e5bf2bf03c1dd82234d955e41c5028e547834ee Mon Sep 17 00:00:00 2001 From: zvolsky Date: Tue, 16 Aug 2016 13:38:13 +0200 Subject: [PATCH 077/436] F error message in admin/default/design (install of plugin) --- applications/admin/controllers/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 95c7d842..86f315c0 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -1087,7 +1087,7 @@ def design(): redirect(URL('design', args=app)) else: session.flash = \ - T('unable to create application "%s"', request.vars.filename) + T('unable to install plugin "%s"', filename) redirect(URL(r=request, args=app)) elif isinstance(request.vars.pluginfile, str): session.flash = T('plugin not specified') From e87378b07f143a9d90e383430afe1309094ada38 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 16 Aug 2016 19:12:47 -0500 Subject: [PATCH 078/436] allow a cllback in streamer --- applications/examples/static/css/stupid.css | 4 +-- gluon/packages/dal | 2 +- gluon/streamer.py | 35 +++++++++++---------- scripts/setup-web2py-nginx-uwsgi-ubuntu.sh | 6 ++-- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/applications/examples/static/css/stupid.css b/applications/examples/static/css/stupid.css index 08f42938..80bf9b09 100644 --- a/applications/examples/static/css/stupid.css +++ b/applications/examples/static/css/stupid.css @@ -312,8 +312,8 @@ a:not(.btn):not(.noeffect):after { /*** accordion ***/ .accordion>label{cursor:pointer} -.accordion>input ~ label:before {content:"ā–² "; color:#ddd} -.accordion>input:checked ~ label:before {content:"ā–¼ "; color:#ddd} +.accordion>input ~ label:before {content:"\25b2"; color:#ddd} +.accordion>input:checked ~ label:before {content:"\25bc"; color:#ddd} .accordion>input {display:none} .accordion>input:checked ~ *:not(label) { max-height: 1000px !important; diff --git a/gluon/packages/dal b/gluon/packages/dal index 77e0d3f3..79ec4e99 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 77e0d3f386e326a2016a7e2139900ce72d7ddada +Subproject commit 79ec4e99d09b768ca2dcc42ac32f408529e0745f diff --git a/gluon/streamer.py b/gluon/streamer.py index dd7b78b2..cfedb264 100644 --- a/gluon/streamer.py +++ b/gluon/streamer.py @@ -25,22 +25,25 @@ regex_stop_range = re.compile('(?<=\-)\d+') DEFAULT_CHUNK_SIZE = 64 * 1024 -def streamer(stream, chunk_size=DEFAULT_CHUNK_SIZE, bytes=None): - offset = 0 - while bytes is None or offset < bytes: - if not bytes is None and bytes - offset < chunk_size: - chunk_size = bytes - offset - data = stream.read(chunk_size) - length = len(data) - if not length: - break - else: - yield data - if length < chunk_size: - break - offset += length - stream.close() - +def streamer(stream, chunk_size=DEFAULT_CHUNK_SIZE, bytes=None, callback=None): + try: + offset = 0 + while bytes is None or offset < bytes: + if not bytes is None and bytes - offset < chunk_size: + chunk_size = bytes - offset + data = stream.read(chunk_size) + length = len(data) + if not length: + break + else: + yield data + if length < chunk_size: + break + offset += length + finally: + stream.close() + if callback: + callback() def stream_file_or_304_or_206( static_file, diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh index 883aec81..4755081b 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -214,18 +214,18 @@ then fi /etc/init.d/nginx start -start uwsgi-emperor +systemctl start emperor.uwsgi.service echo < Date: Wed, 17 Aug 2016 16:08:05 -0400 Subject: [PATCH 079/436] added PKCS #7 v1.5 padding updated secure_dumps to use SHA256 fixed secure_dumps using truncated encryption_key hash fixed pad ignoring blocksize argument --- gluon/tests/test_utils.py | 37 ++++++++++++++++++++-- gluon/utils.py | 66 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/gluon/tests/test_utils.py b/gluon/tests/test_utils.py index 21db1d88..fde8d54f 100644 --- a/gluon/tests/test_utils.py +++ b/gluon/tests/test_utils.py @@ -10,7 +10,9 @@ fix_sys_path(__file__) from gluon.utils import md5_hash, compare, is_valid_ip_address, web2py_uuid +import pickle import hashlib +import gluon.utils from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from gluon.utils import simple_hash, get_digest, secure_dumps, secure_loads, basestring @@ -85,7 +87,30 @@ class TestUtils(unittest.TestCase): # TODO: def test_get_callable_argspec(self): - # TODO: def test_pad(self): + def test_pad(self): + test_cases = [ + (16, b'mydata'), # verify data padding and unpad identity + (32, b'mydata '), # verify space is not stripped + (8, b'mydata\x01'), # verify "padding" bytes are ignored + (4, b'mydata'), # verify multiblock behavior + (2, b''), # verify empty string behavior + ] + for (testlen,teststr) in test_cases: + padded = gluon.utils.pad(teststr,testlen) + unpadded = gluon.utils.unpad(padded,testlen) + self.assertTrue(len(padded) > len(teststr)) + self.assertTrue(len(padded)%testlen == 0) + self.assertEqual(teststr, unpadded) + + testobj = {'a': 1, 'b': 2} + pickled = pickle.dumps(testobj) + padded = gluon.utils.pad(pickled) + unpadded = gluon.utils.unpad(padded) + unpickled = pickle.loads(unpadded) + self.assertEqual(pickled, unpadded) + self.assertEqual(testobj, unpickled) + self.assertTrue(len(padded) > len(pickled)) + self.assertTrue(len(padded)%32==0) def test_secure_dumps_and_loads(self): """ Tests secure_dumps and secure_loads""" @@ -95,7 +120,13 @@ class TestUtils(unittest.TestCase): original = secure_loads(secured, testkey) self.assertEqual(testobj, original) self.assertTrue(isinstance(secured, bytes)) - self.assertTrue(b':' in secured) + self.assertTrue(secured.count(b':') == 2) + + secured_deprecated = gluon.utils.secure_dumps_deprecated(testobj, testkey) + original_deprecated = secure_loads(secured_deprecated, testkey) + self.assertEqual(testobj, original_deprecated) + self.assertTrue(isinstance(secured_deprecated, bytes)) + self.assertTrue(secured_deprecated.count(b':') == 1) large_testobj = [x for x in range(1000)] secured_comp = secure_dumps(large_testobj, testkey, compression_level=9) @@ -115,7 +146,7 @@ class TestUtils(unittest.TestCase): self.assertEqual(wrong2, None) wrong3 = secure_loads(secured, 'wrongkey', 'wronghash') self.assertEqual(wrong3, None) - wrong4 = secure_loads('abc', 'a', 'b') + wrong4 = secure_loads(b'abc', 'a', 'b') self.assertEqual(wrong4, None) # TODO: def test_initialize_urandom(self): diff --git a/gluon/utils.py b/gluon/utils.py index 8589cd2f..a9e9f842 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -157,25 +157,81 @@ def get_callable_argspec(fn): return inspect.getargspec(inspectable) -def pad(s, n=32, padchar=b' '): - return s + (32 - len(s) % 32) * padchar +def pad(s, n=32): + # PKCS7v1.5 https://www.ietf.org/rfc/rfc2315.txt + padlen = n - len(s) % n + return s + bytes(bytearray(padlen * [padlen])) + + +def unpad(s, n=32): + padlen = s[-1] + if isinstance(padlen,str): + padlen = ord(padlen) # python2 + if (padlen < 1) | (padlen > n): # avoid short-circuit + # return garbage to minimize side channels + return bytes(bytearray(len(s)*[0])) + return s[:-padlen] def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): + dump = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + if compression_level: + dump = zlib.compress(dump, compression_level) + encryption_key = to_bytes(encryption_key) + if not hash_key: + hash_key = hashlib.sha256(encryption_key).digest() + cipher, IV = AES_new(pad(encryption_key)[:32]) + encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) + signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.sha256).hexdigest()) + return b'hmac256:' + signature + b':' + encrypted_data + + +def secure_loads(data, encryption_key, hash_key=None, compression_level=None): + components = data.count(b':') + if components == 1: + return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) + if components != 2: + return None + version,signature,encrypted_data = data.split(b':', 2) + if version != b'hmac256': + return None + encryption_key = to_bytes(encryption_key) + if not hash_key: + hash_key = hashlib.sha256(encryption_key).digest() + actual_signature = hmac.new(to_bytes(hash_key), encrypted_data, hashlib.sha256).hexdigest() + if not compare(to_native(signature), actual_signature): + return None + encrypted_data = base64.urlsafe_b64decode(encrypted_data) + IV, encrypted_data = encrypted_data[:16], encrypted_data[16:] + cipher, _ = AES_new(pad(encryption_key)[:32], IV=IV) + try: + data = unpad(cipher.decrypt(encrypted_data)) + if compression_level: + data = zlib.decompress(data) + return pickle.loads(data) + except Exception as e: + return None + + +def __pad_deprecated(s, n=32, padchar=b' '): + return s + (32 - len(s) % 32) * padchar + + +def secure_dumps_deprecated(data, encryption_key, hash_key=None, compression_level=None): encryption_key = to_bytes(encryption_key) if not hash_key: hash_key = sha1(encryption_key).hexdigest() dump = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) if compression_level: dump = zlib.compress(dump, compression_level) - key = pad(encryption_key)[:32] + key = __pad_deprecated(encryption_key)[:32] cipher, IV = AES_new(key) encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest()) return signature + b':' + encrypted_data -def secure_loads(data, encryption_key, hash_key=None, compression_level=None): +def secure_loads_deprecated(data, encryption_key, hash_key=None, compression_level=None): encryption_key = to_bytes(encryption_key) data = to_native(data) if ':' not in data: @@ -187,7 +243,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None): actual_signature = hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest() if not compare(signature, actual_signature): return None - key = pad(encryption_key)[:32] + key = __pad_deprecated(encryption_key)[:32] encrypted_data = base64.urlsafe_b64decode(encrypted_data) IV, encrypted_data = encrypted_data[:16], encrypted_data[16:] cipher, _ = AES_new(key, IV=IV) From b96d0697eecb3c8e5c7b466ea2d483a73b12e0a3 Mon Sep 17 00:00:00 2001 From: kelson Date: Wed, 17 Aug 2016 16:18:44 -0400 Subject: [PATCH 080/436] use constant-time comparison to verify websocket hmac --- gluon/contrib/websocket_messaging.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/websocket_messaging.py b/gluon/contrib/websocket_messaging.py index 84fe55cc..ae8fbc4f 100644 --- a/gluon/contrib/websocket_messaging.py +++ b/gluon/contrib/websocket_messaging.py @@ -93,6 +93,8 @@ import sys import optparse import time import sys +import gluon.utils + if (sys.version_info[0] == 2): from urllib import urlencode, urlopen def to_bytes(obj, charset='utf-8', errors='strict'): @@ -141,7 +143,8 @@ class PostHandler(tornado.web.RequestHandler): print('%s:MESSAGE to %s:%s' % (time.time(), group, message)) if hmac_key: signature = self.request.arguments['signature'][0] - if not to_bytes(hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()) == signature: + actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() + if not gluon.utils.compare(to_native(signature), actual_signature): self.send_error(401) for client in listeners.get(group, []): client.write_message(message) @@ -160,13 +163,14 @@ class TokenHandler(tornado.web.RequestHandler): message = self.request.arguments['message'][0] if hmac_key: signature = self.request.arguments['signature'][0] - if not to_bytes(hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()) == signature: + actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() + if not gluon.utils.compare(to_native(signature), actual_signature): self.send_error(401) tokens[message] = None class DistributeHandler(tornado.websocket.WebSocketHandler): - + def check_origin(self, origin): return True From f8f96ba613e7068bfdc7badd4482cd23091bbb63 Mon Sep 17 00:00:00 2001 From: kelson Date: Wed, 17 Aug 2016 16:50:06 -0400 Subject: [PATCH 081/436] fixed `/app/static` throwing AttributeError instead of HTTP(404) --- gluon/rewrite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/rewrite.py b/gluon/rewrite.py index fc14557a..fc4d1153 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -638,9 +638,10 @@ def regex_url_in(request, environ): request.raw_args = request.raw_args[1:] if match.group('c') == 'static': application = match.group('a') - version, filename = None, match.group('z').replace(' ', '_') + version, filename = None, match.group('z') if not filename: raise HTTP(404) + filename = filename.replace(' ','_') items = filename.split('/', 1) if regex_version.match(items[0]): version, filename = items From fe058bf8179ba279ccf25318f99d21abd825537a Mon Sep 17 00:00:00 2001 From: kelson Date: Wed, 17 Aug 2016 17:17:34 -0400 Subject: [PATCH 082/436] fixed auth.add_membership succeeding with invalid group_id/user_id --- gluon/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gluon/tools.py b/gluon/tools.py index f88a01bc..e0838f9f 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -4439,6 +4439,10 @@ class Auth(object): group_id = self.id_group(group_id) # interpret group_id as a role if not user_id and self.user: user_id = self.user.id + if not group_id: + raise ValueError('group_id not provided or invalid') + if not user_id: + raise ValueError('user_id not provided or invalid') membership = self.table_membership() db = membership._db record = db((membership.user_id == user_id) & From 2b0166a8de808b758ec6b21fe0aefae18b12c94f Mon Sep 17 00:00:00 2001 From: kelson Date: Wed, 17 Aug 2016 18:14:39 -0400 Subject: [PATCH 083/436] add_membership unittests --- gluon/tests/test_tools.py | 49 +++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index fa963144..5454ccf8 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -870,13 +870,48 @@ class TestAuth(unittest.TestCase): count_log_event_test_after = self.db(self.db.auth_event.id > 0).count() self.assertEqual(count_log_event_test_after, count_log_event_test_before) - # Waiting guidance : https://github.com/web2py/web2py/issues/1300 - # def test_add_membership(self): - # self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() - # # failing case - # rtn = self.auth.add_membership('not_existing_role_name') - # # self.assertEqual(rtn, 'test') - # self.assertEqual(self.db(self.db.auth_group.role == 'not_existing_role_name').select().first(), 'test') + def test_add_membership(self): + user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare() + user_id = user.id + role_name = 'test_add_membership_group' + group_id = self.auth.add_group(role_name) + self.assertFalse(self.auth.has_membership(role_name)) + + self.auth.add_membership(group_id=group_id, user_id=user_id) + self.assertTrue(self.auth.has_membership(group_id, user_id=user_id)) + self.auth.del_membership(group_id=group_id, user_id=user_id) + self.assertFalse(self.auth.has_membership(group_id, user_id=user_id)) + + self.auth.add_membership(role=role_name, user_id=user_id) + self.assertTrue(self.auth.has_membership(group_id, user_id=user_id)) + self.auth.del_membership(group_id=group_id, user_id=user_id) + self.assertFalse(self.auth.has_membership(group_id, user_id=user_id)) + + with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + self.auth.add_membership(group_id='not_existing_group_name', user_id=user_id) + with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + self.auth.add_membership(role='not_existing_role_name', user_id=user_id) + with self.assertRaisesRegexp(ValueError, '^user_id not provided or invalid$'): + self.auth.add_membership(group_id=group_id, user_id=None) + with self.assertRaisesRegexp(ValueError, '^user_id not provided or invalid$'): + self.auth.add_membership(role=role_name, user_id=None) + + self.auth.login_user(user) + + self.auth.add_membership(group_id=group_id) + self.assertTrue(self.auth.has_membership(group_id)) + self.auth.del_membership(group_id=group_id) + self.assertFalse(self.auth.has_membership(group_id)) + + self.auth.add_membership(role=role_name) + self.assertTrue(self.auth.has_membership(group_id)) + self.auth.del_membership(group_id=group_id) + self.assertFalse(self.auth.has_membership(group_id)) + + with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + self.auth.add_membership(group_id='not_existing_group_name') + with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + self.auth.add_membership(role='not_existing_role_name') def test_del_membership(self): self.auth.login_user(self.db(self.db.auth_user.username == 'bart').select().first()) # bypass login_bare() From 3bed4eee8c4a0a3c06636feff2bd754f6a309302 Mon Sep 17 00:00:00 2001 From: kelson Date: Wed, 17 Aug 2016 18:27:44 -0400 Subject: [PATCH 084/436] fixed deprecated assertRaisesRegexp added additional coverage tests --- gluon/tests/test_tools.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index 5454ccf8..91a9de6d 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -887,13 +887,13 @@ class TestAuth(unittest.TestCase): self.auth.del_membership(group_id=group_id, user_id=user_id) self.assertFalse(self.auth.has_membership(group_id, user_id=user_id)) - with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + with self.myassertRaisesRegex(ValueError, '^group_id not provided or invalid$'): self.auth.add_membership(group_id='not_existing_group_name', user_id=user_id) - with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + with self.myassertRaisesRegex(ValueError, '^group_id not provided or invalid$'): self.auth.add_membership(role='not_existing_role_name', user_id=user_id) - with self.assertRaisesRegexp(ValueError, '^user_id not provided or invalid$'): + with self.myassertRaisesRegex(ValueError, '^user_id not provided or invalid$'): self.auth.add_membership(group_id=group_id, user_id=None) - with self.assertRaisesRegexp(ValueError, '^user_id not provided or invalid$'): + with self.myassertRaisesRegex(ValueError, '^user_id not provided or invalid$'): self.auth.add_membership(role=role_name, user_id=None) self.auth.login_user(user) @@ -908,9 +908,23 @@ class TestAuth(unittest.TestCase): self.auth.del_membership(group_id=group_id) self.assertFalse(self.auth.has_membership(group_id)) - with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + # default usage (group_id=role_name) + self.auth.add_membership(role_name) + self.assertTrue(self.auth.has_membership(group_id)) + self.auth.del_membership(group_id=group_id) + self.assertFalse(self.auth.has_membership(group_id)) + + # re-adding a membership should return the existing membership + record0_id = self.auth.add_membership(group_id) + self.assertTrue(self.auth.has_membership(group_id)) + record1_id = self.auth.add_membership(group_id) + self.assertEqual(record0_id, record1_id) + self.auth.del_membership(group_id=group_id) + self.assertFalse(self.auth.has_membership(group_id)) + + with self.myassertRaisesRegex(ValueError, '^group_id not provided or invalid$'): self.auth.add_membership(group_id='not_existing_group_name') - with self.assertRaisesRegexp(ValueError, '^group_id not provided or invalid$'): + with self.myassertRaisesRegex(ValueError, '^group_id not provided or invalid$'): self.auth.add_membership(role='not_existing_role_name') def test_del_membership(self): From 2a3f90a30fe6ec5d8548617b10d65638da5ce07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 24 Aug 2016 15:47:46 -0400 Subject: [PATCH 085/436] No cleartext input password for setup nginx uwsgi ubuntu script --- scripts/setup-web2py-nginx-uwsgi-ubuntu.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh index 4755081b..3669f055 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -19,7 +19,8 @@ done if [ "$nopassword" -eq 0 ] then echo -e "Web2py Admin Password: \c " - read PW + read -s PW + printf "\n" # fix no new line artifact of "read -s" to avoid cleartext password fi # Upgrade and install needed software apt-get update From 93054a2dca74e5cb4fabe4493c29f035099b8c0e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Aug 2016 00:09:29 -0500 Subject: [PATCH 086/436] pydal v16.08 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 79ec4e99..77e0d3f3 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 79ec4e99d09b768ca2dcc42ac32f408529e0745f +Subproject commit 77e0d3f386e326a2016a7e2139900ce72d7ddada From 1bf499e3beef99d0a487800cc84ed129d35a743f Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 29 Aug 2016 23:01:22 +0200 Subject: [PATCH 087/436] first step towards dropping sys.path dance --- CHANGELOG | 6 ++++-- gluon/__init__.py | 6 ++++-- gluon/sql.py | 2 +- gluon/tests/test_appadmin.py | 8 -------- gluon/tests/test_cache.py | 9 --------- gluon/tests/test_compileapp.py | 14 ++++++-------- gluon/tests/test_contenttype.py | 6 ------ gluon/tests/test_contribs.py | 5 ----- gluon/tests/test_dal.py | 8 -------- gluon/tests/test_fileutils.py | 7 ------- gluon/tests/test_globals.py | 6 ------ gluon/tests/test_html.py | 6 ------ gluon/tests/test_http.py | 7 ------- gluon/tests/test_is_url.py | 7 ------- gluon/tests/test_languages.py | 9 --------- gluon/tests/test_old_doctests.py | 7 +------ gluon/tests/test_recfile.py | 5 ----- gluon/tests/test_router.py | 14 +------------- gluon/tests/test_routes.py | 12 ------------ gluon/tests/test_scheduler.py | 3 --- gluon/tests/test_serializers.py | 6 ------ gluon/tests/test_sqlhtml.py | 10 +--------- gluon/tests/test_storage.py | 7 ------- gluon/tests/test_template.py | 7 ------- gluon/tests/test_tools.py | 7 ------- gluon/tests/test_utils.py | 15 +++------------ gluon/tests/test_validators.py | 8 -------- gluon/tests/test_web.py | 9 +-------- 28 files changed, 22 insertions(+), 194 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 208a41a9..5f5c52a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,8 @@ - py3.5 syntax compatible (see #1353 for details) - dropped web shell from admin - scheduler new feature: you can now specify intervals with cron +- tests can only be run with the usual web2py.py --run_system_tests OR with + python -m unittest -v gluon.tests on the root dir ## 2.14.6 @@ -12,7 +14,7 @@ - Fixed some newly discovered security issues in admin: CSRF vulnerability in admin that allows disabling apps Brute force password attack vulnerability in admin - (thanks Narendra and Leonel) + (thanks Narendra and Leonel) ## 2.14.1-5 @@ -129,7 +131,7 @@ Many thanks to Richard and Simone for their work and dedication. - Added Portuguese, Catalan, and Burmese translations - Allow map_hyphen to work for application names, thanks Tim Nyborg - New module appconfig.py, thanks Niphlod -- Added geospatial support to Teradata adaptor, thanks Andrew Willimott +- Added geospatial support to Teradata adaptor, thanks Andrew Willimott - Many bug fixes diff --git a/gluon/__init__.py b/gluon/__init__.py index c0e306f2..6a5e25f1 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -16,8 +16,10 @@ __all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE import os import sys try: - sys.path.insert(1,os.path.join( - os.path.dirname(os.path.abspath(__file__)), "packages", "dal")) + pydalpath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "packages", "dal") + if pydalpath not in sys.path: + sys.path.append(pydalpath) import pydal sys.modules['pydal'] = pydal except ImportError: diff --git a/gluon/sql.py b/gluon/sql.py index c70d7295..3339ba07 100644 --- a/gluon/sql.py +++ b/gluon/sql.py @@ -12,7 +12,7 @@ Just for backward compatibility """ __all__ = ['DAL', 'Field', 'DRIVERS'] -from dal import DAL, Field, SQLCustomType +from gluon.dal import DAL, Field, SQLCustomType from pydal.base import BaseAdapter from pydal.drivers import DRIVERS from pydal.objects import Table, Query, Set, Expression, Row, Rows diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py index c47ac54f..9d7e0493 100644 --- a/gluon/tests/test_appadmin.py +++ b/gluon/tests/test_appadmin.py @@ -8,11 +8,6 @@ from __future__ import print_function import os import sys import unittest -import json - -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon.compileapp import run_controller_in, run_view_in @@ -165,6 +160,3 @@ class TestAppAdmin(unittest.TestCase): request._vars = data self.assertRaises(HTTP, self.run_function) - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_cache.py b/gluon/tests/test_cache.py index 47e11d34..4fcda0fb 100644 --- a/gluon/tests/test_cache.py +++ b/gluon/tests/test_cache.py @@ -6,10 +6,6 @@ """ import os import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - from gluon.storage import Storage from gluon.cache import CacheInRam, CacheOnDisk, Cache @@ -147,8 +143,3 @@ class TestCache(unittest.TestCase): db.t_a.drop() db.close() - -if __name__ == '__main__': - setUpModule() # pre-python-2.7 - unittest.main() - tearDownModule() diff --git a/gluon/tests/test_compileapp.py b/gluon/tests/test_compileapp.py index 956c28a4..95645012 100644 --- a/gluon/tests/test_compileapp.py +++ b/gluon/tests/test_compileapp.py @@ -4,19 +4,20 @@ """ Unit tests for utils.py """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) +import os +import shutil from gluon.compileapp import compile_application, remove_compiled_application from gluon.fileutils import w2p_pack, w2p_unpack from gluon.globals import Request -from gluon.admin import app_compile, app_create, app_cleanup, check_new_version, app_uninstall +from gluon.admin import app_compile, app_create, app_cleanup, check_new_version +from gluon.admin import app_uninstall from gluon.main import global_settings -import os, shutil + WEB2PY_VERSION_URL = "http://web2py.com/examples/default/version" + class TestPack(unittest.TestCase): """ Tests the compileapp.py module """ @@ -55,6 +56,3 @@ class TestPack(unittest.TestCase): self.assertEqual(app_uninstall(new_app, request), True) self.assertNotEqual(check_new_version(global_settings.web2py_version, WEB2PY_VERSION_URL), -1) return - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_contenttype.py b/gluon/tests/test_contenttype.py index 1a476a62..cb71a8d5 100644 --- a/gluon/tests/test_contenttype.py +++ b/gluon/tests/test_contenttype.py @@ -6,9 +6,7 @@ """ import unittest -from .fix_path import fix_sys_path -fix_sys_path(__file__) from gluon.contenttype import contenttype from gluon._compat import iteritems @@ -37,7 +35,3 @@ class TestContentType(unittest.TestCase): # test without dot extension rtn = contenttype('png') self.assertEqual(rtn, 'text/plain; charset=utf-8') - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py index 577056e4..633fa08b 100644 --- a/gluon/tests/test_contribs.py +++ b/gluon/tests/test_contribs.py @@ -5,9 +5,7 @@ import unittest import os -from .fix_path import fix_sys_path -fix_sys_path(__file__) from gluon._compat import to_bytes from gluon.storage import Storage from gluon.contrib import fpdf as fpdf @@ -67,6 +65,3 @@ class TestContribs(unittest.TestCase): self.assertEqual(myappconfig.take('config3.key2'), 2) current.request = {} - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 4c7e0b24..d4e09df3 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -7,9 +7,6 @@ import sys import os import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon.dal import DAL, Field @@ -118,8 +115,3 @@ class TestDALAdapters(unittest.TestCase): os.environ["DB"] = "postgres:pg8000://postgres:@localhost/pydal" result = self._run_tests() self.assertTrue(result) - - -if __name__ == '__main__': - unittest.main() - tearDownModule() diff --git a/gluon/tests/test_fileutils.py b/gluon/tests/test_fileutils.py index cd405b00..5e408796 100644 --- a/gluon/tests/test_fileutils.py +++ b/gluon/tests/test_fileutils.py @@ -3,9 +3,6 @@ import unittest import datetime -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon.fileutils import parse_version @@ -25,7 +22,3 @@ class TestFileUtils(unittest.TestCase): # Semantic Beta rtn = parse_version('Version 2.14.1-beta+timestamp.2016.03.21.22.35.26') self.assertEqual(rtn, (2, 14, 1, 'beta', datetime.datetime(2016, 3, 21, 22, 35, 26))) - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index d2336093..1ad85001 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -8,9 +8,6 @@ import re import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon.globals import Request, Response, Session from gluon import URL @@ -185,6 +182,3 @@ class testResponse(unittest.TestCase): current.session._fixup_before_save() cookie = str(current.response.cookies) self.assertTrue('httponly' not in cookie.lower()) - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py index c861e511..d7a75517 100644 --- a/gluon/tests/test_html.py +++ b/gluon/tests/test_html.py @@ -6,9 +6,7 @@ """ import unittest -from .fix_path import fix_sys_path -fix_sys_path(__file__) from gluon.html import A, ASSIGNJS, B, BEAUTIFY, P, BODY, BR, BUTTON, CAT, CENTER, CODE, COL, COLGROUP, DIV, SPAN, URL, verifyURL from gluon.html import truncate_string, EM, FIELDSET, FORM, H1, H2, H3, H4, H5, H6, HEAD, HR, HTML, I, IFRAME, IMG, INPUT, EMBED @@ -706,7 +704,3 @@ class TestData(unittest.TestCase): def test_Adata(self): self.assertEqual(A('<>', data=dict(abc='', cde='standard'), _a='1', _b='2').xml(), b'<>') - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_http.py b/gluon/tests/test_http.py index a344743b..7091b0a6 100644 --- a/gluon/tests/test_http.py +++ b/gluon/tests/test_http.py @@ -4,10 +4,6 @@ """Unit tests for http.py """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - from gluon.http import HTTP, defined_status @@ -39,6 +35,3 @@ class TestHTTP(unittest.TestCase): gen_status_str(code, message)) # test wrong call detection - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_is_url.py b/gluon/tests/test_is_url.py index d75724c4..d4a3162b 100644 --- a/gluon/tests/test_is_url.py +++ b/gluon/tests/test_is_url.py @@ -5,10 +5,6 @@ Unit tests for IS_URL() """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - from gluon.validators import IS_URL, IS_HTTP_URL, IS_GENERIC_URL from gluon.validators import unicode_to_ascii_authority @@ -694,6 +690,3 @@ class TestSimple(unittest.TestCase): # mode = 'generic' doesn't consider allowed_tlds rtn = IS_URL(mode='generic', allowed_tlds=['com', 'net', 'org'])('domain.ninja') self.assertEqual(rtn, ('domain.ninja', None)) - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index acc621ea..cb2fb6bc 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -10,9 +10,6 @@ import os import shutil import tempfile import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon import languages from gluon._compat import PY2 @@ -182,9 +179,3 @@ def index(): for key in ['hello', 'world', '%s %%{shop}', 'ahoy']: self.assertTrue(key in en_dict) self.assertTrue(key in pt_dict) - - - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_old_doctests.py b/gluon/tests/test_old_doctests.py index 2da2ed3e..7fc74d54 100644 --- a/gluon/tests/test_old_doctests.py +++ b/gluon/tests/test_old_doctests.py @@ -1,18 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" Unit tests for old doctests in validators.py, utf8.py, html.py, - markmin2html.py. +""" Unit tests for old doctests in utf8.py, html.py, markmin2html.py. Don't abuse doctests, web2py > 2.4.5 will accept only unittests """ import sys import os import doctest import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - def load_tests(loader, tests, ignore): diff --git a/gluon/tests/test_recfile.py b/gluon/tests/test_recfile.py index a2d711d3..03e36382 100644 --- a/gluon/tests/test_recfile.py +++ b/gluon/tests/test_recfile.py @@ -8,9 +8,7 @@ import unittest import os import shutil import uuid -from .fix_path import fix_sys_path -fix_sys_path(__file__) from gluon import recfile @@ -74,6 +72,3 @@ class TestRecfile(unittest.TestCase): self.assertRaises(IOError, recfile.remove, filename) self.assertRaises(IOError, recfile.open, filename, "r") - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index edec86cd..20b58358 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -3,25 +3,18 @@ """Unit tests for rewrite.py routers option""" from __future__ import print_function -import sys import os import unittest import tempfile import logging -if os.path.isdir('gluon'): - sys.path.insert(0,os.path.realpath('gluon')) # running from web2py base -else: - sys.path.insert(0,os.path.realpath('../../')) # running from gluon/tests/ - os.environ['web2py_path'] = os.path.realpath('../../') # for settings - from gluon.rewrite import load, filter_url, filter_err, get_effective_router, map_url_out from gluon.html import URL from gluon.fileutils import abspath from gluon.settings import global_settings from gluon.http import HTTP from gluon.storage import Storage -from gluon._compat import to_bytes, to_native, PY2 +from gluon._compat import to_bytes, PY2 logger = None oldcwd = None @@ -1518,8 +1511,3 @@ class TestRouter(unittest.TestCase): self.assertEqual(filter_url('http://welcome.com/welcome/admin/index', domain='welcome', out=True), "/welcome/admin") - -if __name__ == '__main__': - setUpModule() # pre-2.7 - unittest.main() - tearDownModule() diff --git a/gluon/tests/test_routes.py b/gluon/tests/test_routes.py index 16bb8a09..0fc5f2b8 100644 --- a/gluon/tests/test_routes.py +++ b/gluon/tests/test_routes.py @@ -9,12 +9,6 @@ import unittest import tempfile import logging -if os.path.isdir('gluon'): - sys.path.insert(0,os.path.realpath('gluon')) # running from web2py base -else: - sys.path.insert(0,os.path.realpath('../')) # running from gluon/tests/ - os.environ['web2py_path'] = os.path.realpath('../../') # for settings - from gluon.rewrite import load, filter_url, filter_err, get_effective_router, regex_filter_out, regex_select from gluon.html import URL from gluon.fileutils import abspath @@ -456,9 +450,3 @@ routes_out = [ self.assertEqual( filter_url('http://domain.com/index/a%20bc', env=True).request_uri, "/init/default/index/a bc") - - -if __name__ == '__main__': - setUpModule() # pre-2.7 - unittest.main() - tearDownModule() diff --git a/gluon/tests/test_scheduler.py b/gluon/tests/test_scheduler.py index 29cd877a..b473fe7b 100644 --- a/gluon/tests/test_scheduler.py +++ b/gluon/tests/test_scheduler.py @@ -10,9 +10,6 @@ import glob import datetime import sys -from .fix_path import fix_sys_path -fix_sys_path(__file__) - from gluon.storage import Storage from gluon.languages import translator from gluon.scheduler import JobGraph, Scheduler, CronParser diff --git a/gluon/tests/test_serializers.py b/gluon/tests/test_serializers.py index 0ae8dcb6..a7f2a44c 100644 --- a/gluon/tests/test_serializers.py +++ b/gluon/tests/test_serializers.py @@ -10,8 +10,6 @@ from .fix_path import fix_sys_path import datetime import decimal -fix_sys_path(__file__) - from gluon.serializers import * from gluon.storage import Storage # careful with the import path 'cause of isinstance() checks @@ -61,7 +59,3 @@ class TestSerializers(unittest.TestCase): # if unicode_keys is false, the standard behaviour is assumed base_load = loads_json(base_enc, unicode_keys=False) self.assertFalse(base == base_load) - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index 39b3d8fc..d0b519a0 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -8,11 +8,7 @@ import os import sys import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - -from sqlhtml import safe_int, SQLFORM, SQLTABLE +from gluon.sqlhtml import safe_int, SQLFORM, SQLTABLE DEFAULT_URI = os.getenv('DB', 'sqlite:memory') @@ -440,7 +436,3 @@ class TestSQLTABLE(unittest.TestCase): # def test_represented(self): # pass - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 48adce2e..6af22d19 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -4,9 +4,6 @@ """ Unit tests for storage.py """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon.storage import Storage, StorageList, List from gluon.http import HTTP @@ -152,7 +149,3 @@ class TestList(unittest.TestCase): a = List((1, 2, 3)) self.assertEqual(a[0], 1) self.assertEqual(a[::-1], [3, 2, 1]) - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_template.py b/gluon/tests/test_template.py index 47ac8d44..9d21d775 100644 --- a/gluon/tests/test_template.py +++ b/gluon/tests/test_template.py @@ -5,9 +5,6 @@ """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) from gluon import template from gluon.template import render @@ -136,7 +133,3 @@ class TestTemplate(unittest.TestCase): render(filename=pjoin('views', 'default', 'noescape.html'), context={'NOESCAPE': template.NOESCAPE}), '') - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index 91a9de6d..1fdf80f1 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -12,10 +12,6 @@ import smtplib import datetime import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - DEFAULT_URI = os.getenv('DB', 'sqlite:memory') from gluon.dal import DAL, Field @@ -1362,6 +1358,3 @@ class TestExpose(unittest.TestCase): with self.assertRaises(HTTP): self.make_expose(base='inside', show='link_to_file3') - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_utils.py b/gluon/tests/test_utils.py index fde8d54f..8b94844f 100644 --- a/gluon/tests/test_utils.py +++ b/gluon/tests/test_utils.py @@ -4,17 +4,12 @@ """ Unit tests for utils.py """ import unittest -from .fix_path import fix_sys_path - -fix_sys_path(__file__) +import pickle +from hashlib import md5 from gluon.utils import md5_hash, compare, is_valid_ip_address, web2py_uuid - -import pickle -import hashlib import gluon.utils -from hashlib import md5, sha1, sha224, sha256, sha384, sha512 -from gluon.utils import simple_hash, get_digest, secure_dumps, secure_loads, basestring +from gluon.utils import simple_hash, get_digest, secure_dumps, secure_loads class TestUtils(unittest.TestCase): @@ -182,7 +177,3 @@ class TestUtils(unittest.TestCase): # TODO: def test_is_loopback_ip_address(self): # TODO: def test_getipaddrinfo(self): - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 19a18e16..c98b4c26 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -7,10 +7,6 @@ import unittest import datetime import decimal import re -from .fix_path import fix_sys_path - -fix_sys_path(__file__) - from gluon.validators import * from gluon._compat import PY2, to_bytes @@ -1122,7 +1118,3 @@ this is the content of the fake file self.assertEqual(rtn, ('2001::8ffa:fe22:b3af', None)) rtn = IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af') self.assertEqual(rtn, ('2001::8ffa:fe22:b3af', 'invalid subnet provided')) - - -if __name__ == '__main__': - unittest.main() diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index f7e9ddf3..4a3ef3bf 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -9,11 +9,8 @@ import os import unittest import subprocess import time -import signal -from .fix_path import fix_sys_path -fix_sys_path(__file__) from gluon.contrib.webclient import WebClient from gluon._compat import urllib2, PY2 @@ -32,7 +29,7 @@ def startwebserver(): break path = os.path.abspath(os.path.join(path, '..')) web2py_exec = os.path.join(path, 'web2py.py') - webserverprocess = subprocess.Popen([sys.executable, web2py_exec, '-a', 'testpass']) + webserverprocess = subprocess.Popen([sys.executable, web2py_exec, '-a', 'testpass']) print('Sleeping before web2py starts...') for a in range(1, 11): time.sleep(1) @@ -141,7 +138,3 @@ class TestWeb(LiveTest): # check internal server error returned (issue 153) assert(s.status == 500) assert(s.text == xml_response) - - -if __name__ == '__main__': - unittest.main() From a1e1bf350096ada1bf668f01bf1db0ab1516d9e5 Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 31 Aug 2016 00:49:37 +0200 Subject: [PATCH 088/436] fix sys.path dance part #2 and scheduler tests for py3 --- CHANGELOG | 4 ++++ gluon/admin.py | 9 +++++-- gluon/newcron.py | 2 +- gluon/scheduler.py | 9 +++++-- gluon/tests/__init__.py | 3 +-- gluon/tests/test_old_doctests.py | 9 ++++--- gluon/widget.py | 40 ++++++++++++++++---------------- 7 files changed, 44 insertions(+), 32 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5f5c52a9..e4701ea0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,10 @@ - py3.5 syntax compatible (see #1353 for details) - dropped web shell from admin - scheduler new feature: you can now specify intervals with cron +- gluon/* removed from sys.path. Applications relying on statements like e.g. + "from storage import Storage" + will need to be rewritten with + "from gluon.storage import Storage" - tests can only be run with the usual web2py.py --run_system_tests OR with python -m unittest -v gluon.tests on the root dir diff --git a/gluon/admin.py b/gluon/admin.py index 8c70c348..3eb3bc54 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -140,7 +140,7 @@ def app_compile(app, request, skip_failed_views=False): None if everything went ok, traceback text if errors are found """ - from compileapp import compile_application, remove_compiled_application + from gluon.compileapp import compile_application, remove_compiled_application folder = apath(app, request) try: failed_views = compile_application(folder, skip_failed_views) @@ -442,9 +442,14 @@ def create_missing_folders(): path = abspath(path, gluon=True) if not os.path.exists(path): os.mkdir(path) + """ + OLD sys.path dance paths = (global_settings.gluon_parent, abspath( 'site-packages', gluon=True), abspath('gluon', gluon=True), '') - [add_path_first(path) for path in paths] + """ + paths = (global_settings.gluon_parent, abspath( + 'site-packages', gluon=True), '') + [add_path_first(path) for p in paths] def create_missing_app_folders(request): diff --git a/gluon/newcron.py b/gluon/newcron.py index 0bd1d689..1a878589 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -19,7 +19,7 @@ import sched import re import datetime import platform -import fileutils +import gluon.fileutils from functools import reduce try: import cPickle as pickle diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 1b9a478e..11dccd16 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -29,7 +29,7 @@ from json import loads, dumps from gluon import DAL, Field, IS_NOT_EMPTY, IS_IN_SET, IS_NOT_IN_DB, IS_EMPTY_OR from gluon import IS_INT_IN_RANGE, IS_DATETIME, IS_IN_DB from gluon.utils import web2py_uuid -from gluon._compat import Queue, long, iteritems +from gluon._compat import Queue, long, iteritems, PY2 from gluon.storage import Storage USAGE = """ @@ -245,7 +245,8 @@ class CronParser(object): elif period == 'hr': check = all(0 <= i <= 23 for i in values) elif period == 'dom': - check = all(1 <= i <= 31 or i == 'l' for i in values) + domrange = list(range(1, 32)) + ['l'] + check = all(i in domrange for i in values) elif period == 'mon': check = all(1 <= i <= 12 for i in values) elif period == 'dow': @@ -412,6 +413,8 @@ class CronParser(object): def _decode_list(lst): + if not PY2: + return lst newlist = [] for i in lst: if isinstance(i, unicode): @@ -423,6 +426,8 @@ def _decode_list(lst): def _decode_dict(dct): + if not PY2: + return dct newdict = {} for k, v in iteritems(dct): if isinstance(k, unicode): diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 70d55565..2f111fe3 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -1,4 +1,3 @@ -import sys from .test_http import * from .test_contenttype import * @@ -22,8 +21,8 @@ from .test_compileapp import * from .test_appadmin import * from .test_web import * from .test_sqlhtml import * +from .test_scheduler import * if sys.version[:3] == '2.7': from .test_is_url import * - from .test_scheduler import * from .test_old_doctests import * diff --git a/gluon/tests/test_old_doctests.py b/gluon/tests/test_old_doctests.py index 7fc74d54..d1303cc5 100644 --- a/gluon/tests/test_old_doctests.py +++ b/gluon/tests/test_old_doctests.py @@ -4,22 +4,21 @@ """ Unit tests for old doctests in utf8.py, html.py, markmin2html.py. Don't abuse doctests, web2py > 2.4.5 will accept only unittests """ -import sys -import os + import doctest import unittest def load_tests(loader, tests, ignore): tests.addTests( - doctest.DocTestSuite('html') + doctest.DocTestSuite('gluon.html') ) tests.addTests( - doctest.DocTestSuite('utf8') + doctest.DocTestSuite('gluon.utf8') ) tests.addTests( - doctest.DocTestSuite('contrib.markmin.markmin2html', + doctest.DocTestSuite('gluon.contrib.markmin.markmin2html', ) ) diff --git a/gluon/widget.py b/gluon/widget.py index 73692f9d..af3b7689 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -25,6 +25,7 @@ import logging import getpass from gluon import main, newcron + from gluon.fileutils import read_file, write_file, create_welcome_w2p from gluon.settings import global_settings from gluon.shell import run, test @@ -40,8 +41,8 @@ ProgramInfo = '''%s %s %s''' % (ProgramName, ProgramAuthor, ProgramVersion) -if not sys.version[:3] in ['2.7']: - msg = 'Warning: web2py requires Python 2.7 but you are running:\n%s' +if sys.version_info < (2, 7) and (3, 0) < sys.version_info < (3, 5): + msg = 'Warning: web2py requires at least Python 2.7/3.5 but you are running:\n%s' msg = msg % sys.version sys.stderr.write(msg) @@ -56,28 +57,27 @@ def run_system_tests(options): major_version = sys.version_info[0] minor_version = sys.version_info[1] call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests'] - if major_version == 2: - if minor_version in (7,): - if options.with_coverage: - try: - import coverage - coverage_config = os.environ.get( - "COVERAGE_PROCESS_START", - os.path.join('gluon', 'tests', 'coverage.ini')) - - call_args = ['coverage', 'run', '--rcfile=%s' % - coverage_config, - '-m', 'unittest', '-v', 'gluon.tests'] - except: - sys.stderr.write('Coverage was not installed, skipping\n') + if options.with_coverage: + has_coverage = False + coverage_exec = 'coverage2' if major_version == 2 else 'coverage3' + try: + import coverage + has_coverage = True + except: + sys.stderr.write('Coverage was not installed, skipping\n') + coverage_config_file = os.path.join('gluon', 'tests', 'coverage.ini') + coverage_config = os.environ.setdefault("COVERAGE_PROCESS_START", + coverage_config_file) + call_args = [coverage_exec, 'run', '--rcfile=%s' % + coverage_config, '-m', 'unittest', '-v', 'gluon.tests'] + if major_version == 2: sys.stderr.write("Python 2.7\n") + else: + sys.stderr.write("Experimental Python 3.x.\n") + if has_coverage: ret = subprocess.call(call_args) else: - sys.stderr.write("unknown python 2.x version\n") ret = 256 - else: - sys.stderr.write("Experimental Python 3.x.\n") - ret = subprocess.call(call_args) sys.exit(ret and 1) From d1d765455fbc9274bd4f92bc87493f352622bb4a Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 31 Aug 2016 02:04:45 +0200 Subject: [PATCH 089/436] fixes #1439 --- gluon/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/admin.py b/gluon/admin.py index 3eb3bc54..90b50c27 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -449,7 +449,7 @@ def create_missing_folders(): """ paths = (global_settings.gluon_parent, abspath( 'site-packages', gluon=True), '') - [add_path_first(path) for p in paths] + [add_path_first(p) for p in paths] def create_missing_app_folders(request): From 38f1e318cf21e6aaff77d788df731ebc6d3dd345 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 00:17:40 -0700 Subject: [PATCH 090/436] Update translations --- applications/admin/languages/af.py | 0 applications/admin/languages/bg.py | 0 applications/admin/languages/cs.py | 445 ++++---------- applications/admin/languages/de.py | 0 applications/admin/languages/default.py | 0 applications/admin/languages/es.py | 226 ++++++- applications/admin/languages/fr.py | 629 ++++++++++++++++---- applications/admin/languages/he.py | 0 applications/admin/languages/it.py | 0 applications/admin/languages/ja.py | 0 applications/admin/languages/my-mm.py | 0 applications/admin/languages/pl.py | 0 applications/admin/languages/plural-en.py | 0 applications/admin/languages/plural-ru.py | 0 applications/admin/languages/plural-uk.py | 0 applications/admin/languages/pt-br.py | 0 applications/admin/languages/pt.py | 0 applications/admin/languages/ro.py | 0 applications/admin/languages/ru.py | 0 applications/admin/languages/sr-cr.py | 0 applications/admin/languages/sr-lt.py | 0 applications/admin/languages/tr.py | 0 applications/admin/languages/uk.py | 0 applications/admin/languages/zh-tw.py | 0 applications/admin/languages/zh.py | 0 applications/welcome/languages/ar.py | 90 ++- applications/welcome/languages/ca.py | 9 + applications/welcome/languages/cs.py | 196 +++--- applications/welcome/languages/de.py | 41 +- applications/welcome/languages/default.py | 0 applications/welcome/languages/es.py | 74 ++- applications/welcome/languages/fr-ca.py | 28 +- applications/welcome/languages/fr.py | 41 +- applications/welcome/languages/hi.py | 29 +- applications/welcome/languages/hu.py | 28 +- applications/welcome/languages/id.py | 82 ++- applications/welcome/languages/it.py | 23 +- applications/welcome/languages/my-mm.py | 10 + applications/welcome/languages/my.py | 89 ++- applications/welcome/languages/nl.py | 28 +- applications/welcome/languages/pl.py | 28 +- applications/welcome/languages/plural-cs.py | 0 applications/welcome/languages/plural-en.py | 0 applications/welcome/languages/plural-es.py | 0 applications/welcome/languages/plural-ru.py | 0 applications/welcome/languages/plural-uk.py | 0 applications/welcome/languages/pt-br.py | 59 +- applications/welcome/languages/pt.py | 93 ++- applications/welcome/languages/ro.py | 28 +- applications/welcome/languages/ru.py | 28 +- applications/welcome/languages/sk.py | 158 +++-- applications/welcome/languages/tr.py | 61 +- applications/welcome/languages/uk.py | 24 +- applications/welcome/languages/zh-cn.py | 28 +- applications/welcome/languages/zh-tw.py | 28 +- applications/welcome/languages/zh.py | 28 +- 56 files changed, 1904 insertions(+), 727 deletions(-) mode change 100644 => 100755 applications/admin/languages/af.py mode change 100644 => 100755 applications/admin/languages/bg.py mode change 100644 => 100755 applications/admin/languages/cs.py mode change 100644 => 100755 applications/admin/languages/de.py mode change 100644 => 100755 applications/admin/languages/default.py mode change 100644 => 100755 applications/admin/languages/es.py mode change 100644 => 100755 applications/admin/languages/fr.py mode change 100644 => 100755 applications/admin/languages/he.py mode change 100644 => 100755 applications/admin/languages/it.py mode change 100644 => 100755 applications/admin/languages/ja.py mode change 100644 => 100755 applications/admin/languages/my-mm.py mode change 100644 => 100755 applications/admin/languages/pl.py mode change 100644 => 100755 applications/admin/languages/plural-en.py mode change 100644 => 100755 applications/admin/languages/plural-ru.py mode change 100644 => 100755 applications/admin/languages/plural-uk.py mode change 100644 => 100755 applications/admin/languages/pt-br.py mode change 100644 => 100755 applications/admin/languages/pt.py mode change 100644 => 100755 applications/admin/languages/ro.py mode change 100644 => 100755 applications/admin/languages/ru.py mode change 100644 => 100755 applications/admin/languages/sr-cr.py mode change 100644 => 100755 applications/admin/languages/sr-lt.py mode change 100644 => 100755 applications/admin/languages/tr.py mode change 100644 => 100755 applications/admin/languages/uk.py mode change 100644 => 100755 applications/admin/languages/zh-tw.py mode change 100644 => 100755 applications/admin/languages/zh.py mode change 100644 => 100755 applications/welcome/languages/ar.py mode change 100644 => 100755 applications/welcome/languages/ca.py mode change 100644 => 100755 applications/welcome/languages/cs.py mode change 100644 => 100755 applications/welcome/languages/de.py mode change 100644 => 100755 applications/welcome/languages/default.py mode change 100644 => 100755 applications/welcome/languages/es.py mode change 100644 => 100755 applications/welcome/languages/fr-ca.py mode change 100644 => 100755 applications/welcome/languages/fr.py mode change 100644 => 100755 applications/welcome/languages/hi.py mode change 100644 => 100755 applications/welcome/languages/hu.py mode change 100644 => 100755 applications/welcome/languages/it.py mode change 100644 => 100755 applications/welcome/languages/my-mm.py mode change 100644 => 100755 applications/welcome/languages/nl.py mode change 100644 => 100755 applications/welcome/languages/pl.py mode change 100644 => 100755 applications/welcome/languages/plural-cs.py mode change 100644 => 100755 applications/welcome/languages/plural-en.py mode change 100644 => 100755 applications/welcome/languages/plural-es.py mode change 100644 => 100755 applications/welcome/languages/plural-ru.py mode change 100644 => 100755 applications/welcome/languages/plural-uk.py mode change 100644 => 100755 applications/welcome/languages/pt-br.py mode change 100644 => 100755 applications/welcome/languages/pt.py mode change 100644 => 100755 applications/welcome/languages/ro.py mode change 100644 => 100755 applications/welcome/languages/ru.py mode change 100644 => 100755 applications/welcome/languages/sk.py mode change 100644 => 100755 applications/welcome/languages/tr.py mode change 100644 => 100755 applications/welcome/languages/uk.py mode change 100644 => 100755 applications/welcome/languages/zh-cn.py mode change 100644 => 100755 applications/welcome/languages/zh-tw.py mode change 100644 => 100755 applications/welcome/languages/zh.py diff --git a/applications/admin/languages/af.py b/applications/admin/languages/af.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/bg.py b/applications/admin/languages/bg.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/cs.py b/applications/admin/languages/cs.py old mode 100644 new mode 100755 index 0edeb8e2..1b9481ef --- a/applications/admin/languages/cs.py +++ b/applications/admin/languages/cs.py @@ -2,131 +2,89 @@ { '!langcode!': 'cs-cz', '!langname!': 'čeÅ”tina', -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': 'Kolonka "Upravit" je nepovinný výraz, například "pole1=\'novĆ”hodnota\'". (AvÅ”ak výsledky databĆ”zovĆ©ho JOINu nelze mazat ani upravovat.)', -'"User Exception" debug mode. ': '"UživatelskĆ” výjimka", Debug mód.', -'"User Exception" debug mode. An error ticket could be issued!': '"UživatelskĆ” výjimka", Debug mód. Může být vystaven chybový tiket.', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': 'Kolonka "Upravit" je nepovinný výraz, například "pole1=\'novĆ”hodnota\'". Výsledky databĆ”zovĆ©ho JOINu nemůžete mazat ani upravovat.', +'"User Exception" debug mode. An error ticket could be issued!': '"User Exception" debug mode. An error ticket could be issued!', '%%{Row} in Table': '%%{řÔdek} v tabulce', '%%{Row} selected': 'označených %%{řÔdek}', -'%s': '%s', '%s %%{row} deleted': '%s smazaných %%{zĆ”znam}', '%s %%{row} updated': '%s upravených %%{zĆ”znam}', '%s selected': '%s označených', -'%s students registered': '%s studentÅÆ registrovĆ”no', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', '(requires internet access)': '(vyžaduje připojenĆ­ k internetu)', -'(requires internet access, experimental)': '(vyžaduje internetovĆ© připojenĆ­, experimentĆ”lnĆ­)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(například "cs-cs")', -'(version %s)': '(verze %s)', -'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', -'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Searching: **%s** %%{file}': 'HledĆ”nĆ­: **%s** %%{soubor}', -'Abort': 'Ukončit', 'About': 'O programu', 'About application': 'O aplikaci', -'Accept Terms': 'Souhlasit s podmĆ­nkami', 'Access Control': 'ŘízenĆ­ přístupu', 'Add breakpoint': 'Přidat bod přeruÅ”enĆ­', -'Additional code for your application': 'DalŔí kód pro VaÅ”i aplikaci (pro příkaz import). Neběží ve specifickĆ©m režimu ani ve vlĆ”knech jako model/kontrolĆ©r/Å”ablona, ale jako standardnĆ­ python moduly. Ty tedy můžete umĆ­stit sem (pouze pro tuto aplikaci) nebo používat systĆ©mově dostupnĆ©.', -'Admin design page': 'Admin design strĆ”nka', -'admin disabled because no admin password': 'admin je zakĆ”zĆ”n, protože chybĆ­ heslo administrĆ”tora', -'admin disabled because not supported on google app engine': 'admin je zakĆ”zĆ”n kvÅÆli chybějĆ­cĆ­ podpoře na Google App Engine', -'admin disabled because too many invalid login attempts': 'Admin je zakĆ”zĆ”n po příliÅ” mnoha nesprĆ”vných pokusech o přihlÔŔenĆ­', -'admin disabled because unable to access password file': 'Admin je zakĆ”zĆ”n, protože nelze čƭst soubor s heslem', -'Admin is disabled because insecure channel': 'Admin je zakĆ”zĆ”n na nezabezpečenĆ©m připojenĆ­', +'Additional code for your application': 'DalŔí kód pro VaÅ”i aplikaci', +'Admin design page': 'Admin design page', 'Admin language': 'jazyk rozhranĆ­', -'Admin versioning page': 'Admin verzovacĆ­ strĆ”nka', 'Administrative interface': 'pro administrĆ”torskĆ© rozhranĆ­ klikněte sem', 'Administrative Interface': 'AdministrĆ”torskĆ© rozhranĆ­', 'administrative interface': 'rozhranĆ­ pro sprĆ”vu', 'Administrator Password:': 'AdministrĆ”torskĆ© heslo:', 'Ajax Recipes': 'Recepty s ajaxem', -'An error occured, please %s the page': 'DoÅ”lo k chybě, prosĆ­m %s strĆ”nku', +'An error occured, please %s the page': 'An error occured, please %s the page', 'and rename it:': 'a přejmenovat na:', -'App does not exist or you are not authorized': 'Aplikace neexistuje nebo vĆ”m chybĆ­ oprĆ”vněnĆ­', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zanĆ” bez zabezpečenĆ©ho spojenĆ­', -'Application': 'Aplikace', +'Application': 'Application', 'application "%s" uninstalled': 'application "%s" odinstalovĆ”na', -'Application cannot be generated in demo mode': 'Aplikace nemůže být vytvořena v demo módu', 'application compiled': 'aplikace zkompilovĆ”na', -'Application exists already': 'Aplikace již existuje', -'application is compiled and cannot be designed': 'aplikace je přeložena a nelze ji editovat', 'Application name:': 'NĆ”zev aplikace:', -'Application updated via git pull': 'Aplikace byla aktualizovĆ”na pomocĆ­ git pull', 'are not used': 'nepoužita', 'are not used yet': 'jeÅ”tě nepoužita', -'Are you sure you want to delete file "%s"?': 'Skutečně chcete smazat soubor "%s"?', -'Are you sure you want to delete plugin "%s"?': 'Skutečně chcete smazat plugin "%s"?', 'Are you sure you want to delete this object?': 'Opravdu chcete odstranit tento objekt?', 'Are you sure you want to uninstall application "%s"?': 'Opravdu chcete odinstalovat aplikaci "%s"?', -'Are you sure?': 'Jste si jist(a)?', -'arguments': 'argumenty', -'at char %s': 'na pozici znaku %s', -'at line %s': 'na řÔdku %s', -'ATTENTION:': 'POZOR:', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'POZOR: PřihlÔŔenĆ­ vyžaduje zabezpečenĆ© (HTTPS) připojenĆ­ nebo spouÅ”těnĆ­ na localhost.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'POZOR: TESTOVƁNƍ NENƍ BEZPEČNƉ PŘI SOUBĚŽNƝCH VLƁKNECH. NESPOUÅ TĚJ VƍCE TESTÅ® SOUBĚŽNĚ.', -'ATTENTION: you cannot edit the running application!': 'POZOR: Nelze editovat spuÅ”těnou aplikaci.', -'Autocomplete Python Code': 'Autocomplete Python kód', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', 'Available Databases and Tables': 'DostupnĆ© databĆ”ze a tabulky', 'back': 'zpět', -'Back to the plugins list': 'Zpět do seznamu pluginÅÆ', -'Back to wizard': 'ZpĆ”tky do prÅÆvodce', -'Basics': 'ZĆ”klady', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', 'Begin': 'Začƭt', 'breakpoint': 'bod přeruÅ”enĆ­', 'Breakpoints': 'Body přeruÅ”enĆ­', 'breakpoints': 'body přeruÅ”enĆ­', -'Bulk Register': 'HromadnĆ” registrace', -'Bulk Student Registration': 'HromadnĆ” registrace studentÅÆ', 'Buy this book': 'Koupit web2py knihu', 'Cache': 'Cache', 'cache': 'cache', -'Cache Cleared': 'Cache byla vymazĆ”na', 'Cache Keys': 'Klƭče cache', 'cache, errors and sessions cleaned': 'cache, chyby a relace byly pročiÅ”těny', 'can be a git repo': 'může to být git repo', 'Cancel': 'Storno', 'Cannot be empty': 'Nemůže být prĆ”zdnĆ©', -'Cannot compile: there are errors in your app:': 'Nelze zkompilovat: ve vaŔí aplikaci jsou chyby:', -'cannot create file': 'nelze vytvořit soubor', -'cannot upload file "%(filename)s"': 'nelze nahrĆ”t soubor "%(filename)s"', 'Change Admin Password': 'Změnit heslo pro sprĆ”vu', 'Change admin password': 'Změnit heslo pro sprĆ”vu aplikacĆ­', -'change editor settings': 'změnit nastavenĆ­ editoru', 'Change password': 'Změna hesla', -'Changelog': 'ŽurnĆ”l změn', 'check all': 'vÅ”e označit', 'Check for upgrades': 'Zkusit aktualizovat', 'Check to delete': 'Označit ke smazĆ”nĆ­', 'Check to delete:': 'Označit ke smazĆ”nĆ­:', 'Checking for upgrades...': 'ZjiŔńuji, zda jsou k dispozici aktualizace...', 'Clean': 'Pročistit', -'Clear': 'Inicializovat', 'Clear CACHE?': 'Vymazat CACHE?', 'Clear DISK': 'Vymazat DISK', 'Clear RAM': 'Vymazat RAM', 'Click row to expand traceback': 'Pro rozbalenĆ­ stopy, klikněte na řÔdek', 'Click row to view a ticket': 'Pro zobrazenĆ­ chyby (ticketu), klikněte na řÔdku...', 'Client IP': 'IP adresa klienta', -'code': 'kód', -'Code listing': 'Výpis kódu', +'code': 'code', +'Code listing': 'Code listing', 'collapse/expand all': 'vÅ”e sbalit/rozbalit', -'Command': 'Příkaz', -'Comment:': 'KomentÔř:', -'Commit': 'Potvrdit', -'Commit form': 'Potvrdit formulÔř', -'Committed files': 'PotvrzenĆ© soubory', 'Community': 'Komunita', 'Compile': 'Zkompilovat', -'Compile (all or nothing)': 'Přeložit (vÅ”e nebo nic)', -'Compile (skip failed views)': 'Přeložit (přeskočit chybnĆ© Å”ablony)', 'compiled application removed': 'zkompilovanĆ” aplikace smazĆ”na', 'Components and Plugins': 'Komponenty a zĆ”suvnĆ© moduly', 'Condition': 'PodmĆ­nka', -'continue': 'pokračovat', +'continue': 'continue', 'Controller': 'KontrolĆ©r (Controller)', 'Controllers': 'KontrolĆ©ry', 'controllers': 'kontrolĆ©ry', @@ -134,12 +92,9 @@ 'Count': 'Počet', 'Create': 'Vytvořit', 'create file with filename:': 'vytvořit soubor s nĆ”zvem:', -'Create/Upload': 'Vytvořit/NahrĆ”t', 'created by': 'vytvořil', 'Created By': 'Vytvořeno - kým', -'Created by:': 'Vytvořil:', 'Created On': 'Vytvořeno - kdy', -'Created on:': 'Vytvořeno:', 'crontab': 'crontab', 'Current request': 'AktuĆ”lnĆ­ požadavek', 'Current response': 'AktuĆ”lnĆ­ odpověď', @@ -150,19 +105,18 @@ 'data uploaded': 'data nahrĆ”na', 'Database': 'RozhranĆ­ databĆ”ze', 'Database %s select': 'databĆ”ze %s výběr', -'Database administration': 'Administrace databĆ”ze', +'Database administration': 'Database administration', 'database administration': 'sprĆ”va databĆ”ze', -'Database Administration (appadmin)': 'Administrace databĆ”ze (appadmin)', 'Date and Time': 'Datum a čas', 'day': 'den', 'db': 'db', 'DB Model': 'DatabĆ”zový model', 'Debug': 'LaděnĆ­', -'defines tables': 'definuje tabulky', +'defines tables': 'defines tables', 'Delete': 'Smazat', 'delete': 'smazat', 'delete all checked': 'smazat vÅ”e označenĆ©', -'delete plugin': 'zruÅ”it plugin', +'delete plugin': 'delete plugin', 'Delete this file (you will be asked to confirm deletion)': 'Smazat tento soubor (budete požÔdĆ”n o potvrzenĆ­ mazĆ”nĆ­)', 'Delete:': 'Smazat:', 'deleted after first hit': 'smazat po prvnĆ­m dosaženĆ­', @@ -170,265 +124,166 @@ 'Deploy': 'NahrĆ”t', 'Deploy on Google App Engine': 'NahrĆ”t na Google App Engine', 'Deploy to OpenShift': 'NahrĆ”t na OpenShift', -'Deploy to pythonanywhere': 'NahrĆ”t na PythonAnywhere', -'Deploy to PythonAnywhere': 'NahrĆ”t na PythonAnywhere', -'Deployment form': 'ForumlÔř pro deployment (nasazenĆ­)', -'Deployment Interface': 'RozhranĆ­ pro deployment (nasazenĆ­)', 'Deployment Recipes': 'Postupy pro deployment', 'Description': 'Popis', -'Description:': 'Popis:', 'design': 'nĆ”vrh', 'Detailed traceback description': 'Podrobný výpis prostředĆ­', 'details': 'podrobnosti', 'direction: ltr': 'směr: ltr', -'directory not found': 'adresÔř nebyl nalezen', 'Disable': 'Zablokovat', -'Disabled': 'BlokovĆ”no', -'disabled in demo mode': 'zakĆ”zĆ”no v demo módu', -'disabled in GAE mode': 'zakĆ”zĆ”no v GAE módu', -'disabled in multi user mode': 'zakĆ”zĆ”no ve vĆ­ceuživatelskĆ©m módu', 'DISK': 'DISK', 'Disk Cache Keys': 'Klƭče diskovĆ© cache', 'Disk Cleared': 'Disk smazĆ”n', -'Display line numbers': 'Zobrazit čƭsla řÔdkÅÆ', -'DO NOT use the "Pack compiled" feature.': 'NEPOUÅ½ĆVEJ vlastnost "Zabalit zkompilovanĆ©".', 'docs': 'dokumentace', -'Docs': 'Dokumentace', 'Documentation': 'Dokumentace', "Don't know what to do?": 'NevĆ­te kudy kam?', 'done!': 'hotovo!', -'Downgrade': 'Downgrade (vrĆ”cenĆ­ verze)', 'Download': 'StĆ”hnout', -'Download .w2p': 'StaženĆ­ .w2p', -'Download as .exe': 'StĆ”hnout jako .exe', 'download layouts': 'stĆ”hnout moduly rozvrženĆ­ strĆ”nky', -'Download layouts from repository': 'StĆ”hnout moduly rozvrženĆ­ z repozitÔře', 'download plugins': 'stĆ”hnout zĆ”suvnĆ© moduly', -'Download plugins from repository': 'StĆ”hnout pluginy z repozitÔře', 'E-mail': 'E-mail', 'Edit': 'Upravit', -'edit all': 'editovat vÅ”e', +'edit all': 'edit all', 'Edit application': 'SprĆ”va aplikace', -'edit controller': 'editovat controller', -'edit controller:': 'editovat kontrolĆ©r:', +'edit controller': 'edit controller', 'Edit current record': 'Upravit aktuĆ”lnĆ­ zĆ”znam', 'Edit Profile': 'Upravit profil', -'edit views:': 'upravit Å”ablonu (view):', -'Editing %s': 'Editace %s', +'edit views:': 'upravit pohled:', 'Editing file "%s"': 'Úprava souboru "%s"', 'Editing Language file': 'Úprava jazykovĆ©ho souboru', -'Editing Plural Forms File': 'EditovĆ”nĆ­ souboru množných čƭsel', -'Editor': 'Editor', -'Email Address': 'EmailovĆ” adresa', +'Editing Plural Forms File': 'Editing Plural Forms File', 'Email and SMS': 'Email a SMS', 'Enable': 'Odblokovat', -'Enable Close-Tag': 'Povolit Close-Tag', -'Enable Code Folding': 'Povolit sdružovĆ”nĆ­ kódu', 'enter a number between %(min)g and %(max)g': 'zadejte čƭslo mezi %(min)g a %(max)g', 'enter an integer between %(min)g and %(max)g': 'zadejte celĆ© čƭslo mezi %(min)g a %(max)g', 'Error': 'Chyba', 'Error logs for "%(app)s"': 'Seznam výskytu chyb pro aplikaci "%(app)s"', 'Error snapshot': 'Snapshot chyby', -'Error ticket': 'Tiket chyby', +'Error ticket': 'Ticket chyby', 'Errors': 'Chyby', -'Exception %(extype)s: %(exvalue)s': 'Výjimka %(extype)s: %(exvalue)s', -'Exception %s': 'Výjimka %s', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Prvky instance výjimky', -'Exit Fullscreen': 'Ukončit režim celĆ© obrazovky', -'Expand Abbreviation': 'Rozvinout zkratku', -'Expand Abbreviation (html files only)': 'Rozvinout zkratku (pouze html soubory)', +'Expand Abbreviation': 'Expand Abbreviation', 'export as csv file': 'exportovat do .csv souboru', -'Exports:': 'Exporty:', 'exposes': 'vystavuje', 'exposes:': 'vystavuje funkce:', 'extends': 'rozÅ”iřuje', 'failed to compile file because:': 'soubor se nepodařilo zkompilovat, protože:', -'failed to reload module because:': 'nepodařilo se restartovat modul, protože:', 'FAQ': 'Často kladenĆ© dotazy', 'File': 'Soubor', 'file': 'soubor', -'file "%(filename)s" created': 'soubor "%(filename)s" byl vytvořen', -'file "%(filename)s" deleted': 'soubor "%(filename)s" byl zruÅ”en', -'file "%(filename)s" uploaded': 'soubor "%(filename)s" byl nahrĆ”n', -'file "%s" of %s restored': 'soubor "%s" z %s byl obnoven', -'file changed on disk': 'soubor se na disku změnil', -'file does not exist': 'soubor neexistuje', -'file not found': 'soubor nebyl nalezen', +'file "%(filename)s" created': 'file "%(filename)s" created', 'file saved on %(time)s': 'soubor uložen %(time)s', 'file saved on %s': 'soubor uložen %s', -'filename': 'jmĆ©no souboru', 'Filename': 'NĆ”zev souboru', -'Files added': 'Soubory byly přidĆ”ny', 'filter': 'filtr', 'Find Next': 'NajĆ­t dalŔí', 'Find Previous': 'NajĆ­t předchozĆ­', 'First name': 'KřestnĆ­ jmĆ©no', 'Forgot username?': 'Zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', 'forgot username?': 'zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', -'Form has errors': 'Ve formulÔři jsou chyby', 'Forms and Validators': 'FormulÔře a validĆ”tory', -'Frames': 'Framy', +'Frames': 'Frames', 'Free Applications': 'Aplikace zdarma', -'Functions with no doctests will result in [passed] tests.': 'Funkce bez doctestÅÆ se projevĆ­ jako [ĆŗspěŔný] test.', -'GAE Email': 'GAE e-mail', -'GAE Output': 'GAE výstup', -'GAE Password': 'GAE heslo', +'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', 'Generate': 'Vytvořit', 'Get from URL:': 'StĆ”hnout z internetu:', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', 'Globals##debug': 'GlobĆ”lnĆ­ proměnnĆ©', 'go!': 'OK!', -'Google App Engine Deployment Interface': 'Google App Engine - rozhranĆ­ pro nasazenĆ­', -'Google Application Id': 'ID Google Aplikace', -'Goto': 'PřejĆ­t na', -'graph model': 'grafický model', -'Graph Model': 'Grafický model', +'Goto': 'Goto', +'graph model': 'graph model', 'Group %(group_id)s created': 'Skupina %(group_id)s vytvořena', 'Group ID': 'ID skupiny', 'Groups': 'Skupiny', 'Hello World': 'Ahoj světe', 'Help': 'NĆ”pověda', -'here': 'zde', 'Hide/Show Translated strings': 'Skrýt/Zobrazit přeloženĆ© texty', -'Highlight current line': 'Zvýraznit aktuĆ”lnĆ­ řÔdek', 'Hits': 'KolikrĆ”t dosaženo', 'Home': 'DomovskĆ” strĆ”nka', 'honored only if the expression evaluates to true': 'brĆ”t v potaz jen když se tato podmĆ­nka vyhodnotĆ­ kladně', 'How did you get here?': 'Jak jste se sem vlastně dostal?', -'If start the downgrade, be patient, it may take a while to rollback': 'SpustĆ­te-li downgrade verze, vyčkejte, protože vrĆ”cenĆ­ změn může trvat dlouho', -'If start the upgrade, be patient, it may take a while to download': 'SpustĆ­te-li upgrade, vyčkejte, protože stahovĆ”nĆ­ může trvat dlouho', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Jestliže přehled výŔe obsahuje čƭslo chybovĆ©ho tiketu, znamenĆ” to chybu v kontrolĆ©ru, jeÅ”tě před pokusem vykonat doctesty. (Často je to zpÅÆsobeno chybou odsazenĆ­ nebo chybou mimo kód funkce.) Zelený nadpis označuje, že žÔdný test nehavaroval. (V tom případě dĆ­lčƭ výsledky nejsou uvedeny.)', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Jestliže přehled výŔe obsahuje čƭslo chybovĆ©ho tiketu, znamenĆ” to chybu v kontrolĆ©ru, jeÅ”tě před pokusem vykonat doctesty. (Často je to zpÅÆsobeno chybou odsazenĆ­ nebo chybou mimo kód funkce.) Zelený nadpis označuje, že žÔdný test nehavaroval. (V tom případě dĆ­lčƭ výsledky nejsou uvedeny.)', -'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'Jestliže vaÅ”e aplikace používĆ” jinou databĆ”zi než SQLite, budete muset na PythonAnywhere konfigurovat jejĆ­ DAL() připojenĆ­.', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'import': 'import', 'Import/Export': 'Import/Export', -'In development, use the default Rocket webserver that is currently supported by this debugger.': 'Při vývoji je doporučeno použít předvolený webserver Rocket, se kterým tento debugger spolupracuje.', 'includes': 'zahrnuje', -'Indent with tabs': 'OdsazenĆ­ tabelĆ”tory', 'Index': 'Index', 'insert new': 'vložit nový zĆ”znam ', 'insert new %s': 'vložit nový zĆ”znam %s', -'inspect attributes': 'prohlĆ©dnout atributy', +'inspect attributes': 'inspect attributes', 'Install': 'Instalovat', -'Installation of %(plugin)s for %(app)s': 'Instalace %(plugin)s pro %(app)s', 'Installed applications': 'NainstalovanĆ© aplikace', 'Interaction at %s line %s': 'Interakce v %s, na řÔdce %s', 'Interactive console': 'InteraktivnĆ­ příkazovĆ” řÔdka', -'internal error': 'vnitřnĆ­ chyba', -'internal error: %s': 'vnitřnĆ­ chyba: %s', 'Internal State': 'VnitřnĆ­ stav', 'Introduction': 'Úvod', -'Invalid action': 'ChybnĆ” akce', -'Invalid application name': 'NesprĆ”vnĆ© jmĆ©no aplikace', -'invalid circular reference': 'nepovolený kruhový odkaz', 'Invalid email': 'Neplatný email', -'Invalid git repository specified.': 'Byl zadĆ”n nesprĆ”vný git repozitÔř.', 'Invalid password': 'NesprĆ”vnĆ© heslo', -'invalid password': 'nesprĆ”vnĆ© heslo', 'invalid password.': 'neplatnĆ© heslo', 'Invalid Query': 'Neplatný dotaz', 'invalid request': 'Neplatný požadavek', -'Invalid request': 'NesprĆ”vný požadavek (request)', -'invalid table names (auth_* tables already defined)': 'chybnĆ” jmĆ©na tabulek (auth_* tabulky už byly definovĆ”ny)', -'invalid ticket': 'chybný tiket', 'Is Active': 'Je aktivnĆ­', 'It is %s %%{day} today.': 'Dnes je to %s %%{den}.', 'Key': 'Klƭč', -'Key bindings': 'Vazby klĆ”ves', -'Key bindings for ZenCoding Plugin': 'Vazby klĆ”ves pro ZenCoding Plugin', -'Keyboard shortcuts': 'KlĆ”vesovĆ© zkratky', -'kill process': 'likvidovat proces', -'language file "%(filename)s" created/updated': 'jazykový soubor "%(filename)s" byl vytvořen/aktualizovĆ”n', -'Language files (static strings) updated': 'JazykovĆ© soubory (statickĆ© řetězce) byly aktualizovĆ”ny', +'Key bindings': 'Vazby klĆ­ÄÅÆ', +'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', 'languages': 'jazyky', 'Languages': 'Jazyky', 'Last name': 'PříjmenĆ­', -'Last Revision': 'MinulĆ” verze', 'Last saved on:': 'Naposledy uloženo:', 'Layout': 'RozvrženĆ­ strĆ”nky (layout)', 'Layout Plugins': 'Moduly rozvrženĆ­ strĆ”nky (Layout Plugins)', 'Layouts': 'RozvrženĆ­ strĆ”nek', 'License for': 'Licence pro', -'License:': 'Licence:', -'Line Nr': 'Č.řÔdku', 'Line number': 'Číslo řÔdku', 'LineNo': 'Č.řÔdku', -'lists by exception': 'výpis podle výjimky', -'lists by ticket': 'výpis podle tiketu', -'Live Chat': 'Online chat', -'Loading...': 'NahrĆ”vĆ”m...', +'Live Chat': 'Online pokec', 'loading...': 'nahrĆ”vĆ”m...', -'Local Apps': 'LokĆ”lnĆ­ aplikace', -'locals': 'lokĆ”lnĆ­ proměnnĆ©', +'locals': 'locals', 'Locals##debug': 'LokĆ”lnĆ­ proměnnĆ©', 'Logged in': 'PřihlÔŔenĆ­ proběhlo ĆŗspěŔně', 'Logged out': 'OdhlÔŔenĆ­ proběhlo ĆŗspěŔně', -'login': 'přihlĆ”sit se', 'Login': 'PřihlĆ”sit se', -'Login successful': 'PřihlÔŔenĆ­ bylo ĆŗspěŔnĆ©', +'login': 'přihlĆ”sit se', 'Login to the Administrative Interface': 'PřihlĆ”sit se do SprĆ”vce aplikacĆ­', -'Login/Register': 'PřihlĆ”sit se / Registrovat', 'logout': 'odhlĆ”sit se', 'Logout': 'OdhlĆ”sit se', -'lost password': 'ztracenĆ© heslo', 'Lost Password': 'Zapomněl jste heslo', 'Lost password?': 'Zapomněl jste heslo?', 'lost password?': 'zapomněl jste heslo?', -'Main Menu': 'HlavnĆ­ nabĆ­dka', -'Manage': 'Spravovat', -'Manage %(action)s': 'Spravovat %(action)s', -'Manage Access Control': 'Spravovat řízenĆ­ přístupu', -'Manage Admin Users/Students': 'Spravovat Admin uživatele / Studenty', -'Manage Cache': 'Spravovat cache', -'Manage Students': 'Spravovat studenty', -'Memberships': 'ČlenstvĆ­ ve skupinĆ”ch', +'Manage': 'Manage', +'Manage Cache': 'Manage Cache', 'Menu Model': 'Model rozbalovacĆ­ nabĆ­dky', -'merge': 'sloučit', 'Models': 'Modely', 'models': 'modely', 'Modified By': 'Změněno - kým', 'Modified On': 'Změněno - kdy', 'Modules': 'Moduly', 'modules': 'moduly', -'Multi User Mode': 'VĆ­ceuživatelský mód', 'My Sites': 'SprĆ”va aplikacĆ­', 'Name': 'JmĆ©no', 'new application "%s" created': 'novĆ” aplikace "%s" vytvořena', -'new application "%s" imported': 'novĆ” aplikace "%s" byla importovĆ”na', 'New Application Wizard': 'Nový prÅÆvodce aplikacĆ­', 'New application wizard': 'Nový prÅÆvodce aplikacĆ­', 'New password': 'NovĆ© heslo', -'new plugin installed': 'nový plugin byl instalovĆ”n', -'New plugin installed: %s': 'Nový plugin byl instalovĆ”n: %s', 'New Record': 'Nový zĆ”znam', 'new record inserted': 'nový zĆ”znam byl založen', -'New simple application': 'Vytvořit novou aplikaci', -'next': 'dalŔí', -'next %s rows': 'dalŔích %s řÔdkÅÆ', +'New simple application': 'Vytvořit primitivnĆ­ aplikaci', +'next': 'next', 'next 100 rows': 'dalŔích 100 řÔdkÅÆ', -'NO': 'NE', -'no changes': 'beze změn', 'No databases in this application': 'V tĆ©to aplikaci nejsou žÔdnĆ© databĆ”ze', 'No Interaction yet': 'JeÅ”tě žÔdnĆ” interakce nenastala', -'no match': 'nenalezena shoda', -'no package selected': 'nebyla vybrĆ”na žÔdnĆ” package', -'no permission to uninstall "%s"': 'chybĆ­ oprĆ”vněnĆ­ odinstalovat "%s"', 'No ticket_storage.txt found under /private folder': 'Soubor ticket_storage.txt v adresÔři /private nenalezen', -'Node:': 'Uzel (node):', -'Not Authorized': 'ChybĆ­ autorizace', -'Not supported': 'NenĆ­ podporovĆ”no', -'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'PoznĆ”mka: Dostanete-li chybu s github status code = 128, ujistěte se, že systĆ©m a ĆŗÄet z něhož provĆ”dĆ­te nasazenĆ­ mĆ” odpovĆ­dajĆ­cĆ­ ssh klƭč, konfigurovaný v OpenShift ĆŗÄtu.', 'Object or table name': 'Objekt či tabulka', 'Old password': 'PÅÆvodnĆ­ heslo', -"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": 'Pro použitĆ­ tohoto debuggeru na produkci je potřeba konfigurovat webserver, aby používal jeden proces a vĆ­ce vlĆ”ken.', 'online designer': 'online nĆ”vrhÔř', 'Online examples': 'Příklady online', -'Open new app in new window': 'Otevřít novou aplikaci v novĆ©m okně', -'OpenShift Deployment Interface': 'OpenShift rozhranĆ­ pro nasazenĆ­ aplikace', -'OpenShift Output': 'OpenShift výstup', -'or alternatively': 'nebo případně', -'Or Get from URL:': 'Nebo zĆ­skat z URL adresy:', +'Open new app in new window': 'Open new app in new window', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'nebo importovat z .csv souboru', 'Origin': 'PÅÆvod', 'Original/Translation': 'OriginĆ”l/Překlad', @@ -438,53 +293,30 @@ 'Overwrite installed app': 'Přepsat instalovanou aplikaci', 'Pack all': 'Zabalit', 'Pack compiled': 'Zabalit zkompilovanĆ©', -'Pack custom': 'Zabalit volitelně (custom)', -'pack plugin': 'zabalit plugin', -'Password': 'Heslo', +'pack plugin': 'pack plugin', 'password': 'heslo', -'password changed': 'heslo bylo změněno', +'Password': 'Heslo', "Password fields don't match": 'Hesla se neshodujĆ­', -'Past revisions': 'MinulĆ” verze', -'Path to appcfg.py': 'Cesta ke appcfg.py', -'Path to local openshift repo root.': 'Cesta ke kořenu (rootu) lokĆ”lnĆ­ho OpenShift repozitÔře.', -'Peeking at file': 'SledovĆ”nĆ­ souboru', -'Permission': 'OprĆ”vněnĆ­', -'Permissions': 'OprĆ”vněnĆ­', +'Peeking at file': 'Peeking at file', 'Please': 'ProsĆ­m', -'Please wait, giving pythonanywhere a moment...': 'ProsĆ­m, čekejte na dokončenĆ­ činnosti PythonAnywhere...', -'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" byl odstraněn', -'Plugin "%s" in application': 'Plugin "%s" v aplikaci', -'plugin not specified': 'plugin nebyl určen', -'Plugin page': 'StrĆ”nka pluginÅÆ', +'Plugin "%s" in application': 'Plugin "%s" in application', 'plugins': 'zĆ”suvnĆ© moduly', 'Plugins': 'ZĆ”suvnĆ© moduly', -'Plural Form #%s': 'MnožnĆ© čƭslo #%s', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'MnožnĆ” čƭsla:', -'Powered by': 'používĆ” technologii', +'Powered by': 'PohĆ”něno', 'Preface': 'Předmluva', -'Preferences saved correctly': 'NastavenĆ­ byla ĆŗspěŔně uložena', -'Preferences saved on session only': 'NastavenĆ­ byla uložena pouze pro toto sezenĆ­', -'previous %s rows': 'předchozĆ­ch %s řÔdkÅÆ', 'previous 100 rows': 'předchozĆ­ch 100 řÔdkÅÆ', 'Private files': 'SoukromĆ© soubory', 'private files': 'soukromĆ© soubory', 'profile': 'profil', 'Project Progress': 'Vývoj projektu', -'Pull': 'Pull', -'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull selhal, některĆ© soubory nelze zkopĆ­rovat. Pro podrobnosti zkontrolujte logy.', -'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull nelze provĆ©st, protože mĆ”te nesloučenĆ© soubory. VyřeÅ”te tyto konflikty a pak akci opakujte.', -'Push': 'Push', -'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push selhal, protože mĆ”te nesloučenĆ© soubory. VyřeÅ”te tyto konflikty a pak akci opakujte.', -'pygraphviz library not found': 'pygraphviz knihovna nebyla nalezena', 'Python': 'Python', -'PythonAnywhere Apps': 'PythonAnywhere aplikace', -'PythonAnywhere Password': 'PythonAnywhere heslo', 'Query:': 'Dotaz:', 'Quick Examples': 'KrĆ”tkĆ© příklady', 'RAM': 'RAM', 'RAM Cache Keys': 'Klƭče RAM Cache', 'Ram Cleared': 'RAM smazĆ”na', -'Rapid Search': 'RychlĆ© hledĆ”nĆ­', 'Readme': 'NĆ”pověda', 'Recipes': 'Postupy jak na to', 'Record': 'ZĆ”znam', @@ -503,167 +335,114 @@ 'Removed Breakpoint on %s at line %s': 'Bod přeruÅ”enĆ­ smazĆ”n - soubor %s na řÔdce %s', 'Replace': 'Zaměnit', 'Replace All': 'Zaměnit vÅ”e', -'Repository (%s)': 'RepozitÔř (%s)', -'request': 'požadavek (request)', -'requires distutils, but not installed': 'vyžaduje distutils, jenže ty nejsou instalovĆ”ny', -'requires python-git, but not installed': 'vyžaduje python-git, který ale nenĆ­ nainstalovĆ”n', +'request': 'request', 'Reset Password key': 'Reset registračnĆ­ho klƭče', -'Resolve Conflict file': 'VyřeÅ”it konflikty', -'response': 'odpověď (response)', +'response': 'response', 'restart': 'restart', 'restore': 'obnovit', 'Retrieve username': 'ZĆ­skat přihlaÅ”ovacĆ­ jmĆ©no', 'return': 'return', -'Revert': 'VrĆ”tit se k pÅÆvodnĆ­mu', 'revert': 'vrĆ”tit se k pÅÆvodnĆ­mu', -'reverted to revision %s': 'vrĆ”ceno k verzi %s', -'Revision %s': 'Verze %s', -'Revision:': 'Verze:', 'Role': 'Role', -'Roles': 'Role', 'Rows in Table': 'ZĆ”znamy v tabulce', 'Rows selected': 'ZĆ”znamÅÆ zobrazeno', 'rules are not defined': 'pravidla nejsou definovĆ”na', -'Run tests': 'Spustit testy', -'Run tests in this file': 'Spustit testy v souboru', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "SpustĆ­ testy v tomto souboru (ke spuÅ”těnĆ­ vÅ”ech testÅÆ, použijte tlačƭtko 'test')", 'Running on %s': 'Běží na %s', 'Save': 'Uložit', -'Save file:': 'Uložit soubor:', -'Save file: %s': 'Uložit soubor: %s', -'Save model as...': 'Uložit model jako...', +'Save file:': 'Save file:', 'Save via Ajax': 'Uložit pomocĆ­ Ajaxu', 'Saved file hash:': 'hash uloženĆ©ho souboru:', -'Screenshot %s': 'Screenshot %s', -'Search': 'HledĆ”nĆ­', -'Select Files to Package': 'Vybrat soubory pro package', 'Semantic': 'Modul semantic', 'Services': 'Služby', -'session': 'session (sezenĆ­)', -'session expired': 'vyprÅ”ela session', -'Session saved correctly': 'Session byla ĆŗspěŔně uložena', -'Session saved on session only': 'Session byla uložena jen pro toto sezenĆ­', +'session': 'session', +'session expired': 'session expired', 'Set Breakpoint on %s at line %s: %s': 'Bod přeruÅ”enĆ­ nastaven v souboru %s na řÔdce %s: %s', 'shell': 'příkazovĆ” řÔdka', -'Showing %s to %s of %s %s found': 'Zobrazuji %s až %s z %s %s nalezených', -'Singular Form': 'JednotnĆ© čƭslo', +'Singular Form': 'Singular Form', 'Site': 'SprĆ”va aplikacĆ­', 'Size of cache:': 'Velikost cache:', -'skip to generate': 'přeskočit pro vytvořenĆ­', -'some files could not be removed': 'některĆ© soubory nelze odstranit', -'Something went wrong please wait a few minutes before retrying': 'Něco se nepodařilo. Vyčkejte několik minut a pak zkuste znova', +'skip to generate': 'skip to generate', 'Sorry, could not find mercurial installed': 'Bohužel mercurial nenĆ­ nainstalovĆ”n.', -'source : db': 'zdroj : db', -'source : filesystem': 'zdroj : souborový systĆ©m', 'Start a new app': 'Vytvořit novou aplikaci', 'Start searching': 'Začƭt hledĆ”nĆ­', 'Start wizard': 'Spustit prÅÆvodce', 'state': 'stav', -'Static': 'StatickĆ© soubory', +'Static': 'Static', 'static': 'statickĆ© soubory', 'Static files': 'StatickĆ© soubory', 'Statistics': 'Statistika', -'Step': 'Krok', -'step': 'krok', -'stop': 'zastavit', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'Stylesheet': 'CSS styly', 'submit': 'odeslat', 'Submit': 'Odeslat', 'successful': 'ĆŗspěŔně', 'Support': 'Podpora', 'Sure you want to delete this object?': 'Opravdu chcete smazat tento objekt?', -'switch to : db': 'přepnout na : db', -'switch to : filesystem': 'přepnout na : souborový systĆ©m', -'Tab width (# characters)': 'Å Ć­Å™ka tabelĆ”toru (# znakÅÆ)', 'Table': 'tabulka', 'Table name': 'NĆ”zev tabulky', 'Temporary': 'Dočasný', 'test': 'test', -'Testing application': 'ZkuÅ”ebnĆ­ aplikace', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Dotaz" je například "db.tabulka1.pole1==\'hodnota\'". Dotaz se dvěma tabulkami "db.tabulka1.pole1==db.tabulka2.pole2" vytvoří SQL JOIN.', -'The app exists, was created by wizard, continue to overwrite!': 'Aplikace existuje, byla vytvořena prÅÆvodcem. PokračovĆ”nĆ­m ji přepĆ­Å”ete !', -'The app exists, was NOT created by wizard, continue to overwrite!': 'Aplikace existuje, a NEBYLA vytvořena prÅÆvodcem. PokračovĆ”nĆ­m ji přepĆ­Å”ete !', +'Testing application': 'Testing application', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Dotaz" je podmĆ­nka, například "db.tabulka1.pole1==\'hodnota\'". PodmĆ­nka "db.tabulka1.pole1==db.tabulka2.pole2" pak vytvoří SQL JOIN.', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika aplikace: každĆ” URL je mapovĆ”na na funkci vystavovanou kontrolĆ©rem.', 'The Core': 'JĆ”dro (The Core)', -'The data representation, define database tables and sets': 'Modely se vykonajĆ­ při každĆ©m přístupu. Zde se obvykle definuje předevŔím reprezentace dat: PřipojenĆ­ k databĆ”zi a struktura tabulek databĆ”ze', -'The output of the file is a dictionary that was rendered by the view %s': 'Výstup ze souboru je dictionary (slovnĆ­k), který se zobrazil pomocĆ­ Å”ablony (view) %s.', -'The presentations layer, views are also known as templates': 'PrezentačnĆ­ vrstva: Å”ablony (neboli pohledy, templaty, view). Mixuje Html, Python kód a Python data.', +'The data representation, define database tables and sets': 'Reprezentace dat: definovat tabulky databĆ”ze a zĆ”znamy', +'The output of the file is a dictionary that was rendered by the view %s': 'Výstup ze souboru je slovnĆ­k, který se zobrazil v pohledu %s.', +'The presentations layer, views are also known as templates': 'PrezentačnĆ­ vrstva: pohledy či templaty (Å”ablony)', 'The Views': 'Pohledy (The Views)', -'Theme': 'TĆ©ma', -'There are no controllers': 'Nejsou vytvořeny žÔdnĆ© controllery', -'There are no models': 'NenĆ­ vytvořen žÔdný model', -'There are no modules': 'Nejsou přidĆ”ny žÔdnĆ© moduly', +'There are no controllers': 'There are no controllers', +'There are no modules': 'There are no modules', 'There are no plugins': 'ŽÔdnĆ© moduly nejsou instalovĆ”ny.', 'There are no private files': 'ŽÔdnĆ© soukromĆ© soubory neexistujĆ­.', -'There are no static files': 'Nejsou přidĆ”ny žÔdnĆ© statickĆ© soubory', -'There are no translators': 'NenĆ­ vytvořen žÔdný překlad', -'There are no translators, only default language is supported': 'NenĆ­ vytvořen žÔdný překlad, je podporovĆ”n jen defaultnĆ­ jazyk', -'There are no views': 'Nejsou vytvořeny žÔdnĆ© Å”ablony (views)', -'These files are not served, they are only available from within your app': 'Tyto soubory jsou přístupnĆ© jen běžícĆ­ aplikaci. Nejsou dostupnĆ© uživatelÅÆm, ani se nekopĆ­rujĆ­ do případnĆ©ho vývojovĆ©ho repozitÔře. Hesla a citlivĆ” nastavenĆ­ nedĆ”vejte nikam jinam.', -'These files are served without processing, your images go here': 'Tyto soubory jsou stahovĆ”ny přímo, bez jakĆ©koli přídavnĆ© logiky, sem patří např. obrĆ”zky.', +'There are no static files': 'There are no static files', +'There are no translators, only default language is supported': 'There are no translators, only default language is supported', +'There are no views': 'There are no views', +'These files are not served, they are only available from within your app': 'Tyto soubory jsou klientÅÆm nepřístupnĆ©. K dispozici jsou pouze v rĆ”mci aplikace.', +'These files are served without processing, your images go here': 'Tyto soubory jsou servĆ­rovĆ”ny bez přídavnĆ© logiky, sem patří např. obrĆ”zky.', 'This App': 'Tato aplikace', -"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": 'Tento debugger nebude pracovat sprĆ”vně, jestliže vÔŔ webový server nepracuje pomocĆ­ vlĆ”ken nebo když používĆ”te vĆ­ce procesÅÆ dĆ©monÅÆ.', -'This is a copy of the scaffolding application': 'Toto je kopie vzorovĆ© aplikace.', -'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'Toto je experimentĆ”lnĆ­ vlastnost, kterĆ” vyžaduje dalŔí testovĆ”nĆ­. NĆ”vrat verze jen na vlastnĆ­ riziko.', -'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'Toto je experimentĆ”lnĆ­ vlastnost, kterĆ” vyžaduje dalŔí testovĆ”nĆ­. Upgrade verze jen na vlastnĆ­ riziko.', -'This is the %(filename)s template': 'Toto je Å”ablona %(filename)s', -"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": 'Tato strĆ”nka umožňuje potvrdit vaÅ”e změny do OpenShift aplikačnĆ­ho repozitÔře a odeslat je do vaŔí cloud instance. Předpokladem je, že jste už vytvořili aplikačnĆ­ instanci pomocĆ­ Web2py předlohy a že tento repozitÔř mĆ”te někde na disku tak, aby k němu Web2py mělo přístup. Tato funkcionalita vyžaduje, aby byl instalovĆ”n GitPython a aby mohl být nalezen pomocĆ­ cesty, se kterou Web2py pracuje.', -'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'Tato strĆ”nka umožňuje zkopĆ­rovat vaÅ”i aplikaci do Google App Engine cloudu. Pamatujte, že nejprve je třeba vytvořit indexy lokĆ”lně, čehož dosĆ”hnete instalacĆ­ Google appserver a jednĆ­m lokĆ”lnĆ­m spuÅ”těnĆ­m aplikace. V opačnĆ©m případě bude dochĆ”zet k chybĆ”m vyhledĆ”vĆ”nĆ­. Pozor: v zĆ”vislosti na rychlosti sĆ­tě může nasazenĆ­ trvat dlouhou dobu. Pozor: bude přepsĆ”n vÔŔ soubor app.yaml. BĚHEM SPUÅ TĚNƍ NESPOUÅ TĚJTE PODRUHƉ.', +'This is a copy of the scaffolding application': 'Toto je kopie aplikace skelet.', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'This is the %(filename)s template', 'this page to see if a breakpoint was hit and debug interaction is required.': 'tuto strĆ”nku, abyste uviděli, zda se dosĆ”hlo bodu přeruÅ”enĆ­.', -'This will pull changes from the remote repo for application "%s"?': 'Toto stĆ”hne (pull) změny ze vzdĆ”lenĆ©ho repozitÔře pro aplikaci "%s"?', -'This will push changes to the remote repo for application "%s".': 'Toto nahraje (push) změny do vzdĆ”lenĆ©ho repozitÔře pro aplikaci "%s".', -'Ticket': 'Tiket', -'Ticket ID': 'ID tiketu', -'Ticket Missing': 'Chybový tiket chybĆ­', +'Ticket': 'Ticket', +'Ticket ID': 'Ticket ID', 'Time in Cache (h:m:s)': 'Čas v Cache (h:m:s)', 'Timestamp': 'ČasovĆ© razĆ­tko', 'to previous version.': 'k předchozĆ­ verzi.', -'To create a plugin, name a file/folder plugin_[name]': 'ZĆ”suvný modul vytvoříte tak, že pojmenujete skupinu souborÅÆ nebo adresÔř(e) plugin_[jmĆ©no modulu]', +'To create a plugin, name a file/folder plugin_[name]': 'ZĆ”suvný modul vytvoříte tak, že pojmenujete soubor/adresÔř plugin_[jmĆ©no modulu]', 'To emulate a breakpoint programatically, write:': 'K nastavenĆ­ bodu přeruÅ”enĆ­ v kódu programu, napiÅ”te:', 'to use the debugger!': ', abyste mohli ladĆ­cĆ­ program používat!', 'toggle breakpoint': 'vyp./zap. bod přeruÅ”enĆ­', -'Toggle comment': 'Přepnout komentÔř', 'Toggle Fullscreen': 'Na celou obrazovku a zpět', 'too short': 'PříliÅ” krĆ”tkĆ©', -'Traceback': 'Hierarchie volĆ”nĆ­', +'Traceback': 'Traceback', 'Translation strings for the application': 'Překlad textÅÆ pro aplikaci', -'try something like': 'zkuste něco jako', +'try something like': 'try something like', 'Try the mobile interface': 'Zkuste rozhranĆ­ pro mobilnĆ­ zařízenĆ­', -'try view': 'vyzkouÅ”et Å”ablonu (view)', +'try view': 'try view', 'Twitter': 'Twitter', -'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'ZapiÅ”te příkaz PDB debuggeru a stiskněte Return (Enter) pro jeho provedenĆ­.', -'Type python statement in here and hit Return (Enter) to execute it.': 'ZapiÅ”te příkaz pythonu a stiskněte Return (Enter) pro jeho provedenĆ­.', -'Type some Python code in here and hit Return (Enter) to execute it.': 'ZapiÅ”te kód v jazyce python a stiskněte Return (Enter) pro jeho provedenĆ­.', -'Unable to check for upgrades': 'Nelze zjistit informaci o aktualizacĆ­ch', -'unable to create application "%s"': 'nelze vytvořit aplikaci "%s"', -'unable to delete file "%(filename)s"': 'nelze zruÅ”it soubor "%(filename)s"', -'unable to delete file plugin "%(plugin)s"': 'nelze zruÅ”it plugin "%(plugin)s"', -'Unable to determine the line number!': 'Nelze určit čƭslo řÔdky!', -'Unable to download app because:': 'Nelze stĆ”hnout aplikaci, protože:', -'unable to download layout': 'nelze stĆ”hnout Å”ablonu (layout)', -'unable to download plugin: %s': 'nelze stĆ”hnout plugin: %s', -'Unable to download the list of plugins': 'Nelze stĆ”hnout seznam pluginÅÆ', -'unable to install plugin "%s"': 'nelze instalovat plugin "%s"', +'Type python statement in here and hit Return (Enter) to execute it.': 'Type python statement in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Unable to check for upgrades', 'unable to parse csv file': 'csv soubor nedĆ” sa zpracovat', -'unable to uninstall "%s"': 'nelze instalovat "%s"', -'unable to upgrade because "%s"': 'nelze upgradovat, protože "%s"', 'uncheck all': 'vÅ”e odznačit', 'Uninstall': 'Odinstalovat', -'Unsupported webserver working mode: %s': 'Nepodporovaný mód webovĆ©ho serveru: %s', 'update': 'aktualizovat', -'update all languages': 'aktualizovat vÅ”echny jazyky o novĆ© texty ze zdrojových souborÅÆ', +'update all languages': 'aktualizovat vÅ”echny jazyky', 'Update:': 'Upravit:', 'Upgrade': 'Upgrade', -'upgrade now': 'upgradovat nynĆ­', -'upgrade now to %s': 'upgradovat nynĆ­ na %s', +'upgrade now': 'upgrade now', +'upgrade now to %s': 'upgrade now to %s', 'upload': 'nahrĆ”t', -'Upload': 'Upload (nahrĆ”t)', +'Upload': 'Upload', 'Upload a package:': 'NahrĆ”t balĆ­k:', 'Upload and install packed application': 'NahrĆ”t a instalovat zabalenou aplikaci', 'upload file:': 'nahrĆ”t soubor:', 'upload plugin file:': 'nahrĆ”t soubor modulu:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Použijte (...)&(...) pro AND, (...)|(...) pro OR a ~(...) pro NOT pro sestavenĆ­ složitějŔích dotazÅÆ.', -'User': 'Uživatel', 'User %(id)s Logged-in': 'Uživatel %(id)s přihlÔŔen', 'User %(id)s Logged-out': 'Uživatel %(id)s odhlÔŔen', 'User %(id)s Password changed': 'Uživatel %(id)s změnil heslo', @@ -672,46 +451,30 @@ 'User %(id)s Username retrieved': 'Uživatel %(id)s si nachal zaslat přihlaÅ”ovacĆ­ jmĆ©no', 'User ID': 'ID uživatele', 'Username': 'PřihlaÅ”ovacĆ­ jmĆ©no', -'Users': 'UživatelĆ©', -'Using the shell may lock the database to other users of this app.': 'PoužitĆ­ příkazovĆ©ho shellu může uzamknout databĆ”zi ostatnĆ­m uživatelÅÆm tĆ©to aplikace.', -'variables': 'proměnnĆ©', +'variables': 'variables', 'Verify Password': 'Zopakujte heslo', 'Version': 'Verze', 'Version %s.%s.%s (%s) %s': 'Verze %s.%s.%s (%s) %s', 'Versioning': 'VerzovĆ”nĆ­', 'Videos': 'Videa', -'View': 'Å ablona (View)', -'Views': 'Å ablony (Views)', -'views': 'Å”ablony (views)', -'Warning!': 'Pozor!', -'WARNING:': 'POZOR:', -'WARNING: The following views could not be compiled:': 'POZOR: NĆ”sledujĆ­cĆ­ Å”ablony se nepodařilo zkompilovat:', -'Web Framework': 'Webový framework', -'web2py Admin Password': 'web2py Heslo administrĆ”tora', -'web2py apps to deploy': 'web2py aplikace k nasazenĆ­', -'web2py Debugger': 'web2py Debugger', -'web2py downgrade': 'web2py downgrade', +'View': 'Pohled (View)', +'Views': 'Pohledy', +'views': 'pohledy', +'Web Framework': 'Web Framework', 'web2py is up to date': 'MĆ”te aktuĆ”lnĆ­ verzi web2py.', 'web2py online debugger': 'LadĆ­cĆ­ online web2py program', -'web2py Recent Tweets': 'NedĆ”vnĆ© tweety na Twitteru o web2py', -'web2py upgrade': 'aktualizace Web2py', -'web2py upgraded; please restart it': 'Web2py bylo aktualizovĆ”no; prosĆ­m restarujte jej', +'web2py Recent Tweets': 'Å těbetĆ”nĆ­ na Twitteru o web2py', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', 'Welcome': 'VĆ­tejte', -'Welcome to web2py': 'Vitejte ve Web2py aplikaci.', -'Welcome to web2py!': 'VĆ­tejte ve Web2py aplikaci.', +'Welcome to web2py': 'Vitejte ve web2py', +'Welcome to web2py!': 'VĆ­tejte ve web2py!', 'Which called the function %s located in the file %s': 'kterĆ” zavolala funkci %s v souboru (kontrolĆ©ru) %s.', -'Working...': 'Pracuji...', -'WSGI reference name': 'jmĆ©no WSGI reference', -'YES': 'ANO', -'Yes': 'Ano', 'You are successfully running web2py': 'ÚspěŔně jste spustili web2py.', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'Nastavovat a mazat body přeruÅ”enĆ­ je též možno v rĆ”mci editovĆ”nĆ­ zdrojovĆ©ho souboru přes tlačƭtko Vyp./Zap. bod přeruÅ”enĆ­', -'You can inspect variables using the console bellow': 'Níže pomocĆ­ příkazovĆ© řÔdky si můžete prohlĆ©dnout proměnnĆ©', -'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You can modify this application and adapt it to your needs': 'Tuto aplikaci si můžete upravit a přizpÅÆsobit ji svým potřebĆ”m.', -'You have one more login attempt before you are locked out': 'MĆ”te jen jeden dalŔí pokus k přihlÔŔenĆ­ před zablokovĆ”nĆ­m', 'You need to set up and reach a': 'Je třeba nejprve nastavit a dojĆ­t až na', -'You only need these if you have already registered': 'Toto potřebujete jen tehdy, jestliže jste se už registroval(a)', 'You visited the url %s': 'NavÅ”tĆ­vili jste strĆ”nku %s,', 'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Aplikace bude blokovĆ”na než se klikne na jedno z tlačƭtek (dalŔí, krok, pokračovat, atd.)', +'You can inspect variables using the console bellow': 'Níže pomocĆ­ příkazovĆ© řÔdky si můžete prohlĆ©dnout proměnnĆ©', } diff --git a/applications/admin/languages/de.py b/applications/admin/languages/de.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/default.py b/applications/admin/languages/default.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/es.py b/applications/admin/languages/es.py old mode 100644 new mode 100755 index c4501c12..a1db7b4e --- a/applications/admin/languages/es.py +++ b/applications/admin/languages/es.py @@ -3,29 +3,40 @@ '!langcode!': 'es', '!langname!': 'EspaƱol', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"actualice" es una expresión opcional como "campo1=\'nuevo_valor\'". No se puede actualizar o eliminar resultados de un JOIN', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s filas eliminadas', '%s %%{row} updated': '%s filas actualizadas', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access, experimental)': '(requiere acceso a internet, experimental)', '(something like "it-it")': '(algo como "it-it")', '(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(archivo **gluon/contrib/plural_rules/%s.py** no se ha encontrado)', '@markmin\x01An error occured, please [[reload %s]] the page': 'Ocurrió un error, por favor [[recargue %s]] la pĆ”gina', '@markmin\x01Number of entries: **%s**': 'NĆŗmero de entradas: **%s**', '@markmin\x01Searching: **%s** %%{file}': 'Buscando: **%s** archivos', 'A new version of web2py is available': 'Hay una nueva versión de web2py disponible', 'A new version of web2py is available: %s': 'Hay una nueva versión de web2py disponible: %s', +'Abort': 'Abort', 'About': 'Acerca de', 'About application': 'Acerca de la aplicación', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'additional code for your application': 'código adicional para su aplicación', 'Additional code for your application': 'Código adicional para su aplicación', +'Admin design page': 'Admin design page', 'admin disabled because no admin password': 'admin deshabilitado por falta de contraseƱa', 'admin disabled because not supported on google app engine': 'admin deshabilitado, no es soportado en GAE', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'admin deshabilitado, imposible acceder al archivo con la contraseƱa', 'Admin is disabled because insecure channel': 'Admin deshabilitado, el canal no es seguro', 'Admin is disabled because unsecure channel': 'Admin deshabilitado, el canal no es seguro', 'Admin language': 'Lenguaje de administración', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'interfaz administrativa', 'Administrator Password:': 'ContraseƱa del Administrador:', 'An error occured, please %s the page': 'Ha ocurrido un error, por favor %s la pĆ”gina', @@ -34,12 +45,15 @@ 'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'admin deshabilitado, el canal no es seguro', +'Application': 'Application', 'application "%s" uninstalled': 'aplicación "%s" desinstalada', 'application %(appname)s installed with md5sum: %(digest)s': 'application %(appname)s installed with md5sum: %(digest)s', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', 'application compiled': 'aplicación compilada', 'Application exists already': 'Application exists already', 'application is compiled and cannot be designed': 'la aplicación estĆ” compilada y no puede ser modificada', 'Application name:': 'Nombre de la aplicación:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'are not used', 'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'ĀæEstĆ” seguro que desea eliminar el archivo "%s"?', @@ -52,6 +66,7 @@ 'arguments': 'argumentos', 'at char %s': 'en el carĆ”cter %s', 'at line %s': 'en la lĆ­nea %s', +'ATTENTION:': 'ATTENTION:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENCIƓN: Inicio de sesión requiere una conexión segura (HTTPS) o localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENCIƓN: NO EJECUTE VARIAS PRUEBAS SIMULTANEAMENTE, NO SON THREAD SAFE.', 'ATTENTION: you cannot edit the running application!': 'ATENCIƓN: Ā”no puede modificar la aplicación que se ejecuta!', @@ -61,11 +76,19 @@ 'Available Databases and Tables': 'Bases de Datos y Tablas Disponibles', 'back': 'atrĆ”s', 'Back to the plugins list': 'Regresar a la lista de plugins', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', 'breakpoint': 'punto de ruptura', +'Breakpoints': 'Breakpoints', 'breakpoints': 'puntos de ruptura', 'browse': 'navegar', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', 'Cache': 'CachĆ©', 'cache': 'cachĆ©', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cachĆ©, errores y sesiones eliminados', 'can be a git repo': 'puede ser un repositorio git', 'Cancel': 'Cancelar', @@ -74,29 +97,41 @@ 'Cannot compile: there are errors in your app:': 'No se puede compilar: hay errores en su aplicación:', 'cannot create file': 'no es posible crear archivo', 'cannot upload file "%(filename)s"': 'no es posible subir archivo "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'cambie contraseƱa admin', 'change editor settings': 'cambiar la configuración del editor', 'Change Password': 'Cambie ContraseƱa', +'Changelog': 'Changelog', 'check all': 'marcar todos', 'Check for upgrades': 'buscar actualizaciones', 'Check to delete': 'Marque para eliminar', 'Checking for upgrades...': 'Buscando actualizaciones...', 'Clean': 'Limpiar', +'Clear': 'Clear', 'Clear CACHE?': 'ĀæLimpiar CACHƉ?', 'Clear DISK': 'Limpiar DISCO', 'Clear RAM': 'Limpiar RAM', 'click here for online examples': 'haga click aquĆ­ para ver ejemplos en lĆ­nea', 'click here for the administrative interface': 'haga click aquĆ­ para usar la interfaz administrativa', 'Click row to expand traceback': 'Click en la fila para expandir el rastreo', +'Click row to view a ticket': 'Click row to view a ticket', 'click to check for upgrades': 'haga clic para buscar actualizaciones', 'click to open': 'click para abrir', 'Client IP': 'IP del Cliente', 'code': 'código', 'Code listing': 'Listado de código', 'collapse/expand all': 'contraer/expandir todo', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', 'commit (mercurial)': 'confirmar (mercurial)', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'Compilar', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'aplicación compilada removida', +'Condition': 'Condition', 'continue': 'continuar', 'Controllers': 'Controladores', 'controllers': 'controladores', @@ -108,6 +143,9 @@ 'Create new simple application': 'Cree una nueva aplicación', 'Create/Upload': 'Crear/Subir', 'created by': 'creado por', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'crontab', 'Current request': 'Solicitud en curso', 'Current response': 'Respuesta en curso', @@ -117,7 +155,10 @@ 'customize me!': 'AdĆ”ptame!', 'data uploaded': 'datos subidos', 'database': 'base de datos', +'Database': 'Database', +'Database %s select': 'Database %s select', 'database %s select': 'selección en base de datos %s', +'Database administration': 'Database administration', 'database administration': 'administración base de datos', 'Database Administration (appadmin)': 'Administración de Base de Datos (appadmin)', 'Date and Time': 'Fecha y Hora', @@ -130,30 +171,51 @@ 'delete plugin': 'eliminar plugin', 'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirĆ” confirmación)', 'Delete:': 'Eliminar:', +'deleted after first hit': 'deleted after first hit', 'Demo': 'Demo', 'Deploy': 'Deploy', 'Deploy on Google App Engine': 'Instale en Google App Engine', 'Deploy to OpenShift': 'Instale en OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', 'Description': 'Descripción', +'Description:': 'Description:', 'design': 'modificar', 'DESIGN': 'DISEƑO', 'Design for': 'DiseƱo para', 'Detailed traceback description': 'Descripción detallada del rastreo', 'details': 'detalles', 'direction: ltr': 'dirección: ltr', +'directory not found': 'directory not found', 'Disable': 'Deshabilitar', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', 'DISK': 'DISCO', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'documentos', 'Docs': 'Documentos', 'Done!': 'Listo!', 'done!': 'listo!', +'Downgrade': 'Downgrade', 'Download': 'Descargar', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download files via http:': 'descargar archivos via http:', 'download layouts': 'descargar layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'descargar plugins', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'Correo electrónico', 'EDIT': 'EDITAR', 'Edit': 'editar', +'edit all': 'edit all', 'Edit application': 'Editar aplicación', 'edit controller': 'editar controlador', 'edit controller:': 'editar controlador:', @@ -166,9 +228,14 @@ 'Editing Language file': 'Editando archivo de lenguaje', 'Editing myclientapi': 'Editando myclientapi', 'Editing myemail': 'Editando myemail', +'Editing Plural Forms File': 'Editing Plural Forms File', 'Editing rbare': 'Editando rbare', 'Editing ul': 'Editando ul', +'Editor': 'Editor', +'Email Address': 'Email Address', 'Enable': 'Habilitar', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Enterprise Web Framework': 'Framework Web Empresarial', 'Error': 'Error', 'Error logs for "%(app)s"': 'BitĆ”cora de errores en "%(app)s"', @@ -176,11 +243,14 @@ 'Error ticket': 'Error ticket', 'Errors': 'errores', 'Errors in form, please check it out.': 'Errores en el formulario, verifique por favor.', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Atributos de la instancia de Excepción', 'Exit Fullscreen': 'Salir de pantalla completa', 'Expand Abbreviation': 'Expandir abreviación', 'Expand Abbreviation (html files only)': 'Expandir Abreviación (sólo archivos html)', 'export as csv file': 'exportar como archivo CSV', +'Exports:': 'Exports:', 'exposes': 'expone', 'exposes:': 'expone:', 'extends': 'extiende', @@ -195,27 +265,53 @@ 'file "%s" of %s restored': 'archivo "%s" de %s restaurado', 'file changed on disk': 'archivo modificado en el disco', 'file does not exist': 'archivo no existe', +'file not found': 'file not found', 'file saved on %(time)s': 'archivo guardado %(time)s', 'file saved on %s': 'archivo guardado %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'filter', 'Find Next': 'Buscar próximo', 'Find Previous': 'Bucar anterior', 'First name': 'Nombre', +'Form has errors': 'Form has errors', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', 'Globals##debug': 'Globals', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', 'graph model': 'graficación del modelo', +'Graph Model': 'Graph Model', 'Group ID': 'ID de Grupo', 'Hello World': 'Hola Mundo', 'Help': 'ayuda', 'here': 'aquĆ­', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'htmledit', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Si el reporte anterior contiene un nĆŗmero de tiquete este indica un falla en la ejecución del controlador, antes de cualquier intento de ejecutat doctests. Esto generalmente se debe a un error en la indentación o un error por fuera del código de la función.\r\nUn titulo verde indica que todas las pruebas pasaron (si existen). En dicho caso los resultados no se muestran.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', 'Image': 'Imagen', +'import': 'import', 'Import/Export': 'Importar/Exportar', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'incluye', +'Indent with tabs': 'Indent with tabs', 'insert new': 'inserte nuevo', 'insert new %s': 'inserte nuevo %s', 'inspect attributes': 'inspeccionar atributos', @@ -226,47 +322,72 @@ 'Interaction at %s line %s': 'Interacción en %s lĆ­nea %s', 'Interactive console': 'Terminal interactiva', 'internal error': 'error interno', +'internal error: %s': 'internal error: %s', 'Internal State': 'Estado Interno', 'Invalid action': 'Acción invĆ”lida', 'Invalid application name': 'Nombre de aplicación no vĆ”lido', +'invalid circular reference': 'invalid circular reference', 'Invalid email': 'Correo invĆ”lido', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'contraseƱa invĆ”lida', 'invalid password.': 'contraseƱa invĆ”lida.', 'Invalid Query': 'Consulta invĆ”lida', 'invalid request': 'solicitud invĆ”lida', 'Invalid request': 'Petición invĆ”lida', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'tiquete invĆ”lido', +'Key': 'Key', 'Key bindings': 'Key bindings', 'Key bindings for ZenCoding Plugin': 'Key bindings para el Plugin ZenCoding', 'Keyboard shortcuts': 'Atajos de teclado', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'archivo de lenguaje "%(filename)s" creado/actualizado', 'Language files (static strings) updated': 'Archivos de lenguaje (cadenas estĆ”ticas) actualizados', 'languages': 'lenguajes', 'Languages': 'Lenguajes', 'languages updated': 'lenguajes actualizados', 'Last name': 'Apellido', +'Last Revision': 'Last Revision', 'Last saved on:': 'Guardado en:', 'License for': 'Licencia para', 'License:': 'Licencia:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', 'lists by ticket': 'listas por ticket', +'Loading...': 'Loading...', 'loading...': 'cargando...', +'Local Apps': 'Local Apps', 'locals': 'locals', 'Locals##debug': 'Locals', 'Login': 'Inicio de sesión', 'login': 'inicio de sesión', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'Inicio de sesión para la Interfaz Administrativa', +'Login/Register': 'Login/Register', 'Logout': 'fin de sesión', 'Lost Password': 'ContraseƱa perdida', +'lost password': 'lost password', +'Main Menu': 'Main Menu', 'manage': 'gestionar', 'Manage': 'Gestionar', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', 'Manage Cache': 'Administrar CachĆ©', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', 'merge': 'combinar', 'Models': 'Modelos', 'models': 'modelos', +'Modified On': 'Modified On', 'Modules': 'Módulos', 'modules': 'módulos', +'Multi User Mode': 'Multi User Mode', 'Name': 'Nombre', 'new application "%s" created': 'nueva aplicación "%s" creada', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', 'New application wizard': 'Asistente para nueva aplicación', 'new plugin installed': 'nuevo plugin instalado', 'New plugin installed: %s': 'Nuevo plugin instalado: %s', @@ -278,14 +399,25 @@ 'new record inserted': 'nuevo registro insertado', 'New simple application': 'Nueva aplicación', 'next': 'siguiente', +'next %s rows': 'next %s rows', 'next 100 rows': '100 filas siguientes', 'NO': 'NO', +'no changes': 'no changes', 'No databases in this application': 'No hay bases de datos en esta aplicación', 'No Interaction yet': 'No hay interacción', 'no match': 'no encontrado', 'no package selected': 'ningĆŗn paquete seleccionado', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': 'No se encontró ticket_storage.txt en la carpeta /private', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'diseƱador en lĆ­nea', +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', 'or alternatively': 'o alternativamente', 'Or Get from URL:': 'O obtener desde una URL:', 'or import from csv file': 'o importar desde archivo CSV', @@ -302,28 +434,52 @@ 'PAM authenticated user, cannot change password here': 'usuario autenticado por PAM, no puede cambiar la contraseƱa aquĆ­', 'Password': 'ContraseƱa', 'password changed': 'contraseƱa cambiada', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'Visualizando archivo', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Please': 'Por favor', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'Plugin': 'Plugin', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" eliminado', 'Plugin "%s" in application': 'Plugin "%s" en aplicación', +'plugin not specified': 'plugin not specified', 'Plugin page': 'PĆ”gina del plugin', 'plugins': 'plugins', 'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Este sitio usa', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 filas anteriores', 'Private files': 'Archivos privados', 'private files': 'archivos privados', 'Project Progress': 'Progreso del Proyecto', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Consulta:', 'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', 'Rapid Search': 'BĆŗsqueda rĆ”pida', +'Record': 'Record', 'record': 'registro', 'record does not exist': 'el registro no existe', 'record id': 'id de registro', 'Record ID': 'ID de Registro', +'Record id': 'Record id', 'refresh': 'recargar', +'register': 'register', 'Register': 'RegĆ­strese', 'Registration key': 'ContraseƱa de Registro', 'reload': 'recargar', @@ -335,15 +491,25 @@ 'Repository (%s)': 'Repositorio (%s)', 'Repository: %s': 'Repositorio: %s', 'request': 'petición', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', 'Resolve Conflict file': 'archivo Resolución de Conflicto', 'response': 'respuesta', +'restart': 'restart', 'restore': 'restaurar', 'return': 'return', 'revert': 'revertir', +'Revert': 'Revert', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', 'Role': 'Rol', +'Roles': 'Roles', 'Rows in table': 'Filas en la tabla', +'Rows in Table': 'Rows in Table', 'Rows selected': 'Filas seleccionadas', 'rules are not defined': 'reglas no estĆ”n definidas', +'Run tests': 'Run tests', 'Run tests in this file': 'Ejecute tests en este archivo', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Ejecute tests en este archivo (para ejecutarlo en todos los archivos, podrĆ­as usar el botón etiquetado como 'test')", 'Running on %s': 'Ejecutando en %s', @@ -351,18 +517,31 @@ 'save': 'guardar', 'Save file:': 'Guardar archivo:', 'Save file: %s': 'Guardar archivo: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Guardar vĆ­a Ajax', 'Saved file hash:': 'Hash del archivo guardado:', 'Screenshot %s': 'Screenshot %s', 'Screenshots': 'Screenshots', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': 'seleccionado(s)', 'session': 'sesión', 'session expired': 'sesión expirada', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Establecer punto de ruptura en %s en la lĆ­nea %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'sitio', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'algunos archivos no pudieron ser removidos', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', 'source : filesystem': 'fuente : sistema de archivos', +'Start a new app': 'Start a new app', 'Start searching': 'Iniciar bĆŗsqueda', 'Start wizard': 'Iniciar asistente', 'state': 'estado', @@ -370,6 +549,7 @@ 'static': 'estĆ”ticos', 'Static files': 'Archivos estĆ”ticos', 'Statistics': 'EstadĆ­sticas', +'Step': 'Step', 'step': 'paso', 'stop': 'parar', 'submit': 'enviar', @@ -378,32 +558,49 @@ 'successful': 'exitoso', 'Sure you want to delete this object?': 'ĀæEstĆ” seguro que desea eliminar este objeto?', 'switch to : db': 'cambiar a : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'tabla', +'Table': 'Table', 'Table name': 'Nombre de la tabla', +'Temporary': 'Temporary', 'test': 'probar', 'Testing application': 'Probando aplicación', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "consulta" es una condición como "db.tabla1.campo1==\'valor\'". Algo como "db.tabla1.campo1==db.tabla2.campo2" resulta en un JOIN SQL.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'La lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'the data representation, define database tables and sets': 'la representación de datos, define tablas y conjuntos de base de datos', 'The data representation, define database tables and sets': 'La representación de datos, define tablas y conjuntos de base de datos', 'The presentations layer, views are also known as templates': 'La capa de presentación, las vistas tambiĆ©n son llamadas plantillas', 'the presentations layer, views are also known as templates': 'la capa de presentación, las vistas tambiĆ©n son llamadas plantillas', +'Theme': 'Theme', 'There are no controllers': 'No hay controladores', 'There are no models': 'No hay modelos', 'There are no modules': 'No hay módulos', 'There are no plugins': 'No hay plugins', 'There are no private files': 'No hay archivos privados', 'There are no static files': 'No hay archivos estĆ”ticos', +'There are no translators': 'There are no translators', 'There are no translators, only default language is supported': 'No hay traductores, sólo el lenguaje por defecto es soportado', 'There are no views': 'No hay vistas', 'These files are not served, they are only available from within your app': 'Estos archivos no se proveen, ellos sólo estĆ”n disponibles para su aplicación', 'These files are served without processing, your images go here': 'Estos archivos se proveen sin procesar, sus imĆ”genes van aquĆ­', 'these files are served without processing, your images go here': 'estos archivos se proveen sin procesar, sus imĆ”genes van aquĆ­', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', 'This is the %(filename)s template': 'EstĆ” es la plantilla %(filename)s', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', 'this page to see if a breakpoint was hit and debug interaction is required.': 'esta pĆ”gina para ver si un punto de ruptura fue configurado y la depuración es requerida.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Timestamp', 'TM': 'MR', 'to previous version.': 'a la versión previa.', @@ -420,25 +617,34 @@ 'try something like': 'intente algo como', 'Try the mobile interface': 'Pruebe la interfaz móvil', 'try view': 'Pruebe la vista', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', 'Type some Python code in here and hit Return (Enter) to execute it.': 'Escriba algĆŗn código Python aquĆ­ y teclee la tecla Enter para ejecutarlo', 'Unable to check for upgrades': 'No es posible verificar la existencia de actualizaciones', 'unable to create application "%s"': 'no es posible crear la aplicación "%s"', 'unable to delete file "%(filename)s"': 'no es posible eliminar el archivo "%(filename)s"', 'unable to delete file plugin "%(plugin)s"': 'no es posible eliminar plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', 'Unable to download': 'No es posible la descarga', 'Unable to download app': 'No es posible descargar la aplicación', 'Unable to download app because:': 'No es posible descargar la aplicación porque:', 'Unable to download because': 'No es posible descargar porque', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'no es posible analizar el archivo CSV', 'unable to uninstall "%s"': 'no es posible instalar "%s"', 'unable to upgrade because "%s"': 'no es posible actualizar porque "%s"', 'uncheck all': 'desmarcar todos', 'Uninstall': 'desinstalar', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'actualizar', 'update all languages': 'actualizar todos los lenguajes', 'Update:': 'Actualice:', +'Upgrade': 'Upgrade', 'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'actualize web2py ahora', +'upload': 'upload', 'Upload': 'Subir', 'Upload & install packed application': 'Suba e instale aplicación empaquetada', 'Upload a package:': 'Subir un paquete:', @@ -448,7 +654,11 @@ 'upload file:': 'suba un archivo:', 'upload plugin file:': 'suba un archivo de plugin:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para crear consultas mĆ”s complejas.', +'User': 'User', 'User ID': 'ID de Usuario', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', 'Version': 'Versión', 'versioning': 'versiones', @@ -456,16 +666,28 @@ 'view': 'vista', 'Views': 'Vistas', 'views': 'vistas', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py estĆ” actualizado', 'web2py online debugger': 'web2py debugger en lĆ­nea', 'web2py Recent Tweets': 'Tweets Recientes de web2py', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'web2py actualizado; favor reiniciar', 'Welcome to web2py': 'Bienvenido a web2py', +'WSGI reference name': 'WSGI reference name', 'YES': 'Sƍ', 'Yes': 'SĆ­', 'You are going to install': 'Vas a instalar', -'You need to set up and reach a': 'Necesitas configurar y obtener un', -'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Tu aplicación serĆ” bloqueada hasta que des click en un botón de acción (siguiente, paso, continuar, etc.)', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', 'You can inspect variables using the console below': 'Puedes inspeccionar las variables utilizando la terminal de abajo', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'Necesitas configurar y obtener un', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Tu aplicación serĆ” bloqueada hasta que des click en un botón de acción (siguiente, paso, continuar, etc.)', } diff --git a/applications/admin/languages/fr.py b/applications/admin/languages/fr.py old mode 100644 new mode 100755 index 66326765..c8a180b1 --- a/applications/admin/languages/fr.py +++ b/applications/admin/languages/fr.py @@ -3,277 +3,646 @@ '!langcode!': 'fr', '!langname!': 'FranƧais', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" est une expression en option tels que "field1 = \'newvalue\'". Vous ne pouvez pas mettre Ć  jour ou supprimer les rĆ©sultats d\'une jointure "a JOIN"', -'%Y-%m-%d': '%d-%m-%Y', -'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': 'lignes %s supprimĆ©es', '%s %%{row} updated': 'lignes %s mises Ć  jour', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%d-%m-%Y', +'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)', '(requires internet access)': '(nĆ©cessite un accĆØs Internet)', +'(requires internet access, experimental)': '(nĆ©cessite un accĆØs Internet, expĆ©rimentale)', '(something like "it-it")': '(quelque chose comme "it-it") ', +'(version %s)': '(version %s)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Cherche: **%s** fichiers', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available: %s': 'Une nouvelle version de web2py est disponible: %s ', 'A new version of web2py is available: Version 1.68.2 (2009-10-21 09:59:29)\n': 'Une nouvelle version de web2py est disponible: Version 1.68.2 (2009-10-21 09:59:29)\r\n', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: nĆ©cessite une connexion sĆ©curisĆ©e (HTTPS) ou ĆŖtre en localhost. ', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: les tests ne sont pas thread-safe DONC NE PAS EFFECTUER DES TESTS MULTIPLES SIMULTANƉMENT.', -'ATTENTION: you cannot edit the running application!': "ATTENTION: vous ne pouvez pas modifier l'application qui tourne!", +'Abort': 'Abort', 'About': 'Ć  propos', 'About application': "A propos de l'application", +'Accept Terms': 'Termes acceptĆ©s', +'Add breakpoint': 'Ajouter une interruption', 'Additional code for your application': 'Code additionnel pour votre application', +'additional code for your application': 'code supplĆ©mentaire pour votre application', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin dĆ©sactivĆ©e car aucun mot de passe admin', +'admin disabled because not supported on google app engine': 'admin dĆ©sactivĆ©e car non prise en charge sur Google Apps engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': "admin dĆ©sactivĆ©e car incapable d'accĆ©der au fichier mot de passe", 'Admin is disabled because insecure channel': 'Admin est dĆ©sactivĆ© parce que canal non sĆ©curisĆ©', 'Admin language': "Language de l'admin", +'Admin versioning page': 'Admin versioning page', +'administrative interface': "interface d'administration", 'Administrator Password:': 'Mot de passe Administrateur:', +'An error occured, please [[reload %s]] the page': 'Une erreur c’est produite, s’il vous plait [[reload %s]] la page', +'and rename it (required):': 'et renommez-la (obligatoire):', +'and rename it:': 'et renommez-le:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin est dĆ©sactivĆ© parce que canal non sĆ©curisĆ©', +'Application': 'Application', +'application "%s" uninstalled': 'application "%s" dĆ©sinstallĆ©e', +'application %(appname)s installed with md5sum: %(digest)s': 'application %(appname)s installĆ©e avec md5sum: %(digest)s', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'application compilĆ©e', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': "l'application est compilĆ©e et ne peut ĆŖtre modifiĆ©e", 'Application name:': "Nom de l'application:", +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'Êtes-vous sĆ»r de vouloir supprimer le fichier Ā«%sĀ»?', 'Are you sure you want to delete plugin "%s"?': 'Êtes-vous sĆ»r de vouloir supprimer le plugin "%s"?', 'Are you sure you want to delete this object?': 'Êtes-vous sĆ»r de vouloir supprimer cet objet?', 'Are you sure you want to uninstall application "%s"?': "Êtes-vous sĆ»r de vouloir dĆ©sinstaller l'application Ā«%sĀ»?", 'Are you sure you want to upgrade web2py now?': 'Êtes-vous sĆ»r de vouloir mettre Ć  jour web2py maintenant?', +'Are you sure?': 'Etes vous sĆ»r?', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: nĆ©cessite une connexion sĆ©curisĆ©e (HTTPS) ou ĆŖtre en localhost. ', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: les tests ne sont pas thread-safe DONC NE PAS EFFECTUER DES TESTS MULTIPLES SIMULTANƉMENT.', +'ATTENTION: you cannot edit the running application!': "ATTENTION: vous ne pouvez pas modifier l'application qui tourne!", +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Bases de donnĆ©es et tables disponible', +'Available Databases and Tables': 'Available Databases and Tables', +'back': 'retour', +'Back to the plugins list': 'Retour Ć  la liste de plugins', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyĆ©s', +'can be a git repo': 'can be a git repo', +'Cancel': 'Retour', 'Cannot be empty': 'Ne peut pas ĆŖtre vide', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Ne peut pas compiler: il y a des erreurs dans votre application. corriger les erreurs et essayez Ć  nouveau.', 'Cannot compile: there are errors in your app:': 'Ne peut pas compiler: il y a des erreurs dans votre application:', +'cannot create file': 'ne peut pas crĆ©er de fichier', +'cannot upload file "%(filename)s"': 'ne peut pas charger le fichier "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Changer le mot de passe admin', +'change editor settings': 'change editor settings', +'Changelog': 'Changelog', +'check all': 'tout vĆ©rifier ', 'Check for upgrades': 'VĆ©rifier les mises Ć  jour', 'Check to delete': 'Cocher pour supprimer', 'Checking for upgrades...': 'VĆ©rification des mises Ć  jour ... ', 'Clean': 'nettoyer', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', +'click to check for upgrades': 'Cliquez pour vĆ©rifier les mises jour', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'tout rĆ©duire/agrandir', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'compiler', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'application compilĆ©e enlevĆ©e', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'ContrĆ“leurs', +'controllers': 'contrĆ“leurs', +'Count': 'Count', 'Create': 'CrĆ©er', +'create file with filename:': 'crĆ©er un fichier avec nom de fichier:', +'create new application:': 'crĆ©er une nouvelle application:', 'Create new simple application': 'CrĆ©er une nouvelle application', +'Create/Upload': 'Create/Upload', +'created by': 'crƩƩ par', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'crontab', 'Current request': 'RequĆŖte actuelle', 'Current response': 'RĆ©ponse actuelle', 'Current session': 'Session en cours', -'Date and Time': 'Date et heure', -'Delete': 'Supprimer', -'Delete this file (you will be asked to confirm deletion)': 'Supprimer ce fichier (on vous demandera de confirmer la suppression)', -'Delete:': 'Supprimer:', -'Deploy': 'DĆ©ployer', -'Deploy on Google App Engine': 'DĆ©ployer sur Google App Engine', -'EDIT': 'MODIFIER', -'Edit': 'modifier', -'Edit application': "Modifier l'application", -'Edit current record': 'Modifier cette entrĆ©e', -'Editing Language file': 'Modifier le fichier de langue', -'Editing file': 'Modifier le fichier', -'Editing file "%s"': 'Modifier le fichier "% s" ', -'Enterprise Web Framework': 'Enterprise Web Framework', -'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"', -'Errors': 'erreurs', -'Exception instance attributes': "Attributs d'instance Exception", -'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraĆ®neront des tests [passed] .', -'Help': 'aide', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': "Si le rapport ci-dessus contient un numĆ©ro de ticket, cela indique une dĆ©faillance dans l'exĆ©cution du contrĆ“leur, avant toute tentative d'exĆ©cuter les doctests. Cela est gĆ©nĆ©ralement dĆ» Ć  une erreur d'indentation ou une erreur Ć  l'extĆ©rieur du code de la fonction.\r\nUn titre vert indique que tous les tests (si dĆ©finis) sont passĆ©s. Dans ce cas, les rĆ©sultats des essais ne sont pas affichĆ©es.", -'Import/Export': 'Importer/Exporter', -'Install': 'Installer', -'Installed applications': 'Applications installĆ©es', -'Internal State': 'Ɖtat Interne', -'Invalid Query': 'RequĆŖte non valide', -'Invalid action': 'Action non valide', -'Language files (static strings) updated': 'Fichiers de langue (chaĆ®nes statiques) mis Ć  jour ', -'Languages': 'Langues', -'Last saved on:': 'DerniĆØre sauvegarde le:', -'License for': 'Licence pour', -'Login': 'Connexion', -'Login to the Administrative Interface': "Se connecter Ć  l'interface d'administration", -'Logout': 'dĆ©connexion', -'Models': 'ModĆØles', -'Modules': 'Modules', -'NO': 'NON', -'New Record': 'Nouvelle EntrĆ©e', -'New application wizard': 'Assistant nouvelle application', -'New simple application': 'Nouvelle application simple', -'No databases in this application': 'Aucune base de donnĆ©es dans cette application', -'Original/Translation': 'Original / Traduction', -'Overwrite installed app': "Ɖcraser l'application installĆ©e", -'PAM authenticated user, cannot change password here': 'Utilisateur authentifiĆ© par PAM, vous ne pouvez pas changer le mot de passe ici', -'Pack all': 'tout empaqueter', -'Pack compiled': 'paquet compilĆ©', -'Peeking at file': 'Jeter un oeil au fichier', -'Plugin "%s" in application': 'Plugin "%s" dans l\'application', -'Plugins': 'Plugins', -'Powered by': 'PropulsĆ© par', -'Query:': 'RequĆŖte: ', -'Remove compiled': 'retirer compilĆ©', -'Resolve Conflict file': 'RĆ©soudre les conflits de fichiers', -'Rows in table': 'Lignes de la table', -'Rows selected': 'Lignes sĆ©lectionnĆ©es', -"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Lancer les tests dans ce fichier (pour lancer tous les fichiers, vous pouvez Ć©galement utiliser le bouton nommĆ© 'test')", -'Save': 'Enregistrer', -'Saved file hash:': 'Hash du Fichier enregistrĆ©:', -'Site': 'Site', -'Start wizard': "DĆ©marrer l'assistant", -'Static files': 'Fichiers statiques', -'Sure you want to delete this object?': 'Vous ĆŖtes sĆ»r de vouloir supprimer cet objet? ', -'TM': 'MD', -'Testing application': "Test de l'application", -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "requĆŖte" est une condition comme "db.table1.field1==\'value\'". Quelque chose comme "db.table1.field1==db.table2.field2" aboutit Ć  un JOIN SQL.', -'The application logic, each URL path is mapped in one exposed function in the controller': "La logique de l'application, chaque chemin d'URL est mappĆ© avec une fonction exposĆ©e dans le contrĆ“leur", -'The data representation, define database tables and sets': 'La reprĆ©sentation des donnĆ©es, dĆ©finir les tables et ensembles de la base de donnĆ©es', -'The presentations layer, views are also known as templates': "Les couches de prĆ©sentation, les vues sont Ć©galement appelĆ©es modples", -'There are no controllers': "Il n'y a pas de contrĆ“leurs", -'There are no models': "Il n'y a pas de modĆØles", -'There are no modules': "Il n'y a pas de modules", -'There are no plugins': "Il n'y a pas de plugins", -'There are no static files': "Il n'y a pas de fichiers statiques", -'There are no translators, only default language is supported': "Il n'y a pas de traducteurs, seule la langue par dĆ©faut est prise en charge", -'There are no views': "Il n'y a pas de vues", -'These files are served without processing, your images go here': 'Ces fichiers sont renvoyĆ©s sans traitement, vos images viennent ici', -'This is the %(filename)s template': 'Ceci est le modĆØle %(filename)s ', -'Ticket': 'Ticket', -'To create a plugin, name a file/folder plugin_[name]': 'Pour crĆ©er un plugin, crĆ©er un fichier /dossier plugin_[nom]', -'Translation strings for the application': "ChaĆ®nes de traduction pour l'application", -'Unable to check for upgrades': 'Impossible de vĆ©rifier les mises Ć  jour', -'Unable to download': 'Impossible de tĆ©lĆ©charger', -'Unable to download app': "Impossible de tĆ©lĆ©charger l'app", -'Unable to download app because:': "Impossible de tĆ©lĆ©charger l'app car:", -'Unable to download because': 'Impossible de tĆ©lĆ©charger car', -'Uninstall': 'dĆ©sinstaller', -'Update:': 'Mise Ć  jour:', -'Upload & install packed application': "Charger & installer l'application empaquetĆ©e", -'Upload a package:': 'Charger un paquet:', -'Upload existing application': 'Charger une application existante', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilisez (...)&(...) pour AND, (...)|(...) pour OR, et ~(...) pour NOT afin de construire des requĆŖtes plus complexes. ', -'Use an url:': 'Utiliser une url:', -'Version': 'Version', -'Views': 'Vues', -'Web Framework': 'Framework Web', -'YES': 'OUI', -'additional code for your application': 'code supplĆ©mentaire pour votre application', -'admin disabled because no admin password': 'admin dĆ©sactivĆ©e car aucun mot de passe admin', -'admin disabled because not supported on google app engine': 'admin dĆ©sactivĆ©e car non prise en charge sur Google Apps engine', -'admin disabled because unable to access password file': "admin dĆ©sactivĆ©e car incapable d'accĆ©der au fichier mot de passe", -'administrative interface': "interface d'administration", -'and rename it (required):': 'et renommez-la (obligatoire):', -'and rename it:': 'et renommez-le:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin est dĆ©sactivĆ© parce que canal non sĆ©curisĆ©', -'application "%s" uninstalled': 'application "%s" dĆ©sinstallĆ©e', -'application %(appname)s installed with md5sum: %(digest)s': 'application %(appname)s installĆ©e avec md5sum: %(digest)s', -'application compiled': 'application compilĆ©e', -'application is compiled and cannot be designed': "l'application est compilĆ©e et ne peut ĆŖtre modifiĆ©e", -'arguments': 'arguments', -'back': 'retour', -'cache': 'cache', -'cache, errors and sessions cleaned': 'cache, erreurs et sessions nettoyĆ©s', -'cannot create file': 'ne peut pas crĆ©er de fichier', -'cannot upload file "%(filename)s"': 'ne peut pas charger le fichier "%(filename)s"', -'check all': 'tout vĆ©rifier ', -'click to check for upgrades': 'Cliquez pour vĆ©rifier les mises jour', -'code': 'code', -'collapse/expand all': 'tout rĆ©duire/agrandir', -'compiled application removed': 'application compilĆ©e enlevĆ©e', -'controllers': 'contrĆ“leurs', -'create file with filename:': 'crĆ©er un fichier avec nom de fichier:', -'create new application:': 'crĆ©er une nouvelle application:', -'created by': 'crƩƩ par', -'crontab': 'crontab', 'currently running': 'tourne actuellement', 'currently saved or': 'actuellement enregistrĆ© ou', 'data uploaded': 'donnĆ©es chargĆ©es', +'Database': 'Database', 'database': 'base de donnĆ©es', +'Database %s select': 'Database %s select', 'database %s select': 'base de donnĆ©es %s sĆ©lectionner', +'Database administration': 'Database administration', 'database administration': 'administration base de donnĆ©es', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Date et heure', 'db': 'bdd', +'Debug': 'Debug', 'defines tables': 'dĆ©finit les tables', +'Delete': 'Supprimer', 'delete': 'supprimer', 'delete all checked': 'supprimer tout ce qui est cochĆ©', 'delete plugin': ' supprimer le plugin', +'Delete this file (you will be asked to confirm deletion)': 'Supprimer ce fichier (on vous demandera de confirmer la suppression)', +'Delete:': 'Supprimer:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', +'Deploy': 'DĆ©ployer', +'Deploy on Google App Engine': 'DĆ©ployer sur Google App Engine', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', 'design': 'conception', +'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', +'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'docs', +'Docs': 'Docs', 'done!': 'fait!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'tĆ©lĆ©charger layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'tĆ©lĆ©charger plugins', +'Download plugins from repository': 'Download plugins from repository', +'EDIT': 'MODIFIER', +'Edit': 'modifier', +'edit all': 'edit all', +'Edit application': "Modifier l'application", 'edit controller': 'modifier contrĆ“leur', +'edit controller:': 'edit controller:', +'Edit current record': 'Modifier cette entrĆ©e', 'edit views:': 'modifier vues:', +'Editing %s': 'Editing %s', +'Editing file': 'Modifier le fichier', +'Editing file "%s"': 'Modifier le fichier "% s" ', +'Editing Language file': 'Modifier le fichier de langue', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', +'Enterprise Web Framework': 'Enterprise Web Framework', +'Error': 'Error', +'Error logs for "%(app)s"': 'Journal d\'erreurs pour "%(app)s"', +'Error snapshot': 'Error snapshot', +'Error ticket': 'Error ticket', +'Errors': 'erreurs', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': "Attributs d'instance Exception", +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'export au format CSV', +'Exports:': 'Exports:', 'exposes': 'expose', 'exposes:': 'expose:', 'extends': 'Ć©tend', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'impossible de recharger le module', 'failed to reload module because:': 'impossible de recharger le module car:', +'File': 'File', 'file "%(filename)s" created': 'fichier "%(filename)s" crƩƩ', 'file "%(filename)s" deleted': 'fichier "%(filename)s" supprimĆ©', 'file "%(filename)s" uploaded': 'fichier "%(filename)s" chargĆ©', 'file "%s" of %s restored': 'fichier "%s" de %s restaurĆ©', 'file changed on disk': 'fichier modifiĆ© sur le disque', 'file does not exist': "fichier n'existe pas", +'file not found': 'file not found', 'file saved on %(time)s': 'fichier enregistrĆ© le %(time)s', 'file saved on %s': 'fichier enregistrĆ© le %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'filtre', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'Form has errors': 'Form has errors', +'Frames': 'Frames', +'Functions with no doctests will result in [passed] tests.': 'Des fonctions sans doctests entraĆ®neront des tests [passed] .', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'Graph Model': 'Graph Model', +'graph model': 'graph model', +'Help': 'aide', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'edition html', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': "Si le rapport ci-dessus contient un numĆ©ro de ticket, cela indique une dĆ©faillance dans l'exĆ©cution du contrĆ“leur, avant toute tentative d'exĆ©cuter les doctests. Cela est gĆ©nĆ©ralement dĆ» Ć  une erreur d'indentation ou une erreur Ć  l'extĆ©rieur du code de la fonction.\r\nUn titre vert indique que tous les tests (si dĆ©finis) sont passĆ©s. Dans ce cas, les rĆ©sultats des essais ne sont pas affichĆ©es.", +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Importer/Exporter', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'inclus', +'Indent with tabs': 'Indent with tabs', 'index': 'index', 'insert new': 'insĆ©rer nouveau', 'insert new %s': 'insĆ©rer nouveau %s', +'inspect attributes': 'inspect attributes', +'Install': 'Installer', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'Applications installĆ©es', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'erreur interne', +'internal error: %s': 'internal error: %s', +'Internal State': 'Ɖtat Interne', +'Invalid action': 'Action non valide', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'mot de passe invalide', +'invalid password.': 'invalid password.', +'Invalid Query': 'RequĆŖte non valide', 'invalid request': 'Demande incorrecte', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'ticket non valide', +'Key': 'Key', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'fichier de langue "%(filename)s" crƩƩ/mis Ć  jour', +'Language files (static strings) updated': 'Fichiers de langue (chaĆ®nes statiques) mis Ć  jour ', 'languages': 'langues', +'Languages': 'Langues', +'Last Revision': 'Last Revision', +'Last saved on:': 'DerniĆØre sauvegarde le:', +'License for': 'Licence pour', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'Chargement ...', +'Local Apps': 'Local Apps', +'locals': 'locals', +'Locals##debug': 'Locals##debug', +'Login': 'Connexion', 'login': 'connexion', +'Login successful': 'Login successful', +'Login to the Administrative Interface': "Se connecter Ć  l'interface d'administration", +'Login/Register': 'Login/Register', +'Logout': 'dĆ©connexion', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', +"Mercurial Version Control System Interface[[NEWLINE]]for application '%s'": "Mercurial Version Control System Interface[[NEWLINE]]for application '%s'", 'merge': 'fusionner', +'Models': 'ModĆØles', 'models': 'modĆØles', +'Modified On': 'Modified On', +'Modules': 'Modules', 'modules': 'modules', +'Multi User Mode': 'Multi User Mode', 'new application "%s" created': 'nouvelle application "%s" crƩƩe', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', +'New application wizard': 'Assistant nouvelle application', 'new plugin installed': 'nouveau plugin installĆ©', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'Nouvelle EntrĆ©e', 'new record inserted': 'nouvelle entrĆ©e insĆ©rĆ©e', +'New simple application': 'Nouvelle application simple', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': '100 lignes suivantes', +'NO': 'NON', +'no changes': 'no changes', +'No databases in this application': 'Aucune base de donnĆ©es dans cette application', +'No Interaction yet': 'No Interaction yet', 'no match': 'aucune correspondance', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +'Number of entries: **%s**': 'Number of entries: **%s**', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'ou importer depuis un fichier CSV ', 'or provide app url:': "ou fournir l'URL de l'app:", 'or provide application url:': "ou fournir l'URL de l'application:", +'Original/Translation': 'Original / Traduction', +'Overview': 'Overview', +'Overwrite installed app': "Ɖcraser l'application installĆ©e", +'Pack all': 'tout empaqueter', +'Pack compiled': 'paquet compilĆ©', +'Pack custom': 'Pack custom', 'pack plugin': 'paquet plugin', +'PAM authenticated user, cannot change password here': 'Utilisateur authentifiĆ© par PAM, vous ne pouvez pas changer le mot de passe ici', 'password changed': 'mot de passe modifiĆ©', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': 'Jeter un oeil au fichier', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" supprimĆ©', +'Plugin "%s" in application': 'Plugin "%s" dans l\'application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugins', +'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': 'PropulsĆ© par', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 lignes prĆ©cĆ©dentes', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'RequĆŖte: ', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'entrĆ©e', 'record does not exist': "l'entrĆ©e n'existe pas", 'record id': 'id entrĆ©e', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', +'Reload routes': 'Reload routes', +'Remove compiled': 'retirer compilĆ©', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', +'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': 'RĆ©soudre les conflits de fichiers', +'response': 'response', +'restart': 'restart', 'restore': 'restaurer', +'return': 'return', +'Revert': 'Revert', 'revert': 'revenir', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows in table': 'Lignes de la table', +'Rows selected': 'Lignes sĆ©lectionnĆ©es', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Lancer les tests dans ce fichier (pour lancer tous les fichiers, vous pouvez Ć©galement utiliser le bouton nommĆ© 'test')", +'Running on %s': 'Running on %s', +'Save': 'Enregistrer', 'save': 'sauver', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Save via Ajax', +'Saved file hash:': 'Hash du Fichier enregistrĆ©:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Searching: **%s** %%{file}': 'Searching: **%s** %%{file}', +'Select Files to Package': 'Select Files to Package', 'selected': 'sĆ©lectionnĆ©s', +'session': 'session', 'session expired': 'la session a expirĆ© ', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', +'Site': 'Site', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'certains fichiers ne peuvent pas ĆŖtre supprimĆ©s', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', +'Start wizard': "DĆ©marrer l'assistant", 'state': 'Ć©tat', 'static': 'statiques', +'Static': 'Static', +'Static files': 'Fichiers statiques', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'submit': 'envoyer', +'Submit': 'Submit', +'successful': 'successful', +'Sure you want to delete this object?': 'Vous ĆŖtes sĆ»r de vouloir supprimer cet objet? ', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'table', +'Table': 'Table', +'Temporary': 'Temporary', 'test': 'tester', +'Testing application': "Test de l'application", +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "requĆŖte" est une condition comme "db.table1.field1==\'value\'". Quelque chose comme "db.table1.field1==db.table2.field2" aboutit Ć  un JOIN SQL.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': "la logique de l'application, chaque chemin d'URL est mappĆ© dans une fonction exposĆ©e dans le contrĆ“leur", +'The application logic, each URL path is mapped in one exposed function in the controller': "La logique de l'application, chaque chemin d'URL est mappĆ© avec une fonction exposĆ©e dans le contrĆ“leur", 'the data representation, define database tables and sets': 'La reprĆ©sentation des donnĆ©es, dĆ©finir les tables et ensembles de la base de donnĆ©es', +'The data representation, define database tables and sets': 'La reprĆ©sentation des donnĆ©es, dĆ©finir les tables et ensembles de la base de donnĆ©es', +'The presentations layer, views are also known as templates': 'Les couches de prĆ©sentation, les vues sont Ć©galement appelĆ©es modples', 'the presentations layer, views are also known as templates': 'la couche de prĆ©sentation, les vues sont Ć©galement appelĆ©es modĆØles', +'Theme': 'Theme', +'There are no controllers': "Il n'y a pas de contrĆ“leurs", +'There are no models': "Il n'y a pas de modĆØles", +'There are no modules': "Il n'y a pas de modules", +'There are no plugins': "Il n'y a pas de plugins", +'There are no private files': 'There are no private files', +'There are no static files': "Il n'y a pas de fichiers statiques", +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': "Il n'y a pas de traducteurs, seule la langue par dĆ©faut est prise en charge", +'There are no views': "Il n'y a pas de vues", +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'Ces fichiers sont renvoyĆ©s sans traitement, vos images viennent ici', 'these files are served without processing, your images go here': 'ces fichiers sont servis sans transformation, vos images vont ici', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'Ceci est le modĆØle %(filename)s ', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Ticket', +'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'TM': 'MD', 'to previous version.': 'Ć  la version prĆ©cĆ©dente.', +'To create a plugin, name a file/folder plugin_[name]': 'Pour crĆ©er un plugin, crĆ©er un fichier /dossier plugin_[nom]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Traceback', 'translation strings for the application': "chaĆ®nes de traduction de l'application", +'Translation strings for the application': "ChaĆ®nes de traduction pour l'application", 'try': 'essayer', 'try something like': 'essayez quelque chose comme', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Impossible de vĆ©rifier les mises Ć  jour', 'unable to create application "%s"': 'impossible de crĆ©er l\'application "%s"', 'unable to delete file "%(filename)s"': 'impossible de supprimer le fichier "%(filename)s"', 'unable to delete file plugin "%(plugin)s"': 'impossible de supprimer le plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': 'Impossible de tĆ©lĆ©charger', +'Unable to download app': "Impossible de tĆ©lĆ©charger l'app", +'Unable to download app because:': "Impossible de tĆ©lĆ©charger l'app car:", +'Unable to download because': 'Impossible de tĆ©lĆ©charger car', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': "impossible d'analyser les fichiers CSV", 'unable to uninstall "%s"': 'impossible de dĆ©sinstaller "%s"', 'unable to upgrade because "%s"': 'impossible de mettre Ć  jour car "%s"', 'uncheck all': 'tout dĆ©cocher', +'Uninstall': 'dĆ©sinstaller', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'mettre Ć  jour', 'update all languages': 'mettre Ć  jour toutes les langues', +'Update:': 'Mise Ć  jour:', +'Upgrade': 'Upgrade', 'upgrade now': 'mettre Ć  jour maintenant', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'mettre Ć  jour web2py maintenant', 'upload': 'charger', +'Upload': 'Upload', +'Upload & install packed application': "Charger & installer l'application empaquetĆ©e", +'Upload a package:': 'Charger un paquet:', +'Upload and install packed application': 'Upload and install packed application', 'upload application:': "charger l'application:", +'Upload existing application': 'Charger une application existante', 'upload file:': 'charger le fichier:', 'upload plugin file:': 'charger fichier plugin:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilisez (...)&(...) pour AND, (...)|(...) pour OR, et ~(...) pour NOT afin de construire des requĆŖtes plus complexes. ', +'Use an url:': 'Utiliser une url:', 'user': 'utilisateur', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', +'Version': 'Version', 'versioning': 'versioning', +'Versioning': 'Versioning', 'view': 'vue', +'Views': 'Vues', 'views': 'vues', -'web2py Recent Tweets': 'Tweets rĆ©cents sur web2py ', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Framework Web', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py est Ć  jour', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'Tweets rĆ©cents sur web2py ', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'web2py mis Ć  jour; veuillez le redĆ©marrer', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': 'OUI', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/he.py b/applications/admin/languages/he.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/it.py b/applications/admin/languages/it.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/ja.py b/applications/admin/languages/ja.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/my-mm.py b/applications/admin/languages/my-mm.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/pl.py b/applications/admin/languages/pl.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/plural-en.py b/applications/admin/languages/plural-en.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/plural-ru.py b/applications/admin/languages/plural-ru.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/plural-uk.py b/applications/admin/languages/plural-uk.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/pt-br.py b/applications/admin/languages/pt-br.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/pt.py b/applications/admin/languages/pt.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/ro.py b/applications/admin/languages/ro.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/ru.py b/applications/admin/languages/ru.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/sr-cr.py b/applications/admin/languages/sr-cr.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/sr-lt.py b/applications/admin/languages/sr-lt.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/tr.py b/applications/admin/languages/tr.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/uk.py b/applications/admin/languages/uk.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/zh-tw.py b/applications/admin/languages/zh-tw.py old mode 100644 new mode 100755 diff --git a/applications/admin/languages/zh.py b/applications/admin/languages/zh.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/ar.py b/applications/welcome/languages/ar.py old mode 100644 new mode 100755 index a9e06cf2..c3908eba --- a/applications/welcome/languages/ar.py +++ b/applications/welcome/languages/ar.py @@ -1,48 +1,82 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'ar', '!langname!': 'Arabic', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', '%s %%(shop)': '%s %%(shop)', '%s %%(shop[0])': '%s %%(shop[0])', '%s %%{quark[0]}': '%s %%{quark[0]}', +'%s %%{row} deleted': '%s %%{row} deleted', +'%s %%{row} updated': '%s %%{row} updated', '%s %%{shop[0]}': '%s %%{shop[0]}', '%s %%{shop}': '%s %%{shop}', +'%s selected': '%s selected', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'?': '?', '@markmin\x01**Hello World**': '**Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم**', 'About': 'نبذة', 'Access Control': 'Ł…ŲŖŲ­ŁƒŁ…Ų§ŲŖ Ų§Ł„ŁˆŲµŁˆŁ„', +'admin': 'admin', 'Administrative Interface': 'ŁˆŲ§Ų¬Ł‡Ų© Ų§Ł„ŲŖŲ­ŁƒŁ…', 'Ajax Recipes': 'وصفات أجاكس', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'Are you sure you want to delete this object?': 'هل أنت Ł…ŲŖŲ£ŁƒŲÆ بحذف هذا Ų§Ł„ŁƒŲ§Ų¦Ł† ؟', +'Available Databases and Tables': 'Available Databases and Tables', 'Buy this book': 'أؓتري هذا Ų§Ł„ŁƒŲŖŲ§ŲØ', +"Buy web2py's book": "Buy web2py's book", +'cache': 'cache', +'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'Cannot be empty': 'لا ŁŠŁ…ŁƒŁ† بأن ŁŠŁƒŁˆŁ† Ų®Ų§Ł„ŁŠ', 'Check to delete': 'Ų£Ų®ŲŖŲ± للحذف', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Client IP': 'IP المستخدم', 'Community': 'المجتمع', 'Components and Plugins': 'العناصر ŁˆŲ§Ł„Ų„Ų¶Ų§ŁŲ§ŲŖ', +'Config.ini': 'Config.ini', 'Controller': 'Ł…ŲŖŲ­ŁƒŁ…', 'Copyright': 'Ų§Ł„Ų­Ł‚ŁˆŁ‚', 'Created By': 'أنؓئ بواسطة', 'Created On': 'أنؓئ في', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'customize me!': 'Ų§Ł„ŲŖŲ®ŲµŁŠŲµ', +'data uploaded': 'data uploaded', 'Database': 'قاعدة Ų§Ł„ŲØŁŠŲ§Ł†Ų§ŲŖ', +'Database %s select': 'Database %s select', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'db': 'db', 'DB Model': 'Ł†Ł…ŁˆŲ°Ų¬ قاعدة Ų§Ł„ŲØŁŠŲ§Ł†Ų§ŲŖ', +'Delete:': 'Delete:', 'Demo': 'ŲŖŲ¬Ų±ŲØŲ©', 'Deployment Recipes': 'Ų§Ł„ŁˆŲµŁŲ§ŲŖ Ų§Ł„Ł…Ł†Ų“ŁˆŲ±Ų©', 'Description': 'Ų§Ł„ŁˆŲµŁ', +'design': 'design', +'Design': 'Design', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', 'Documentation': 'المستندات', -"Don't know what to do?": "لا تعلم Ł…Ų§Ų°Ų§ ستفعل ؟", +"Don't know what to do?": 'لا تعلم Ł…Ų§Ų°Ų§ ستفعل ؟', +'done!': 'done!', 'Download': 'ŲŖŲ­Ł…ŁŠŁ„', 'E-mail': 'Ų§Ł„ŲØŲ±ŁŠŲÆ Ų§Ł„Ų„Ł„ŁƒŲŖŲ±ŁˆŁ†ŁŠ', +'Edit current record': 'Edit current record', 'Email and SMS': 'Ų§Ł„ŲØŲ±ŁŠŲÆ Ų§Ł„Ų„Ł„ŁƒŲŖŲ±ŁˆŁ†ŁŠ ŁˆŲ§Ł„Ų±Ų³Ų§Ł„Ų© Ų§Ł„Ł†ŲµŁŠŲ©', 'enter an integer between %(min)g and %(max)g': 'أدخل Ų¹ŲÆŲÆ صحيح ŲØŁŠŁ† %(min)g و %(man)g', 'enter date and time as %(format)s': 'أدخل Ų§Ł„ŲŖŲ§Ų±ŁŠŲ® ŁˆŲ§Ł„ŁˆŁ‚ŲŖ ŁƒŲ§Ł„Ł†Ł…Ų· %(format)', 'Errors': 'الأخطاؔ', +'export as csv file': 'export as csv file', 'FAQ': 'الأسئلة الؓائعة', 'First name': 'الأسم Ų§Ł„Ų£ŁˆŁ„', 'Forms and Validators': 'ال؄ستمارات ŁˆŲ§Ł„Ł…ŲÆŁ‚Ł‚Ų§ŲŖ', 'Free Applications': 'ŲŖŲ·ŲØŁŠŁ‚Ų§ŲŖ Ł…Ų¬Ų§Ł†ŁŠŲ©', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Ų§Ł„Ł…Ų¬Ł…ŁˆŲ¹Ų© %(group_id)s قد أنؓئت', 'Group ID': 'Ł‡ŁˆŁŠŲ© Ų§Ł„Ł…Ų¬Ł…ŁˆŲ¹Ų©', 'Group uniquely assigned to user %(id)s': 'Ų§Ł„Ł…Ų¬Ł…ŁˆŲ¹Ų© Ł…Ų®ŲµŲµŲ© للمستخدم %(id)s', @@ -50,44 +84,73 @@ 'Hello World': 'Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم', 'Hello World ## comment': ' Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم', 'Hello World## comment': 'Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم', +'Helping web2py': 'Helping web2py', 'Home': 'Ų§Ł„Ų±Ų¦ŁŠŲ³ŁŠŲ©', 'How did you get here?': 'كيف Ų£Ų³ŲŖŲ·Ų¹ŲŖ Ų§Ł„ŁˆŲµŁˆŁ„ ؄لى هنا ؟', +'import': 'import', +'Import/Export': 'Import/Export', +'Internal State': 'Internal State', 'Introduction': 'مقدمة', 'Invalid email': 'بريد Ų„Ł„ŁƒŲŖŲ±ŁˆŁ†ŁŠ غير صالح', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', 'Is Active': 'نؓط', +'Key': 'Key', 'Last name': 'أسم العائلة', 'Layout': 'النسق', 'Layout Plugins': 'نسّق ال؄ضافات', 'Layouts': 'لأنساق', 'Live Chat': 'المحادثة Ų§Ł„Ų­ŁŠŁ‘Ų©', +'Log In': 'Log In', 'Logged in': 'ŲŖŁ… ŲŖŲ³Ų¬ŁŠŁ„ Ų§Ł„ŲÆŲ®ŁˆŁ„', 'Logged out': 'ŲŖŁ… ŲŖŲ³Ų¬ŁŠŁ„ Ų§Ł„Ų®Ų±ŁˆŲ¬', 'Login': 'ŲŖŲ³Ų¬ŁŠŁ„ Ų§Ł„ŲÆŲ®ŁˆŁ„', 'Logout': 'ŲŖŲ³Ų¬ŁŠŁ„ Ų§Ł„Ų®Ų±ŁˆŲ¬', 'Lost Password': 'فقدت ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ±', 'Lost password?': 'هل فقدت ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ± ؟', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'قالب Ų§Ł„Ł‚ŁˆŲ§Ų¦Ł…', 'Modified By': 'عًدلت بواسطة', 'Modified On': 'Ų¹ŁŲÆŁ„ŲŖ في', 'My Sites': 'Ł…ŁˆŁ‚Ų¹ŁŠ', 'Name': 'الأسم', +'New Record': 'New Record', +'new record inserted': 'new record inserted', +'next %s rows': 'next %s rows', +'No databases in this application': 'No databases in this application', 'Object or table name': 'أسم Ų§Ł„ŁƒŲ§Ų¦Ł† أو Ų§Ł„Ų¬ŲÆŁˆŁ„', +'Online book': 'Online book', 'Online examples': 'أمثلة على الأنترنت', +'or import from csv file': 'or import from csv file', 'Origin': 'أصل', 'Other Plugins': '؄ضافات أخرى', 'Other Recipes': 'وصفات أخرى', 'Overview': 'نظرة Ų¹Ų§Ł…Ų©', 'Password': 'ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ±', -"Password fields don't match": "Ų­Ł‚ŁˆŁ„ ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ± لا ŲŖŲŖŲ·Ų§ŲØŁ‚", +"Password fields don't match": 'Ų­Ł‚ŁˆŁ„ ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ± لا ŲŖŲŖŲ·Ų§ŲØŁ‚', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'الرجاؔ Ų„Ų¹Ų§ŲÆŲ© Ų„ŲÆŲ®Ų§Ł„ ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ±', 'Plugins': 'ال؄ضافات', 'Powered by': 'Ł…ŲÆŲ¹ŁˆŁ… بواسطة', 'Preface': 'المدخل', +'previous %s rows': 'previous %s rows', 'Profile': 'الملف Ų§Ł„Ų“Ų®ŲµŁŠ', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'ŲØŲ§ŁŠŲ«ŁˆŁ†', +'Query:': 'Query:', 'Quick Examples': 'أمثلة سريعة', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', 'Recipes': 'وصفات', +'Record': 'Record', +'record does not exist': 'record does not exist', 'Record ID': 'Ł‡ŁˆŁŠŲ© السجل ', +'Record id': 'Record id', 'Register': 'Ų§Ł„ŲŖŲ³Ų¬ŁŠŁ„', 'Registration identifier': 'Ł…ŁŲ¹Ų±Ł Ų§Ł„ŲŖŲ³Ų¬ŁŠŁ„', 'Registration key': 'رمز Ų§Ł„ŲŖŲ³Ų¬ŁŠŁ„', @@ -95,20 +158,38 @@ 'Remember me (for 30 days)': 'ŲŖŲ°ŁƒŲ±Ł†ŁŠ ( ؄لى 30 ŁŠŁˆŁ…)', 'Reset Password key': 'Ų„Ų¹Ų§ŲÆŲ© ŲøŲØŲ· مفتاح ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ±', 'Role': 'دور', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', +'Save model as...': 'Save model as...', 'Semantic': 'ŲÆŁ„ŁŠŁ„ Ł„ŁŲøŁŠ', 'Services': 'Ų®ŲÆŁ…Ų§ŲŖ', +'Sign Up': 'Sign Up', +'Size of cache:': 'Size of cache:', +'state': 'state', +'Statistics': 'Statistics', 'Stylesheet': 'Ų£Ų³Ł„ŁˆŲØ النمط', +'submit': 'submit', 'Support': 'الدعم', +'Table': 'Table', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', 'The Core': 'Ų§Ł„Ł†ŁˆŲ§Ų©', -'The output of the file is a dictionary that was rendered by the view %s': 'نتاج هذا الملف Ł‡Łˆ Ł‚Ų§Ł…ŁˆŲ³ قًدم بواسطة العارض %s', +'The output of the file is a dictionary that was rendered by the view %s': 'نتاج هذا الملف Ł‡Łˆ Ł‚Ų§Ł…ŁˆŲ³ قًدم بواسطة العارض %s', 'The Views': 'المؓاهدات', 'This App': 'هذا Ų§Ł„ŲŖŲ·ŲØŁŠŁ‚', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'البصمة Ų§Ł„Ų²Ł…Ł†ŁŠŲ©', +'Traceback': 'Traceback', 'Twitter': 'تويتر', +'unable to parse csv file': 'unable to parse csv file', +'Update:': 'Update:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', 'User %(id)s Logged-in': 'المستخدم %(id)s قد سجل ŲÆŲ®ŁˆŁ„Ł‡', 'User %(id)s Logged-out': 'المستخدم %(id)s قد سجل Ų®Ų±ŁˆŲ¬Ł‡', 'User %(id)s Registered': 'المستخدم %(id)s مسجل', 'User ID': 'Ł‡ŁˆŁŠŲ© المستخدم', +'Users': 'Users', 'value already in database or empty': 'Ų§Ł„Ł‚ŁŠŁ…Ų© Ł…ŁˆŲ¬ŁˆŲÆŲ© مسبقاً أو فارغة', 'Verify Password': 'تأكيد ŁƒŁ„Ł…Ų© Ų§Ł„Ł…Ų±ŁˆŲ±', 'Videos': 'Ų§Ł„ŁŁŠŲÆŁŠŁˆŁ‡Ų§ŲŖ', @@ -116,6 +197,7 @@ 'Welcome': 'Ł…Ų±Ų­ŲØŲ§Ł‹', 'Welcome to web2py!': 'Ł…Ų±Ų­ŲØŲ§Ł‹ ŲØŁƒŁ… في ويب2 باي !', 'Which called the function %s located in the file %s': 'الدالة المسماة %s Ł…ŁˆŲ¬ŁˆŲÆŲ© في ملف %s', +'Working...': 'Working...', 'You are successfully running web2py': 'Ų£Ų³ŲŖŲ·Ų¹ŲŖ تثبيت web2py بنجاح !', 'You can modify this application and adapt it to your needs': 'تستطيع ŲŖŲ¹ŲÆŁŠŁ„ هذا Ų§Ł„ŲŖŲ·ŲØŁŠŁ‚ لما ŁŠŁ†Ų§Ų³ŲØ ؄حتياجك', 'You visited the url %s': ' ملقد Ų²Ų±ŲŖ الرابط %s', diff --git a/applications/welcome/languages/ca.py b/applications/welcome/languages/ca.py old mode 100644 new mode 100755 index e0f3109e..2fb827f1 --- a/applications/welcome/languages/ca.py +++ b/applications/welcome/languages/ca.py @@ -11,6 +11,7 @@ '%Y-%m-%d': '%d/%m/%Y', '%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', '(something like "it-it")': '(similar a "això-això")', +'?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'Hi ha hagut un error, si us plau [[recarregui %s]] la pĆ gina', '@markmin\x01Number of entries: **%s**': "Nombre d'entrades: **%s**", 'A new version of web2py is available': 'Hi ha una nova versió de wep2py disponible', @@ -22,6 +23,7 @@ 'Add': 'Afegir', 'Add Record': 'Afegeix registre', 'additional code for your application': '`codi addicional per a la seva aplicació', +'admin': 'admin', 'admin disabled because no admin password': 'admin inhabilitat per falta de contrasenya', 'admin disabled because not supported on google app engine': 'admin inhabilitat, no Ć©s suportat en GAE', 'admin disabled because unable to access password file': 'admin inhabilitat, impossible accedir al fitxer con la contrasenya', @@ -57,6 +59,7 @@ 'Available Databases and Tables': 'Bases de dades i taules disponibles', 'Back': 'Endarrera', 'Buy this book': 'Compra aquest lllibre', +"Buy web2py's book": "Buy web2py's book", 'Cache': 'CachĆ©', 'cache': 'cachĆ©', 'Cache Cleared': 'CachĆ© Netejada', @@ -88,6 +91,7 @@ 'compile': 'compilar', 'compiled application removed': 'aplicació compilada eliminada', 'Components and Plugins': 'Components i Plugins', +'Config.ini': 'Config.ini', 'contains': 'contĆ©', 'Controller': 'Controlador', 'Controllers': 'Controladors', @@ -128,6 +132,7 @@ 'Description': 'Descripció', 'design': 'diseny', 'DESIGN': 'DISENY', +'Design': 'Design', 'Design for': 'Diseny per a', 'detecting': 'detectant', 'DISK': 'DISC', @@ -193,6 +198,7 @@ 'Forms and Validators': 'Formularis i validadors', 'Free Applications': 'Aplicacions Lliures', 'Functions with no doctests will result in [passed] tests.': 'Funcions sense doctests equivalen a pruebas [aceptades].', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Grupo %(group_id)s creat', 'Group ID': 'ID de Grup', 'Group uniquely assigned to user %(id)s': 'Grup assignat Ćŗnicament al usuari %(id)s', @@ -200,6 +206,7 @@ 'Hello': 'Hola', 'Hello World': 'Hola Món', 'help': 'ajuda', +'Helping web2py': 'Helping web2py', 'Home': 'Inici', 'Hosted by': 'Hosted by', 'How did you get here?': 'Com has arribat aquĆ­?', @@ -260,6 +267,7 @@ 'Manage %(action)s': 'Manage %(action)s', 'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Gestionar la CachĆ©', +'Memberships': 'Memberships', 'Menu Model': 'Model "menu"', 'merge': 'combinar', 'Models': 'Models', @@ -288,6 +296,7 @@ 'not in': 'no a', 'Object or table name': 'Nom del objecte o taula', 'Old password': 'Contrasenya anterior', +'Online book': 'Online book', 'Online examples': 'Ejemples en lĆ­nia', 'Or': 'O', 'or import from csv file': 'o importar desde fitxer CSV', diff --git a/applications/welcome/languages/cs.py b/applications/welcome/languages/cs.py old mode 100644 new mode 100755 index c2169745..11d980db --- a/applications/welcome/languages/cs.py +++ b/applications/welcome/languages/cs.py @@ -12,10 +12,10 @@ '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', '(requires internet access)': '(vyžaduje připojenĆ­ k internetu)', -'(requires internet access, experimental)': '(vyžaduje internetovĆ© připojenĆ­, experimentĆ”lnĆ­)', -'(something like "it-it")': '(například "cs-cz")', +'(requires internet access, experimental)': '(requires internet access, experimental)', +'(something like "it-it")': '(například "cs-cs")', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', -'@markmin\x01An error occured, please [[reload %s]] the page': 'DoÅ”lo k chybě, prosĆ­m [[obnovte strĆ”nku %s]]', '@markmin\x01Searching: **%s** %%{file}': 'HledĆ”nĆ­: **%s** %%{soubor}', 'About': 'O programu', 'About application': 'O aplikaci', @@ -30,11 +30,11 @@ 'administrative interface': 'rozhranĆ­ pro sprĆ”vu', 'Administrator Password:': 'AdministrĆ”torskĆ© heslo:', 'Ajax Recipes': 'Recepty s ajaxem', -'An error occured, please %s the page': 'DoÅ”lo k chybě, prosĆ­m %s strĆ”nku', +'An error occured, please %s the page': 'An error occured, please %s the page', 'and rename it:': 'a přejmenovat na:', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zanĆ” bez zabezpečenĆ©ho spojenĆ­', -'Application': 'Aplikace', +'Application': 'Application', 'application "%s" uninstalled': 'application "%s" odinstalovĆ”na', 'application compiled': 'aplikace zkompilovĆ”na', 'Application name:': 'NĆ”zev aplikace:', @@ -42,23 +42,24 @@ 'are not used yet': 'jeÅ”tě nepoužita', 'Are you sure you want to delete this object?': 'Opravdu chcete odstranit tento objekt?', 'Are you sure you want to uninstall application "%s"?': 'Opravdu chcete odinstalovat aplikaci "%s"?', -'arguments': 'argumenty', -'at char %s': 'na pozici znaku %s', -'at line %s': 'na řÔdku %s', -'ATTENTION:': 'POZOR:', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', 'Available Databases and Tables': 'DostupnĆ© databĆ”ze a tabulky', 'back': 'zpět', -'Back to wizard': 'ZpĆ”tky do prÅÆvodce', -'Basics': 'ZĆ”klady', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', 'Begin': 'Začƭt', 'breakpoint': 'bod přeruÅ”enĆ­', 'Breakpoints': 'Body přeruÅ”enĆ­', 'breakpoints': 'body přeruÅ”enĆ­', -'Buy this book': 'Koupit Web2py knihu', -"Buy web2py's book": 'Koupit Web2py knihu', +'Buy this book': 'Koupit web2py knihu', +"Buy web2py's book": "Buy web2py's book", 'Cache': 'Cache', 'cache': 'cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Klƭče cache', 'cache, errors and sessions cleaned': 'cache, chyby a relace byly pročiÅ”těny', 'can be a git repo': 'může to být git repo', @@ -79,8 +80,8 @@ 'Click row to expand traceback': 'Pro rozbalenĆ­ stopy, klikněte na řÔdek', 'Click row to view a ticket': 'Pro zobrazenĆ­ chyby (ticketu), klikněte na řÔdku...', 'Client IP': 'IP adresa klienta', -'code': 'kód', -'Code listing': 'Výpis kódu', +'code': 'code', +'Code listing': 'Code listing', 'collapse/expand all': 'vÅ”e sbalit/rozbalit', 'Community': 'Komunita', 'Compile': 'Zkompilovat', @@ -88,7 +89,7 @@ 'Components and Plugins': 'Komponenty a zĆ”suvnĆ© moduly', 'Condition': 'PodmĆ­nka', 'Config.ini': 'Config.ini', -'continue': 'pokračovat', +'continue': 'continue', 'Controller': 'KontrolĆ©r (Controller)', 'Controllers': 'KontrolĆ©ry', 'controllers': 'kontrolĆ©ry', @@ -109,18 +110,19 @@ 'data uploaded': 'data nahrĆ”na', 'Database': 'RozhranĆ­ databĆ”ze', 'Database %s select': 'databĆ”ze %s výběr', -'Database administration': 'Administrace databĆ”ze', +'Database administration': 'Database administration', 'database administration': 'sprĆ”va databĆ”ze', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Datum a čas', 'day': 'den', 'db': 'db', 'DB Model': 'DatabĆ”zový model', 'Debug': 'LaděnĆ­', -'defines tables': 'definuje tabulky', +'defines tables': 'defines tables', 'Delete': 'Smazat', 'delete': 'smazat', 'delete all checked': 'smazat vÅ”e označenĆ©', -'delete plugin': 'zruÅ”it plugin', +'delete plugin': 'delete plugin', 'Delete this file (you will be asked to confirm deletion)': 'Smazat tento soubor (budete požÔdĆ”n o potvrzenĆ­ mazĆ”nĆ­)', 'Delete:': 'Smazat:', 'deleted after first hit': 'smazat po prvnĆ­m dosaženĆ­', @@ -141,36 +143,35 @@ 'Disk Cleared': 'Disk smazĆ”n', 'docs': 'dokumentace', 'Documentation': 'Dokumentace', -"Don't know what to do?": 'Kde najdu dalŔí informace ?', +"Don't know what to do?": 'NevĆ­te kudy kam?', 'done!': 'hotovo!', 'Download': 'StĆ”hnout', 'download layouts': 'stĆ”hnout moduly rozvrženĆ­ strĆ”nky', 'download plugins': 'stĆ”hnout zĆ”suvnĆ© moduly', 'E-mail': 'E-mail', 'Edit': 'Upravit', -'edit all': 'editovat vÅ”e', +'edit all': 'edit all', 'Edit application': 'SprĆ”va aplikace', -'edit controller': 'editovat controller', +'edit controller': 'edit controller', 'Edit current record': 'Upravit aktuĆ”lnĆ­ zĆ”znam', 'Edit Profile': 'Upravit profil', 'edit views:': 'upravit pohled:', 'Editing file "%s"': 'Úprava souboru "%s"', 'Editing Language file': 'Úprava jazykovĆ©ho souboru', -'Editing Plural Forms File': 'EditovĆ”nĆ­ souboru množných čƭsel', +'Editing Plural Forms File': 'Editing Plural Forms File', 'Email and SMS': 'Email a SMS', 'Enable': 'Odblokovat', 'enter a number between %(min)g and %(max)g': 'zadejte čƭslo mezi %(min)g a %(max)g', -'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g', 'enter an integer between %(min)g and %(max)g': 'zadejte celĆ© čƭslo mezi %(min)g a %(max)g', 'Error': 'Chyba', 'Error logs for "%(app)s"': 'Seznam výskytu chyb pro aplikaci "%(app)s"', 'Error snapshot': 'Snapshot chyby', 'Error ticket': 'Ticket chyby', 'Errors': 'Chyby', -'Exception %(extype)s: %(exvalue)s': 'Výjimka %(extype)s: %(exvalue)s', -'Exception %s': 'Výjimka %s', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Prvky instance výjimky', -'Expand Abbreviation': 'Expandovat zkratku', +'Expand Abbreviation': 'Expand Abbreviation', 'export as csv file': 'exportovat do .csv souboru', 'exposes': 'vystavuje', 'exposes:': 'vystavuje funkce:', @@ -179,7 +180,7 @@ 'FAQ': 'Často kladenĆ© dotazy', 'File': 'Soubor', 'file': 'soubor', -'file "%(filename)s" created': 'soubor "%(filename)s" byl vytvořen', +'file "%(filename)s" created': 'file "%(filename)s" created', 'file saved on %(time)s': 'soubor uložen %(time)s', 'file saved on %s': 'soubor uložen %s', 'Filename': 'NĆ”zev souboru', @@ -190,7 +191,7 @@ 'Forgot username?': 'Zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', 'forgot username?': 'zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', 'Forms and Validators': 'FormulÔře a validĆ”tory', -'Frames': 'Framy', +'Frames': 'Frames', 'Free Applications': 'Aplikace zdarma', 'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', 'Generate': 'Vytvořit', @@ -199,28 +200,29 @@ 'Git Push': 'Git Push', 'Globals##debug': 'GlobĆ”lnĆ­ proměnnĆ©', 'go!': 'OK!', -'Goto': 'PřejĆ­t na', -'graph model': 'grafický model', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Skupina %(group_id)s vytvořena', 'Group ID': 'ID skupiny', 'Groups': 'Skupiny', -'Hello World': 'Ahoj vÅ”ichni', +'Hello World': 'Ahoj světe', 'Help': 'NĆ”pověda', -'Helping web2py': 'Podpořte Web2py', +'Helping web2py': 'Helping web2py', 'Hide/Show Translated strings': 'Skrýt/Zobrazit přeloženĆ© texty', 'Hits': 'KolikrĆ”t dosaženo', 'Home': 'DomovskĆ” strĆ”nka', 'honored only if the expression evaluates to true': 'brĆ”t v potaz jen když se tato podmĆ­nka vyhodnotĆ­ kladně', -'How did you get here?': 'Jak se Ti tato strĆ”nka vlastně zobrazila?', +'How did you get here?': 'Jak jste se sem vlastně dostal?', 'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\r\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'import': 'import', 'Import/Export': 'Import/Export', 'includes': 'zahrnuje', 'Index': 'Index', 'insert new': 'vložit nový zĆ”znam ', 'insert new %s': 'vložit nový zĆ”znam %s', -'inspect attributes': 'prohlĆ©dnout atributy', +'inspect attributes': 'inspect attributes', 'Install': 'Instalovat', 'Installed applications': 'NainstalovanĆ© aplikace', 'Interaction at %s line %s': 'Interakce v %s, na řÔdce %s', @@ -236,7 +238,7 @@ 'It is %s %%{day} today.': 'Dnes je to %s %%{den}.', 'Key': 'Klƭč', 'Key bindings': 'Vazby klĆ­ÄÅÆ', -'Key bindings for ZenCoding Plugin': 'Key bindings pro ZenCoding Plugin', +'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', 'languages': 'jazyky', 'Languages': 'Jazyky', 'Last name': 'PříjmenĆ­', @@ -247,11 +249,11 @@ 'License for': 'Licence pro', 'Line number': 'Číslo řÔdku', 'LineNo': 'Č.řÔdku', -'Live Chat': 'Online chat', +'Live Chat': 'Online pokec', 'loading...': 'nahrĆ”vĆ”m...', 'locals': 'locals', 'Locals##debug': 'LokĆ”lnĆ­ proměnnĆ©', -'Log In': 'PřihlĆ”sit se', +'Log In': 'Log In', 'Logged in': 'PřihlÔŔenĆ­ proběhlo ĆŗspěŔně', 'Logged out': 'OdhlÔŔenĆ­ proběhlo ĆŗspěŔně', 'Login': 'PřihlĆ”sit se', @@ -262,8 +264,11 @@ 'Lost Password': 'Zapomněl jste heslo', 'Lost password?': 'Zapomněl jste heslo?', 'lost password?': 'zapomněl jste heslo?', -'Manage': 'Spravovat', -'Manage Cache': 'Spravovat cache', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Model rozbalovacĆ­ nabĆ­dky', 'Models': 'Modely', 'models': 'modely', @@ -274,25 +279,26 @@ 'My Sites': 'SprĆ”va aplikacĆ­', 'Name': 'JmĆ©no', 'new application "%s" created': 'novĆ” aplikace "%s" vytvořena', -'New application wizard': 'Nový prÅÆvodce aplikacĆ­', 'New Application Wizard': 'Nový prÅÆvodce aplikacĆ­', +'New application wizard': 'Nový prÅÆvodce aplikacĆ­', 'New password': 'NovĆ© heslo', 'New Record': 'Nový zĆ”znam', 'new record inserted': 'nový zĆ”znam byl založen', -'New simple application': 'Vytvořit novou aplikaci', -'next': 'dalŔí', +'New simple application': 'Vytvořit primitivnĆ­ aplikaci', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'dalŔích 100 řÔdkÅÆ', 'No databases in this application': 'V tĆ©to aplikaci nejsou žÔdnĆ© databĆ”ze', 'No Interaction yet': 'JeÅ”tě žÔdnĆ” interakce nenastala', 'No ticket_storage.txt found under /private folder': 'Soubor ticket_storage.txt v adresÔři /private nenalezen', 'Object or table name': 'Objekt či tabulka', 'Old password': 'PÅÆvodnĆ­ heslo', -'Online book': 'Online kniha', +'Online book': 'Online book', 'online designer': 'online nĆ”vrhÔř', -'Online examples': 'UkĆ”zka aplikace: web2py strĆ”nky', -'Open new app in new window': 'Otevřít novou aplikaci v novĆ©m okně', -'or alternatively': 'nebo případně', -'Or Get from URL:': 'Nebo zĆ­skat z URL adresy:', +'Online examples': 'Příklady online', +'Open new app in new window': 'Open new app in new window', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'nebo importovat z .csv souboru', 'Origin': 'PÅÆvod', 'Original/Translation': 'OriginĆ”l/Překlad', @@ -302,24 +308,28 @@ 'Overwrite installed app': 'Přepsat instalovanou aplikaci', 'Pack all': 'Zabalit', 'Pack compiled': 'Zabalit zkompilovanĆ©', -'pack plugin': 'pack (zabalit) plugin', +'pack plugin': 'pack plugin', 'password': 'heslo', 'Password': 'Heslo', "Password fields don't match": 'Hesla se neshodujĆ­', -'Peeking at file': 'SledovĆ”nĆ­ souboru', +'Peeking at file': 'Peeking at file', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Please': 'ProsĆ­m', -'Plugin "%s" in application': 'Plugin "%s" v aplikaci', +'Plugin "%s" in application': 'Plugin "%s" in application', 'plugins': 'zĆ”suvnĆ© moduly', 'Plugins': 'ZĆ”suvnĆ© moduly', -'Plural Form #%s': 'MnožnĆ© čƭslo #%s', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'MnožnĆ” čƭsla:', -'Powered by': 'PoužívĆ” technologii', +'Powered by': 'PohĆ”něno', 'Preface': 'Předmluva', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'předchozĆ­ch 100 řÔdkÅÆ', 'Private files': 'SoukromĆ© soubory', 'private files': 'soukromĆ© soubory', 'profile': 'profil', 'Project Progress': 'Vývoj projektu', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Dotaz:', 'Quick Examples': 'KrĆ”tkĆ© příklady', @@ -353,38 +363,40 @@ 'return': 'return', 'revert': 'vrĆ”tit se k pÅÆvodnĆ­mu', 'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'ZĆ”znamy v tabulce', 'Rows selected': 'ZĆ”znamÅÆ zobrazeno', 'rules are not defined': 'pravidla nejsou definovĆ”na', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "SpustĆ­ testy v tomto souboru (ke spuÅ”těnĆ­ vÅ”ech testÅÆ, použijte tlačƭtko 'test')", 'Running on %s': 'Běží na %s', 'Save': 'Uložit', -'Save file:': 'Uložit soubor:', +'Save file:': 'Save file:', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Uložit pomocĆ­ Ajaxu', 'Saved file hash:': 'hash uloženĆ©ho souboru:', 'Semantic': 'Modul semantic', 'Services': 'Služby', 'session': 'session', -'session expired': 'vyprÅ”ela session', +'session expired': 'session expired', 'Set Breakpoint on %s at line %s: %s': 'Bod přeruÅ”enĆ­ nastaven v souboru %s na řÔdce %s: %s', 'shell': 'příkazovĆ” řÔdka', -'Sign Up': 'Registrovat se', -'Singular Form': 'JednotnĆ© čƭslo', +'Sign Up': 'Sign Up', +'Singular Form': 'Singular Form', 'Site': 'SprĆ”va aplikacĆ­', 'Size of cache:': 'Velikost cache:', -'skip to generate': 'přeskočit pro vytvořenĆ­', +'skip to generate': 'skip to generate', 'Sorry, could not find mercurial installed': 'Bohužel mercurial nenĆ­ nainstalovĆ”n.', 'Start a new app': 'Vytvořit novou aplikaci', 'Start searching': 'Začƭt hledĆ”nĆ­', 'Start wizard': 'Spustit prÅÆvodce', 'state': 'stav', -'Static': 'StatickĆ© soubory', +'Static': 'Static', 'static': 'statickĆ© soubory', 'Static files': 'StatickĆ© soubory', 'Statistics': 'Statistika', -'Step': 'Krok', -'step': 'krok', -'stop': 'zastavit', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'Stylesheet': 'CSS styly', 'submit': 'odeslat', 'Submit': 'Odeslat', @@ -395,30 +407,30 @@ 'Table name': 'NĆ”zev tabulky', 'Temporary': 'Dočasný', 'test': 'test', -'Testing application': 'ZkuÅ”ebnĆ­ aplikace', +'Testing application': 'Testing application', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Dotaz" je podmĆ­nka, například "db.tabulka1.pole1==\'hodnota\'". PodmĆ­nka "db.tabulka1.pole1==db.tabulka2.pole2" pak vytvoří SQL JOIN.', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika aplikace: každĆ” URL je mapovĆ”na na funkci vystavovanou kontrolĆ©rem.', 'The Core': 'JĆ”dro (The Core)', 'The data representation, define database tables and sets': 'Reprezentace dat: definovat tabulky databĆ”ze a zĆ”znamy', -'The output of the file is a dictionary that was rendered by the view %s': 'Funkce vrĆ”tila dictionary (slovnĆ­k) hodnot, a ty se vypsaly pomocĆ­ Å”ablony %s.', +'The output of the file is a dictionary that was rendered by the view %s': 'Výstup ze souboru je slovnĆ­k, který se zobrazil v pohledu %s.', 'The presentations layer, views are also known as templates': 'PrezentačnĆ­ vrstva: pohledy či templaty (Å”ablony)', 'The Views': 'Pohledy (The Views)', -'There are no controllers': 'Nejsou vytvořeny žÔdnĆ© controllery', -'There are no modules': 'Nejsou přidĆ”ny žÔdnĆ© moduly', -'There are no plugins': 'ŽÔdnĆ© pluginy nejsou instalovĆ”ny.', +'There are no controllers': 'There are no controllers', +'There are no modules': 'There are no modules', +'There are no plugins': 'ŽÔdnĆ© moduly nejsou instalovĆ”ny.', 'There are no private files': 'ŽÔdnĆ© soukromĆ© soubory neexistujĆ­.', -'There are no static files': 'Nejsou přidĆ”ny žÔdnĆ© statickĆ© soubory', +'There are no static files': 'There are no static files', 'There are no translators, only default language is supported': 'There are no translators, only default language is supported', -'There are no views': 'Nejsou vytvořeny žÔdnĆ© Å”ablony (views)', +'There are no views': 'There are no views', 'These files are not served, they are only available from within your app': 'Tyto soubory jsou klientÅÆm nepřístupnĆ©. K dispozici jsou pouze v rĆ”mci aplikace.', 'These files are served without processing, your images go here': 'Tyto soubory jsou servĆ­rovĆ”ny bez přídavnĆ© logiky, sem patří např. obrĆ”zky.', 'This App': 'Tato aplikace', 'This is a copy of the scaffolding application': 'Toto je kopie aplikace skelet.', 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', -'This is the %(filename)s template': 'Toto je Å”ablona %(filename)s', +'This is the %(filename)s template': 'This is the %(filename)s template', 'this page to see if a breakpoint was hit and debug interaction is required.': 'tuto strĆ”nku, abyste uviděli, zda se dosĆ”hlo bodu přeruÅ”enĆ­.', -'Ticket': 'Tiket', -'Ticket ID': 'ID tiketu', +'Ticket': 'Ticket', +'Ticket ID': 'Ticket ID', 'Time in Cache (h:m:s)': 'Čas v Cache (h:m:s)', 'Timestamp': 'ČasovĆ© razĆ­tko', 'to previous version.': 'k předchozĆ­ verzi.', @@ -428,15 +440,15 @@ 'toggle breakpoint': 'vyp./zap. bod přeruÅ”enĆ­', 'Toggle Fullscreen': 'Na celou obrazovku a zpět', 'too short': 'PříliÅ” krĆ”tkĆ©', -'Traceback': 'Hierarchie volĆ”nĆ­', +'Traceback': 'Traceback', 'Translation strings for the application': 'Překlad textÅÆ pro aplikaci', -'try something like': 'zkuste něco jako', +'try something like': 'try something like', 'Try the mobile interface': 'Zkuste rozhranĆ­ pro mobilnĆ­ zařízenĆ­', -'try view': 'vyzkouÅ”et Å”ablonu (view)', +'try view': 'try view', 'Twitter': 'Twitter', 'Type python statement in here and hit Return (Enter) to execute it.': 'Type python statement in here and hit Return (Enter) to execute it.', 'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', -'Unable to check for upgrades': 'Nelze zjistit informaci o aktualizacĆ­ch', +'Unable to check for upgrades': 'Unable to check for upgrades', 'unable to parse csv file': 'csv soubor nedĆ” sa zpracovat', 'uncheck all': 'vÅ”e odznačit', 'Uninstall': 'Odinstalovat', @@ -444,15 +456,16 @@ 'update all languages': 'aktualizovat vÅ”echny jazyky', 'Update:': 'Upravit:', 'Upgrade': 'Upgrade', -'upgrade now': 'upgradovat nynĆ­', -'upgrade now to %s': 'upgradovat nynĆ­ na %s', +'upgrade now': 'upgrade now', +'upgrade now to %s': 'upgrade now to %s', 'upload': 'nahrĆ”t', -'Upload': 'Upload (nahrĆ”t)', +'Upload': 'Upload', 'Upload a package:': 'NahrĆ”t balĆ­k:', 'Upload and install packed application': 'NahrĆ”t a instalovat zabalenou aplikaci', 'upload file:': 'nahrĆ”t soubor:', 'upload plugin file:': 'nahrĆ”t soubor modulu:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Použijte (...)&(...) pro AND, (...)|(...) pro OR a ~(...) pro NOT pro sestavenĆ­ složitějŔích dotazÅÆ.', +'User': 'User', 'User %(id)s Logged-in': 'Uživatel %(id)s přihlÔŔen', 'User %(id)s Logged-out': 'Uživatel %(id)s odhlÔŔen', 'User %(id)s Password changed': 'Uživatel %(id)s změnil heslo', @@ -461,7 +474,8 @@ 'User %(id)s Username retrieved': 'Uživatel %(id)s si nachal zaslat přihlaÅ”ovacĆ­ jmĆ©no', 'User ID': 'ID uživatele', 'Username': 'PřihlaÅ”ovacĆ­ jmĆ©no', -'variables': 'proměnnĆ©', +'Users': 'Users', +'variables': 'variables', 'Verify Password': 'Zopakujte heslo', 'Version': 'Verze', 'Version %s.%s.%s (%s) %s': 'Verze %s.%s.%s (%s) %s', @@ -470,22 +484,22 @@ 'View': 'Pohled (View)', 'Views': 'Pohledy', 'views': 'pohledy', -'Web Framework': 'Webový framework', +'Web Framework': 'Web Framework', 'web2py is up to date': 'MĆ”te aktuĆ”lnĆ­ verzi web2py.', 'web2py online debugger': 'LadĆ­cĆ­ online web2py program', 'web2py Recent Tweets': 'Å těbetĆ”nĆ­ na Twitteru o web2py', -'web2py upgrade': 'aktualizace Web2py', -'web2py upgraded; please restart it': 'Web2py bylo aktualizovĆ”no; prosĆ­m restarujte jej', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', 'Welcome': 'VĆ­tejte', -'Welcome to web2py': 'Vitejte ve Web2py aplikaci', -'Welcome to web2py!': 'VĆ­tejte ve Web2py aplikaci.', -'Which called the function %s located in the file %s': 'TĆ­m byla zavolĆ”na funkce %s ze souboru (kontrolĆ©ru) %s.', -'Working...': 'Pracuji...', -'You are successfully running web2py': 'Spustil(a) jsi webový server a Web2py.', +'Welcome to web2py': 'Vitejte ve web2py', +'Welcome to web2py!': 'VĆ­tejte ve web2py!', +'Which called the function %s located in the file %s': 'kterĆ” zavolala funkci %s v souboru (kontrolĆ©ru) %s.', +'Working...': 'Working...', +'You are successfully running web2py': 'ÚspěŔně jste spustili web2py.', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'Nastavovat a mazat body přeruÅ”enĆ­ je též možno v rĆ”mci editovĆ”nĆ­ zdrojovĆ©ho souboru přes tlačƭtko Vyp./Zap. bod přeruÅ”enĆ­', 'You can inspect variables using the console bellow': 'Níže pomocĆ­ příkazovĆ© řÔdky si můžete prohlĆ©dnout proměnnĆ©', -'You can modify this application and adapt it to your needs': 'V ADMIN rozhranĆ­ můžeÅ” Vytvořit novou aplikaci jako kopii ukĆ”zkovĆ© Welcome aplikace. A začƭt upravovat: modely, kontrolĆ©ry, Å”ablony pro URL adresy, kterĆ© požadujeÅ”.', +'You can modify this application and adapt it to your needs': 'Tuto aplikaci si můžete upravit a přizpÅÆsobit ji svým potřebĆ”m.', 'You need to set up and reach a': 'Je třeba nejprve nastavit a dojĆ­t až na', -'You visited the url %s': 'Zadal jsi URL adresu %s.', +'You visited the url %s': 'NavÅ”tĆ­vili jste strĆ”nku %s,', 'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Aplikace bude blokovĆ”na než se klikne na jedno z tlačƭtek (dalŔí, krok, pokračovat, atd.)', } diff --git a/applications/welcome/languages/de.py b/applications/welcome/languages/de.py old mode 100644 new mode 100755 index 91264a42..2e40b349 --- a/applications/welcome/languages/de.py +++ b/applications/welcome/languages/de.py @@ -1,32 +1,41 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'de', '!langname!': 'Deutsch (DE)', -'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '""Update" ist ein optionaler Ausdruck wie "Feld1 = \'newvalue". JOIN Ergebnisse kƶnnen nicht aktualisiert oder gelƶscht werden', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "feld1=\'newvalue\'". JOIN Ergebnisse kƶnnen nicht aktualisiert oder gelƶscht werden', '%s %%(shop)': '%s %%(shop)', '%s %%(shop[0])': '%s %%(shop[0])', '%s %%{quark[0]}': '%s %%{quark[0]}', -'%s %%{row} deleted': '%s %%{row} deleted', -'%s %%{row} updated': '%s %%{row} updated', +'%s %%{row} deleted': '%s %%{row} gelƶscht', +'%s %%{row} updated': '%s %%{row} aktualisiert', '%s %%{shop[0]}': '%s %%{shop[0]}', '%s %%{shop}': '%s %%{shop}', -'%s selected': '%s selected', +'%s selected': '%s ausgewƤhlt', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**nicht verfügbar** (benƶtigt die Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] Bibliothek)', '?': '?', '@markmin\x01**Hello World**': '**Hallo Welt**', '@markmin\x01An error occured, please [[reload %s]] the page': 'Ein Fehler ist aufgetreten, bitte [[laden %s]] Sie die Seite neu', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**nicht verfügbar**``:rot (benƶtigt die Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] Bibliothek)', 'About': 'Über', 'Access Control': 'Zugangskontrolle', +'admin': 'admin', 'Administrative Interface': 'AdministrationsoberflƤche', 'Ajax Recipes': 'Ajax Rezepte', +'An error occured, please [[reload %s]] the page': 'Ein Fehler ist aufgetreten, bitte [[laden %s]] Sie die Seite neu', 'appadmin is disabled because insecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals', 'Are you sure you want to delete this object?': 'Sind Sie sich sicher, dass Sie dieses Objekt lƶschen wollen?', 'Available Databases and Tables': 'Verfügbare Datenbanken und Tabellen', 'Buy this book': 'Dieses Buch kaufen', +"Buy web2py's book": "web2py's Buch kaufen", 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache geleert', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache enthƤlt items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.', 'Cache Keys': 'Cache Schlüssel', 'Cannot be empty': 'Darf nicht leer sein', 'Check to delete': 'AuswƤhlen um zu lƶschen', @@ -36,6 +45,7 @@ 'Client IP': 'Client IP', 'Community': 'Community', 'Components and Plugins': 'Komponenten und Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Controller', 'Copyright': 'Copyright', 'Created By': 'Erstellt von', @@ -55,9 +65,11 @@ 'Deployment Recipes': 'Entwicklungsrezepte', 'Description': 'Beschreibung', 'design': 'Design', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk gelƶscht', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK enthƤlt items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.', 'Documentation': 'Dokumentation', "Don't know what to do?": 'Wissen Sie nicht weiter?', 'done!': 'Fertig!', @@ -82,6 +94,8 @@ 'Hello World': 'Hallo Welt', 'Hello World ## Kommentar': 'Hallo Welt ', 'Hello World## Kommentar': 'Hallo Welt', +'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Trefferquote: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} und **%(misses)s** %%{miss(misses)})', 'Home': 'Startseite', 'How did you get here?': 'Wie sind Sie hier her gelangt?', 'import': 'Importieren', @@ -98,6 +112,7 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'Logged in': 'Eingeloggt', 'Logged out': 'Ausgeloggt', 'Login': 'Einloggen', @@ -117,7 +132,9 @@ 'new record inserted': 'neuer Eintrag hinzugefügt', 'next %s rows': 'nƤchste %s Reihen', 'No databases in this application': 'Keine Datenbank in dieser Anwendung', +'Number of entries: **%s**': 'Nummer der EintrƤge: **%s**', 'Object or table name': 'Objekt- oder Tabellenname', +'Online book': 'Online Buch', 'Online examples': 'Online Beispiele', 'or import from csv file': 'oder von csv Datei importieren', 'Origin': 'Ursprung', @@ -126,8 +143,8 @@ 'Overview': 'Überblick', 'Password': 'Passwort', "Password fields don't match": 'Passwortfelder sind nicht gleich', -'Permission': 'Permission', -'Permissions': 'Permissions', +'Permission': 'Erlaubnis', +'Permissions': 'Erlaubnisse', 'please input your password again': 'Bitte geben Sie ihr Passwort erneut ein', 'Plugins': 'Plugins', 'Powered by': 'Unterstützt von', @@ -140,7 +157,8 @@ 'Quick Examples': 'Kurze Beispiele', 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', -'Ram Cleared': 'Ram Cleared', +'Ram Cleared': 'Ram gelƶscht', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM enthƤlt items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.', 'Recipes': 'Rezepte', 'Record': 'Eintrag', 'record does not exist': 'Eintrag existiert nicht', @@ -159,6 +177,7 @@ 'Save model as...': 'Speichere Vorlage als...', 'Semantic': 'Semantik', 'Services': 'Dienste', +'Sign Up': 'Registrieren', 'Size of cache:': 'Cachegröße:', 'state': 'Status', 'Statistics': 'Statistik', @@ -171,10 +190,10 @@ 'The output of the file is a dictionary that was rendered by the view %s': 'Die Ausgabe der Datei ist ein "dictionary", welches vom "view" %s gerendert wurde', 'The Views': 'Die Views', 'This App': 'Diese App', -'This email already has an account': 'This email already has an account', +'This email already has an account': 'Diese email besitzt bereits ein Konto', 'Time in Cache (h:m:s)': 'Zeit im Cache (h:m:s)', 'Timestamp': 'Zeitstempel', -'Traceback': 'Traceback', +'Traceback': 'Zurückverfolgen', 'Twitter': 'Twitter', 'unable to parse csv file': 'csv Datei konnte nicht geparst werden', 'Update:': 'Update:', @@ -196,4 +215,4 @@ 'You are successfully running web2py': 'web2py wird erfolgreich ausgeführt', 'You can modify this application and adapt it to your needs': 'Sie kƶnnen diese Anwendung verƤndern und Ihren Bedürfnissen anpassen', 'You visited the url %s': 'Sie haben die URL %s besucht', -} +} \ No newline at end of file diff --git a/applications/welcome/languages/default.py b/applications/welcome/languages/default.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/es.py b/applications/welcome/languages/es.py old mode 100644 new mode 100755 index 7579cc37..c2154767 --- a/applications/welcome/languages/es.py +++ b/applications/welcome/languages/es.py @@ -8,11 +8,17 @@ '%s %%{row} deleted': '%s %%{fila} %%{eliminada}', '%s %%{row} updated': '%s %%{fila} %%{actualizada}', '%s selected': '%s %%{seleccionado}', -'%Y-%m-%d': '%d/%m/%Y', -'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', -'(something like "it-it")': '(algo como "eso-eso")', +'%Y-%m-%d': '%d/%m/%A', +'%Y-%m-%d %H:%M:%S': '%d/%m/%A %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'(something like "it-it")': '(algo como "it-it")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**no disponible** (requiere la libreria [[guppy http://pypi.python.org/pypi/guppy/ popup]] de Python)', +'?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'Ha ocurrido un error, por favor [[recargar %s]] la pĆ”gina', '@markmin\x01Number of entries: **%s**': 'NĆŗmero de entradas: **%s**', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'Hay una nueva versión de web2py disponible', 'A new version of web2py is available: %s': 'Hay una nueva versión de web2py disponible: %s', 'About': 'Acerca de', @@ -21,6 +27,7 @@ 'Access Control': 'Control de Acceso', 'Add': 'AƱadir', 'additional code for your application': 'código adicional para su aplicación', +'admin': 'admin', 'admin disabled because no admin password': 'admin deshabilitado por falta de contraseƱa', 'admin disabled because not supported on google app engine': 'admin deshabilitado, no es soportado en GAE', 'admin disabled because unable to access password file': 'admin deshabilitado, imposible acceder al archivo con la contraseƱa', @@ -31,11 +38,12 @@ 'Administrator Password:': 'ContraseƱa del Administrador:', 'Ajax Recipes': 'Recetas AJAX', 'An error occured, please %s the page': 'Ha ocurrido un error, por favor %s la pĆ”gina', +'An error occured, please [[reload %s]] the page': 'Ha ocurrido un error, por favor [[reload %s]] la pagina', 'And': 'Y', 'and rename it (required):': 'y renómbrela (requerido):', 'and rename it:': ' y renómbrelo:', 'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'admin deshabilitado, el canal no es seguro', +'appadmin is disabled because insecure channel': 'appadmin deshabilitado, el canal no es seguro', 'application "%s" uninstalled': 'aplicación "%s" desinstalada', 'application compiled': 'aplicación compilada', 'application is compiled and cannot be designed': 'la aplicación estĆ” compilada y no puede ser modificada', @@ -46,7 +54,7 @@ 'Are you sure you want to uninstall application "%s"': 'ĀæEstĆ” seguro que desea desinstalar la aplicación "%s"', 'Are you sure you want to uninstall application "%s"?': 'ĀæEstĆ” seguro que desea desinstalar la aplicación "%s"?', 'at': 'en', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENCION: Inicio de sesión requiere una conexión segura (HTTPS) o localhost.', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENCION: Inicio de sesión requiere una conexión segura (HTTPS) o corriendo en localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENCION: NO EJECUTE VARIAS PRUEBAS SIMULTANEAMENTE, NO SON THREAD SAFE.', 'ATTENTION: you cannot edit the running application!': 'ATENCION: no puede modificar la aplicación que estĆ” ejecutandose!', 'Authentication': 'Autenticación', @@ -55,8 +63,11 @@ 'Available Databases and Tables': 'Bases de datos y tablas disponibles', 'Back': 'AtrĆ”s', 'Buy this book': 'Compra este libro', +"Buy web2py's book": "Compra el libro de web2py", 'Cache': 'CachĆ©', 'cache': 'cachĆ©', +'Cache Cleared': 'Cache Limpiada', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'La Cache contiene items con **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} de antiguedad.', 'Cache Keys': 'Llaves de la CachĆ©', 'cache, errors and sessions cleaned': 'cachĆ©, errores y sesiones eliminados', 'Cannot be empty': 'No puede estar vacĆ­o', @@ -83,6 +94,7 @@ 'compile': 'compilar', 'compiled application removed': 'aplicación compilada eliminada', 'Components and Plugins': 'Componentes y Plugins', +'Config.ini': 'Config.ini', 'contains': 'contiene', 'Controller': 'Controlador', 'Controllers': 'Controladores', @@ -90,7 +102,7 @@ 'Copyright': 'Copyright', 'create file with filename:': 'cree archivo con nombre:', 'Create new application': 'Cree una nueva aplicación', -'create new application:': 'nombre de la nueva aplicación:', +'create new application:': 'cree una nueva aplicación:', 'Created By': 'Creado Por', 'Created On': 'Creado En', 'CSV (hidden cols)': 'CSV (columnas ocultas)', @@ -98,7 +110,7 @@ 'Current response': 'Respuesta en curso', 'Current session': 'Sesión en curso', 'currently saved or': 'actualmente guardado o', -'customize me!': 'Ā”AdĆ”ptame!', +'customize me!': 'Ā”Personalizame!', 'data uploaded': 'datos subidos', 'Database': 'Base de datos', 'Database %s select': 'selección en base de datos %s', @@ -119,11 +131,13 @@ 'Description': 'Descripción', 'design': 'diseƱo', 'DESIGN': 'DISEƑO', +'Design': 'DiseƱo', 'Design for': 'DiseƱo por', 'detecting': 'detectando', 'DISK': 'DISCO', 'Disk Cache Keys': 'Llaves de CachĆ© en Disco', 'Disk Cleared': 'Disco limpiado', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'El DISCO contiene items con **%(hours)02d** %%{hora(hours)} **%(min)02d** %%{minuto(min)} **%(sec)02d** %%{segundo(sec)} de antiguedad.', 'Documentation': 'Documentación', "Don't know what to do?": 'ĀæNo sabe que hacer?', 'done!': 'Ā”hecho!', @@ -143,14 +157,14 @@ 'Email and SMS': 'Correo electrónico y SMS', 'Email sent': 'Correo electrónico enviado', 'End of impersonation': 'Fin de suplantación', -'enter a number between %(min)g and %(max)g': 'introduzca un nĆŗmero entre %(min)g y %(max)g', +'enter a number between %(min)g and %(max)g': 'introduzca un numero entre %(min)g y %(max)g', 'enter a value': 'introduzca un valor', -'enter an integer between %(min)g and %(max)g': 'introduzca un entero entre %(min)g y %(max)g', +'enter an integer between %(min)g and %(max)g': 'introduzca un nĆŗmero entero entre %(min)g y %(max)g', 'enter date and time as %(format)s': 'introduzca fecha y hora como %(format)s', -'Error logs for "%(app)s"': 'BitĆ”cora de errores en "%(app)s"', +'Error logs for "%(app)s"': 'BitĆ”cora de errores para "%(app)s"', 'errors': 'errores', 'Errors': 'Errores', -'Errors in form, please check it out.': 'Hay errores en el formulario, por favor comprĆŗebelo.', +'Errors in form, please check it out.': 'Hay errores en el formulario, por favor compruĆ©belo.', 'export as csv file': 'exportar como archivo CSV', 'Export:': 'Exportar:', 'exposes': 'expone', @@ -171,12 +185,15 @@ 'Forms and Validators': 'Formularios y validadores', 'Free Applications': 'Aplicaciones Libres', 'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', +'Graph Model': 'Modelo en Grafo', 'Group %(group_id)s created': 'Grupo %(group_id)s creado', 'Group ID': 'ID de Grupo', 'Group uniquely assigned to user %(id)s': 'Grupo asignado Ćŗnicamente al usuario %(id)s', 'Groups': 'Grupos', 'Hello World': 'Hola Mundo', 'help': 'ayuda', +'Helping web2py': 'Ayudando a web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Inicio', 'How did you get here?': 'ĀæCómo llegaste aquĆ­?', 'htmledit': 'htmledit', @@ -199,9 +216,9 @@ 'Invalid login': 'Inicio de sesión invĆ”lido', 'invalid password': 'contraseƱa invĆ”lida', 'Invalid Query': 'Consulta invĆ”lida', -'invalid request': 'solicitud invĆ”lida', +'invalid request': 'Solicitud invĆ”lida', 'Invalid reset password': 'Reinicio de contraseƱa invĆ”lido', -'invalid ticket': 'tiquete invĆ”lido', +'invalid ticket': 'Tiquete invĆ”lido', 'Is Active': 'EstĆ” Activo', 'Key': 'Llave', 'language file "%(filename)s" created/updated': 'archivo de lenguaje "%(filename)s" creado/actualizado', @@ -217,6 +234,7 @@ 'License for': 'Licencia para', 'Live Chat': 'Chat en vivo', 'loading...': 'cargando...', +'Log In': 'Log In', 'Logged in': 'Sesión iniciada', 'Logged out': 'Sesión finalizada', 'Login': 'Inicio de sesión', @@ -229,9 +247,12 @@ 'Lost password?': 'ĀæOlvidó la contraseƱa?', 'lost password?': 'Āæolvidó la contraseƱa?', 'Main Menu': 'MenĆŗ principal', +'Manage %(action)s': 'Gestionar %(action)s', +'Manage Access Control': 'Gestionar control de acceso', 'Manage Cache': 'Gestionar la CachĆ©', +'Memberships': 'Membresias', 'Menu Model': 'Modelo "menu"', -'merge': 'combinar', +'merge': 'Combinar', 'Models': 'Modelos', 'models': 'modelos', 'Modified By': 'Modificado Por', @@ -248,14 +269,17 @@ 'New password': 'ContraseƱa nueva', 'New Record': 'Registro nuevo', 'new record inserted': 'nuevo registro insertado', +'next %s rows': 'siguiente %s filas', 'next 100 rows': '100 filas siguientes', 'NO': 'NO', 'No databases in this application': 'No hay bases de datos en esta aplicación', 'No records found': 'No se han encontrado registros', 'Not authorized': 'No autorizado', 'not in': 'no en', +'Number of entries: **%s**': 'Numero de entradas: **%s**', 'Object or table name': 'Nombre del objeto o tabla', 'Old password': 'ContraseƱa vieja', +'Online book': 'Libro Online', 'Online examples': 'Ejemplos en lĆ­nea', 'Or': 'O', 'or import from csv file': 'o importar desde archivo CSV', @@ -272,14 +296,18 @@ "Password fields don't match": 'Los campos de contraseƱa no coinciden', 'Password reset': 'Reinicio de contraseƱa', 'Peeking at file': 'Visualizando archivo', +'Permission': 'Permiso', +'Permissions': 'Permisos', 'Phone': 'TelĆ©fono', 'please input your password again': 'por favor introduzca su contraseƱa otra vez', 'Plugins': 'Plugins', 'Powered by': 'Este sitio usa', 'Preface': 'Prefacio', +'previous %s rows': 'fila %s anterior', 'previous 100 rows': '100 filas anteriores', 'Profile': 'Perfil', 'Profile updated': 'Perfil actualizado', +'pygraphviz library not found': 'Libreria pygraphviz no encontrada', 'Python': 'Python', 'Query Not Supported: %s': 'Consulta No Soportada: %s', 'Query:': 'Consulta:', @@ -287,6 +315,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'Llaves de la CachĆ© en RAM', 'Ram Cleared': 'Ram Limpiada', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'La RAM contiene items con **%(hours)02d** %%{hora(hours)} **%(min)02d** %%{minuto(min)} **%(sec)02d** %%{segundo(sec)} de antiguedad.', 'Recipes': 'Recetas', 'Record': 'Registro', 'Record %(id)s created': 'Registro %(id)s creado', @@ -310,22 +339,25 @@ 'Retrieve username': 'Recuperar nombre de usuario', 'revert': 'revertir', 'Role': 'Rol', +'Roles': 'Roles', 'Rows in Table': 'Filas en la tabla', 'Rows selected': 'Filas seleccionadas', 'save': 'guardar', +'Save model as...': 'Guardar modelo como...', 'Saved file hash:': 'Hash del archivo guardado:', 'Search': 'Buscar', 'Semantic': 'SemĆ”ntica', 'Services': 'Servicios', 'session expired': 'sesión expirada', 'shell': 'terminal', +'Sign Up': 'Registrarse', 'site': 'sitio', 'Size of cache:': 'TamaƱo de la CachĆ©:', 'some files could not be removed': 'algunos archivos no pudieron ser removidos', 'start': 'inicio', 'starts with': 'comienza por', 'state': 'estado', -'static': 'estĆ”ticos', +'static': 'estĆ”tico', 'Static files': 'Archivos estĆ”ticos', 'Statistics': 'EstadĆ­sticas', 'Stylesheet': 'Hoja de estilo', @@ -366,17 +398,18 @@ 'Toggle comment': 'Alternar comentario', 'Toggle Fullscreen': 'Alternar pantalla completa', 'too short': 'demasiado corto', +'Traceback': 'Rastrear', 'translation strings for the application': 'cadenas de caracteres de traducción para la aplicación', 'try': 'intente', 'try something like': 'intente algo como', -'TSV (Excel compatible)': 'TSV (compatible Excel)', -'TSV (Excel compatible, hidden cols)': 'TSV (compatible Excel, columnas ocultas)', +'TSV (Excel compatible)': 'TSV (compatible con Excel)', +'TSV (Excel compatible, hidden cols)': 'TSV (compatible con Excel, columnas ocultas)', 'Twitter': 'Twitter', 'Unable to check for upgrades': 'No es posible verificar la existencia de actualizaciones', 'unable to create application "%s"': 'no es posible crear la aplicación "%s"', 'unable to delete file "%(filename)s"': 'no es posible eliminar el archivo "%(filename)s"', 'Unable to download': 'No es posible la descarga', -'Unable to download app': 'No es posible descarga la aplicación', +'Unable to download app': 'No es posible descargar la aplicación', 'unable to parse csv file': 'no es posible analizar el archivo CSV', 'unable to uninstall "%s"': 'no es posible instalar "%s"', 'uncheck all': 'desmarcar todos', @@ -389,6 +422,7 @@ 'Upload existing application': 'Suba esta aplicación', 'upload file:': 'suba archivo:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para crear consultas mĆ”s complejas.', +'User': 'Usuario', 'User %(id)s is impersonating %(other_id)s': 'El usuario %(id)s estĆ” suplantando %(other_id)s', 'User %(id)s Logged-in': 'El usuario %(id)s inició la sesión', 'User %(id)s Logged-out': 'El usuario %(id)s finalizó la sesión', @@ -405,12 +439,13 @@ 'User Logged-out': 'El usuario finalizó la sesión', 'Username': 'Nombre de usuario', 'Username retrieve': 'Recuperar nombre de usuario', +'Users': 'Usuarios', 'value already in database or empty': 'el valor ya existe en la base de datos o estĆ” vacĆ­o', 'value not allowed': 'valor no permitido', 'value not in database': 'el valor no estĆ” en la base de datos', 'Verify Password': 'Verificar ContraseƱa', 'Version': 'Versión', -'versioning': 'versiones', +'versioning': 'versionado', 'Videos': 'VĆ­deos', 'View': 'Vista', 'view': 'vista', @@ -431,3 +466,4 @@ 'You visited the url %s': 'Usted visitó la url %s', 'Your username is: %(username)s': 'Su nombre de usuario es: %(username)s', } + diff --git a/applications/welcome/languages/fr-ca.py b/applications/welcome/languages/fr-ca.py old mode 100644 new mode 100755 index 7693d5b4..310afadc --- a/applications/welcome/languages/fr-ca.py +++ b/applications/welcome/languages/fr-ca.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'fr-ca', '!langname!': 'FranƧais (Canadien)', @@ -8,9 +8,11 @@ '%s selected': '%s sĆ©lectionnĆ©', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'?': '?', 'about': 'Ć  propos', 'About': 'ƀ propos', 'Access Control': "ContrĆ“le d'accĆØs", +'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': "Interface d'administration", 'Ajax Recipes': 'Recettes Ajax', @@ -19,8 +21,10 @@ 'Authentication': 'Authentification', 'Available Databases and Tables': 'Bases de donnĆ©es et tables disponibles', 'Buy this book': 'Acheter ce livre', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Ne peut pas ĆŖtre vide', 'change password': 'changer le mot de passe', @@ -32,6 +36,7 @@ 'Client IP': 'IP client', 'Community': 'CommunautĆ©', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'ContrĆ“leur', 'Copyright': "Droit d'auteur", 'Current request': 'Demande actuelle', @@ -41,6 +46,7 @@ 'data uploaded': 'donnĆ©es tĆ©lĆ©chargĆ©es', 'Database': 'base de donnĆ©es', 'Database %s select': 'base de donnĆ©es %s select', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'db', 'DB Model': 'ModĆØle DB', 'Delete:': 'Supprimer:', @@ -48,6 +54,7 @@ 'Deployment Recipes': 'Recettes de dĆ©ploiement ', 'Description': 'Descriptif', 'design': 'design', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -69,11 +76,13 @@ 'Forms and Validators': 'Formulaires et Validateurs', 'Free Applications': 'Applications gratuites', 'Function disabled': 'Fonction dĆ©sactivĆ©e', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': '%(group_id)s groupe crƩƩ', 'Group ID': 'Groupe ID', 'Group uniquely assigned to user %(id)s': "Groupe unique attribuĆ© Ć  l'utilisateur %(id)s", 'Groups': 'Groupes', 'Hello World': 'Bonjour le monde', +'Helping web2py': 'Helping web2py', 'Home': 'Accueil', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -93,6 +102,7 @@ 'Layouts': 'layouts', 'Live chat': 'Clavardage en direct', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'Logged in': 'ConnectĆ©', 'login': 'connectez-vous', 'Login': 'Connectez-vous', @@ -101,14 +111,19 @@ 'Lost Password': 'Mot de passe perdu', 'lost password?': 'mot de passe perdu?', 'Main Menu': 'Menu principal', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu modĆØle', 'My Sites': 'My Sites', 'Name': 'Nom', 'New Record': 'Nouvel enregistrement', 'new record inserted': 'nouvel enregistrement insĆ©rĆ©', +'next %s rows': 'next %s rows', 'next 100 rows': '100 prochaines lignes', 'No databases in this application': "Cette application n'a pas de bases de donnĆ©es", +'Online book': 'Online book', 'Online examples': 'Exemples en ligne', 'or import from csv file': "ou importer d'un fichier CSV", 'Origin': 'Origine', @@ -118,12 +133,16 @@ 'password': 'mot de passe', 'Password': 'Mot de passe', "Password fields don't match": 'Les mots de passe ne correspondent pas', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': "S'il vous plaĆ®t entrer votre mot de passe", 'Plugins': 'Plugiciels', 'Powered by': 'AlimentĆ© par', 'Preface': 'PrĆ©face', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 lignes prĆ©cĆ©dentes', 'profile': 'profile', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'RequĆŖte:', 'Quick Examples': 'Examples Rapides', @@ -149,10 +168,13 @@ 'Reset Password key': 'RĆ©initialiser le mot clĆ©', 'Resources': 'Ressources', 'Role': 'RĆ“le', +'Roles': 'Roles', 'Rows in Table': 'Lignes du tableau', 'Rows selected': 'Lignes sĆ©lectionnĆ©es', +'Save model as...': 'Save model as...', 'Semantic': 'SĆ©mantique', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'Ć©tat', 'Statistics': 'Statistics', @@ -171,14 +193,17 @@ 'This is a copy of the scaffolding application': "Ceci est une copie de l'application Ć©chafaudage", 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Horodatage', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': "incapable d'analyser le fichier cvs", 'Update:': 'Mise Ć  jour:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT pour construire des requĆŖtes plus complexes.', +'User': 'User', 'User %(id)s Logged-in': 'Utilisateur %(id)s connectĆ©', 'User %(id)s Registered': 'Utilisateur %(id)s enregistrĆ©', 'User ID': 'ID utilisateur', 'User Voice': 'User Voice', +'Users': 'Users', 'value already in database or empty': 'valeur dĆ©jĆ  dans la base ou vide', 'Verify Password': 'VĆ©rifiez le mot de passe', 'Videos': 'VidĆ©os', @@ -189,6 +214,7 @@ 'Welcome to web2py': 'Bienvenue Ć  web2py', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Qui a appelĆ© la fonction %s se trouvant dans le fichier %s', +'Working...': 'Working...', 'You are successfully running web2py': 'Vous roulez avec succĆØs web2py', 'You can modify this application and adapt it to your needs': "Vous pouvez modifier cette application et l'adapter Ć  vos besoins", 'You visited the url %s': "Vous avez visitĆ© l'URL %s", diff --git a/applications/welcome/languages/fr.py b/applications/welcome/languages/fr.py old mode 100644 new mode 100755 index c3922ad3..08701bae --- a/applications/welcome/languages/fr.py +++ b/applications/welcome/languages/fr.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'fr', '!langname!': 'FranƧais', @@ -8,18 +8,29 @@ '%s selected': '%s sĆ©lectionnĆ©', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'ƀ propos', 'Access Control': "ContrĆ“le d'accĆØs", +'admin': 'admin', 'Administrative Interface': "Interface d'administration", 'Administrative interface': "Interface d'administration", 'Ajax Recipes': 'Recettes Ajax', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': "appadmin est dĆ©sactivĆ©e parce que le canal n'est pas sĆ©curisĆ©", 'Are you sure you want to delete this object?': 'Êtes-vous sĆ»r de vouloir supprimer cet objet?', 'Authentication': 'Authentification', 'Available Databases and Tables': 'Bases de donnĆ©es et tables disponibles', 'Buy this book': 'Acheter ce livre', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'ClĆ©s de cache', 'Cannot be empty': 'Ne peut pas ĆŖtre vide', 'change password': 'changer le mot de passe', @@ -31,6 +42,7 @@ 'Client IP': 'IP client', 'Community': 'CommunautĆ©', 'Components and Plugins': 'Composants et Plugins', +'Config.ini': 'Config.ini', 'Controller': 'ContrĆ“leur', 'Copyright': 'Copyright', 'Created By': 'CrƩƩ par', @@ -42,6 +54,7 @@ 'data uploaded': 'donnĆ©es tĆ©lĆ©chargĆ©es', 'Database': 'base de donnĆ©es', 'Database %s select': 'base de donnĆ©es %s selectionnĆ©e', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'bdd', 'DB Model': 'ModĆØle BDD', 'Delete:': 'Supprimer:', @@ -49,9 +62,11 @@ 'Deployment Recipes': 'Recettes de dĆ©ploiement', 'Description': 'Description', 'design': 'design', +'Design': 'Design', 'DISK': 'DISQUE', 'Disk Cache Keys': 'ClĆ©s de cache du disque', 'Disk Cleared': 'Disque vidĆ©', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": 'Vous ne savez pas quoi faire?', 'done!': 'fait!', @@ -70,12 +85,15 @@ 'Forms and Validators': 'Formulaires et Validateurs', 'Free Applications': 'Applications gratuites', 'Function disabled': 'Fonction dĆ©sactivĆ©e', +'Graph Model': 'Graph Model', 'Group ID': 'Groupe ID', 'Groups': 'Groupes', 'Hello World': 'Bonjour le monde', +'Helping web2py': 'En train d’aider web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Accueil', 'How did you get here?': 'Comment ĆŖtes-vous arrivĆ© ici?', -'import': 'import', +'import': 'Importer', 'Import/Export': 'Importer/Exporter', 'Index': 'Index', 'insert new': 'insĆ©rer un nouveau', @@ -93,6 +111,7 @@ 'Layouts': 'Mises en page', 'Live chat': 'Chat en direct', 'Live Chat': 'Chat en direct', +'Log In': 'Log In', 'login': 'connectez-vous', 'Login': 'Connectez-vous', 'logout': 'dĆ©connectez-vous', @@ -101,7 +120,10 @@ 'Lost password?': 'Mot de passe perdu?', 'lost password?': 'mot de passe perdu?', 'Main Menu': 'Menu principal', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'GĆ©rer le Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu modĆØle', 'Modified By': 'ModifiĆ© par', 'Modified On': 'ModifiĆ© le', @@ -109,9 +131,12 @@ 'Name': 'Nom', 'New Record': 'Nouvel enregistrement', 'new record inserted': 'nouvel enregistrement insĆ©rĆ©', +'next %s rows': 'next %s rows', 'next 100 rows': '100 prochaines lignes', 'No databases in this application': "Cette application n'a pas de bases de donnĆ©es", +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Objet ou nom de table', +'Online book': 'Online book', 'Online examples': 'Exemples en ligne', 'or import from csv file': "ou importer d'un fichier CSV", 'Origin': 'Origine', @@ -120,16 +145,21 @@ 'Overview': 'PrĆ©sentation', 'Password': 'Mot de passe', "Password fields don't match": 'Les mots de passe ne correspondent pas', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'AlimentĆ© par', 'Preface': 'PrĆ©face', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 lignes prĆ©cĆ©dentes', +'pygraphviz library not found': 'BibliothĆØque pygraphviz introuvable', 'Python': 'Python', 'Query:': 'RequĆŖte:', 'Quick Examples': 'Exemples Rapides', 'RAM': 'RAM', 'RAM Cache Keys': 'ClĆ©s de cache de la RAM', 'Ram Cleared': 'Ram vidĆ©e', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Readme': 'Lisez-moi', 'Recipes': 'Recettes', 'Record': 'enregistrement', @@ -145,10 +175,13 @@ 'Reset Password key': 'RĆ©initialiser le mot clĆ©', 'Resources': 'Ressources', 'Role': 'RĆ“le', +'Roles': 'Roles', 'Rows in Table': 'Lignes du tableau', 'Rows selected': 'Lignes sĆ©lectionnĆ©es', +'Save model as...': 'Enregistrer le modĆØle sous...', 'Semantic': 'SĆ©mantique', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Taille du cache:', 'state': 'Ć©tat', 'Statistics': 'Statistiques', @@ -167,14 +200,17 @@ 'This is a copy of the scaffolding application': "Ceci est une copie de l'application Ć©chafaudage", 'Time in Cache (h:m:s)': 'Temps en Cache (h:m:s)', 'Timestamp': 'Horodatage', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': "incapable d'analyser le fichier cvs", 'Update:': 'Mise Ć  jour:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Employez (...)&(...) pour AND, (...)|(...) pour OR, and ~(...) pour NOT afin de construire des requĆŖtes plus complexes.', +'User': 'User', 'User %(id)s Logged-in': 'Utilisateur %(id)s connectĆ©', 'User %(id)s Registered': 'Utilisateur %(id)s enregistrĆ©', 'User ID': 'ID utilisateur', 'User Voice': "Voix de l'utilisateur", +'Users': 'Users', 'Verify Password': 'VĆ©rifiez le mot de passe', 'Videos': 'VidĆ©os', 'View': 'PrĆ©sentation', @@ -184,6 +220,7 @@ 'Welcome to web2py': 'Bienvenue Ć  web2py', 'Welcome to web2py!': 'Bienvenue Ć  web2py!', 'Which called the function %s located in the file %s': 'Qui a appelĆ© la fonction %s se trouvant dans le fichier %s', +'Working...': 'Working...', 'You are successfully running web2py': 'Vous exĆ©cutez avec succĆØs web2py', 'You can modify this application and adapt it to your needs': "Vous pouvez modifier cette application et l'adapter Ć  vos besoins", 'You visited the url %s': "Vous avez visitĆ© l'URL %s", diff --git a/applications/welcome/languages/hi.py b/applications/welcome/languages/hi.py old mode 100644 new mode 100755 index 46a47e86..19fe70fd --- a/applications/welcome/languages/hi.py +++ b/applications/welcome/languages/hi.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'hi-in', '!langname!': 'ą¤¹ą¤æą¤Øą„ą¤¦ą„€', @@ -8,8 +8,10 @@ '%s selected': '%s ą¤šą„ą¤Øą¤¾ ą¤¹ą„ą¤†', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'?': '?', 'About': 'About', 'Access Control': 'Access Control', +'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'ą¤Ŗą„ą¤°ą¤¶ą¤¾ą¤øą¤Øą¤æą¤• ą¤‡ą¤‚ą¤Ÿą¤°ą¤«ą„‡ą¤ø ą¤•ą„‡ ą¤²ą¤æą¤ यहाँ ą¤•ą„ą¤²ą¤æą¤• ą¤•ą¤°ą„‡ą¤‚', 'Ajax Recipes': 'Ajax Recipes', @@ -17,8 +19,10 @@ 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Available Databases and Tables': 'ą¤‰ą¤Ŗą¤²ą¤¬ą„ą¤§ ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø और तालिका', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'ą¤–ą¤¾ą¤²ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ सकता', 'Change Password': 'ą¤Ŗą¤¾ą¤øą¤µą¤°ą„ą¤” ą¤¬ą¤¦ą¤²ą„‡ą¤‚', @@ -29,6 +33,7 @@ 'Clear RAM': 'Clear RAM', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Controller', 'Copyright': 'Copyright', 'Current request': 'ą¤µą¤°ą„ą¤¤ą¤®ą¤¾ą¤Ø ą¤…ą¤Øą„ą¤°ą„‹ą¤§', @@ -38,12 +43,14 @@ 'data uploaded': 'औाटा ą¤…ą¤Ŗą¤²ą„‹ą¤” ą¤øą¤®ą„ą¤Ŗą¤Øą„ą¤Ø ', 'Database': 'ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø', 'Database %s select': 'ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø %s ą¤šą„ą¤Øą„€ ą¤¹ą„ą¤ˆ', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'db', 'DB Model': 'DB Model', 'Delete:': 'मिटाना:', 'Demo': 'Demo', 'Deployment Recipes': 'Deployment Recipes', 'design': 'रचना ą¤•ą¤°ą„‡ą¤‚', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -62,9 +69,11 @@ 'FAQ': 'FAQ', 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', +'Graph Model': 'Graph Model', 'Groups': 'Groups', 'Hello from MyApp': 'Hello from MyApp', 'Hello World': 'Hello World', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -81,28 +90,38 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': 'login', 'Login': 'ą¤²ą„‰ą¤— इन', 'logout': 'logout', 'Logout': 'ą¤²ą„‰ą¤— ą¤†ą¤‰ą¤Ÿ', 'Lost Password': 'ą¤Ŗą¤¾ą¤øą¤µą¤°ą„ą¤” ą¤–ą„‹ गया', 'Main Menu': 'Main Menu', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu Model', 'My Sites': 'My Sites', 'New Record': 'नया ą¤°ą„‡ą¤•ą„‰ą¤°ą„ą¤”', 'new record inserted': 'नया ą¤°ą„‡ą¤•ą„‰ą¤°ą„ą¤” औाला', +'next %s rows': 'next %s rows', 'next 100 rows': 'ą¤…ą¤—ą¤²ą„‡ 100 ą¤Ŗą¤‚ą¤•ą„ą¤¤ą¤æą¤Æą¤¾ą¤', 'No databases in this application': 'इस ą¤…ą¤Øą„ą¤Ŗą„ą¤°ą¤Æą„‹ą¤— ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą¤‚', +'Online book': 'Online book', 'Online examples': 'ऑनलाइन उदाहरण ą¤•ą„‡ ą¤²ą¤æą¤ यहाँ ą¤•ą„ą¤²ą¤æą¤• ą¤•ą¤°ą„‡ą¤‚', 'or import from csv file': 'या csv फ़ाइल ą¤øą„‡ आयात', 'Other Plugins': 'Other Plugins', 'Other Recipes': 'Other Recipes', 'Overview': 'Overview', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'Powered by', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'ą¤Ŗą¤æą¤›ą¤²ą„‡ 100 ą¤Ŗą¤‚ą¤•ą„ą¤¤ą¤æą¤Æą¤¾ą¤', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'ą¤Ŗą„ą¤°ą¤¶ą„ą¤Ø:', 'Quick Examples': 'Quick Examples', @@ -115,10 +134,14 @@ 'Record id': 'ą¤°ą¤æą¤•ą„‰ą¤°ą„ą¤” ą¤Ŗą¤¹ą¤šą¤¾ą¤Øą¤•ą¤°ą„ą¤¤ą¤¾ (ą¤†ą¤ˆą¤”ą„€)', 'Register': 'ą¤Ŗą¤‚ą¤œą„€ą¤•ą„ƒą¤¤ (ą¤°ą¤œą¤æą¤øą„ą¤Ÿą¤°) करना ', 'register': 'register', +'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'तालिका ą¤®ą„‡ą¤‚ ą¤Ŗą¤‚ą¤•ą„ą¤¤ą¤æą¤Æą¤¾ą¤ ', 'Rows selected': 'चयनित (ą¤šą„ą¤Øą„‡ ą¤—ą¤Æą„‡) ą¤Ŗą¤‚ą¤•ą„ą¤¤ą¤æą¤Æą¤¾ą¤ ', +'Save model as...': 'Save model as...', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'ą¤øą„ą¤„ą¤æą¤¤ą¤æ', 'Statistics': 'Statistics', @@ -133,16 +156,20 @@ 'The Views': 'The Views', 'This App': 'This App', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'csv फ़ाइल ą¤Ŗą¤¾ą¤°ą„ą¤ø ą¤•ą¤°ą¤Øą„‡ ą¤®ą„‡ą¤‚ ą¤…ą¤øą¤®ą¤°ą„ą¤„', 'Update:': 'ą¤…ą¤¦ą„ą¤Æą¤¤ą¤Ø करना:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', +'Users': 'Users', 'Videos': 'Videos', 'View': 'View', 'Welcome %s': 'Welcome %s', 'Welcome to web2py': 'ą¤µą„‡ą¤¬ą„Øą¤Ŗą¤¾ą¤‡ (web2py) ą¤®ą„‡ą¤‚ आपका ą¤øą„ą¤µą¤¾ą¤—ą¤¤ ą¤¹ą„ˆ', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', diff --git a/applications/welcome/languages/hu.py b/applications/welcome/languages/hu.py old mode 100644 new mode 100755 index bc32c8dd..c020c409 --- a/applications/welcome/languages/hu.py +++ b/applications/welcome/languages/hu.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'hu', '!langname!': 'Magyar', @@ -8,8 +8,10 @@ '%s selected': '%s kivĆ”lasztott', '%Y-%m-%d': '%Y.%m.%d.', '%Y-%m-%d %H:%M:%S': '%Y.%m.%d. %H:%M:%S', +'?': '?', 'About': 'About', 'Access Control': 'Access Control', +'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'az adminisztrĆ”ciós felületĆ©rt kattints ide', 'Ajax Recipes': 'Ajax Recipes', @@ -17,8 +19,10 @@ 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Available Databases and Tables': 'ElĆ©rhető adatbĆ”zisok Ć©s tĆ”blĆ”k', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'gyorsĆ­tótĆ”r', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Nem lehet üres', 'change password': 'jelszó megvĆ”ltoztatĆ”sa', @@ -29,6 +33,7 @@ 'Client IP': 'Client IP', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Controller', 'Copyright': 'Copyright', 'Current request': 'Jelenlegi lekĆ©rdezĆ©s', @@ -38,6 +43,7 @@ 'data uploaded': 'adat feltƶltve', 'Database': 'adatbĆ”zis', 'Database %s select': 'adatbĆ”zis %s kivĆ”lasztĆ”s', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'db', 'DB Model': 'DB Model', 'Delete:': 'Tƶrƶl:', @@ -45,6 +51,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'Description', 'design': 'design', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -64,9 +71,11 @@ 'First name': 'First name', 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', +'Graph Model': 'Graph Model', 'Group ID': 'Group ID', 'Groups': 'Groups', 'Hello World': 'Hello VilĆ”g', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -85,19 +94,25 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': 'belĆ©p', 'logout': 'kilĆ©p', 'lost password': 'elveszett jelszó', 'Lost Password': 'Lost Password', 'Main Menu': 'Főmenü', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menü model', 'My Sites': 'My Sites', 'Name': 'Name', 'New Record': 'Új bejegyzĆ©s', 'new record inserted': 'Ćŗj bejegyzĆ©s felvĆ©ve', +'next %s rows': 'next %s rows', 'next 100 rows': 'kƶvetkező 100 sor', 'No databases in this application': 'Nincs adatbĆ”zis ebben az alkalmazĆ”sban', +'Online book': 'Online book', 'Online examples': 'online pĆ©ldĆ”kĆ©rt kattints ide', 'or import from csv file': 'vagy betƶltĆ©s csv fĆ”jlból', 'Origin': 'Origin', @@ -105,10 +120,14 @@ 'Other Recipes': 'Other Recipes', 'Overview': 'Overview', 'Password': 'Password', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'Powered by', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'előző 100 sor', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'LekĆ©rdezĆ©s:', 'Quick Examples': 'Quick Examples', @@ -125,10 +144,13 @@ 'Registration key': 'Registration key', 'Reset Password key': 'Reset Password key', 'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'Sorok a tĆ”blĆ”ban', 'Rows selected': 'KivĆ”lasztott sorok', +'Save model as...': 'Save model as...', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'Ć”llapot', 'Statistics': 'Statistics', @@ -145,17 +167,21 @@ 'This App': 'This App', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Timestamp', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'nem lehet a csv fĆ”jlt beolvasni', 'Update:': 'FrissĆ­t:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', 'User ID': 'User ID', +'Users': 'Users', 'Videos': 'Videos', 'View': 'NĆ©zet', 'Welcome %s': 'Welcome %s', 'Welcome to web2py': 'Isten hozott a web2py-ban', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', diff --git a/applications/welcome/languages/id.py b/applications/welcome/languages/id.py index 0bb837dc..e34864ed 100755 --- a/applications/welcome/languages/id.py +++ b/applications/welcome/languages/id.py @@ -1,7 +1,8 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'id', '!langname!': 'Indonesian', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', '%d days ago': '%d hari yang lalu', '%d hours ago': '%d jam yang lalu', '%d minutes ago': '%d menit yang lalu', @@ -25,11 +26,14 @@ '1 week ago': '1 minggu yang lalu', '1 year ago': '1 tahun yang lalu', '< Previous': '< Sebelumnya', +'?': '?', 'About': 'Tentang', 'About application': 'Tentang Aplikasi', +'Access Control': 'Access Control', 'Add': 'Tambah', 'Additional code for your application': 'Tambahan kode untuk aplikasi Anda', 'Address': 'Alamat', +'admin': 'admin', 'Admin language': 'Bahasa Admin', 'administrative interface': 'antarmuka administrative', 'Administrator Password:': 'Administrator Kata Sandi:', @@ -48,6 +52,11 @@ 'Available Databases and Tables': 'Database dan Tabel yang tersedia', 'Back': 'Kembali', 'Buy this book': 'Beli buku ini', +"Buy web2py's book": "Buy web2py's book", +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, kesalahan dan sesi dibersihkan', 'can be a git repo': 'bisa menjadi repo git', 'Cancel': 'Batalkan', @@ -69,7 +78,9 @@ 'Compile': 'Kompilasi', 'compiled application removed': 'aplikasi yang dikompilasi dihapus', 'Components and Plugins': 'Komponen dan Plugin', +'Config.ini': 'Config.ini', 'contains': 'mengandung', +'Controller': 'Controller', 'Controllers': 'Kontrolir', 'controllers': 'kontrolir', 'Copyright': 'Hak Cipta', @@ -78,18 +89,30 @@ 'create file with filename:': 'buat file dengan nama:', 'created by': 'dibuat oleh', 'CSV (hidden cols)': 'CSV (kolom tersembunyi)', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'currently running': 'sedang berjalan', 'data uploaded': 'data diunggah', +'Database': 'Database', 'Database %s select': 'Memilih Database %s', 'database administration': 'administrasi database', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'db': 'db', +'DB Model': 'DB Model', 'defines tables': 'mendefinisikan tabel', 'Delete': 'Hapus', 'delete all checked': 'menghapus semua yang di centang', 'Delete this file (you will be asked to confirm deletion)': 'Hapus file ini (Anda akan diminta untuk mengkonfirmasi penghapusan)', 'Delete:': 'Hapus:', +'Demo': 'Demo', +'Deployment Recipes': 'Deployment Recipes', 'Description': 'Keterangan', 'design': 'disain', +'Design': 'Design', 'direction: ltr': 'petunjuk: ltr', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Dihapus', 'Documentation': 'Dokumentasi', "Don't know what to do?": 'Tidak tahu apa yang harus dilakukan?', @@ -101,6 +124,8 @@ 'Duration': 'Durasi', 'Edit': 'Mengedit', 'Edit application': 'Mengedit Aplikasi', +'Edit current record': 'Edit current record', +'Email and SMS': 'Email and SMS', 'Email sent': 'Email dikirim', 'enter a valid email address': 'masukkan alamat email yang benar', 'enter a valid URL': 'masukkan URL yang benar', @@ -112,17 +137,21 @@ 'Export:': 'Ekspor:', 'exposes': 'menghadapkan', 'extends': 'meluaskan', +'FAQ': 'FAQ', 'filter': 'menyaring', 'First Name': 'Nama Depan', 'Forgot username?': 'Lupa nama pengguna?', +'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Aplikasi Gratis', 'Gender': 'Jenis Kelamin', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Grup %(group_id)s dibuat', 'Group uniquely assigned to user %(id)s': 'Grup unik yang diberikan kepada pengguna %(id)s', 'Groups': 'Grup', 'Guest': 'Tamu', 'Hello World': 'Halo Dunia', 'Help': 'Bantuan', +'Helping web2py': 'Helping web2py', 'Home': 'Halaman Utama', 'How did you get here?': 'Bagaimana kamu bisa di sini?', 'Image': 'Gambar', @@ -132,14 +161,21 @@ 'Install': 'Memasang', 'Installation': 'Instalasi', 'Installed applications': 'Aplikasi yang diinstal', +'Internal State': 'Internal State', 'Introduction': 'Pengenalan', 'Invalid email': 'Email tidak benar', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Key': 'Key', 'Language': 'Bahasa', 'languages': 'bahasa', 'Languages': 'Bahasa', 'Last Name': 'Nama Belakang', +'Layout': 'Layout', 'License for': 'Lisensi untuk', +'Live Chat': 'Live Chat', 'loading...': 'sedang memuat...', +'Log In': 'Log In', 'Logged in': 'Masuk', 'Logged out': 'Keluar', 'Login': 'Masuk', @@ -149,17 +185,24 @@ 'Lost password?': 'Lupa kata sandi?', 'Maintenance': 'Pemeliharaan', 'Manage': 'Mengelola', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Mengelola Cache', -'models': 'model', +'Memberships': 'Memberships', +'Menu Model': 'Menu Model', 'Models': 'Model', +'models': 'model', 'Modules': 'Modul', 'modules': 'modul', 'My Sites': 'Situs Saya', 'New': 'Baru', 'new application "%s" created': 'aplikasi baru "%s" dibuat', 'New password': 'Kata sandi baru', +'New Record': 'New Record', +'new record inserted': 'new record inserted', 'New simple application': 'Aplikasi baru sederhana', 'News': 'Berita', +'next %s rows': 'next %s rows', 'next 100 rows': '100 baris berikutnya', 'Next >': 'Berikutnya >', 'Next Page': 'Halaman Berikutnya', @@ -168,6 +211,7 @@ 'not a Zip Code': 'bukan Kode Pos', 'Note': 'Catatan', 'Old password': 'Kata sandi lama', +'Online book': 'Online book', 'Online examples': 'Contoh Online', 'Or': 'Atau', 'or alternatively': 'atau alternatif', @@ -183,22 +227,33 @@ 'Password': 'Kata sandi', 'Password changed': 'Kata sandi berubah', "Password fields don't match": 'Kata sandi tidak sama', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'silahkan masukan kata sandi anda lagi', 'plugins': 'plugin', 'Plugins': 'Plugin', 'Plural-Forms:': 'Bentuk-Jamak:', 'Powered by': 'Didukung oleh', 'Preface': 'Pendahuluan', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 baris sebelumnya', 'Previous Page': 'Halaman Sebelumnya', -'private files': 'file pribadi', 'Private files': 'File pribadi', +'private files': 'file pribadi', 'Profile': 'Profil', 'Profile updated': 'Profil diperbarui', 'Project Progress': 'Perkembangan Proyek', +'pygraphviz library not found': 'pygraphviz library not found', +'Python': 'Python', +'Query:': 'Query:', 'Quick Examples': 'Contoh Cepat', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Dihapus', 'Recipes': 'Resep', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', 'Register': 'Daftar', 'Registration successful': 'Pendaftaran berhasil', 'reload': 'memuat kembali', @@ -206,6 +261,8 @@ 'Remember me (for 30 days)': 'Ingat saya (selama 30 hari)', 'Remove compiled': 'Hapus Kompilasi', 'Request reset password': 'Meminta reset kata sandi', +'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'Baris dalam Tabel', 'Rows selected': 'Baris dipilih', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Jalankan tes di file ini (untuk menjalankan semua file, Anda juga dapat menggunakan tombol berlabel 'test')", @@ -216,17 +273,26 @@ 'Select Files to Package': 'Pilih Berkas untuk Paket', 'Send Email': 'Kirim Email', 'Service': 'Layanan', +'Services': 'Services', +'Sign Up': 'Sign Up', 'Site': 'Situs', 'Size of cache:': 'Ukuran cache:', 'starts with': 'dimulai dengan', -'static': 'statis', +'state': 'state', 'Static': 'Statis', +'static': 'statis', 'Statistics': 'Statistik', +'Stylesheet': 'Stylesheet', +'submit': 'submit', 'Support': 'Mendukung', 'Table': 'Tabel', 'test': 'tes', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika aplikasi, setiap jalur URL dipetakan dalam satu fungsi terpapar di kontrolir', +'The Core': 'The Core', 'The data representation, define database tables and sets': 'Representasi data, mendefinisikan tabel database dan set', +'The output of the file is a dictionary that was rendered by the view %s': 'The output of the file is a dictionary that was rendered by the view %s', +'The Views': 'The Views', 'There are no plugins': 'Tidak ada plugin', 'There are no private files': 'Tidak ada file pribadi', 'These files are not served, they are only available from within your app': 'File-file ini tidak dilayani, mereka hanya tersedia dari dalam aplikasi Anda', @@ -235,8 +301,10 @@ 'Time in Cache (h:m:s)': 'Waktu di Cache (h: m: s)', 'To create a plugin, name a file/folder plugin_[name]': 'Untuk membuat sebuah plugin, nama file / folder plugin_ [nama]', 'too short': 'terlalu pendek', +'Traceback': 'Traceback', 'Translation strings for the application': 'Terjemahan string untuk aplikasi', 'Try the mobile interface': 'Coba antarmuka ponsel', +'Twitter': 'Twitter', 'Unable to download because:': 'Tidak dapat mengunduh karena:', 'unable to parse csv file': 'tidak mampu mengurai file csv', 'update all languages': 'memperbarui semua bahasa', @@ -246,17 +314,21 @@ 'Upload and install packed application': 'Upload dan pasang aplikasi yang dikemas', 'upload file:': 'unggah file:', 'upload plugin file:': 'unggah file plugin:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', 'User %(id)s Logged-in': 'Pengguna %(id)s Masuk', 'User %(id)s Logged-out': 'Pengguna %(id)s Keluar', 'User %(id)s Password changed': 'Pengguna %(id)s Kata Sandi berubah', 'User %(id)s Password reset': 'Pengguna %(id)s Kata Sandi telah direset', 'User %(id)s Profile updated': 'Pengguna %(id)s Profil diperbarui', 'User %(id)s Registered': 'Pengguna %(id)s Terdaftar', +'Users': 'Users', 'value already in database or empty': 'data sudah ada dalam database atau kosong', 'value not allowed': 'data tidak benar', 'value not in database': 'data tidak ada dalam database', 'Verify Password': 'Verifikasi Kata Sandi', 'Version': 'Versi', +'Videos': 'Videos', 'View': 'Lihat', 'Views': 'Lihat', 'views': 'lihat', @@ -266,6 +338,8 @@ 'Website': 'Situs Web', 'Welcome': 'Selamat Datang', 'Welcome to web2py!': 'Selamat Datang di web2py!', +'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'Anda berhasil menjalankan web2py', 'You can modify this application and adapt it to your needs': 'Anda dapat memodifikasi aplikasi ini dan menyesuaikan dengan kebutuhan Anda', 'You visited the url %s': 'Anda mengunjungi url %s', diff --git a/applications/welcome/languages/it.py b/applications/welcome/languages/it.py old mode 100644 new mode 100755 index 77a706be..1f779c13 --- a/applications/welcome/languages/it.py +++ b/applications/welcome/languages/it.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!=': '!=', '!langcode!': 'it', @@ -16,11 +16,13 @@ '=': '=', '>': '>', '>=': '>=', +'?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Number of entries: **%s**': 'Numero di entitĆ : **%s**', 'About': 'About', 'Access Control': 'Controllo Accessi', 'Add': 'Aggiungi', +'admin': 'admin', 'Administrative Interface': 'Interfaccia Amministrativa', 'Administrative interface': 'Interfaccia amministrativa', 'Ajax Recipes': 'Ajax Recipes', @@ -31,8 +33,10 @@ 'Available Databases and Tables': 'Database e tabelle disponibili', 'Back': 'Indietro', 'Buy this book': 'Compra questo libro', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Non può essere vuoto', 'Change password': 'Cambia Password', @@ -47,6 +51,7 @@ 'Cognome': 'Cognome', 'Community': 'Community', 'Components and Plugins': 'Componenti and Plugin', +'Config.ini': 'Config.ini', 'contains': 'contiene', 'Controller': 'Controller', 'Copyright': 'Copyright', @@ -61,6 +66,7 @@ 'data uploaded': 'dati caricati', 'Database': 'Database', 'Database %s select': 'Database %s select', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'db', 'DB Model': 'Modello di DB', 'Delete': 'Cancella', @@ -69,6 +75,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'Descrizione', 'design': 'progetta', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -103,6 +110,7 @@ 'hello world': 'salve mondo', 'Hello World': 'Salve Mondo', 'Hello World in a flash!': 'Salve Mondo in un flash!', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'Come sei arrivato qui?', 'HTML': 'HTML', @@ -124,6 +132,7 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'Logged in': 'Loggato', 'Logged out': 'Disconnesso', 'login': 'accesso', @@ -134,7 +143,10 @@ 'Lost password?': 'Password smarrita?', 'lost password?': 'dimenticato la password?', 'Main Menu': 'Menu principale', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu Modelli', 'Modified By': 'Modificato da', 'Modified On': 'Modificato il', @@ -144,6 +156,7 @@ 'New password': 'Nuova password', 'New Record': 'Nuovo elemento (record)', 'new record inserted': 'nuovo record inserito', +'next %s rows': 'next %s rows', 'next 100 rows': 'prossime 100 righe', 'No databases in this application': 'Nessun database presente in questa applicazione', 'No records found': 'Nessun record trovato', @@ -152,6 +165,7 @@ 'not authorized': 'non autorizzato', 'Object or table name': 'Oggeto o nome tabella', 'Old password': 'Vecchia password', +'Online book': 'Online book', 'Online examples': 'Vedere gli esempi', 'Or': 'O', 'or import from csv file': 'oppure importa da file CSV', @@ -161,10 +175,13 @@ 'Overview': 'Overview', 'Password': 'Password', "Password fields don't match": 'I campi password non sono uguali', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'perfavore reimmeti la tua password', 'Plugins': 'Plugins', 'Powered by': 'Powered by', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 righe precedenti', 'Profile': 'Profilo', 'pygraphviz library not found': 'pygraphviz library not found', @@ -189,6 +206,7 @@ 'Request reset password': 'Richiedi il reset della password', 'Reset Password key': 'Resetta chiave Password ', 'Role': 'Ruolo', +'Roles': 'Roles', 'Rows in Table': 'Righe nella tabella', 'Rows selected': 'Righe selezionate', 'Save model as...': 'Salva modello come...', @@ -196,6 +214,7 @@ 'Search': 'Ricerca', 'Semantic': 'Semantic', 'Services': 'Servizi', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'starts with': 'comincia con', 'state': 'stato', @@ -225,6 +244,7 @@ 'Update': 'Aggiorna', 'Update:': 'Aggiorna:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).', +'User': 'User', 'User %(id)s Logged-in': 'User %(id)s Logged-in', 'User %(id)s Logged-out': 'User %(id)s Logged-out', 'User %(id)s Password changed': 'User %(id)s Password changed', @@ -232,6 +252,7 @@ 'User %(id)s Profile updated': 'User %(id)s Profile updated', 'User %(id)s Registered': 'User %(id)s Registered', 'User ID': 'ID Utente', +'Users': 'Users', 'value already in database or empty': 'valore giĆ  presente nel database o vuoto', 'Verify Password': 'Verifica Password', 'Videos': 'Videos', diff --git a/applications/welcome/languages/my-mm.py b/applications/welcome/languages/my-mm.py old mode 100644 new mode 100755 index 36a03029..97e39ffe --- a/applications/welcome/languages/my-mm.py +++ b/applications/welcome/languages/my-mm.py @@ -10,10 +10,12 @@ '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(something like "it-it")', +'?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'About': 'နကြောင်း', 'Access Control': 'į€”į€žį€Æį€¶į€øį€•į€¼į€Æ ခြင်းဆိုင်ရာ ထိန်းချုပ်ရန်', 'Additional code for your application': 'Additional code for your application', +'admin': 'admin', 'Admin language': 'Admin language', 'administrative interface': 'administrative interface', 'Administrative Interface': 'စီမံခန့်ခွဲရာ နင်တာဖေ့စ်', @@ -27,8 +29,10 @@ 'Are you sure you want to delete this object?': 'į€žį€„į€ŗ ဒီနရာ ဖျက်ရန် į€žį€±į€į€»į€¬į€•į€«į€žį€œį€¬į€øį‹', 'Available Databases and Tables': 'į€”į€žį€Æį€¶į€øį€•į€¼į€Æį€”į€­į€Æį€„į€ŗį€žį€±į€¬ į€’į€±į€į€¬į€˜į€±į€…į€ŗį€·į€™į€»į€¬į€øį€”į€¾į€„į€ŗį€· į€‡į€šį€¬į€øį€™į€»į€¬į€ø', 'Buy this book': 'ဒီစာနုပ်ကို į€į€šį€ŗį€•į€«', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'can be a git repo': 'can be a git repo', 'Cannot be empty': 'į€”į€œį€½į€į€ŗ မဖြစ်ရပါ', @@ -44,6 +48,7 @@ 'Community': 'į€”į€žį€­į€Æį€„į€ŗį€øį€”į€į€­į€Æį€„į€ŗį€ø', 'Compile': 'Compile', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'į€€į€½į€”į€ŗį€‘į€›į€­į€Æį€œį€¬', 'Controllers': 'į€€į€½į€”į€ŗį€‘į€›į€­į€Æį€œį€¬į€™į€»į€¬į€ø', 'controllers': 'controllers', @@ -76,6 +81,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'ဖော်ပြချက်', 'design': 'design', +'Design': 'Design', 'direction: ltr': 'direction: ltr', 'Disable': 'ပိတ်ရန်', 'DISK': 'DISK', @@ -109,6 +115,7 @@ 'Groups': 'နဖွဲ့များ', 'Hello World': 'į€™į€„į€ŗį€¹į€‚į€œį€¬į€•į€« į€€į€™į€¹į€˜į€¬į€€į€¼į€®į€øį‹', 'Help': 'į€”į€€į€°į€”į€Šį€®', +'Helping web2py': 'Helping web2py', 'Home': 'į€™į€°į€œį€žį€­į€Æį€·', 'How did you get here?': 'į€žį€„į€ŗ į€˜į€šį€ŗį€œį€­į€Æ į€›į€±į€¬į€€į€ŗį€œį€¬į€į€²į€·į€žį€œį€²į‹', 'import': 'į€žį€½į€„į€ŗį€øį€šį€°į€›į€”į€ŗ', @@ -131,6 +138,7 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'နပြင်နဆင်များ', 'Live Chat': 'တိုက်ရိုက် į€†į€€į€ŗį€žį€½į€šį€ŗ ပြောကြားရန်', +'Log In': 'Log In', 'Login': 'į€į€„į€ŗį€›į€±į€¬į€€į€ŗį€”į€žį€Æį€¶į€øį€•į€¼į€Æį€›į€”į€ŗ', 'Login to the Administrative Interface': 'Login to the Administrative Interface', 'Logout': 'ထွက်ရန်', @@ -158,6 +166,7 @@ 'No databases in this application': 'ဒီ application တွင် į€™į€Šį€ŗį€žį€Šį€ŗį€· į€’į€±į€į€¬į€˜į€±į€…į€ŗį€·į€™į€¾ မရှိပါ', 'no package selected': 'no package selected', 'Object or table name': 'Object or table name', +'Online book': 'Online book', 'Online examples': 'į€”į€½į€”į€ŗį€œį€­į€Æį€„į€ŗį€ø နမူနာများ', 'or alternatively': 'or alternatively', 'Or Get from URL:': 'Or Get from URL:', @@ -211,6 +220,7 @@ 'Semantic': 'Semantic', 'Services': 'Services', 'shell': 'shell', +'Sign Up': 'Sign Up', 'Site': 'Site', 'Size of cache:': 'Size of cache:', 'Start wizard': 'Start wizard', diff --git a/applications/welcome/languages/my.py b/applications/welcome/languages/my.py index d5b85f95..cf9589b6 100755 --- a/applications/welcome/languages/my.py +++ b/applications/welcome/languages/my.py @@ -1,7 +1,8 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'my', '!langname!': 'Malay', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', '%d days ago': '%d hari yang lalu', '%d hours ago': '%d jam yang lalu', '%d minutes ago': '%d minit yang lalu', @@ -25,23 +26,34 @@ '1 week ago': '1 minggu yang lalu', '1 year ago': '1 tahun yang lalu', '< Previous': '< Sebelumnya', +'?': '?', 'About': 'Mengenai', +'Access Control': 'Access Control', 'Add': 'Tambah', +'admin': 'admin', 'Admin language': 'Bahasa admin', 'Administrator Password:': 'Kata laluan Administrator:', 'Ajax Recipes': 'Resipi Ajax', 'An error occured, please %s the page': 'Kesilapan telah berlaku, sila %s laman', 'And': 'Dan', 'and rename it:': 'dan menamakan itu:', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'are not used yet': 'tidak digunakan lagi', 'Are you sure you want to delete this object?': 'Apakah anda yakin anda mahu memadam ini?', +'Available Databases and Tables': 'Available Databases and Tables', 'Back': 'Kembali', 'Buy this book': 'Beli buku ini', +"Buy web2py's book": "Buy web2py's book", +'cache': 'cache', +'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, kesilapan dan sesi dibersihkan', 'Cancel': 'Batal', 'Cannot be empty': 'Tidak boleh kosong', 'Change admin password': 'Tukar kata laluan admin', 'Change password': 'Tukar kata laluan', +'Check to delete': 'Check to delete', 'Clean': 'Bersihkan', 'Clear': 'Hapus', 'Clear CACHE?': 'Hapus CACHE?', @@ -51,25 +63,42 @@ 'Close': 'Tutup', 'Community': 'Komuniti', 'Components and Plugins': 'Komponen dan Plugin', +'Config.ini': 'Config.ini', 'contains': 'mengandung', +'Controller': 'Controller', 'Copyright': 'Hak Cipta', 'Create': 'Buat', 'create file with filename:': 'mencipta fail dengan nama:', 'created by': 'dicipta oleh', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'currently running': 'sedang berjalan', 'data uploaded': 'data diunggah', +'Database': 'Database', +'Database %s select': 'Database %s select', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'db': 'db', +'DB Model': 'DB Model', 'Delete': 'Hapus', 'Delete this file (you will be asked to confirm deletion)': 'Padam fail ini (anda akan diminta untuk mengesahkan pemadaman)', 'Delete:': 'Hapus:', +'Demo': 'Demo', +'Deployment Recipes': 'Deployment Recipes', 'design': 'disain', +'Design': 'Design', 'direction: ltr': 'arah: ltr', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Dihapuskan', 'Documentation': 'Dokumentasi', "Don't know what to do?": 'Tidak tahu apa yang perlu dilakukan?', 'done!': 'selesai!', 'Download': 'Unduh', 'Duration': 'Tempoh', +'Edit current record': 'Edit current record', 'Email : ': 'Emel : ', +'Email and SMS': 'Email and SMS', 'Email sent': 'Emel dihantar', 'enter a valid email address': 'masukkan alamat emel yang benar', 'enter a valid URL': 'masukkan URL yang benar', @@ -78,17 +107,21 @@ 'Errors': 'Kesalahan', 'export as csv file': 'eksport sebagai file csv', 'Export:': 'Eksport:', +'FAQ': 'FAQ', 'File': 'Fail', 'filter': 'menapis', 'First Name': 'Nama Depan', 'Forgot username?': 'Lupa nama pengguna?', +'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Aplikasi Percuma', 'Gender': 'Jenis Kelamin', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Kumpulan %(group_id)s dicipta', 'Group uniquely assigned to user %(id)s': 'Kumpulan unik yang diberikan kepada pengguna %(id)s', 'Groups': 'Kumpulan', 'Hello World': 'Halo Dunia', 'Help': 'Bantuan', +'Helping web2py': 'Helping web2py', 'Home': 'Laman Utama', 'How did you get here?': 'Bagaimana kamu boleh di sini?', 'Image': 'Gambar', @@ -97,14 +130,21 @@ 'includes': 'termasuk', 'Install': 'Pasang', 'Installation': 'Pemasangan', +'Internal State': 'Internal State', 'Introduction': 'Pengenalan', 'Invalid email': 'Emel tidak benar', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Key': 'Key', 'Language': 'Bahasa', 'languages': 'bahasa', 'Languages': 'Bahasa', 'Last Name': 'Nama Belakang', +'Layout': 'Layout', 'License for': 'lesen untuk', +'Live Chat': 'Live Chat', 'loading...': 'sedang memuat...', +'Log In': 'Log In', 'Logged in': 'Masuk', 'Logged out': 'Keluar', 'Login': 'Masuk', @@ -113,20 +153,29 @@ 'Lost password?': 'Lupa kata laluan?', 'Maintenance': 'Penyelenggaraan', 'Manage': 'Menguruskan', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Menguruskan Cache', -'models': 'model', +'Memberships': 'Memberships', +'Menu Model': 'Menu Model', 'Models': 'Model', +'models': 'model', 'Modules': 'Modul', 'modules': 'modul', 'My Sites': 'Laman Saya', 'New': 'Baru', 'New password': 'Kata laluan baru', +'New Record': 'New Record', +'new record inserted': 'new record inserted', +'next %s rows': 'next %s rows', 'next 100 rows': '100 baris seterusnya', 'Next >': 'Seterusnya >', 'Next Page': 'Laman Seterusnya', +'No databases in this application': 'No databases in this application', 'No ticket_storage.txt found under /private folder': 'Ticket_storage.txt tidak dijumpai di bawah folder /private', 'not a Zip Code': 'bukan Pos', 'Old password': 'Kata laluan lama', +'Online book': 'Online book', 'Online examples': 'Contoh Online', 'Or': 'Atau', 'or alternatively': 'atau sebagai alternatif', @@ -139,27 +188,41 @@ 'Password': 'Kata laluan', 'Password changed': 'Kata laluan berubah', "Password fields don't match": 'Kata laluan tidak sama', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'sila masukan kata laluan anda lagi', 'plugins': 'plugin', 'Plugins': 'Plugin', 'Powered by': 'Disokong oleh', 'Preface': 'Pendahuluan', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 baris sebelumnya', 'Previous Page': 'Laman Sebelumnya', -'private files': 'fail peribadi', 'Private files': 'Fail peribadi', +'private files': 'fail peribadi', 'Profile': 'Profil', 'Profile updated': 'Profil dikemaskini', 'Project Progress': 'Kemajuan Projek', +'pygraphviz library not found': 'pygraphviz library not found', +'Python': 'Python', +'Query:': 'Query:', 'Quick Examples': 'Contoh Cepat', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Dihapuskan', 'Recipes': 'Resipi', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', 'Register': 'Daftar', 'Registration successful': 'Pendaftaran berjaya', 'reload': 'memuat kembali', 'Reload routes': 'Memuat laluan kembali', 'Remember me (for 30 days)': 'Ingat saya (selama 30 hari)', 'Request reset password': 'Meminta reset kata laluan', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', 'Rows selected': 'Baris dipilih', 'Running on %s': 'Berjalan pada %s', 'Save model as...': 'Simpan model sebagai ...', @@ -167,14 +230,24 @@ 'Search': 'Cari', 'Select Files to Package': 'Pilih Fail untuk Pakej', 'Send Email': 'Kirim Emel', +'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Saiz cache:', 'Solution': 'Penyelesaian', 'starts with': 'bermula dengan', +'state': 'state', 'static': 'statik', 'Static': 'Statik', 'Statistics': 'Statistik', +'Stylesheet': 'Stylesheet', +'submit': 'submit', 'Support': 'Menyokong', +'Table': 'Table', 'test': 'ujian', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The Core': 'The Core', +'The output of the file is a dictionary that was rendered by the view %s': 'The output of the file is a dictionary that was rendered by the view %s', +'The Views': 'The Views', 'There are no plugins': 'Tiada plugin', 'There are no private files': 'Tiada fail peribadi', 'These files are not served, they are only available from within your app': 'Fail-fail ini tidak disampaikan, mereka hanya boleh didapati dari dalam aplikasi anda', @@ -184,6 +257,8 @@ 'Title': 'Judul', 'To create a plugin, name a file/folder plugin_[name]': 'Untuk mencipta plugin, nama fail/folder plugin_ [nama]', 'too short': 'terlalu pendek', +'Traceback': 'Traceback', +'Twitter': 'Twitter', 'Unable to download because:': 'Tidak dapat memuat turun kerana:', 'unable to parse csv file': 'tidak mampu mengurai file csv', 'update all languages': 'mengemaskini semua bahasa', @@ -193,24 +268,30 @@ 'Upload a package:': 'Unggah pakej:', 'upload file:': 'unggah fail:', 'upload plugin file:': 'unggah fail plugin:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', 'User %(id)s Logged-in': 'Pengguna %(id)s Masuk', 'User %(id)s Logged-out': 'Pengguna %(id)s Keluar', 'User %(id)s Password changed': 'Pengguna %(id)s Kata Laluan berubah', 'User %(id)s Password reset': 'Pengguna %(id)s Kata Laluan telah direset', 'User %(id)s Profile updated': 'Pengguna %(id)s Profil dikemaskini', 'User %(id)s Registered': 'Pengguna %(id)s Didaftarkan', +'Users': 'Users', 'value not allowed': 'data tidak benar', 'Verify Password': 'Pengesahan Kata Laluan', 'Version': 'Versi', 'Versioning': 'Pembuatan Sejarah', +'Videos': 'Videos', 'View': 'Lihat', -'Views': 'Lihat', 'views': 'Lihat', +'Views': 'Lihat', 'Web Framework': 'Rangka Kerja Web', 'web2py Recent Tweets': 'Tweet terbaru web2py', 'Website': 'Laman Web', 'Welcome': 'Selamat Datang', 'Welcome to web2py!': 'Selamat Datang di web2py!', +'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'Anda berjaya menjalankan web2py', 'You can modify this application and adapt it to your needs': 'Anda boleh mengubah suai aplikasi ini dan menyesuaikan dengan keperluan anda', 'You visited the url %s': 'Anda melawat url %s', diff --git a/applications/welcome/languages/nl.py b/applications/welcome/languages/nl.py old mode 100644 new mode 100755 index 39892e52..1000587e --- a/applications/welcome/languages/nl.py +++ b/applications/welcome/languages/nl.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'nl', '!langname!': 'Nederlands', @@ -19,6 +19,7 @@ '=': '=', '>': '>', '>=': '>=', +'?': '?', 'A new version of web2py is available': 'Een nieuwe versie van web2py is beschikbaar', 'A new version of web2py is available: %s': 'Een nieuwe versie van web2py is beschikbaar: %s', 'About': 'Over', @@ -27,6 +28,7 @@ 'Access Control': 'Toegangscontrole', 'Add': 'Toevoegen', 'additional code for your application': 'additionele code voor je applicatie', +'admin': 'admin', 'admin disabled because no admin password': 'admin is uitgezet omdat er geen admin wachtwoord is', 'admin disabled because not supported on google app engine': 'admin is uitgezet omdat dit niet ondersteund wordt op google app engine', 'admin disabled because unable to access password file': 'admin is uitgezet omdat het wachtwoordbestand niet geopend kan worden', @@ -54,8 +56,10 @@ 'Available Databases and Tables': 'Beschikbare databases en tabellen', 'Back': 'Terug', 'Buy this book': 'Koop dit boek', +"Buy web2py's book": "Buy web2py's book", 'Cache': 'Cache', 'cache': 'cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, errors en sessies geleegd', 'Cannot be empty': 'Mag niet leeg zijn', @@ -78,6 +82,7 @@ 'compile': 'compileren', 'compiled application removed': 'gecompileerde applicatie verwijderd', 'Components and Plugins': 'Components en Plugins', +'Config.ini': 'Config.ini', 'contains': 'bevat', 'Controller': 'Controller', 'Controllers': 'Controllers', @@ -98,6 +103,7 @@ 'Database': 'Database', 'Database %s select': 'Database %s select', 'database administration': 'database administratie', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Datum en Tijd', 'db': 'db', 'DB Model': 'DB Model', @@ -112,6 +118,7 @@ 'Description': 'Beschrijving', 'design': 'design', 'DESIGN': 'DESIGN', +'Design': 'Design', 'Design for': 'Design voor', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', @@ -160,12 +167,14 @@ 'Forms and Validators': 'Formulieren en Validators', 'Free Applications': 'Gratis Applicaties', 'Functions with no doctests will result in [passed] tests.': 'Functies zonder doctests zullen resulteren in [passed] tests.', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Groep %(group_id)s gemaakt', 'Group ID': 'Groep ID', 'Group uniquely assigned to user %(id)s': 'Groep is uniek toegekend aan gebruiker %(id)s', 'Groups': 'Groepen', 'Hello World': 'Hallo Wereld', 'help': 'help', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'Hoe ben je hier gekomen?', 'htmledit': 'Bewerk HTML', @@ -201,6 +210,7 @@ 'License for': 'Licentie voor', 'Live Chat': 'Live Chat', 'loading...': 'laden...', +'Log In': 'Log In', 'Logged in': 'Ingelogd', 'Logged out': 'Uitgelogd', 'Login': 'Login', @@ -211,7 +221,10 @@ 'Lost Password': 'Wachtwoord Kwijt', 'Lost password?': 'Wachtwoord kwijt?', 'Main Menu': 'Hoofdmenu', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Beheer Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu Model', 'merge': 'samenvoegen', 'Models': 'Modellen', @@ -227,11 +240,13 @@ 'New password': 'Nieuw wachtwoord', 'New Record': 'Nieuw Record', 'new record inserted': 'nieuw record ingevoegd', +'next %s rows': 'next %s rows', 'next 100 rows': 'volgende 100 rijen', 'NO': 'NEE', 'No databases in this application': 'Geen database in deze applicatie', 'Object or table name': 'Object of tabelnaam', 'Old password': 'Oude wachtwoord', +'Online book': 'Online book', 'Online examples': 'Online voorbeelden', 'Or': 'Of', 'or import from csv file': 'of importeer van csv-bestand', @@ -246,12 +261,16 @@ 'Password': 'Wachtwoord', "Password fields don't match": 'Wachtwoordvelden komen niet overeen', 'Peeking at file': 'Naar bestand aan het gluren', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'geef alstublieft nogmaals uw wachtwoord', 'Plugins': 'Plugins', 'Powered by': 'Powered by', 'Preface': 'Inleiding', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'vorige 100 rijen', 'Profile': 'Profiel', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query': 'Query', 'Query:': 'Query:', @@ -277,9 +296,11 @@ 'restore': 'herstel', 'revert': 'herstel', 'Role': 'Rol', +'Roles': 'Roles', 'Rows in Table': 'Rijen in tabel', 'Rows selected': 'Rijen geselecteerd', 'save': 'bewaar', +'Save model as...': 'Save model as...', 'Save profile': 'Bewaar profiel', 'Saved file hash:': 'Opgeslagen file hash:', 'Search': 'Zoek', @@ -287,6 +308,7 @@ 'Services': 'Services', 'session expired': 'sessie verlopen', 'shell': 'shell', +'Sign Up': 'Sign Up', 'site': 'site', 'Size of cache:': 'Grootte van cache:', 'some files could not be removed': 'sommige bestanden konden niet worden verwijderd', @@ -326,6 +348,7 @@ 'Timestamp': 'Timestamp (timestamp)', 'to previous version.': 'naar vorige versie.', 'too short': 'te kort', +'Traceback': 'Traceback', 'translation strings for the application': 'vertaalstrings voor de applicatie', 'True': 'Waar', 'try': 'probeer', @@ -347,6 +370,7 @@ 'Upload existing application': 'Upload bestaande applicatie', 'upload file:': 'upload bestand', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Gebruik (...)&(...) voor AND, (...)|(...) voor OR, en ~(...) voor NOT om meer complexe queries te maken.', +'User': 'User', 'User %(id)s Logged-in': 'Gebruiker %(id)s Logged-in', 'User %(id)s Logged-out': 'Gebruiker %(id)s Logged-out', 'User %(id)s Password changed': 'Wachtwoord van gebruiker %(id)s is veranderd', @@ -354,6 +378,7 @@ 'User %(id)s Profile updated': 'Profiel van Gebruiker %(id)s geupdate', 'User %(id)s Registered': 'Gebruiker %(id)s Geregistreerd', 'User ID': 'User ID', +'Users': 'Users', 'value already in database or empty': 'waarde al in database of leeg', 'Verify Password': 'Verifieer Wachtwoord', 'versioning': 'versionering', @@ -369,6 +394,7 @@ 'Welcome to web2py': 'Welkom bij web2py', 'Welcome to web2py!': 'Welkom bij web2py!', 'Which called the function %s located in the file %s': 'Die functie %s aanriep en zich bevindt in het bestand %s', +'Working...': 'Working...', 'YES': 'JA', 'You are successfully running web2py': 'Je draait web2py succesvol', 'You can modify this application and adapt it to your needs': 'Je kan deze applicatie aanpassen naar je eigen behoeften', diff --git a/applications/welcome/languages/pl.py b/applications/welcome/languages/pl.py old mode 100644 new mode 100755 index aa1e5701..1e8cae26 --- a/applications/welcome/languages/pl.py +++ b/applications/welcome/languages/pl.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'pl', '!langname!': 'Polska', @@ -8,8 +8,10 @@ '%s selected': '%s wybranych', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'?': '?', 'About': 'About', 'Access Control': 'Access Control', +'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'Kliknij aby przejść do panelu administracyjnego', 'Ajax Recipes': 'Ajax Recipes', @@ -18,8 +20,10 @@ 'Authentication': 'Uwierzytelnienie', 'Available Databases and Tables': 'Dostępne bazy danych i tabele', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Nie może być puste', 'Change Password': 'Zmień hasło', @@ -32,6 +36,7 @@ 'Client IP': 'IP klienta', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Kontroler', 'Copyright': 'Copyright', 'Current request': 'Aktualne żądanie', @@ -41,6 +46,7 @@ 'data uploaded': 'dane wysłane', 'Database': 'baza danych', 'Database %s select': 'wybór z bazy danych %s', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'baza danych', 'DB Model': 'Model bazy danych', 'Delete:': 'Usuń:', @@ -48,6 +54,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'Opis', 'design': 'projektuj', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -69,9 +76,11 @@ 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', 'Function disabled': 'Funkcja wyłączona', +'Graph Model': 'Graph Model', 'Group ID': 'ID grupy', 'Groups': 'Groups', 'Hello World': 'Witaj Świecie', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -90,20 +99,26 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': 'login', 'Login': 'Zaloguj', 'logout': 'logout', 'Logout': 'Wyloguj', 'Lost Password': 'Przypomnij hasło', 'Main Menu': 'Menu główne', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Model menu', 'My Sites': 'My Sites', 'Name': 'Nazwa', 'New Record': 'Nowy rekord', 'new record inserted': 'nowy rekord został wstawiony', +'next %s rows': 'next %s rows', 'next 100 rows': 'następne 100 wierszy', 'No databases in this application': 'Brak baz danych w tej aplikacji', +'Online book': 'Online book', 'Online examples': 'Kliknij aby przejść do interaktywnych przykładów', 'or import from csv file': 'lub zaimportuj z pliku csv', 'Origin': 'Źródło', @@ -112,10 +127,14 @@ 'Overview': 'Overview', 'Password': 'Hasło', "Password fields don't match": 'Pola hasła nie są zgodne ze sobą', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'Zasilane przez', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'poprzednie 100 wierszy', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Zapytanie:', 'Quick Examples': 'Quick Examples', @@ -131,10 +150,13 @@ 'register': 'register', 'Registration key': 'Klucz rejestracji', 'Role': 'Rola', +'Roles': 'Roles', 'Rows in Table': 'Wiersze w tabeli', 'Rows selected': 'Wybrane wiersze', +'Save model as...': 'Save model as...', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'stan', 'Statistics': 'Statistics', @@ -152,12 +174,15 @@ 'This App': 'This App', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Znacznik czasu', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'nie można sparsować pliku csv', 'Update:': 'Uaktualnij:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Użyj (...)&(...) jako AND, (...)|(...) jako OR oraz ~(...) jako NOT do tworzenia bardziej skomplikowanych zapytań.', +'User': 'User', 'User %(id)s Registered': 'Użytkownik %(id)s został zarejestrowany', 'User ID': 'ID użytkownika', +'Users': 'Users', 'Verify Password': 'PotwierdÅŗ hasło', 'Videos': 'Videos', 'View': 'Widok', @@ -165,6 +190,7 @@ 'Welcome to web2py': 'Witaj w web2py', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', diff --git a/applications/welcome/languages/plural-cs.py b/applications/welcome/languages/plural-cs.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/plural-en.py b/applications/welcome/languages/plural-en.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/plural-es.py b/applications/welcome/languages/plural-es.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/plural-ru.py b/applications/welcome/languages/plural-ru.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/plural-uk.py b/applications/welcome/languages/plural-uk.py old mode 100644 new mode 100755 diff --git a/applications/welcome/languages/pt-br.py b/applications/welcome/languages/pt-br.py old mode 100644 new mode 100755 index 15280adc..ff77f771 --- a/applications/welcome/languages/pt-br.py +++ b/applications/welcome/languages/pt-br.py @@ -3,26 +3,37 @@ '!langcode!': 'pt-br', '!langname!': 'PortuguĆŖs (do Brasil)', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" Ć© uma expressĆ£o opcional como "campo1=\'novovalor\'". VocĆŖ nĆ£o pode atualizar ou apagar os resultados de um JOIN', -'%s %%{row} deleted': '%s linhas apagadas', -'%s %%{row} updated': '%s linhas atualizadas', +'%s %%{row} deleted': '%s linha apagadas', +'%s %%{row} updated': '%s linha atualizadas', '%s selected': '%s selecionado', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'?': '?', +'@markmin\x01An error occured, please [[reload %s]] the page': 'Ocorreu um erro, por favor [[reload %s]] a pĆ”gina', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'Sobre', 'Access Control': 'Controle de Acesso', +'admin': 'admin', 'Administrative Interface': 'Interface Administrativa', -'@markmin\x01An error occured, please [[reload %s]] the page': 'Ocorreu um erro, por favor [[reload %s]] a pĆ”gina', 'Administrative interface': 'Interface administrativa', 'Ajax Recipes': 'Receitas de Ajax', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'Administração desativada porque o canal nĆ£o Ć© seguro', -'Are you sure you want to delete this object?': 'VocĆŖ estĆ” certo que deseja apagar este objeto?', +'Are you sure you want to delete this object?': 'VocĆŖ tem certeza que quer apagar este objeto?', 'Available Databases and Tables': 'Bancos de dados e tabelas disponĆ­veis', 'Buy this book': 'Compre o livro', +"Buy web2py's book": "Compre o livro do web2buy", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache limpo', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Chaves de cache', -'Cannot be empty': 'NĆ£o pode ser vazio', -'change password': 'modificar senha', +'Cannot be empty': 'NĆ£o pode estar vazio', +'change password': 'mudar senha', 'Check to delete': 'Marque para apagar', 'Clear CACHE?': 'Limpar CACHE?', 'Clear DISK': 'Limpar DISCO', @@ -30,15 +41,17 @@ 'Client IP': 'IP do cliente', 'Community': 'Comunidade', 'Components and Plugins': 'Componentes e Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Controlador', 'Copyright': 'Copyright', 'Current request': 'Requisição atual', 'Current response': 'Resposta atual', 'Current session': 'SessĆ£o atual', -'customize me!': 'Personalize-me!', +'customize me!': 'personalize-me!', 'data uploaded': 'dados enviados', 'Database': 'banco de dados', 'Database %s select': 'Selecionar banco de dados %s', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'bd', 'DB Model': 'Modelo BD', 'Delete:': 'Apagar:', @@ -46,11 +59,13 @@ 'Deployment Recipes': 'Receitas de deploy', 'Description': 'Descrição', 'design': 'projeto', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Chaves do Cache de Disco', 'Disk Cleared': 'Disco Limpo', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentação', -"Don't know what to do?": "NĆ£o sabe o que fazer?", +"Don't know what to do?": 'NĆ£o sabe o que fazer?', 'done!': 'concluĆ­do!', 'Download': 'Download', 'E-mail': 'E-mail', @@ -59,16 +74,19 @@ 'edit profile': 'editar perfil', 'Edit This App': 'Editar esta aplicação', 'Email and SMS': 'Email e SMS', -'Errors': 'Erros', 'Enter an integer between %(min)g and %(max)g': 'Informe um valor inteiro entre %(min)g e %(max)g', +'Errors': 'Erros', 'export as csv file': 'exportar como um arquivo csv', 'FAQ': 'Perguntas frequentes', 'First name': 'Nome', 'Forms and Validators': 'FormulĆ”rios e Validadores', 'Free Applications': 'AplicaƧƵes gratuitas', +'Graph Model': 'Graph Model', 'Group ID': 'ID do Grupo', 'Groups': 'Grupos', 'Hello World': 'OlĆ” Mundo', +'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Principal', 'How did you get here?': 'Como vocĆŖ chegou aqui?', 'import': 'importar', @@ -88,21 +106,28 @@ 'Layouts': 'Layouts', 'Live chat': 'Chat ao vivo', 'Live Chat': 'Chat ao vivo', +'Log In': 'Log In', 'login': 'Entrar', 'Login': 'Autentique-se', 'logout': 'Sair', 'Lost Password': 'Esqueceu sua senha?', 'lost password?': 'esqueceu sua senha?', 'Main Menu': 'Menu Principal', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Gerenciar Cache', +'Memberships': 'Memberships', 'Menu Model': 'Modelo de Menu', 'My Sites': 'Meus sites', 'Name': 'Nome', 'New Record': 'Novo Registro', 'new record inserted': 'novo registro inserido', +'next %s rows': 'next %s rows', 'next 100 rows': 'próximas 100 linhas', 'No databases in this application': 'NĆ£o hĆ” bancos de dados nesta aplicação', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Nome do objeto do da tabela', +'Online book': 'Online book', 'Online examples': 'Exemplos online', 'or import from csv file': 'ou importar de um arquivo csv', 'Origin': 'Origem', @@ -110,16 +135,21 @@ 'Other Recipes': 'Outras Receitas', 'Overview': 'VisĆ£o Geral', 'Password': 'Senha', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'Desenvolvido com', 'Preface': 'PrefĆ”cio', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 linhas anteriores', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Consulta:', 'Quick Examples': 'Exemplos rĆ”pidos', 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Receitas', 'Record': 'Registro', 'record does not exist': 'registro nĆ£o existe', @@ -127,15 +157,18 @@ 'Record id': 'id do registro', 'Register': 'Registre-se', 'register': 'Registre-se', +'Registration identifier': 'Idenficador de registro', 'Registration key': 'Chave de registro', 'Reset Password key': 'Resetar chave de senha', 'Resources': 'Recursos', 'Role': 'Papel', -'Registration identifier': 'Idenficador de registro', +'Roles': 'Roles', 'Rows in Table': 'Linhas na tabela', 'Rows selected': 'Linhas selecionadas', +'Save model as...': 'Save model as...', 'Semantic': 'SemĆ¢ntico', 'Services': 'ServiƧo', +'Sign Up': 'Sign Up', 'Size of cache:': 'Tamanho do cache:', 'state': 'estado', 'Statistics': 'EstatĆ­sticas', @@ -154,12 +187,15 @@ 'This is a copy of the scaffolding application': 'Isto Ć© uma cópia da aplicação modelo', 'Time in Cache (h:m:s)': 'Tempo em Cache (h:m:s)', 'Timestamp': 'Timestamp', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'nĆ£o foi possĆ­vel analisar arquivo csv', 'Update:': 'Atualizar:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, e ~(...) para NOT para construir consultas mais complexas.', +'User': 'User', 'User ID': 'ID do UsuĆ”rio', 'User Voice': 'OpiniĆ£o dos usuĆ”rios', +'Users': 'Users', 'Videos': 'VĆ­deos', 'View': 'Visualização', 'Web2py': 'Web2py', @@ -168,9 +204,10 @@ 'Welcome to web2py': 'Bem-vindo ao web2py', 'Welcome to web2py!': 'Bem-vindo ao web2py!', 'Which called the function %s located in the file %s': 'Que chamou a função %s localizada no arquivo %s', +'Working...': 'Trabalhando...', 'You are successfully running web2py': 'VocĆŖ estĆ” executando o web2py com sucesso', 'You are successfully running web2py.': 'VocĆŖ estĆ” executando o web2py com sucesso.', 'You can modify this application and adapt it to your needs': 'VocĆŖ pode modificar esta aplicação e adaptĆ”-la Ć s suas necessidades', 'You visited the url %s': 'VocĆŖ acessou a url %s', -'Working...': 'Trabalhando...', } + diff --git a/applications/welcome/languages/pt.py b/applications/welcome/languages/pt.py old mode 100644 new mode 100755 index 8d36be6f..f0945906 --- a/applications/welcome/languages/pt.py +++ b/applications/welcome/languages/pt.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'pt', '!langname!': 'PortuguĆŖs', @@ -8,56 +8,71 @@ '%s selected': '%s seleccionado(s)', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', -'Access Control': 'Access Control', -'Administrative Interface': 'Administrative Interface', -'Administrative interface': 'Painel administrativo', -'Ajax Recipes': 'Ajax Recipes', +'Access Control': 'Controle de acesso', +'admin': 'administrador', +'Administrative Interface': 'Interface Administrativa', +'Administrative interface': 'Painel Administrativo', +'Ajax Recipes': 'Formulas Ajax', +'An error occured, please [[reload %s]] the page': 'Ocorreu um erro, por favor [[recarregue %s]] a pĆ”gina', 'appadmin is disabled because insecure channel': 'appadmin estĆ” desactivada pois o canal Ć© inseguro', -'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', +'Are you sure you want to delete this object?': 'Tem a certeza que quer deletar este objeto?', 'Author Reference Auth User': 'Author Reference Auth User', 'Author Reference Auth User.username': 'Author Reference Auth User.username', 'Available Databases and Tables': 'bases de dados e tabelas disponĆ­veis', -'Buy this book': 'Buy this book', +'Buy this book': 'Comprar este Livro', +"Buy web2py's book": "Comprar Livros web2py", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Limpo', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'nĆ£o pode ser vazio', -'Category Create': 'Category Create', -'Category Select': 'Category Select', +'Category Create': 'Criar Categoria', +'Category Select': 'Selecionar Categoria', 'change password': 'alterar palavra-chave', 'Check to delete': 'seleccione para eliminar', -'Clear CACHE?': 'Clear CACHE?', -'Clear DISK': 'Clear DISK', -'Clear RAM': 'Clear RAM', -'Comment Create': 'Comment Create', -'Comment Select': 'Comment Select', -'Community': 'Community', -'Components and Plugins': 'Components and Plugins', -'Content': 'Content', +'Clear CACHE?': 'apagar CACHE?', +'Clear DISK': 'apagar DISK', +'Clear RAM': 'apagar RAM', +'Comment Create': 'Criar ComentĆ”rio', +'Comment Select': 'Selecionar ComentĆ”rio', +'Community': 'Comunidade', +'Components and Plugins': 'Componentes e Plugins', +'Config.ini': 'Config.ini', +'Content': 'Conteudo', 'Controller': 'Controlador', -'Copyright': 'Direitos de cópia', -'create new category': 'create new category', -'create new comment': 'create new comment', -'create new post': 'create new post', -'Created By': 'Created By', -'Created On': 'Created On', -'Current request': 'pedido currente', -'Current response': 'resposta currente', -'Current session': 'sessĆ£o currente', +'Copyright': 'Direitos de Autor', +'create new category': 'Criar Nova Categoria', +'create new comment': 'Criar Novo ComentĆ”rio', +'create new post': 'Criar novo post', +'Created By': 'Creado Por', +'Created On': 'Creado Em', +'Current request': 'Pedido Corrente', +'Current response': 'Resposta Corrente', +'Current session': 'SessĆ£o Corrente', 'customize me!': 'Personaliza-me!', -'data uploaded': 'informação enviada', +'data uploaded': 'Informação enviada', 'Database': 'base de dados', 'Database %s select': 'selecção de base de dados %s', +'Database Administration (appadmin)': 'Base de Dados da Administração (appadmin)', 'db': 'bd', 'DB Model': 'Modelo de BD', 'Delete:': 'Eliminar:', 'Demo': 'Demo', 'Deployment Recipes': 'Deployment Recipes', 'design': 'design', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'concluĆ­do!', @@ -78,8 +93,11 @@ 'For %s #%s': 'For %s #%s', 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', +'Graph Model': 'Graph Model', 'Groups': 'Groups', 'Hello World': 'OlĆ” Mundo', +'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -97,11 +115,15 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': 'login', 'logout': 'logout', 'Lost Password': 'Lost Password', 'Main Menu': 'Menu Principal', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu do Modelo', 'Modified By': 'Modified By', 'Modified On': 'Modified On', @@ -109,27 +131,35 @@ 'Name': 'Name', 'New Record': 'Novo Registo', 'new record inserted': 'novo registo inserido', +'next %s rows': 'next %s rows', 'next 100 rows': 'próximas 100 linhas', 'No Data': 'No Data', 'No databases in this application': 'NĆ£o hĆ” bases de dados nesta aplicação', +'Number of entries: **%s**': 'Number of entries: **%s**', +'Online book': 'Online book', 'Online examples': 'Exemplos online', 'or import from csv file': 'ou importe a partir de ficheiro csv', 'Other Plugins': 'Other Plugins', 'Other Recipes': 'Other Recipes', 'Overview': 'Overview', 'Password': 'Password', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Post Create': 'Post Create', 'Post Select': 'Post Select', 'Powered by': 'Suportado por', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 linhas anteriores', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Interrogação:', 'Quick Examples': 'Quick Examples', 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'registo', 'record does not exist': 'registo inexistente', @@ -137,8 +167,11 @@ 'Register': 'Register', 'register': 'register', 'Replyto Reference Post': 'Replyto Reference Post', +'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'Linhas numa tabela', 'Rows selected': 'Linhas seleccionadas', +'Save model as...': 'Save model as...', 'search category': 'search category', 'search comment': 'search comment', 'search post': 'search post', @@ -150,6 +183,7 @@ 'show category': 'show category', 'show comment': 'show comment', 'show post': 'show post', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'estado', 'Statistics': 'Statistics', @@ -165,11 +199,14 @@ 'This App': 'This App', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Title': 'Title', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'nĆ£o foi possĆ­vel carregar ficheiro csv', 'Update:': 'Actualização:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Utilize (...)&(...) para AND, (...)|(...) para OR, e ~(...) para NOT para construir interrogaƧƵes mais complexas.', +'User': 'User', 'Username': 'Username', +'Users': 'Users', 'Videos': 'Videos', 'View': 'Vista', 'Welcome %s': 'Bem-vindo(a) %s', @@ -178,7 +215,9 @@ 'Welcome to web2py!': 'Welcome to web2py!', 'When': 'When', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', } + diff --git a/applications/welcome/languages/ro.py b/applications/welcome/languages/ro.py old mode 100644 new mode 100755 index 0f830e3d..8be157eb --- a/applications/welcome/languages/ro.py +++ b/applications/welcome/languages/ro.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!=': '!=', '!langcode!': 'ro', @@ -20,6 +20,7 @@ '=': '=', '>': '>', '>=': '>=', +'?': '?', 'A new version of web2py is available': 'O nouă versiune de web2py este disponibilă', 'A new version of web2py is available: %s': 'O nouă versiune de web2py este disponibilă: %s', 'About': 'Despre', @@ -28,6 +29,7 @@ 'Access Control': 'Control acces', 'Add': 'Adaugă', 'additional code for your application': 'cod suplimentar pentru aplicația dvs.', +'admin': 'admin', 'admin disabled because no admin password': 'administrare dezactivată deoarece parola de administrator nu a fost furnizată', 'admin disabled because not supported on google app engine': 'administrare dezactivată deoarece funcționalitatea nu e suportat pe Google App Engine', 'admin disabled because unable to access password file': 'administrare dezactivată deoarece nu există acces la fișierul cu parole', @@ -56,8 +58,10 @@ 'Available Databases and Tables': 'Baze de date și tabele disponibile', 'Back': 'Ǝnapoi', 'Buy this book': 'Cumpără această carte', +"Buy web2py's book": "Buy web2py's book", 'Cache': 'Cache', 'cache': 'cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Chei cache', 'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', 'Cannot be empty': 'Nu poate fi vid', @@ -80,6 +84,7 @@ 'compile': 'compilare', 'compiled application removed': 'aplicația compilată a fost ștearsă', 'Components and Plugins': 'Componente și plugin-uri', +'Config.ini': 'Config.ini', 'contains': 'conține', 'Controller': 'Controlor', 'Controllers': 'Controlori', @@ -98,6 +103,7 @@ 'Database': 'bază de date', 'Database %s select': 'selectare bază de date %s', 'database administration': 'administrare bază de date', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Data și ora', 'db': 'db', 'DB Model': 'Model bază de date', @@ -112,6 +118,7 @@ 'Description': 'Descriere', 'design': 'design', 'DESIGN': 'DESIGN', +'Design': 'Design', 'Design for': 'Design pentru', 'DISK': 'DISK', 'Disk Cache Keys': 'Chei cache de disc', @@ -160,12 +167,14 @@ 'Forms and Validators': 'Formulare și validatori', 'Free Applications': 'Aplicații gratuite', 'Functions with no doctests will result in [passed] tests.': 'Funcțiile fără doctests vor genera teste [trecute].', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Grup %(group_id)s creat', 'Group ID': 'ID grup', 'Group uniquely assigned to user %(id)s': 'Grup asociat Ć®n mod unic utilizatorului %(id)s', 'Groups': 'Grupuri', 'Hello World': 'Salutare lume', 'help': 'ajutor', +'Helping web2py': 'Helping web2py', 'Home': 'Acasă', 'How did you get here?': 'Cum ați ajuns aici?', 'htmledit': 'editare html', @@ -200,6 +209,7 @@ 'License for': 'Licență pentru', 'Live Chat': 'Chat live', 'loading...': 'Ć®ncarc...', +'Log In': 'Log In', 'Logged in': 'Logat', 'Logged out': 'Delogat', 'Login': 'Autentificare', @@ -210,7 +220,10 @@ 'Lost Password': 'Parolă pierdută', 'Lost password?': 'Parolă pierdută?', 'Main Menu': 'Meniu principal', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Model meniu', 'merge': 'unește', 'Models': 'Modele', @@ -224,11 +237,13 @@ 'New password': 'Parola nouă', 'New Record': 'Ǝnregistrare nouă', 'new record inserted': 'Ć®nregistrare nouă adăugată', +'next %s rows': 'next %s rows', 'next 100 rows': 'următoarele 100 de linii', 'NO': 'NU', 'No databases in this application': 'Aplicație fără bază de date', 'Object or table name': 'Obiect sau nume de tabel', 'Old password': 'Parola veche', +'Online book': 'Online book', 'Online examples': 'Exemple online', 'Or': 'Sau', 'or import from csv file': 'sau importă din fișier csv', @@ -243,12 +258,16 @@ 'Password': 'Parola', "Password fields don't match": 'CĆ¢mpurile de parolă nu se potrivesc', 'Peeking at file': 'Vizualizare fișier', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'introduceți parola din nou', 'Plugins': 'Plugin-uri', 'Powered by': 'Pus Ć®n mișcare de', 'Preface': 'Prefață', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 de linii anterioare', 'Profile': 'Profil', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query': 'Interogare', 'Query:': 'Interogare:', @@ -274,9 +293,11 @@ 'restore': 'restaurare', 'revert': 'revenire', 'Role': 'Rol', +'Roles': 'Roles', 'Rows in Table': 'Linii Ć®n tabel', 'Rows selected': 'Linii selectate', 'save': 'salvare', +'Save model as...': 'Save model as...', 'Save profile': 'Salvează profil', 'Saved file hash:': 'Hash fișier salvat:', 'Search': 'Căutare', @@ -284,6 +305,7 @@ 'Services': 'Servicii', 'session expired': 'sesiune expirată', 'shell': 'line de commandă', +'Sign Up': 'Sign Up', 'site': 'site', 'Size of cache:': 'Size of cache:', 'some files could not be removed': 'anumite fișiere n-au putut fi șterse', @@ -323,6 +345,7 @@ 'Timestamp': 'Moment Ć®n timp (timestamp)', 'to previous version.': 'la versiunea anterioară.', 'too short': 'prea scurt', +'Traceback': 'Traceback', 'translation strings for the application': 'șiruri de caractere folosite la traducerea aplicației', 'True': 'Adevărat', 'try': 'Ć®ncearcă', @@ -344,6 +367,7 @@ 'Upload existing application': 'Ǝncarcă aplicația existentă', 'upload file:': 'Ć®ncarcă fișier:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Folosiți (...)&(...) pentru AND, (...)|(...) pentru OR, și ~(...) pentru NOT, pentru a crea interogări complexe.', +'User': 'User', 'User %(id)s Logged-in': 'Utilizator %(id)s autentificat', 'User %(id)s Logged-out': 'Utilizator %(id)s delogat', 'User %(id)s Password changed': 'Parola utilizatorului %(id)s a fost schimbată', @@ -351,6 +375,7 @@ 'User %(id)s Profile updated': 'Profil utilizator %(id)s actualizat', 'User %(id)s Registered': 'Utilizator %(id)s Ć®nregistrat', 'User ID': 'ID utilizator', +'Users': 'Users', 'value already in database or empty': 'Valoare existentă Ć®n baza de date sau vidă', 'Verify Password': 'Verifică parola', 'versioning': 'versiuni', @@ -366,6 +391,7 @@ 'Welcome to web2py': 'Bun venit la web2py', 'Welcome to web2py!': 'Bun venit la web2py!', 'Which called the function %s located in the file %s': 'Care a apelat funcția %s prezentă Ć®n fișierul %s', +'Working...': 'Working...', 'YES': 'DA', 'You are successfully running web2py': 'Rulați cu succes web2py', 'You can modify this application and adapt it to your needs': 'Puteți modifica și adapta aplicația nevoilor dvs.', diff --git a/applications/welcome/languages/ru.py b/applications/welcome/languages/ru.py old mode 100644 new mode 100755 index 85644ea2..a3f38240 --- a/applications/welcome/languages/ru.py +++ b/applications/welcome/languages/ru.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'ru', '!langname!': 'Русский', @@ -22,8 +22,10 @@ '1 second ago': '1 секунГу Ń‚Š¾Š¼Ńƒ', '1 week ago': '1 Š½ŠµŠ“ŠµŠ»ŃŽ Ń‚Š¾Š¼Ńƒ', '1 year ago': '1 гоГ Ń‚Š¾Š¼Ńƒ', +'?': '?', 'About': 'About', 'Access Control': 'Access Control', +'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'аГминистративный интерфейс', 'Ajax Recipes': 'Ajax Recipes', @@ -31,8 +33,10 @@ 'Are you sure you want to delete this object?': 'Š’Ń‹ ŃƒŠ²ŠµŃ€ŠµŠ½Ń‹, что хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ ŃŃ‚Š¾Ń‚ Š¾Š±ŃŠŠµŠŗŃ‚?', 'Available Databases and Tables': 'Базы Ганных Šø таблицы', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'cache', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'ŠŸŃƒŃŃ‚Š¾Šµ значение Š½ŠµŠ“Š¾ŠæŃƒŃŃ‚ŠøŠ¼Š¾', 'Change Password': 'Дмените ŠæŠ°Ń€Š¾Š»ŃŒ', @@ -44,6 +48,7 @@ 'Client IP': 'Client IP', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'Controller', 'Copyright': 'Copyright', 'Current request': 'Š¢ŠµŠŗŃƒŃ‰ŠøŠ¹ запрос', @@ -53,6 +58,7 @@ 'data uploaded': 'Ганные Š·Š°Š³Ń€ŃƒŠ¶ŠµŠ½Ń‹', 'Database': 'Database', 'Database %s select': 'выбор базы Ганных %s', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'db': 'БД', 'DB Model': 'DB Model', 'Delete:': 'Š£Š“Š°Š»ŠøŃ‚ŃŒ:', @@ -60,6 +66,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'ŠžŠæŠøŃŠ°Š½ŠøŠµ', 'design': 'Гизайн', +'Design': 'Design', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -78,9 +85,11 @@ 'First name': 'Š˜Š¼Ń', 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', +'Graph Model': 'Graph Model', 'Group ID': 'Group ID', 'Groups': 'Groups', 'Hello World': 'Заработало!', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -100,6 +109,7 @@ 'Layout Plugins': 'Layout Plugins', 'Layouts': 'Layouts', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'Logged in': 'ВхоГ выполнен', 'Logged out': 'ВыхоГ выполнен', 'login': 'вхоГ', @@ -108,18 +118,23 @@ 'Logout': 'ВыхоГ', 'Lost Password': 'Забыли ŠæŠ°Ń€Š¾Š»ŃŒ?', 'Lost password?': 'Lost password?', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Menu Model', 'My Sites': 'My Sites', 'Name': 'Name', 'New password': 'ŠŠ¾Š²Ń‹Š¹ ŠæŠ°Ń€Š¾Š»ŃŒ', 'New Record': 'ŠŠ¾Š²Š°Ń запись', 'new record inserted': 'Š½Š¾Š²Š°Ń запись Гобавлена', +'next %s rows': 'next %s rows', 'next 100 rows': 'ŃŠ»ŠµŠ“ŃƒŃŽŃ‰ŠøŠµ 100 строк', 'No databases in this application': 'Š’ приложении нет баз Ганных', 'now': 'сейчас', 'Object or table name': 'Object or table name', 'Old password': 'Дтарый ŠæŠ°Ń€Š¾Š»ŃŒ', +'Online book': 'Online book', 'Online examples': 'примеры он-лайн', 'or import from csv file': 'или импорт ŠøŠ· csv-файла', 'Origin': 'ŠŸŃ€Š¾ŠøŃŃ…Š¾Š¶Š“ŠµŠ½ŠøŠµ', @@ -129,11 +144,15 @@ 'Password': 'ŠŸŠ°Ń€Š¾Š»ŃŒ', 'password': 'ŠæŠ°Ń€Š¾Š»ŃŒ', "Password fields don't match": 'ŠŸŠ°Ń€Š¾Š»Šø не ŃŠ¾Š²ŠæŠ°Š“Š°ŃŽŃ‚', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'Powered by', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'ŠæŃ€ŠµŠ“Ń‹Š“ŃƒŃ‰ŠøŠµ 100 строк', 'profile': 'ŠæŃ€Š¾Ń„ŠøŠ»ŃŒ', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Запрос:', 'Quick Examples': 'Quick Examples', @@ -151,10 +170,13 @@ 'Remember me (for 30 days)': 'Š—Š°ŠæŠ¾Š¼Š½ŠøŃ‚ŃŒ Š¼ŠµŠ½Ń (на 30 Гней)', 'Reset Password key': 'Š”Š±Ń€Š¾ŃŠøŃ‚ŃŒ ŠŗŠ»ŃŽŃ‡ ŠæŠ°Ń€Š¾Š»Ń', 'Role': 'Роль', +'Roles': 'Roles', 'Rows in Table': 'Дтрок в таблице', 'Rows selected': 'ВыГелено строк', +'Save model as...': 'Save model as...', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'ŃŠ¾ŃŃ‚Š¾ŃŠ½ŠøŠµ', 'Statistics': 'Statistics', @@ -172,16 +194,19 @@ 'This App': 'This App', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'ŠžŃ‚Š¼ŠµŃ‚ŠŗŠ° времени', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'unable to parse csv file': 'нечитаемый csv-файл', 'Update:': 'Š˜Š·Š¼ŠµŠ½ŠøŃ‚ŃŒ:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Š”Š»Ń построение сложных запросов ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚е операторы "И": (...)&(...), "Š˜Š›Š˜": (...)|(...), "ŠŠ•": ~(...).', +'User': 'User', 'User %(id)s Logged-in': 'ŠŸŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ %(id)s Š²Š¾ŃˆŃ‘Š»', 'User %(id)s Logged-out': 'ŠŸŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ %(id)s Š²Ń‹ŃˆŠµŠ»', 'User %(id)s Password changed': 'ŠŸŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ %(id)s сменил ŠæŠ°Ń€Š¾Š»ŃŒ', 'User %(id)s Profile updated': 'ŠŸŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ %(id)s обновил ŠæŃ€Š¾Ń„ŠøŠ»ŃŒ', 'User %(id)s Registered': 'ŠŸŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ %(id)s Š·Š°Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€Š¾Š²Š°Š»ŃŃ', 'User ID': 'ID ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń', +'Users': 'Users', 'Verify Password': 'ŠŸŠ¾Š²Ń‚Š¾Ń€ŠøŃ‚Šµ ŠæŠ°Ń€Š¾Š»ŃŒ', 'Videos': 'Videos', 'View': 'View', @@ -189,6 +214,7 @@ 'Welcome to web2py': 'Добро ŠæŠ¾Š¶Š°Š»Š¾Š²Š°Ń‚ŃŒ в web2py', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', diff --git a/applications/welcome/languages/sk.py b/applications/welcome/languages/sk.py old mode 100644 new mode 100755 index 88f8343f..0cf6f411 --- a/applications/welcome/languages/sk.py +++ b/applications/welcome/languages/sk.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'sk', '!langname!': 'Slovenský', @@ -8,116 +8,135 @@ '%s selected': '%s označených', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', -'About': 'About', -'Access Control': 'Access Control', -'Administrative Interface': 'Administrative Interface', +'?': '?', +'About': 'O nĆ”s', +'Access Control': 'Kontrola prĆ­stupu', +'admin': 'admin', +'Administrative Interface': 'AdministrĆ”torskĆ© rozhranie', 'Administrative interface': 'pre administrĆ”torskĆ© rozhranie kliknite sem', -'Ajax Recipes': 'Ajax Recipes', +'Ajax Recipes': 'Ajax recepty', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zaný bez zabezpečenĆ©ho spojenia', -'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', +'Are you sure you want to delete this object?': 'Ste si istý, že chcete vymazaÅ„ tento objekt?', 'Available Databases and Tables': 'DostupnĆ© databĆ”zy a tabuľky', -'Buy this book': 'Buy this book', +'Buy this book': 'KĆŗpte si tĆŗto knihu', +"Buy web2py's book": "KĆŗpte si web2py's knihu", 'cache': 'cache', 'Cache': 'Cache', -'Cache Keys': 'Cache Keys', +'Cache Cleared': 'Cache vymazanĆ©', +'Cache Keys': 'Cache kÄ¾ĆŗÄe', 'Cannot be empty': 'NemÓže byÅ„ prĆ”zdne', 'Check to delete': 'OznačiÅ„ na zmazanie', -'Clear CACHE?': 'Clear CACHE?', -'Clear DISK': 'Clear DISK', -'Clear RAM': 'Clear RAM', -'Community': 'Community', -'Components and Plugins': 'Components and Plugins', -'Controller': 'Controller', +'Clear CACHE?': 'VyčistiÅ„ CACHE?', +'Clear DISK': 'VyčistiÅ„ DISK', +'Clear RAM': 'VyčistiÅ„ RAM', +'Community': 'komunita', +'Components and Plugins': 'Komponenty a pluginy', +'Config.ini': 'Config.ini', +'Controller': 'KontrolĆ©r', 'Copyright': 'Copyright', 'Current request': 'AktuĆ”lna požiadavka', 'Current response': 'AktuĆ”lna odpoveď', 'Current session': 'AktuĆ”lne sedenie', 'customize me!': 'prispĆ“sob ma!', 'data uploaded': 'Ćŗdaje naplnenĆ©', -'Database': 'databĆ”za', -'Database %s select': 'databĆ”za %s výber', +'Database': 'DatabĆ”za', +'Database %s select': 'DatabĆ”za %s výber', +'Database Administration (appadmin)': 'SprĆ”va databĆ”zy (appadmin)', 'db': 'db', 'DB Model': 'DB Model', 'Delete:': 'ZmazaÅ„:', 'Demo': 'Demo', -'Deployment Recipes': 'Deployment Recipes', +'Deployment Recipes': 'Recepty pre nasadenie', 'Description': 'Popis', 'design': 'nĆ”vrh', +'Design': 'NĆ”vrh', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', -'Disk Cleared': 'Disk Cleared', +'Disk Cleared': 'Disk vyčistený', 'Documentation': 'DokumentĆ”cia', -"Don't know what to do?": "Don't know what to do?", +"Don't know what to do?": 'Neviete, čo robiÅ„?', 'done!': 'hotovo!', -'Download': 'Download', +'Download': 'StiahnuÅ„', 'Edit': 'UpraviÅ„', 'Edit current record': 'UpraviÅ„ aktuĆ”lny zĆ”znam', 'Edit Profile': 'UpraviÅ„ profil', -'Email and SMS': 'Email and SMS', -'Errors': 'Errors', +'Email and SMS': 'Email a SMS', +'Errors': 'Chyby', 'export as csv file': 'exportovaÅ„ do csv sĆŗboru', 'FAQ': 'FAQ', 'First name': 'KrstnĆ© meno', -'Forms and Validators': 'Forms and Validators', -'Free Applications': 'Free Applications', +'Forms and Validators': 'FormulĆ”re a schvalovače', +'Free Applications': 'AplikĆ”cie zadarmo', +'Graph Model': 'Model grafu', 'Group ID': 'ID skupiny', -'Groups': 'Groups', +'Groups': 'Skupiny', 'Hello World': 'Ahoj svet', -'Home': 'Home', -'How did you get here?': 'How did you get here?', +'Helping web2py': 'PomĆ”haÅ„ web2py', +'Home': 'Domov', +'How did you get here?': 'Ako ste sa sem dostali?', 'import': 'import', 'Import/Export': 'Import/Export', 'Index': 'Index', -'insert new': 'vložiÅ„ nový zĆ”znam ', -'insert new %s': 'vložiÅ„ nový zĆ”znam %s', +'insert new': 'vložiÅ„ nový', +'insert new %s': 'vložiÅ„ nový%s', 'Internal State': 'VnĆŗtorný stav', -'Introduction': 'Introduction', +'Introduction': 'Úvod', 'Invalid email': 'Neplatný email', 'Invalid password': 'NesprĆ”vne heslo', 'Invalid Query': 'NeplatnĆ” otĆ”zka', 'invalid request': 'NeplatnĆ” požiadavka', -'Key': 'Key', +'Key': 'KÄ¾ĆŗÄ', 'Last name': 'Priezvisko', -'Layout': 'Layout', -'Layout Plugins': 'Layout Plugins', -'Layouts': 'Layouts', -'Live Chat': 'Live Chat', +'Layout': 'Usporiadanie', +'Layout Plugins': 'Pluginy pre usporiadanie', +'Layouts': 'Usporiadania', +'Live Chat': 'Živý Chat', +'Log In': 'PrihlĆ”siÅ„ sa', 'Logged in': 'PrihlĆ”sený', 'Logged out': 'OdhlĆ”sený', 'login': 'prihlĆ”siÅ„', 'logout': 'odhlĆ”siÅ„', 'Lost Password': 'StratenĆ© heslo?', 'lost password?': 'stratenĆ© heslo?', -'Manage Cache': 'Manage Cache', -'Menu Model': 'Menu Model', -'My Sites': 'My Sites', +'Manage %(action)s': 'SpravovaÅ„ %(action)s', +'Manage Access Control': 'SpravovaÅ„ kontrolu prĆ­stupu', +'Manage Cache': 'SpravovaÅ„ cache', +'Memberships': 'ČlenstvĆ”', +'Menu Model': 'Model menu', +'My Sites': 'Moje strĆ”nky', 'Name': 'Meno', 'New password': 'NovĆ© heslo', 'New Record': 'Nový zĆ”znam', 'new record inserted': 'nový zĆ”znam bol vložený', +'next %s rows': 'next %s rows', 'next 100 rows': 'ďalŔích 100 riadkov', 'No databases in this application': 'V tejto aplikĆ”cii nie sĆŗ databĆ”zy', 'Old password': 'StarĆ© heslo', -'Online examples': 'pre online prĆ­klady kliknite sem', +'Online book': 'Online kniha', +'Online examples': 'online prĆ­klady', 'or import from csv file': 'alebo naimportovaÅ„ z csv sĆŗboru', 'Origin': 'PĆ“vod', -'Other Plugins': 'Other Plugins', -'Other Recipes': 'Other Recipes', -'Overview': 'Overview', +'Other Plugins': 'OstatnĆ© pluginy', +'Other Recipes': 'OstatnĆ© recepty', +'Overview': 'NĆ”hľad', 'password': 'heslo', 'Password': 'Heslo', -'Plugins': 'Plugins', -'Powered by': 'Powered by', -'Preface': 'Preface', +'Permission': 'Povolenie', +'Permissions': 'Povolenia', +'Plugins': 'Pluginy', +'Powered by': 'Beží na', +'Preface': 'Predslov', +'previous %s rows': 'predchĆ”dzajĆŗce %s riadky', 'previous 100 rows': 'predchĆ”dzajĆŗcich 100 riadkov', +'pygraphviz library not found': 'knižnica pygraphviz nenĆ”jdenĆ”', 'Python': 'Python', 'Query:': 'OtĆ”zka:', -'Quick Examples': 'Quick Examples', +'Quick Examples': 'Rýchle prĆ­klady', 'RAM': 'RAM', -'RAM Cache Keys': 'RAM Cache Keys', -'Ram Cleared': 'Ram Cleared', -'Recipes': 'Recipes', -'Record': 'zĆ”znam', +'RAM Cache Keys': 'RAM cache kÄ¾ĆŗÄe', +'Ram Cleared': 'Ram vyčistenĆ”', +'Recipes': 'Recepty', +'Record': 'ZĆ”znam', 'record does not exist': 'zĆ”znam neexistuje', 'Record ID': 'ID zĆ”znamu', 'Record id': 'id zĆ”znamu', @@ -127,44 +146,51 @@ 'Remember me (for 30 days)': 'ZapamƤtaj si ma (na 30 dnĆ­)', 'Reset Password key': 'NastaviÅ„ registračný kÄ¾ĆŗÄ', 'Role': 'Rola', -'Rows in Table': 'riadkov v tabuľke', -'Rows selected': 'označených riadkov', -'Semantic': 'Semantic', -'Services': 'Services', -'Size of cache:': 'Size of cache:', +'Roles': 'Role', +'Rows in Table': 'Riadkov v tabuľke', +'Rows selected': 'Označených riadkov', +'Save model as...': 'UložiÅ„ model ako...', +'Semantic': 'Významový', +'Services': 'Služby', +'Sign Up': 'PrihlĆ”siÅ„ sa', +'Size of cache:': 'VeľkosÅ„ cache:', 'state': 'stav', -'Statistics': 'Statistics', +'Statistics': 'Å tatistiky', 'Stylesheet': 'Stylesheet', -'submit': 'submit', +'submit': 'odoslaÅ„', 'Submit': 'OdoslaÅ„', -'Support': 'Support', +'Support': 'Podpora', 'Sure you want to delete this object?': 'Ste si istĆ­, že chcete zmazaÅ„ tento objekt?', -'Table': 'tabuľka', +'Table': 'Tabuľka', 'Table name': 'NĆ”zov tabuľky', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"query" je podmienka ako "db.table1.field1==\'value\'". Niečo ako "db.table1.field1==db.table2.field2" mĆ” za výsledok SQL JOIN.', -'The Core': 'The Core', -'The output of the file is a dictionary that was rendered by the view %s': 'Výstup zo sĆŗboru je slovnĆ­k, ktorý bol zobrazený vo view %s', -'The Views': 'The Views', -'This App': 'This App', +'The Core': 'Jadro', +'The output of the file is a dictionary that was rendered by the view %s': 'Výstup zo sĆŗboru je slovnĆ­k, ktorý bol vykreslený v zobrazenĆ­ %s', +'The Views': 'Zobrazenia', +'This App': 'TĆ”to aplikĆ”cia', 'This is a copy of the scaffolding application': 'Toto je kópia skeletu aplikĆ”cie', -'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Time in Cache (h:m:s)': 'Čas v cache (h:m:s)', 'Timestamp': 'ČasovĆ” pečiatka', +'Traceback': 'VystopovaÅ„', 'Twitter': 'Twitter', 'unable to parse csv file': 'nedĆ” sa načƭtaÅ„ csv sĆŗbor', 'Update:': 'UpraviÅ„:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Použite (...)&(...) pre AND, (...)|(...) pre OR a ~(...) pre NOT na poskladanie komplexnejŔích otĆ”zok.', +'User': 'User', 'User %(id)s Logged-in': 'Používateľ %(id)s prihlĆ”sený', 'User %(id)s Logged-out': 'Používateľ %(id)s odhlĆ”sený', 'User %(id)s Password changed': 'Používateľ %(id)s zmenil heslo', 'User %(id)s Profile updated': 'Používateľ %(id)s upravil profil', 'User %(id)s Registered': 'Používateľ %(id)s sa zaregistroval', 'User ID': 'ID používateľa', +'Users': 'Používatelia', 'Verify Password': 'Zopakujte heslo', -'Videos': 'Videos', +'Videos': 'VideĆ”', 'View': 'ZobraziÅ„', 'Welcome to web2py': 'Vitajte vo web2py', -'Welcome to web2py!': 'Welcome to web2py!', +'Welcome to web2py!': 'Vitajte vo web2py!', 'Which called the function %s located in the file %s': 'Ktorý zavolal funkciu %s nachĆ”dzajĆŗci sa v sĆŗbore %s', +'Working...': 'Pracuje...', 'You are successfully running web2py': 'ÚspeÅ”ne ste spustili web2py', 'You can modify this application and adapt it to your needs': 'MÓžete upraviÅ„ tĆŗto aplikĆ”ciu a prispĆ“sobiÅ„ ju svojim potrebĆ”m', 'You visited the url %s': 'NavÅ”tĆ­vili ste URL %s', diff --git a/applications/welcome/languages/tr.py b/applications/welcome/languages/tr.py old mode 100644 new mode 100755 index bf5d4e1a..23d0b87c --- a/applications/welcome/languages/tr.py +++ b/applications/welcome/languages/tr.py @@ -1,4 +1,4 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- { '!langcode!': 'tr', '!langname!': 'TürkƧe', @@ -6,34 +6,51 @@ '%s %%(shop)': '%s %%(shop)', '%s %%(shop[0])': '%s %%(shop[0])', '%s %%{quark[0]}': '%s %%{quark[0]}', +'%s %%{row} deleted': '%s %%{row} deleted', +'%s %%{row} updated': '%s %%{row} updated', '%s %%{shop[0]}': '%s %%{shop[0]}', '%s %%{shop}': '%s %%{shop}', '%s selected': '%s selected', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'?': '?', '@markmin\x01**Hello World**': '**Merhaba Dünya**', '@markmin\x01An error occured, please [[reload %s]] the page': 'Bir hata oluştu, lütfen sayfayı [[yenileyin yükleyin %s]] ', 'About': 'Hakkında', 'Access Control': 'Erişim Denetimi', +'admin': 'admin', 'Administrative Interface': 'Yƶnetim Arayüzü', 'Ajax Recipes': 'Ajax Tarifleri', 'An error occured, please %s the page': 'Bir hata meydana geldi, lütfen sayfayı %s', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'Apply changes': 'Değişiklikleri uygula', 'Are you sure you want to delete this object?': 'Bu nesneyi silmek istediğinden emin misin?', 'Available Databases and Tables': 'Kullanılabilir Varitabanları ve Tablolar', 'Buy this book': 'Bu kitabı satın alın', +"Buy web2py's book": "Buy web2py's book", 'cache': 'zula', +'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Boş bırakılamaz', 'Change password': 'Parolayı değiştir', 'Check to delete': 'Silmek iƧin denetle', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Client IP': 'İstemci IP', 'Community': 'Topluluk', 'Components and Plugins': 'Bileşenler ve Eklentiler', +'Config.ini': 'Config.ini', 'Controller': 'DenetƧi', 'Copyright': 'Telif', 'Created By': 'Tasarlayan', 'Created On': 'Oluşturma tarihi', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'customize me!': 'burayı değiştir!', +'data uploaded': 'data uploaded', 'Database': 'Veritabanı', 'Database %s select': '%s veritabanı seƧ', 'Database Administration (appadmin)': 'Veritabanı Yƶnetimi (appadmin)', @@ -44,10 +61,16 @@ 'Deployment Recipes': 'Yayınlama tarifleri', 'Description': 'AƧıklama', 'design': 'tasarım', +'Design': 'Design', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', 'Documentation': 'Kitap', "Don't know what to do?": 'Neleri nasıl yapacağını bilmiyor musun?', +'done!': 'done!', 'Download': 'İndir', 'E-mail': 'E-posta', +'Edit current record': 'Edit current record', 'Email and SMS': 'E-posta ve kısa mesaj (SMS)', 'enter a value': 'bir değer giriniz', 'enter an integer between %(min)g and %(max)g': '%(min)g ve %(max)g arasında bir sayı girin', @@ -69,25 +92,36 @@ 'Hello World': 'Merhaba Dünya', 'Hello World ## comment': 'Merhaba Dünya ## yorum ', 'Hello World## comment': 'Merhaba Dünya## yorum ', +'Helping web2py': 'Helping web2py', 'Home': 'Anasayfa', 'How did you get here?': 'Bu sayfayı gƶrüntüleme uğruna neler mi oldu?', 'import': 'import', 'Import/Export': 'Dışa/İƧe Aktar', +'Internal State': 'Internal State', 'Introduction': 'Giriş', +'invalid controller': 'geƧersiz denetleyici', 'Invalid email': 'Yanlış eposta', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', 'Is Active': 'Etkin', 'Kayıt ol': 'Kayıt ol', +'Key': 'Key', 'Last name': 'Soyad', 'Layout': 'Şablon', 'Layout Plugins': 'Şablon Eklentileri', 'Layouts': 'Şablonlar', 'Live Chat': 'Canlı Sohbet', +'Log In': 'Log In', 'Logged in': 'Giriş yapıldı', 'Logged out': 'Ƈıkış yapıldı', 'Login': 'Giriş', 'Logout': 'Terket', 'Lost Password': 'Şifremi unuttum', 'Lost password?': 'Şifrenizimi unuttunuz?', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'Model Menü', 'Modified By': 'Değiştiren', 'Modified On': 'Değiştirilme tarihi', @@ -95,8 +129,12 @@ 'Name': 'İsim', 'New password': 'Yeni parola', 'New Record': 'Yeni Kayıt', +'new record inserted': 'new record inserted', +'next %s rows': 'next %s rows', +'No databases in this application': 'No databases in this application', 'Object or table name': 'Nesne ya da tablo adı', 'Old password': 'Eski parola', +'Online book': 'Online book', 'Online examples': 'Canlı ƶrnekler', 'or import from csv file': 'veya csv dosyasından iƧe aktar', 'Origin': 'Asıl', @@ -105,17 +143,26 @@ 'Overview': 'Gƶz gezdir', 'Password': 'Parola', "Password fields don't match": 'Parolalar uyuşmuyor', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'lütfen parolanızı tekrar girin', 'Plugins': 'Eklentiler', 'Powered by': 'Yazılım Temeli', 'Preface': 'Ɩnzƶs', +'previous %s rows': 'previous %s rows', 'Profile': 'Profil', 'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': 'Sorgu:', 'Quick Examples': 'Hızlı Ɩrnekler', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', 'Recipes': 'Tarifeler', +'Record': 'Record', +'record does not exist': 'record does not exist', 'Record ID': 'Kayıt ID', +'Record id': 'Record id', 'Register': 'Kayıt ol', 'Registration identifier': 'Kayıt belirleyicisi', 'Registration key': 'Kayıt anahtarı', @@ -125,11 +172,16 @@ 'Request reset password': 'Parolanı sıfırla', 'Reset Password key': 'Parola anahtarını sıfırla', 'Role': 'Rol', +'Roles': 'Roles', 'Rows in Table': 'Tablodaki Satırlar', +'Rows selected': 'Rows selected', 'Save model as...': 'Modeli farklı kaydet...', 'Semantic': 'Anlamsal', 'Services': 'Hizmetler', +'Sign Up': 'Sign Up', +'Size of cache:': 'Size of cache:', 'state': 'durum', +'Statistics': 'Statistics', 'Stylesheet': 'Stil Şablonu', 'submit': 'gƶnder', 'Submit': 'Gƶnder', @@ -141,15 +193,20 @@ 'The Views': 'Gƶrünümler', 'This App': 'Bu Uygulama', 'This email already has an account': 'Bu e-postaya ait bir hesap zaten var', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Zaman damgası', +'Traceback': 'Traceback', 'Twitter': 'Twitter', +'unable to parse csv file': 'unable to parse csv file', 'Update:': 'Güncelle:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Karmaşık sorgularda Ve (AND) iƧin (...)&(...) kullanın, Veya (OR) iƧin (...)|(...) kullanın ve DEĞİL (NOT) iƧin ~(...) kullanın. ', +'User': 'User', 'User %(id)s Logged-in': '%(id)s Giriş yaptı', 'User %(id)s Logged-out': '%(id)s Ƨıkış yaptı', 'User %(id)s Password reset': 'Kullanıc %(id)s Parolasını sıfırla', 'User %(id)s Registered': '%(id)s Kayıt oldu', 'User ID': 'Kullanıcı ID', +'Users': 'Users', 'value already in database or empty': 'değer boş ya da veritabanında zaten mevcut', 'Verify Password': 'Parolanı Onayla', 'Videos': 'Videolar', @@ -161,6 +218,4 @@ 'You are successfully running web2py': 'web2py Ƨatısını Ƨalıştırmayı başardın', 'You can modify this application and adapt it to your needs': 'Artık uygulamayı istediğin gibi düzenleyebilirsin!', 'You visited the url %s': '%s adresini ziyaret ettin', -'invalid controller': 'geƧersiz denetleyici', } - diff --git a/applications/welcome/languages/uk.py b/applications/welcome/languages/uk.py old mode 100644 new mode 100755 index 17bfde51..b40fd1dd --- a/applications/welcome/languages/uk.py +++ b/applications/welcome/languages/uk.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'uk', '!langname!': 'Š£ŠŗŃ€Š°Ń—Š½ŃŃŒŠŗŠ°', @@ -22,6 +22,7 @@ '1 second ago': '1 секунГу Ń‚Š¾Š¼Ńƒ', '1 week ago': '1 Ń‚ŠøŠ¶Š“ŠµŠ½ŃŒ Ń‚Š¾Š¼Ńƒ', '1 year ago': '1 рік Ń‚Š¾Š¼Ńƒ', +'?': '?', '@markmin\x01(**%.0d MB**)': '(**``%.0d``:red ŠœŠ‘**)', '@markmin\x01**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{елемент(items)}, **%(bytes)s** %%{байт(bytes)}', '@markmin\x01``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**нема в Š½Š°ŃŠ²Š½Š¾ŃŃ‚Ń–**``:red (ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃ” ŠŸŃ–Ń‚Š¾Š½Ń–Š²ŃŃŒŠŗŠ¾Ń— бібліотеки [[guppy [ŠæŠ¾ŃŠøŠ»Š°Š½Š½Ń Š²Ń–Š“ŠŗŃ€ŠøŃ”Ń‚ŃŒŃŃ у новому вікні] http://pypi.python.org/pypi/guppy/ popup]])', @@ -33,14 +34,17 @@ '@markmin\x01RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': "Час Š¶ŠøŃ‚Ń‚Ń об'єктів в ŠžŠ—ŠŸ-ŠšŠ•ŠØŠ† ŃŃŠ³Š°Ń” **%(hours)02d** %%{гоГину(hours)} **%(min)02d** %%{Ń…Š²ŠøŠ»ŠøŠ½Ńƒ(min)} та **%(sec)02d** %%{секунГу(sec)}.", 'About': 'ŠŸŃ€Š¾ ГоГаток', 'Access Control': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»ŃŒ Š“Š¾ŃŃ‚ŃƒŠæŃƒ', +'admin': 'admin', 'Administrative Interface': 'АГміністративний інтерфейс', 'Ajax Recipes': 'Рецепти Š“Š»Ń Ajax', 'appadmin is disabled because insecure channel': 'Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ незахищенний канал (HTTP). Appadmin вимкнено', 'Are you sure you want to delete this object?': "Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø цей об'єкт?", 'Available Databases and Tables': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Ń– бази Ганих та таблиці', 'Buy this book': 'ŠšŃƒŠæŠøŃ‚Šø книжку', +"Buy web2py's book": "Buy web2py's book", 'cache': 'кеш', 'Cache': 'Кеш', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– кешу', 'Cannot be empty': 'ŠŸŠ¾Ń€Š¾Š¶Š½Ń” Š·Š½Š°Ń‡ŠµŠ½Š½Ń Š½ŠµŠæŃ€ŠøŠæŃƒŃŃ‚ŠøŠ¼Šµ', 'Change password': 'Змінити ŠæŠ°Ń€Š¾Š»ŃŒ', @@ -52,6 +56,7 @@ 'Client IP': 'IP клієнта', 'Community': 'Š”ŠæŃ–Š»ŃŒŠ½Š¾Ń‚Š°', 'Components and Plugins': 'ŠšŠ¾Š¼ŠæŠ¾Š½ŠµŠ½Ń‚Šø та Š²Ń‚ŃƒŠ»ŠŗŠø', +'Config.ini': 'Config.ini', 'Controller': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»ŠµŃ€', 'Copyright': 'ŠŸŃ€Š°Š²Š¾Š²Š»Š°ŃŠ½ŠøŠŗ', 'Created By': 'Дтворив(ла)', @@ -71,6 +76,7 @@ 'Deployment Recipes': 'Дпособи Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń', 'Description': 'ŠžŠæŠøŃ', 'design': 'Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń', +'Design': 'Design', 'DISK': 'Š”Š˜Š”Šš', 'Disk Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– Гискового кешу', 'Disk Cleared': 'Дисковий кеш очищено', @@ -100,6 +106,7 @@ 'Group uniquely assigned to user %(id)s': "Š“Ń€ŃƒŠæŠ° ŃƒŠ½Ń–ŠŗŠ°Š»ŃŒŠ½Š¾ зв'ŃŠ·Š°Š½Š° Š· ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ŠµŠ¼ %(id)s", 'Groups': 'Š“Ń€ŃƒŠæŠø', 'Hello World': 'ŠŸŃ€ŠøŠ²Ń–Ń‚, світ!', +'Helping web2py': 'Helping web2py', 'Home': 'ŠŸŠ¾Ń‡Š°Ń‚Š¾Šŗ', 'How did you get here?': 'ŠÆŠŗ Ń†ŃŒŠ¾Š³Š¾ було Š“Š¾ŃŃŠ³Š½ŃƒŃ‚Š¾?', 'import': 'Š†Š¼ŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø', @@ -120,13 +127,17 @@ 'Layout Plugins': 'Š’Ń‚ŃƒŠ»ŠŗŠø макетів', 'Layouts': 'ŠœŠ°ŠŗŠµŃ‚Šø', 'Live Chat': 'Чат', +'Log In': 'Log In', 'Logged in': 'Š’Ń…Ń–Š“ зГійснено', 'Logged out': 'Š’ŠøŃ…Ń–Š“ зГійснено', 'Login': 'Š’Ń…Ń–Š“', 'Logout': 'Š’ŠøŃ…Ń–Š“', 'Lost Password': 'Š—Š°Š±ŃƒŠ»Šø ŠæŠ°Ń€Š¾Š»ŃŒ', 'Lost password?': 'Š—Š°Š±ŃƒŠ»Šø ŠæŠ°Ń€Š¾Š»ŃŒ?', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Š£ŠæŃ€Š°Š²Š»Ń–Š½Š½Ń кешем', +'Memberships': 'Memberships', 'Menu Model': 'МоГель Š¼ŠµŠ½ŃŽ', 'Modified By': 'Зміни проваГив(ла)', 'Modified On': 'Змінено в', @@ -135,11 +146,13 @@ 'New password': 'ŠŠ¾Š²ŠøŠ¹ ŠæŠ°Ń€Š¾Š»ŃŒ', 'New Record': 'ŠŠ¾Š²ŠøŠ¹ запис', 'new record inserted': 'новий Ń€ŃŠ“Š¾Šŗ ГоГано', +'next %s rows': 'next %s rows', 'next 100 rows': 'Š½Š°ŃŃ‚ŃƒŠæŠ½Ń– 100 Ń€ŃŠ“ŠŗŃ–Š²', 'No databases in this application': 'Даний ГоГаток не Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” базу Ганих', 'now': 'зараз', 'Object or table name': "ŠžŠ±'єкт або назва таблиці", 'Old password': 'Дтарий ŠæŠ°Ń€Š¾Š»ŃŒ', +'Online book': 'Online book', 'Online examples': 'Зразковий Гемо-сайт', 'or import from csv file': 'або Ń–Š¼ŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø Š· csv-Ń„Š°Š¹Š»Ńƒ', 'Origin': 'ŠŸŠ¾Ń…Š¾Š“Š¶ŠµŠ½Š½Ń', @@ -151,10 +164,13 @@ 'Password': 'ŠŸŠ°Ń€Š¾Š»ŃŒ', 'Password changed': 'ŠŸŠ°Ń€Š¾Š»ŃŒ змінено', "Password fields don't match": 'ŠŸŠ°Ń€Š¾Š»ŃŒ не співпав', +'Permission': 'Permission', +'Permissions': 'Permissions', 'please input your password again': 'Š‘ŃƒŠ“ŃŒ-ласка Š²Š²ŠµŠ“Ń–Ń‚ŃŒ ŠæŠ°Ń€Š¾Š»ŃŒ ще раз', 'Plugins': 'Š’Ń‚ŃƒŠ»ŠŗŠø (Plugins)', 'Powered by': 'ŠŸŃ€Š°Ń†ŃŽŃ” на', 'Preface': 'ŠŸŠµŃ€ŠµŠ“Š¼Š¾Š²Š°', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'попереГні 100 Ń€ŃŠ“ŠŗŃ–Š²', 'Profile': 'ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€Šø', 'Profile updated': 'ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€Šø змінено', @@ -180,11 +196,14 @@ 'Request reset password': 'Запит на Š·Š¼Ń–Š½Ńƒ ŠæŠ°Ń€Š¾Š»Ń', 'Reset Password key': 'ŠšŠ»ŃŽŃ‡ ŃŠŗŠøŠ“Š°Š½Š½Ń ŠæŠ°Ń€Š¾Š»Ń', 'Role': 'Роль', +'Roles': 'Roles', 'Rows in Table': 'Š ŃŠ“ŠŗŠø в таблиці', 'Rows selected': 'ВіГмічено Ń€ŃŠ“ŠŗŃ–Š²', +'Save model as...': 'Save model as...', 'Save profile': 'Зберегти параметри', 'Semantic': 'Демантика', 'Services': 'Дервіс', +'Sign Up': 'Sign Up', 'Size of cache:': 'Розмір кешу:', 'state': 'стан', 'Statistics': 'Дтатистика', @@ -202,10 +221,12 @@ 'Time in Cache (h:m:s)': 'Час Š·Š½Š°Ń…Š¾Š“Š¶ŠµŠ½Š½Ń в ŠŗŠµŃˆŃ– (h:m:s)', 'Timestamp': 'ВіГмітка Ń‡Š°ŃŃƒ', 'too short': 'ЗанаГто короткий', +'Traceback': 'Traceback', 'Twitter': 'Твіттер', 'unable to parse csv file': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ розібрати csv-файл', 'Update:': 'ŠžŠ½Š¾Š²ŠøŃ‚Šø:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Š”Š»Ń ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń склаГних запитів Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ (...)&(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ AND, (...)|(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ OR, та ~(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ NOT.', +'User': 'User', 'User %(id)s Logged-in': 'ŠšŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ %(id)s ŃƒŠ²Ń–Š¹ŃˆŠ¾Š²', 'User %(id)s Logged-out': 'ŠšŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ %(id)s вийшов', 'User %(id)s Password changed': 'ŠšŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ %(id)s змінив свій ŠæŠ°Ń€Š¾Š»ŃŒ', @@ -213,6 +234,7 @@ 'User %(id)s Profile updated': 'ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€Šø ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š° %(id)s змінено', 'User %(id)s Registered': 'ŠšŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ %(id)s Š·Š°Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ²Š°Š²ŃŃ', 'User ID': 'ІГ.ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°', +'Users': 'Users', 'value already in database or empty': 'Š·Š½Š°Ń‡ŠµŠ½Š½Ń вже в базі Ганих або порожнє', 'Verify Password': 'ŠŸŠ¾Š²Ń‚Š¾Ń€ŠøŃ‚Šø ŠæŠ°Ń€Š¾Š»ŃŒ', 'Videos': 'ВіГео', diff --git a/applications/welcome/languages/zh-cn.py b/applications/welcome/languages/zh-cn.py old mode 100644 new mode 100755 index 61009f6e..888040c5 --- a/applications/welcome/languages/zh-cn.py +++ b/applications/welcome/languages/zh-cn.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'zh-cn', '!langname!': 'äø­ę–‡', @@ -9,12 +9,14 @@ '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(something like "it-it")': '(ę ¼å¼ē±»ä¼¼ "zh-tw")', +'?': '?', 'A new version of web2py is available': 'ę–°ē‰ˆ web2py å·²ęŽØå‡ŗ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆ web2py å·²ęŽØå‡ŗ: %s', 'about': 'å…³äŗŽ', 'About': 'å…³äŗŽ', 'About application': 'å…³äŗŽęœ¬åŗ”ē”ØēØ‹åŗ', 'Access Control': 'Access Control', +'admin': 'admin', 'Admin is disabled because insecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØéžå®‰å…ØčæžęŽ„ēŽÆå¢ƒäø‹č‡ŖåŠØå…³é—­', 'Admin is disabled because unsecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØéžå®‰å…ØčæžęŽ„ēŽÆå¢ƒäø‹č‡ŖåŠØå…³é—­', 'Administrative Interface': 'Administrative Interface', @@ -33,8 +35,10 @@ 'Authentication': '验证', 'Available Databases and Tables': 'åÆęä¾›ēš„ę•°ę®åŗ“å’Œę•°ę®č”Ø', 'Buy this book': '蓭买本书', +"Buy web2py's book": "Buy web2py's book", 'cache': 'é«˜é€Ÿē¼“å­˜', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '编译失蓄:åŗ”ē”ØēØ‹åŗęœ‰é”™čÆÆļ¼ŒčÆ·ęŽ’é™¤é”™čÆÆåŽå†å°čÆ•ē¼–čÆ‘.', @@ -48,6 +52,7 @@ 'Client IP': 'å®¢ęˆ·ē«Æē½‘å€(IP)', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'ꎧ件', 'Controllers': 'ꎧ件', 'Copyright': 'ē‰ˆęƒę‰€ęœ‰', @@ -61,6 +66,7 @@ 'data uploaded': 'ę•°ę®å·²äøŠä¼ ', 'Database': 'ę•°ę®åŗ“', 'Database %s select': '已选择 %s ę•°ę®åŗ“', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'ę—„ęœŸå’Œę—¶é—“', 'db': 'db', 'DB Model': 'ę•°ę®åŗ“ęØ”åž‹', @@ -71,6 +77,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'ęčæ°', 'DESIGN': '设讔', +'Design': 'Design', 'design': '设讔', 'Design for': 'č®¾č®”ē”ØäŗŽ', 'DISK': 'DISK', @@ -101,9 +108,11 @@ 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', 'Functions with no doctests will result in [passed] tests.': 'ę²’ęœ‰ doctests ēš„å‡½ę•°ä¼šę˜¾ē¤ŗ [passed].', +'Graph Model': 'Graph Model', 'Group ID': 'ē¾¤ē»„ē¼–å·', 'Groups': 'Groups', 'Hello World': 'Hello World', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -129,6 +138,7 @@ 'Layouts': 'Layouts', 'License for': 'č½Æä»¶ęŽˆęƒ', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': '登录', 'Login': '登录', 'Login to the Administrative Interface': 'ē™»å½•åˆ°ē®”ē†å‘˜ē•Œé¢', @@ -137,7 +147,10 @@ 'Lost Password': 'åæ˜č®°åÆ†ē ', 'Lost password?': 'åæ˜č®°åÆ†ē ļ¼Ÿ', 'Main Menu': 'äø»čœå•', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': 'čœå•ęØ”åž‹(menu)', 'Models': 'ę•°ę®ęØ”åž‹', 'Modified By': '修改者', @@ -147,10 +160,12 @@ 'Name': '名字', 'New Record': '新记录', 'new record inserted': 'å·²ę’å…„ę–°č®°å½•', +'next %s rows': 'next %s rows', 'next 100 rows': '往后 100 笔', 'NO': '否', 'No databases in this application': 'čÆ„åŗ”ē”ØēØ‹åŗäøå«ę•°ę®åŗ“', 'Object or table name': 'Object or table name', +'Online book': 'Online book', 'Online examples': 'ē‚¹å‡»čæ›å…„åœØēŗæä¾‹å­', 'or import from csv file': 'ęˆ–åÆ¼å…„CSVꖇ件', 'Origin': 'åŽŸę–‡', @@ -161,10 +176,14 @@ 'Password': '密码', "Password fields don't match": 'åÆ†ē äøåŒ¹é…', 'Peeking at file': '选ꋩꖇ件', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'åŸŗäŗŽäø‹åˆ—ęŠ€ęœÆęž„å»ŗļ¼š', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '往前 100 笔', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': '柄询:', 'Quick Examples': 'Quick Examples', @@ -185,11 +204,14 @@ 'Reset Password key': 'é‡ē½®åÆ†ē ', 'Resolve Conflict file': '解决冲突文件', 'Role': '角色', +'Roles': 'Roles', 'Rows in Table': 'åœØę•°ę®č”Øé‡Œēš„č®°å½•', 'Rows selected': '笔记录被选择', +'Save model as...': 'Save model as...', 'Saved file hash:': 'å·²äæå­˜ę–‡ä»¶ēš„å“ˆåøŒå€¼:', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': 'ēŠ¶ę€', 'Static files': 'é™ę€ę–‡ä»¶', @@ -217,6 +239,7 @@ 'Ticket': 'é—®é¢˜ęø…å•', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'ę—¶é—“ęˆ³', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'Unable to check for upgrades': 'ęŸ„čÆ¢ę–°ē‰ˆęœ¬å¤±č“„', 'Unable to download': '无法下载', @@ -225,9 +248,11 @@ 'Update:': 'ꛓꖰ:', 'Upload existing application': 'äøŠä¼ å·²ęœ‰åŗ”ē”ØēØ‹åŗ', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'ä½æē”Øäø‹åˆ—ę–¹å¼åÆå¾—åˆ°ę›“å¤ę‚ēš„ę”ä»¶č”Øč¾¾å¼, (...)&(...) ä»£č”Øåæ…é”»éƒ½ę»”č¶³, (...)|(...) 代蔨其一, ~(...)åˆ™ä»£č”Øå¦.', +'User': 'User', 'User %(id)s Logged-in': 'ē”Øęˆ· %(id)s 已登录', 'User %(id)s Registered': 'ē”Øęˆ· %(id)s å·²ę³Øå†Œ', 'User ID': 'ē”Øęˆ·ē¼–å·', +'Users': 'Users', 'Verify Password': 'éŖŒčÆåÆ†ē ', 'Videos': '视频', 'View': 'ęŸ„ēœ‹', @@ -237,6 +262,7 @@ 'Welcome to web2py': 'ę¬¢čæŽä½æē”Ø web2py', 'Welcome to web2py!': 'ę¬¢čæŽä½æē”Ø web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'YES': '是', 'You are successfully running web2py': 'ę‚Øå·²ęˆåŠŸčæč”Œ web2py', 'You can modify this application and adapt it to your needs': 'čÆ·ę ¹ę®ę‚Øēš„éœ€č¦äæ®ę”¹ęœ¬ēØ‹åŗ', diff --git a/applications/welcome/languages/zh-tw.py b/applications/welcome/languages/zh-tw.py old mode 100644 new mode 100755 index 5b109604..33e94daf --- a/applications/welcome/languages/zh-tw.py +++ b/applications/welcome/languages/zh-tw.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'zh-cn', '!langname!': 'äø­ę–‡', @@ -9,12 +9,14 @@ '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(something like "it-it")': '(ę ¼å¼é”žä¼¼ "zh-tw")', +'?': '?', 'A new version of web2py is available': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ: %s', 'about': 'é—œę–¼', 'About': 'é—œę–¼', 'About application': 'é—œę–¼ęœ¬ę‡‰ē”ØēØ‹å¼', 'Access Control': 'Access Control', +'admin': 'admin', 'Admin is disabled because insecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', 'Admin is disabled because unsecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', 'Administrative Interface': 'Administrative Interface', @@ -33,8 +35,10 @@ 'Authentication': 'é©—č­‰', 'Available Databases and Tables': 'åÆęä¾›ēš„č³‡ę–™åŗ«å’Œč³‡ę–™č”Ø', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'åæ«å–čØ˜ę†¶é«”', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '焔法編譯:ę‡‰ē”ØēØ‹å¼äø­å«ęœ‰éŒÆčŖ¤ļ¼Œč«‹é™¤éŒÆå¾Œå†č©¦äø€ę¬”.', @@ -48,6 +52,7 @@ 'Client IP': 'å®¢ęˆ¶ē«Æē¶²å€(IP)', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'ꎧ件', 'Controllers': 'ꎧ件', 'Copyright': 'ē‰ˆę¬Šę‰€ęœ‰', @@ -61,6 +66,7 @@ 'data uploaded': 'č³‡ę–™å·²äøŠå‚³', 'Database': '資料庫', 'Database %s select': '已選擇 %s 資料庫', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'ę—„ęœŸå’Œę™‚é–“', 'db': 'db', 'DB Model': '資料庫樔組', @@ -71,6 +77,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'ęčæ°', 'DESIGN': '設計', +'Design': 'Design', 'design': '設計', 'Design for': 'čØ­čØˆē‚ŗäŗ†', 'DISK': 'DISK', @@ -101,9 +108,11 @@ 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', 'Functions with no doctests will result in [passed] tests.': 'ę²’ęœ‰ doctests ēš„å‡½å¼ęœƒé”Æē¤ŗ [passed].', +'Graph Model': 'Graph Model', 'Group ID': 'ē¾¤ēµ„ē·Øč™Ÿ', 'Groups': 'Groups', 'Hello World': 'å—Ø! äø–ē•Œ', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -129,6 +138,7 @@ 'Layouts': 'Layouts', 'License for': 'č»Ÿé«”ē‰ˆę¬Šē‚ŗ', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': '登兄', 'Login': '登兄', 'Login to the Administrative Interface': 'ē™»å…„åˆ°ē®”ē†å“”ä»‹é¢', @@ -137,7 +147,10 @@ 'Lost Password': 'åÆ†ē¢¼éŗåæ˜', 'Lost password?': 'Lost password?', 'Main Menu': '主選單', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': '選單樔組(menu)', 'Models': '資料樔組', 'Modified By': 'Modified By', @@ -147,10 +160,12 @@ 'Name': '名字', 'New Record': 'ę–°ē“€éŒ„', 'new record inserted': 'å·²ę’å…„ę–°ē“€éŒ„', +'next %s rows': 'next %s rows', 'next 100 rows': '往後 100 ē­†', 'NO': '否', 'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', 'Object or table name': 'Object or table name', +'Online book': 'Online book', 'Online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', 'or import from csv file': 'ęˆ–ę˜Æå¾žé€—č™Ÿåˆ†éš”ęŖ”(CSV)åŒÆå…„', 'Origin': 'åŽŸę–‡', @@ -161,10 +176,14 @@ 'Password': '密碼', "Password fields don't match": 'åÆ†ē¢¼ę¬„äøåŒ¹é…', 'Peeking at file': 'éøę“‡ęŖ”ę”ˆ', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'åŸŗę–¼ä»„äø‹ęŠ€č”“ę§‹å»ŗļ¼š', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '往前 100 ē­†', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': '柄詢:', 'Quick Examples': 'Quick Examples', @@ -185,11 +204,14 @@ 'Reset Password key': 'é‡čØ­åÆ†ē¢¼', 'Resolve Conflict file': 'č§£ę±ŗč”ēŖęŖ”ę”ˆ', 'Role': '角色', +'Roles': 'Roles', 'Rows in Table': 'åœØč³‡ę–™č”Øč£ēš„č³‡ę–™', 'Rows selected': '筆資料被選擇', +'Save model as...': 'Save model as...', 'Saved file hash:': 'ęŖ”ę”ˆé›œę¹Šå€¼å·²ē“€éŒ„:', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': '狀態', 'Static files': 'éœę…‹ęŖ”ę”ˆ', @@ -217,6 +239,7 @@ 'Ticket': '問锌單', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'ę™‚é–“ęØ™čØ˜', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'Unable to check for upgrades': 'ē„”ę³•åšå‡ē“šęŖ¢ęŸ„', 'Unable to download': '焔法下載', @@ -225,9 +248,11 @@ 'Update:': 'ꛓꖰ:', 'Upload existing application': 'ę›“ę–°å­˜åœØēš„ę‡‰ē”ØēØ‹å¼', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'ä½æē”Øäø‹åˆ—ę–¹å¼ä¾†ēµ„åˆę›“č¤‡é›œēš„ę¢ä»¶å¼, (...)&(...) ä»£č”ØåŒę™‚å­˜åœØēš„ę¢ä»¶, (...)|(...) ä»£č”Øę“‡äø€ēš„ę¢ä»¶, ~(...)å‰‡ä»£č”Øåå‘ę¢ä»¶.', +'User': 'User', 'User %(id)s Logged-in': '使用者 %(id)s 已登兄', 'User %(id)s Registered': '使用者 %(id)s å·²čØ»å†Š', 'User ID': 'ä½æē”Øč€…ē·Øč™Ÿ', +'Users': 'Users', 'Verify Password': '驗證密碼', 'Videos': 'Videos', 'View': 'č¦–åœ–', @@ -237,6 +262,7 @@ 'Welcome to web2py': 'ę­”čæŽä½æē”Ø web2py', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'YES': '是', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', diff --git a/applications/welcome/languages/zh.py b/applications/welcome/languages/zh.py old mode 100644 new mode 100755 index 263918ce..e92241a2 --- a/applications/welcome/languages/zh.py +++ b/applications/welcome/languages/zh.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'zh-tw', '!langname!': 'äø­ę–‡', @@ -9,12 +9,14 @@ '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(something like "it-it")': '(ę ¼å¼é”žä¼¼ "zh-tw")', +'?': '?', 'A new version of web2py is available': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ: %s', 'about': 'é—œę–¼', 'About': 'é—œę–¼', 'About application': 'é—œę–¼ęœ¬ę‡‰ē”ØēØ‹å¼', 'Access Control': 'Access Control', +'admin': 'admin', 'Admin is disabled because insecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', 'Admin is disabled because unsecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', 'Administrative Interface': 'Administrative Interface', @@ -32,8 +34,10 @@ 'Authentication': 'é©—č­‰', 'Available Databases and Tables': 'åÆęä¾›ēš„č³‡ę–™åŗ«å’Œč³‡ę–™č”Ø', 'Buy this book': 'Buy this book', +"Buy web2py's book": "Buy web2py's book", 'cache': 'åæ«å–čØ˜ę†¶é«”', 'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '焔法編譯:ę‡‰ē”ØēØ‹å¼äø­å«ęœ‰éŒÆčŖ¤ļ¼Œč«‹é™¤éŒÆå¾Œå†č©¦äø€ę¬”.', @@ -47,6 +51,7 @@ 'Client IP': 'å®¢ęˆ¶ē«Æē¶²å€(IP)', 'Community': 'Community', 'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', 'Controller': 'ꎧ件', 'Controllers': 'ꎧ件', 'Copyright': 'ē‰ˆę¬Šę‰€ęœ‰', @@ -58,6 +63,7 @@ 'data uploaded': 'č³‡ę–™å·²äøŠå‚³', 'Database': '資料庫', 'Database %s select': '已選擇 %s 資料庫', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'ę—„ęœŸå’Œę™‚é–“', 'db': 'db', 'DB Model': '資料庫樔組', @@ -68,6 +74,7 @@ 'Deployment Recipes': 'Deployment Recipes', 'Description': 'ęčæ°', 'DESIGN': '設計', +'Design': 'Design', 'design': '設計', 'Design for': 'čØ­čØˆē‚ŗäŗ†', 'DISK': 'DISK', @@ -96,9 +103,11 @@ 'Forms and Validators': 'Forms and Validators', 'Free Applications': 'Free Applications', 'Functions with no doctests will result in [passed] tests.': 'ę²’ęœ‰ doctests ēš„å‡½å¼ęœƒé”Æē¤ŗ [passed].', +'Graph Model': 'Graph Model', 'Group ID': 'ē¾¤ēµ„ē·Øč™Ÿ', 'Groups': 'Groups', 'Hello World': 'å—Ø! äø–ē•Œ', +'Helping web2py': 'Helping web2py', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -123,6 +132,7 @@ 'Layouts': 'Layouts', 'License for': 'č»Ÿé«”ē‰ˆę¬Šē‚ŗ', 'Live Chat': 'Live Chat', +'Log In': 'Log In', 'login': '登兄', 'Login': '登兄', 'Login to the Administrative Interface': 'ē™»å…„åˆ°ē®”ē†å“”ä»‹é¢', @@ -130,7 +140,10 @@ 'Logout': '登出', 'Lost Password': 'åÆ†ē¢¼éŗåæ˜', 'Main Menu': '主選單', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', 'Menu Model': '選單樔組(menu)', 'Models': '資料樔組', 'Modules': 'ēØ‹å¼ęØ”ēµ„', @@ -138,9 +151,11 @@ 'Name': '名字', 'New Record': 'ę–°ē“€éŒ„', 'new record inserted': 'å·²ę’å…„ę–°ē“€éŒ„', +'next %s rows': 'next %s rows', 'next 100 rows': '往後 100 ē­†', 'NO': '否', 'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', +'Online book': 'Online book', 'Online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', 'or import from csv file': 'ęˆ–ę˜Æå¾žé€—č™Ÿåˆ†éš”ęŖ”(CSV)åŒÆå…„', 'Origin': 'åŽŸę–‡', @@ -151,10 +166,14 @@ 'Password': '密碼', "Password fields don't match": 'åÆ†ē¢¼ę¬„äøåŒ¹é…', 'Peeking at file': 'éøę“‡ęŖ”ę”ˆ', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Plugins': 'Plugins', 'Powered by': 'åŸŗę–¼ä»„äø‹ęŠ€č”“ę§‹å»ŗļ¼š', 'Preface': 'Preface', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '往前 100 ē­†', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', 'Query:': '柄詢:', 'Quick Examples': 'Quick Examples', @@ -173,11 +192,14 @@ 'Reset Password key': 'é‡čØ­åÆ†ē¢¼', 'Resolve Conflict file': 'č§£ę±ŗč”ēŖęŖ”ę”ˆ', 'Role': '角色', +'Roles': 'Roles', 'Rows in Table': 'åœØč³‡ę–™č”Øč£ēš„č³‡ę–™', 'Rows selected': '筆資料被選擇', +'Save model as...': 'Save model as...', 'Saved file hash:': 'ęŖ”ę”ˆé›œę¹Šå€¼å·²ē“€éŒ„:', 'Semantic': 'Semantic', 'Services': 'Services', +'Sign Up': 'Sign Up', 'Size of cache:': 'Size of cache:', 'state': '狀態', 'Static files': 'éœę…‹ęŖ”ę”ˆ', @@ -205,6 +227,7 @@ 'Ticket': '問锌單', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'ę™‚é–“ęØ™čØ˜', +'Traceback': 'Traceback', 'Twitter': 'Twitter', 'Unable to check for upgrades': 'ē„”ę³•åšå‡ē“šęŖ¢ęŸ„', 'Unable to download': '焔法下載', @@ -213,9 +236,11 @@ 'Update:': 'ꛓꖰ:', 'Upload existing application': 'ę›“ę–°å­˜åœØēš„ę‡‰ē”ØēØ‹å¼', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'ä½æē”Øäø‹åˆ—ę–¹å¼ä¾†ēµ„åˆę›“č¤‡é›œēš„ę¢ä»¶å¼, (...)&(...) ä»£č”ØåŒę™‚å­˜åœØēš„ę¢ä»¶, (...)|(...) ä»£č”Øę“‡äø€ēš„ę¢ä»¶, ~(...)å‰‡ä»£č”Øåå‘ę¢ä»¶.', +'User': 'User', 'User %(id)s Logged-in': '使用者 %(id)s 已登兄', 'User %(id)s Registered': '使用者 %(id)s å·²čØ»å†Š', 'User ID': 'ä½æē”Øč€…ē·Øč™Ÿ', +'Users': 'Users', 'Verify Password': '驗證密碼', 'Videos': 'Videos', 'View': 'č¦–åœ–', @@ -224,6 +249,7 @@ 'Welcome to web2py': 'ę­”čæŽä½æē”Ø web2py', 'Welcome to web2py!': 'Welcome to web2py!', 'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', 'YES': '是', 'You are successfully running web2py': 'You are successfully running web2py', 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', From 24d1eea554693247b6efe1c3897c7cb866e52f7d Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 00:23:33 -0700 Subject: [PATCH 091/436] Support for custom 503 responses Added support for a custom 503 (temporary maintenance) page based upon the DISABLED file in an app folder --- applications/examples/static/503.html | 1 + applications/welcome/static/503.html | 1 + gluon/main.py | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 applications/examples/static/503.html create mode 100644 applications/welcome/static/503.html diff --git a/applications/examples/static/503.html b/applications/examples/static/503.html new file mode 100644 index 00000000..b063a75b --- /dev/null +++ b/applications/examples/static/503.html @@ -0,0 +1 @@ +

      Temporarily down for maintenance

      \ No newline at end of file diff --git a/applications/welcome/static/503.html b/applications/welcome/static/503.html new file mode 100644 index 00000000..b063a75b --- /dev/null +++ b/applications/welcome/static/503.html @@ -0,0 +1 @@ +

      Temporarily down for maintenance

      \ No newline at end of file diff --git a/gluon/main.py b/gluon/main.py index 971c48a7..27fb2e51 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -25,7 +25,7 @@ import socket import random import string -from gluon._compat import Cookie, urllib2 +from pydal._compat import Cookie, urllib2 #from thread import allocate_lock from gluon.fileutils import abspath, write_file @@ -390,7 +390,11 @@ def wsgibase(environ, responder): % 'invalid request', web2py_error='invalid application') elif not request.is_local and exists(disabled): - raise HTTP(503, "

      Temporarily down for maintenance

      ") + if os.path.exists(os.path.join(request.folder,'static','503.html')): + import urllib + raise HTTP(503, urllib.urlopen(os.path.join(request.folder,'static','503.html')).read()) + else: + raise HTTP(503, "

      Temporarily down for maintenance

      ") # ################################################## # build missing folders From 05537b99161df488e958b0921aebd61c4c61765c Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 00:43:39 -0700 Subject: [PATCH 092/436] Language update --- applications/welcome/languages/ar.py | 11 +++++++++++ applications/welcome/languages/ca.py | 11 +++++++++++ applications/welcome/languages/cs.py | 13 ++++++++++++- applications/welcome/languages/de.py | 2 +- applications/welcome/languages/es.py | 3 +-- applications/welcome/languages/fr-ca.py | 11 +++++++++++ applications/welcome/languages/hi.py | 11 +++++++++++ applications/welcome/languages/hu.py | 11 +++++++++++ applications/welcome/languages/id.py | 11 +++++++++++ applications/welcome/languages/it.py | 11 +++++++++++ applications/welcome/languages/my-mm.py | 11 +++++++++++ applications/welcome/languages/my.py | 11 +++++++++++ applications/welcome/languages/nl.py | 11 +++++++++++ applications/welcome/languages/pl.py | 11 +++++++++++ applications/welcome/languages/pt-br.py | 3 +-- applications/welcome/languages/pt.py | 3 +-- applications/welcome/languages/ro.py | 11 +++++++++++ applications/welcome/languages/ru.py | 11 +++++++++++ applications/welcome/languages/sk.py | 11 +++++++++++ applications/welcome/languages/tr.py | 11 +++++++++++ applications/welcome/languages/uk.py | 11 +++++++++++ applications/welcome/languages/zh-cn.py | 11 +++++++++++ applications/welcome/languages/zh-tw.py | 11 +++++++++++ applications/welcome/languages/zh.py | 11 +++++++++++ 24 files changed, 225 insertions(+), 8 deletions(-) diff --git a/applications/welcome/languages/ar.py b/applications/welcome/languages/ar.py index c3908eba..dce4610f 100755 --- a/applications/welcome/languages/ar.py +++ b/applications/welcome/languages/ar.py @@ -13,13 +13,19 @@ '%s selected': '%s selected', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', '@markmin\x01**Hello World**': '**Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم**', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'نبذة', 'Access Control': 'Ł…ŲŖŲ­ŁƒŁ…Ų§ŲŖ Ų§Ł„ŁˆŲµŁˆŁ„', 'admin': 'admin', 'Administrative Interface': 'ŁˆŲ§Ų¬Ł‡Ų© Ų§Ł„ŲŖŲ­ŁƒŁ…', 'Ajax Recipes': 'وصفات أجاكس', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'Are you sure you want to delete this object?': 'هل أنت Ł…ŲŖŲ£ŁƒŲÆ بحذف هذا Ų§Ł„ŁƒŲ§Ų¦Ł† ؟', 'Available Databases and Tables': 'Available Databases and Tables', @@ -28,6 +34,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'لا ŁŠŁ…ŁƒŁ† بأن ŁŠŁƒŁˆŁ† Ų®Ų§Ł„ŁŠ', 'Check to delete': 'Ų£Ų®ŲŖŲ± للحذف', @@ -61,6 +68,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'المستندات', "Don't know what to do?": 'لا تعلم Ł…Ų§Ų°Ų§ ستفعل ؟', 'done!': 'done!', @@ -85,6 +93,7 @@ 'Hello World ## comment': ' Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم', 'Hello World## comment': 'Ł…Ų±Ų­ŲØŲ§Ł‹ بالعالم', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Ų§Ł„Ų±Ų¦ŁŠŲ³ŁŠŲ©', 'How did you get here?': 'كيف Ų£Ų³ŲŖŲ·Ų¹ŲŖ Ų§Ł„ŁˆŲµŁˆŁ„ ؄لى هنا ؟', 'import': 'import', @@ -121,6 +130,7 @@ 'new record inserted': 'new record inserted', 'next %s rows': 'next %s rows', 'No databases in this application': 'No databases in this application', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'أسم Ų§Ł„ŁƒŲ§Ų¦Ł† أو Ų§Ł„Ų¬ŲÆŁˆŁ„', 'Online book': 'Online book', 'Online examples': 'أمثلة على الأنترنت', @@ -146,6 +156,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'وصفات', 'Record': 'Record', 'record does not exist': 'record does not exist', diff --git a/applications/welcome/languages/ca.py b/applications/welcome/languages/ca.py index 2fb827f1..951894dc 100755 --- a/applications/welcome/languages/ca.py +++ b/applications/welcome/languages/ca.py @@ -10,10 +10,15 @@ '%s selected': '%s %%{seleccionat}', '%Y-%m-%d': '%d/%m/%Y', '%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(similar a "això-això")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'Hi ha hagut un error, si us plau [[recarregui %s]] la pĆ gina', '@markmin\x01Number of entries: **%s**': "Nombre d'entrades: **%s**", +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'Hi ha una nova versió de wep2py disponible', 'A new version of web2py is available: %s': 'Hi ha una nova versió de wep2py disponible: %s', 'About': 'Sobre', @@ -35,6 +40,7 @@ 'Administrator Password:': 'Contrasenya del Administrador:', 'Ajax Recipes': 'Receptes AJAX', 'An error occured, please %s the page': 'Hi ha hagut un error, per favor %s la pĆ gina', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'I', 'and rename it (required):': 'i renombri-la (requerit):', 'and rename it:': " i renombri'l:", @@ -63,6 +69,7 @@ 'Cache': 'CachĆ©', 'cache': 'cachĆ©', 'Cache Cleared': 'CachĆ© Netejada', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Claus de la CachĆ©', 'cache, errors and sessions cleaned': 'cachĆ©, errors i sessions eliminats', 'Cannot be empty': 'No pot estar buit', @@ -138,6 +145,7 @@ 'DISK': 'DISC', 'Disk Cache Keys': 'Claus de CachĆ© en Disc', 'Disk Cleared': 'Disc netejat', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentació', "Don't know what to do?": 'No sap quĆØ fer?', 'done!': 'Ā”fet!', @@ -207,6 +215,7 @@ 'Hello World': 'Hola Món', 'help': 'ajuda', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Inici', 'Hosted by': 'Hosted by', 'How did you get here?': 'Com has arribat aquĆ­?', @@ -294,6 +303,7 @@ 'No records found': "No s'han trobat registres", 'Not authorized': 'No autoritzat', 'not in': 'no a', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Nom del objecte o taula', 'Old password': 'Contrasenya anterior', 'Online book': 'Online book', @@ -334,6 +344,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'Claus de la CachĆ© en RAM', 'Ram Cleared': 'Ram Netjeda', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Receptes', 'Record': 'Registre', 'Record %(id)s created': 'Registre %(id)s creat', diff --git a/applications/welcome/languages/cs.py b/applications/welcome/languages/cs.py index 11d980db..4a86a697 100755 --- a/applications/welcome/languages/cs.py +++ b/applications/welcome/languages/cs.py @@ -11,12 +11,17 @@ '%s selected': '%s označených', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(requires internet access)': '(vyžaduje připojenĆ­ k internetu)', '(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(například "cs-cs")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', '@markmin\x01Searching: **%s** %%{file}': 'HledĆ”nĆ­: **%s** %%{soubor}', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'O programu', 'About application': 'O aplikaci', 'Access Control': 'ŘízenĆ­ přístupu', @@ -31,6 +36,7 @@ 'Administrator Password:': 'AdministrĆ”torskĆ© heslo:', 'Ajax Recipes': 'Recepty s ajaxem', 'An error occured, please %s the page': 'An error occured, please %s the page', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'and rename it:': 'a přejmenovat na:', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zanĆ” bez zabezpečenĆ©ho spojenĆ­', @@ -60,6 +66,7 @@ 'Cache': 'Cache', 'cache': 'cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Klƭče cache', 'cache, errors and sessions cleaned': 'cache, chyby a relace byly pročiÅ”těny', 'can be a git repo': 'může to být git repo', @@ -141,6 +148,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Klƭče diskovĆ© cache', 'Disk Cleared': 'Disk smazĆ”n', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'docs': 'dokumentace', 'Documentation': 'Dokumentace', "Don't know what to do?": 'NevĆ­te kudy kam?', @@ -210,6 +218,7 @@ 'Help': 'NĆ”pověda', 'Helping web2py': 'Helping web2py', 'Hide/Show Translated strings': 'Skrýt/Zobrazit přeloženĆ© texty', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Hits': 'KolikrĆ”t dosaženo', 'Home': 'DomovskĆ” strĆ”nka', 'honored only if the expression evaluates to true': 'brĆ”t v potaz jen když se tato podmĆ­nka vyhodnotĆ­ kladně', @@ -279,8 +288,8 @@ 'My Sites': 'SprĆ”va aplikacĆ­', 'Name': 'JmĆ©no', 'new application "%s" created': 'novĆ” aplikace "%s" vytvořena', -'New Application Wizard': 'Nový prÅÆvodce aplikacĆ­', 'New application wizard': 'Nový prÅÆvodce aplikacĆ­', +'New Application Wizard': 'Nový prÅÆvodce aplikacĆ­', 'New password': 'NovĆ© heslo', 'New Record': 'Nový zĆ”znam', 'new record inserted': 'nový zĆ”znam byl založen', @@ -291,6 +300,7 @@ 'No databases in this application': 'V tĆ©to aplikaci nejsou žÔdnĆ© databĆ”ze', 'No Interaction yet': 'JeÅ”tě žÔdnĆ” interakce nenastala', 'No ticket_storage.txt found under /private folder': 'Soubor ticket_storage.txt v adresÔři /private nenalezen', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Objekt či tabulka', 'Old password': 'PÅÆvodnĆ­ heslo', 'Online book': 'Online book', @@ -336,6 +346,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'Klƭče RAM Cache', 'Ram Cleared': 'RAM smazĆ”na', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Readme': 'NĆ”pověda', 'Recipes': 'Postupy jak na to', 'Record': 'ZĆ”znam', diff --git a/applications/welcome/languages/de.py b/applications/welcome/languages/de.py index 2e40b349..2aefeb38 100755 --- a/applications/welcome/languages/de.py +++ b/applications/welcome/languages/de.py @@ -215,4 +215,4 @@ 'You are successfully running web2py': 'web2py wird erfolgreich ausgeführt', 'You can modify this application and adapt it to your needs': 'Sie kƶnnen diese Anwendung verƤndern und Ihren Bedürfnissen anpassen', 'You visited the url %s': 'Sie haben die URL %s besucht', -} \ No newline at end of file +} diff --git a/applications/welcome/languages/es.py b/applications/welcome/languages/es.py index c2154767..b8c12cf1 100755 --- a/applications/welcome/languages/es.py +++ b/applications/welcome/languages/es.py @@ -63,7 +63,7 @@ 'Available Databases and Tables': 'Bases de datos y tablas disponibles', 'Back': 'AtrĆ”s', 'Buy this book': 'Compra este libro', -"Buy web2py's book": "Compra el libro de web2py", +"Buy web2py's book": 'Compra el libro de web2py', 'Cache': 'CachĆ©', 'cache': 'cachĆ©', 'Cache Cleared': 'Cache Limpiada', @@ -466,4 +466,3 @@ 'You visited the url %s': 'Usted visitó la url %s', 'Your username is: %(username)s': 'Su nombre de usuario es: %(username)s', } - diff --git a/applications/welcome/languages/fr-ca.py b/applications/welcome/languages/fr-ca.py index 310afadc..afa290bf 100755 --- a/applications/welcome/languages/fr-ca.py +++ b/applications/welcome/languages/fr-ca.py @@ -8,7 +8,12 @@ '%s selected': '%s sĆ©lectionnĆ©', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'about': 'Ć  propos', 'About': 'ƀ propos', 'Access Control': "ContrĆ“le d'accĆØs", @@ -16,6 +21,7 @@ 'Administrative Interface': 'Administrative Interface', 'Administrative interface': "Interface d'administration", 'Ajax Recipes': 'Recettes Ajax', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': "appadmin est dĆ©sactivĆ©e parce que le canal n'est pas sĆ©curisĆ©", 'Are you sure you want to delete this object?': 'Êtes-vous sĆ»r de vouloir supprimer cet objet?', 'Authentication': 'Authentification', @@ -25,6 +31,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Ne peut pas ĆŖtre vide', 'change password': 'changer le mot de passe', @@ -58,6 +65,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'fait!', @@ -83,6 +91,7 @@ 'Groups': 'Groupes', 'Hello World': 'Bonjour le monde', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Accueil', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -123,6 +132,7 @@ 'next %s rows': 'next %s rows', 'next 100 rows': '100 prochaines lignes', 'No databases in this application': "Cette application n'a pas de bases de donnĆ©es", +'Number of entries: **%s**': 'Number of entries: **%s**', 'Online book': 'Online book', 'Online examples': 'Exemples en ligne', 'or import from csv file': "ou importer d'un fichier CSV", @@ -149,6 +159,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Readme': 'Lisez-moi', 'Recipes': 'Recettes', 'Record': 'enregistrement', diff --git a/applications/welcome/languages/hi.py b/applications/welcome/languages/hi.py index 19fe70fd..5c741e1c 100755 --- a/applications/welcome/languages/hi.py +++ b/applications/welcome/languages/hi.py @@ -8,13 +8,19 @@ '%s selected': '%s ą¤šą„ą¤Øą¤¾ ą¤¹ą„ą¤†', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', 'Access Control': 'Access Control', 'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'ą¤Ŗą„ą¤°ą¤¶ą¤¾ą¤øą¤Øą¤æą¤• ą¤‡ą¤‚ą¤Ÿą¤°ą¤«ą„‡ą¤ø ą¤•ą„‡ ą¤²ą¤æą¤ यहाँ ą¤•ą„ą¤²ą¤æą¤• ą¤•ą¤°ą„‡ą¤‚', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'अप आऔमिन (appadmin) ą¤…ą¤•ą„ą¤·ą¤® ą¤¹ą„ˆ ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤…ą¤øą„ą¤°ą¤•ą„ą¤·ą¤æą¤¤ ą¤šą„ˆą¤Øą¤²', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Available Databases and Tables': 'ą¤‰ą¤Ŗą¤²ą¤¬ą„ą¤§ ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø और तालिका', @@ -23,6 +29,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'ą¤–ą¤¾ą¤²ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ सकता', 'Change Password': 'ą¤Ŗą¤¾ą¤øą¤µą¤°ą„ą¤” ą¤¬ą¤¦ą¤²ą„‡ą¤‚', @@ -54,6 +61,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'ą¤¹ą„‹ गया!', @@ -74,6 +82,7 @@ 'Hello from MyApp': 'Hello from MyApp', 'Hello World': 'Hello World', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -108,6 +117,7 @@ 'next %s rows': 'next %s rows', 'next 100 rows': 'ą¤…ą¤—ą¤²ą„‡ 100 ą¤Ŗą¤‚ą¤•ą„ą¤¤ą¤æą¤Æą¤¾ą¤', 'No databases in this application': 'इस ą¤…ą¤Øą„ą¤Ŗą„ą¤°ą¤Æą„‹ą¤— ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ ą¤”ą„‡ą¤Ÿą¤¾ą¤¬ą„‡ą¤ø ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą¤‚', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Online book': 'Online book', 'Online examples': 'ऑनलाइन उदाहरण ą¤•ą„‡ ą¤²ą¤æą¤ यहाँ ą¤•ą„ą¤²ą¤æą¤• ą¤•ą¤°ą„‡ą¤‚', 'or import from csv file': 'या csv फ़ाइल ą¤øą„‡ आयात', @@ -128,6 +138,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'Record', 'record does not exist': 'ą¤°ą¤æą¤•ą„‰ą¤°ą„ą¤” ą¤®ą„Œą¤œą„‚ą¤¦ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ', diff --git a/applications/welcome/languages/hu.py b/applications/welcome/languages/hu.py index c020c409..992140ee 100755 --- a/applications/welcome/languages/hu.py +++ b/applications/welcome/languages/hu.py @@ -8,13 +8,19 @@ '%s selected': '%s kivĆ”lasztott', '%Y-%m-%d': '%Y.%m.%d.', '%Y-%m-%d %H:%M:%S': '%Y.%m.%d. %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', 'Access Control': 'Access Control', 'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'az adminisztrĆ”ciós felületĆ©rt kattints ide', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'az appadmin a biztonsĆ”gtalan csatorna miatt letiltva', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Available Databases and Tables': 'ElĆ©rhető adatbĆ”zisok Ć©s tĆ”blĆ”k', @@ -23,6 +29,7 @@ 'cache': 'gyorsĆ­tótĆ”r', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Nem lehet üres', 'change password': 'jelszó megvĆ”ltoztatĆ”sa', @@ -55,6 +62,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'kĆ©sz!', @@ -76,6 +84,7 @@ 'Groups': 'Groups', 'Hello World': 'Hello VilĆ”g', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -112,6 +121,7 @@ 'next %s rows': 'next %s rows', 'next 100 rows': 'kƶvetkező 100 sor', 'No databases in this application': 'Nincs adatbĆ”zis ebben az alkalmazĆ”sban', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Online book': 'Online book', 'Online examples': 'online pĆ©ldĆ”kĆ©rt kattints ide', 'or import from csv file': 'vagy betƶltĆ©s csv fĆ”jlból', @@ -134,6 +144,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'bejegyzĆ©s', 'record does not exist': 'bejegyzĆ©s nem lĆ©tezik', diff --git a/applications/welcome/languages/id.py b/applications/welcome/languages/id.py index e34864ed..621d3d8a 100755 --- a/applications/welcome/languages/id.py +++ b/applications/welcome/languages/id.py @@ -16,8 +16,12 @@ '%s selected': '%s dipilih', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(requires internet access, experimental)': '(membutuhkan akses internet, eksperimental)', '(something like "it-it")': '(sesuatu seperti "it-it")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 hari yang lalu', '1 hour ago': '1 jam yang lalu', '1 minute ago': '1 menit yang lalu', @@ -27,6 +31,7 @@ '1 year ago': '1 tahun yang lalu', '< Previous': '< Sebelumnya', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'Tentang', 'About application': 'Tentang Aplikasi', 'Access Control': 'Access Control', @@ -39,6 +44,7 @@ 'Administrator Password:': 'Administrator Kata Sandi:', 'Ajax Recipes': 'Resep Ajax', 'An error occured, please %s the page': 'Terjadi kesalahan, silakan %s halaman', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'Dan', 'and rename it:': 'dan memberi nama baru itu:', 'Answer': 'Jawaban', @@ -56,6 +62,7 @@ 'Cache': 'Cache', 'cache': 'cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, kesalahan dan sesi dibersihkan', 'can be a git repo': 'bisa menjadi repo git', @@ -114,6 +121,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Dihapus', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Dokumentasi', "Don't know what to do?": 'Tidak tahu apa yang harus dilakukan?', 'done!': 'selesai!', @@ -152,6 +160,7 @@ 'Hello World': 'Halo Dunia', 'Help': 'Bantuan', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Halaman Utama', 'How did you get here?': 'Bagaimana kamu bisa di sini?', 'Image': 'Gambar', @@ -210,6 +219,7 @@ 'No ticket_storage.txt found under /private folder': 'Tidak ditemukan ticket_storage.txt dalam folder /private', 'not a Zip Code': 'bukan Kode Pos', 'Note': 'Catatan', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Old password': 'Kata sandi lama', 'Online book': 'Online book', 'Online examples': 'Contoh Online', @@ -250,6 +260,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Dihapus', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Resep', 'Record': 'Record', 'record does not exist': 'record does not exist', diff --git a/applications/welcome/languages/it.py b/applications/welcome/languages/it.py index 1f779c13..f145075e 100755 --- a/applications/welcome/languages/it.py +++ b/applications/welcome/languages/it.py @@ -11,6 +11,10 @@ '%s selected': '%s selezionato', '%Y-%m-%d': '%d/%m/%Y', '%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '<': '<', '<=': '<=', '=': '=', @@ -19,6 +23,7 @@ '?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Number of entries: **%s**': 'Numero di entitĆ : **%s**', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', 'Access Control': 'Controllo Accessi', 'Add': 'Aggiungi', @@ -27,6 +32,7 @@ 'Administrative interface': 'Interfaccia amministrativa', 'Ajax Recipes': 'Ajax Recipes', 'An error occured, please %s the page': 'ƈ stato rilevato un errore, prego %s la pagina', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'E', 'appadmin is disabled because insecure channel': 'Amministrazione (appadmin) disabilitata: comunicazione non sicura', 'Are you sure you want to delete this object?': 'Sicuro di voler cancellare questo oggetto ?', @@ -37,6 +43,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Non può essere vuoto', 'Change password': 'Cambia Password', @@ -79,6 +86,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentazione', "Don't know what to do?": 'Non sai cosa fare?', 'done!': 'fatto!', @@ -111,6 +119,7 @@ 'Hello World': 'Salve Mondo', 'Hello World in a flash!': 'Salve Mondo in un flash!', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'Come sei arrivato qui?', 'HTML': 'HTML', @@ -163,6 +172,7 @@ 'Nome': 'Nome', 'Non può essere vuoto': 'Non può essere vuoto', 'not authorized': 'non autorizzato', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Oggeto o nome tabella', 'Old password': 'Vecchia password', 'Online book': 'Online book', @@ -191,6 +201,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'Record', 'record does not exist': 'il record non esiste', diff --git a/applications/welcome/languages/my-mm.py b/applications/welcome/languages/my-mm.py index 97e39ffe..38bd3523 100755 --- a/applications/welcome/languages/my-mm.py +++ b/applications/welcome/languages/my-mm.py @@ -8,10 +8,15 @@ '%s selected': '%s ခု į€›į€½į€±į€øį€‘į€¬į€øį€žį€Šį€ŗ', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(something like "it-it")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'နကြောင်း', 'Access Control': 'į€”į€žį€Æį€¶į€øį€•į€¼į€Æ ခြင်းဆိုင်ရာ ထိန်းချုပ်ရန်', 'Additional code for your application': 'Additional code for your application', @@ -21,6 +26,7 @@ 'Administrative Interface': 'စီမံခန့်ခွဲရာ နင်တာဖေ့စ်', 'Administrator Password:': 'Administrator Password:', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'and rename it:': 'and rename it:', 'appadmin is disabled because insecure channel': 'į€…į€­į€į€ŗį€™į€į€»į€›į€žį€±į€¬ į€œį€™į€ŗį€øį€€į€¼į€±į€¬į€„į€ŗį€øį€™į€¾ į€į€„į€ŗį€›į€±į€¬į€€į€ŗį€žį€–į€¼į€„į€ŗį€· appadmin ကို į€”į€žį€Æį€¶į€øį€•į€¼į€Æį မရပါ', 'Application name:': 'Application name:', @@ -33,6 +39,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'can be a git repo': 'can be a git repo', 'Cannot be empty': 'į€”į€œį€½į€į€ŗ မဖြစ်ရပါ', @@ -87,6 +94,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk į€›į€¾į€„į€ŗį€øį€œį€„į€ŗį€øį€•į€¼į€®į€øį€•į€¼į€®', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'စာရွက်စာတမ်း နထောက်နကူများ', "Don't know what to do?": 'į€˜į€¬į€œį€Æį€•į€ŗį€›į€™į€Šį€ŗį€™į€žį€­ į€–į€¼į€…į€ŗį€”į€±į€•į€«į€žį€œį€¬į€øį‹', 'done!': 'į€œį€Æį€•į€ŗį€„į€”į€ŗį€ø ဆောင်ရွက်ပြီးပြီ!', @@ -116,6 +124,7 @@ 'Hello World': 'į€™į€„į€ŗį€¹į€‚į€œį€¬į€•į€« į€€į€™į€¹į€˜į€¬į€€į€¼į€®į€øį‹', 'Help': 'į€”į€€į€°į€”į€Šį€®', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'į€™į€°į€œį€žį€­į€Æį€·', 'How did you get here?': 'į€žį€„į€ŗ į€˜į€šį€ŗį€œį€­į€Æ į€›į€±į€¬į€€į€ŗį€œį€¬į€į€²į€·į€žį€œį€²į‹', 'import': 'į€žį€½į€„į€ŗį€øį€šį€°į€›į€”į€ŗ', @@ -165,6 +174,7 @@ 'next %s rows': 'နောက်နတန်း %s တန်း', 'No databases in this application': 'ဒီ application တွင် į€™į€Šį€ŗį€žį€Šį€ŗį€· į€’į€±į€į€¬į€˜į€±į€…į€ŗį€·į€™į€¾ မရှိပါ', 'no package selected': 'no package selected', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Object or table name', 'Online book': 'Online book', 'Online examples': 'į€”į€½į€”į€ŗį€œį€­į€Æį€„į€ŗį€ø နမူနာများ', @@ -198,6 +208,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram į€›į€¾į€„į€ŗį€øį€œį€„į€ŗį€øį€•į€¼į€®į€øį€•į€¼į€®', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'မှတ်တမ်း', 'record does not exist': 'မှတ်တမ်း မရှိပါ', diff --git a/applications/welcome/languages/my.py b/applications/welcome/languages/my.py index cf9589b6..ef29f272 100755 --- a/applications/welcome/languages/my.py +++ b/applications/welcome/languages/my.py @@ -16,8 +16,12 @@ '%s selected': '%s dipilih', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(requires internet access, experimental)': '(memerlukan akses internet, percubaan)', '(something like "it-it")': '(sesuatu seperti "it-it")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 hari yang lalu', '1 hour ago': '1 jam yang lalu', '1 minute ago': '1 minit yang lalu', @@ -27,6 +31,7 @@ '1 year ago': '1 tahun yang lalu', '< Previous': '< Sebelumnya', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'Mengenai', 'Access Control': 'Access Control', 'Add': 'Tambah', @@ -35,6 +40,7 @@ 'Administrator Password:': 'Kata laluan Administrator:', 'Ajax Recipes': 'Resipi Ajax', 'An error occured, please %s the page': 'Kesilapan telah berlaku, sila %s laman', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'Dan', 'and rename it:': 'dan menamakan itu:', 'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', @@ -47,6 +53,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, kesilapan dan sesi dibersihkan', 'Cancel': 'Batal', @@ -91,6 +98,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Dihapuskan', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Dokumentasi', "Don't know what to do?": 'Tidak tahu apa yang perlu dilakukan?', 'done!': 'selesai!', @@ -122,6 +130,7 @@ 'Hello World': 'Halo Dunia', 'Help': 'Bantuan', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Laman Utama', 'How did you get here?': 'Bagaimana kamu boleh di sini?', 'Image': 'Gambar', @@ -174,6 +183,7 @@ 'No databases in this application': 'No databases in this application', 'No ticket_storage.txt found under /private folder': 'Ticket_storage.txt tidak dijumpai di bawah folder /private', 'not a Zip Code': 'bukan Pos', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Old password': 'Kata laluan lama', 'Online book': 'Online book', 'Online examples': 'Contoh Online', @@ -210,6 +220,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Dihapuskan', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Resipi', 'Record': 'Record', 'record does not exist': 'record does not exist', diff --git a/applications/welcome/languages/nl.py b/applications/welcome/languages/nl.py index 1000587e..7ff72e46 100755 --- a/applications/welcome/languages/nl.py +++ b/applications/welcome/languages/nl.py @@ -11,7 +11,11 @@ '%s selected': '%s geselecteerd', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(zoiets als "nl-nl")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 dag geleden', '1 week ago': '1 week gelden', '<': '<', @@ -20,6 +24,7 @@ '>': '>', '>=': '>=', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'Een nieuwe versie van web2py is beschikbaar', 'A new version of web2py is available: %s': 'Een nieuwe versie van web2py is beschikbaar: %s', 'About': 'Over', @@ -38,6 +43,7 @@ 'Administrative Interface': 'Administratieve Interface', 'Administrator Password:': 'Administrator Wachtwoord', 'Ajax Recipes': 'Ajax Recepten', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'En', 'and rename it (required):': 'en hernoem deze (vereist)', 'and rename it:': 'en hernoem:', @@ -60,6 +66,7 @@ 'Cache': 'Cache', 'cache': 'cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'cache, errors en sessies geleegd', 'Cannot be empty': 'Mag niet leeg zijn', @@ -123,6 +130,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Geleegd', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentatie', "Don't know what to do?": 'Weet je niet wat je moet doen?', 'done!': 'gereed!', @@ -175,6 +183,7 @@ 'Hello World': 'Hallo Wereld', 'help': 'help', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'Hoe ben je hier gekomen?', 'htmledit': 'Bewerk HTML', @@ -244,6 +253,7 @@ 'next 100 rows': 'volgende 100 rijen', 'NO': 'NEE', 'No databases in this application': 'Geen database in deze applicatie', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Object of tabelnaam', 'Old password': 'Oude wachtwoord', 'Online book': 'Online book', @@ -278,6 +288,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Geleegd', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recepten', 'Record': 'Record', 'record does not exist': 'record bestaat niet', diff --git a/applications/welcome/languages/pl.py b/applications/welcome/languages/pl.py index 1e8cae26..f77b6adf 100755 --- a/applications/welcome/languages/pl.py +++ b/applications/welcome/languages/pl.py @@ -8,13 +8,19 @@ '%s selected': '%s wybranych', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', 'Access Control': 'Access Control', 'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'Kliknij aby przejść do panelu administracyjnego', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'administracja aplikacji wyłączona z powodu braku bezpiecznego połączenia', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Authentication': 'Uwierzytelnienie', @@ -24,6 +30,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Nie może być puste', 'Change Password': 'Zmień hasło', @@ -58,6 +65,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'zrobione!', @@ -81,6 +89,7 @@ 'Groups': 'Groups', 'Hello World': 'Witaj Świecie', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -118,6 +127,7 @@ 'next %s rows': 'next %s rows', 'next 100 rows': 'następne 100 wierszy', 'No databases in this application': 'Brak baz danych w tej aplikacji', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Online book': 'Online book', 'Online examples': 'Kliknij aby przejść do interaktywnych przykładów', 'or import from csv file': 'lub zaimportuj z pliku csv', @@ -141,6 +151,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'rekord', 'record does not exist': 'rekord nie istnieje', diff --git a/applications/welcome/languages/pt-br.py b/applications/welcome/languages/pt-br.py index ff77f771..d2e32e20 100755 --- a/applications/welcome/languages/pt-br.py +++ b/applications/welcome/languages/pt-br.py @@ -26,7 +26,7 @@ 'Are you sure you want to delete this object?': 'VocĆŖ tem certeza que quer apagar este objeto?', 'Available Databases and Tables': 'Bancos de dados e tabelas disponĆ­veis', 'Buy this book': 'Compre o livro', -"Buy web2py's book": "Compre o livro do web2buy", +"Buy web2py's book": 'Compre o livro do web2buy', 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache limpo', @@ -210,4 +210,3 @@ 'You can modify this application and adapt it to your needs': 'VocĆŖ pode modificar esta aplicação e adaptĆ”-la Ć s suas necessidades', 'You visited the url %s': 'VocĆŖ acessou a url %s', } - diff --git a/applications/welcome/languages/pt.py b/applications/welcome/languages/pt.py index f0945906..8680e65e 100755 --- a/applications/welcome/languages/pt.py +++ b/applications/welcome/languages/pt.py @@ -27,7 +27,7 @@ 'Author Reference Auth User.username': 'Author Reference Auth User.username', 'Available Databases and Tables': 'bases de dados e tabelas disponĆ­veis', 'Buy this book': 'Comprar este Livro', -"Buy web2py's book": "Comprar Livros web2py", +"Buy web2py's book": 'Comprar Livros web2py', 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Limpo', @@ -220,4 +220,3 @@ 'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', 'You visited the url %s': 'You visited the url %s', } - diff --git a/applications/welcome/languages/ro.py b/applications/welcome/languages/ro.py index 8be157eb..059a19be 100755 --- a/applications/welcome/languages/ro.py +++ b/applications/welcome/languages/ro.py @@ -12,7 +12,11 @@ '%s selected': '%s selectat(e)', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(ceva ce seamănă cu "it-it")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 day ago', '1 week ago': '1 week ago', '<': '<', @@ -21,6 +25,7 @@ '>': '>', '>=': '>=', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'O nouă versiune de web2py este disponibilă', 'A new version of web2py is available: %s': 'O nouă versiune de web2py este disponibilă: %s', 'About': 'Despre', @@ -39,6 +44,7 @@ 'Administrative Interface': 'Interfață administrare', 'Administrator Password:': 'Parolă administrator:', 'Ajax Recipes': 'Rețete Ajax', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'And': 'Și', 'and rename it (required):': 'și renumiți (obligatoriu):', 'and rename it:': ' și renumiți:', @@ -62,6 +68,7 @@ 'Cache': 'Cache', 'cache': 'cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Chei cache', 'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', 'Cannot be empty': 'Nu poate fi vid', @@ -123,6 +130,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Chei cache de disc', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentație', "Don't know what to do?": 'Nu știți ce să faceți?', 'done!': 'gata!', @@ -175,6 +183,7 @@ 'Hello World': 'Salutare lume', 'help': 'ajutor', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Acasă', 'How did you get here?': 'Cum ați ajuns aici?', 'htmledit': 'editare html', @@ -241,6 +250,7 @@ 'next 100 rows': 'următoarele 100 de linii', 'NO': 'NU', 'No databases in this application': 'Aplicație fără bază de date', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Obiect sau nume de tabel', 'Old password': 'Parola veche', 'Online book': 'Online book', @@ -275,6 +285,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'Chei cache RAM', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Rețete', 'Record': 'Ć®nregistrare', 'record does not exist': 'Ć®nregistrare inexistentă', diff --git a/applications/welcome/languages/ru.py b/applications/welcome/languages/ru.py index a3f38240..0f9e09db 100755 --- a/applications/welcome/languages/ru.py +++ b/applications/welcome/languages/ru.py @@ -15,6 +15,10 @@ '%s selected': '%%{!выбрана[0]} %s %%{запись[0]}', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 Гень Ń‚Š¾Š¼Ńƒ', '1 hour ago': '1 час Ń‚Š¾Š¼Ńƒ', '1 minute ago': '1 Š¼ŠøŠ½ŃƒŃ‚Ńƒ Ń‚Š¾Š¼Ńƒ', @@ -23,12 +27,14 @@ '1 week ago': '1 Š½ŠµŠ“ŠµŠ»ŃŽ Ń‚Š¾Š¼Ńƒ', '1 year ago': '1 гоГ Ń‚Š¾Š¼Ńƒ', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'About', 'Access Control': 'Access Control', 'admin': 'admin', 'Administrative Interface': 'Administrative Interface', 'Administrative interface': 'аГминистративный интерфейс', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'Are you sure you want to delete this object?': 'Š’Ń‹ ŃƒŠ²ŠµŃ€ŠµŠ½Ń‹, что хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ ŃŃ‚Š¾Ń‚ Š¾Š±ŃŠŠµŠŗŃ‚?', 'Available Databases and Tables': 'Базы Ганных Šø таблицы', @@ -37,6 +43,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'ŠŸŃƒŃŃ‚Š¾Šµ значение Š½ŠµŠ“Š¾ŠæŃƒŃŃ‚ŠøŠ¼Š¾', 'Change Password': 'Дмените ŠæŠ°Ń€Š¾Š»ŃŒ', @@ -70,6 +77,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': 'готово!', @@ -90,6 +98,7 @@ 'Groups': 'Groups', 'Hello World': 'Заработало!', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -132,6 +141,7 @@ 'next 100 rows': 'ŃŠ»ŠµŠ“ŃƒŃŽŃ‰ŠøŠµ 100 строк', 'No databases in this application': 'Š’ приложении нет баз Ганных', 'now': 'сейчас', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Object or table name', 'Old password': 'Дтарый ŠæŠ°Ń€Š¾Š»ŃŒ', 'Online book': 'Online book', @@ -159,6 +169,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'Record', 'record does not exist': 'запись не найГена', diff --git a/applications/welcome/languages/sk.py b/applications/welcome/languages/sk.py index 0cf6f411..01d31069 100755 --- a/applications/welcome/languages/sk.py +++ b/applications/welcome/languages/sk.py @@ -8,13 +8,19 @@ '%s selected': '%s označených', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'O nĆ”s', 'Access Control': 'Kontrola prĆ­stupu', 'admin': 'admin', 'Administrative Interface': 'AdministrĆ”torskĆ© rozhranie', 'Administrative interface': 'pre administrĆ”torskĆ© rozhranie kliknite sem', 'Ajax Recipes': 'Ajax recepty', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zaný bez zabezpečenĆ©ho spojenia', 'Are you sure you want to delete this object?': 'Ste si istý, že chcete vymazaÅ„ tento objekt?', 'Available Databases and Tables': 'DostupnĆ© databĆ”zy a tabuľky', @@ -23,6 +29,7 @@ 'cache': 'cache', 'Cache': 'Cache', 'Cache Cleared': 'Cache vymazanĆ©', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache kÄ¾ĆŗÄe', 'Cannot be empty': 'NemÓže byÅ„ prĆ”zdne', 'Check to delete': 'OznačiÅ„ na zmazanie', @@ -53,6 +60,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk vyčistený', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'DokumentĆ”cia', "Don't know what to do?": 'Neviete, čo robiÅ„?', 'done!': 'hotovo!', @@ -72,6 +80,7 @@ 'Groups': 'Skupiny', 'Hello World': 'Ahoj svet', 'Helping web2py': 'PomĆ”haÅ„ web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Domov', 'How did you get here?': 'Ako ste sa sem dostali?', 'import': 'import', @@ -111,6 +120,7 @@ 'next %s rows': 'next %s rows', 'next 100 rows': 'ďalŔích 100 riadkov', 'No databases in this application': 'V tejto aplikĆ”cii nie sĆŗ databĆ”zy', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Old password': 'StarĆ© heslo', 'Online book': 'Online kniha', 'Online examples': 'online prĆ­klady', @@ -135,6 +145,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM cache kÄ¾ĆŗÄe', 'Ram Cleared': 'Ram vyčistenĆ”', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recepty', 'Record': 'ZĆ”znam', 'record does not exist': 'zĆ”znam neexistuje', diff --git a/applications/welcome/languages/tr.py b/applications/welcome/languages/tr.py index 23d0b87c..63fb71af 100755 --- a/applications/welcome/languages/tr.py +++ b/applications/welcome/languages/tr.py @@ -13,15 +13,21 @@ '%s selected': '%s selected', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', '@markmin\x01**Hello World**': '**Merhaba Dünya**', '@markmin\x01An error occured, please [[reload %s]] the page': 'Bir hata oluştu, lütfen sayfayı [[yenileyin yükleyin %s]] ', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'Hakkında', 'Access Control': 'Erişim Denetimi', 'admin': 'admin', 'Administrative Interface': 'Yƶnetim Arayüzü', 'Ajax Recipes': 'Ajax Tarifleri', 'An error occured, please %s the page': 'Bir hata meydana geldi, lütfen sayfayı %s', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', 'Apply changes': 'Değişiklikleri uygula', 'Are you sure you want to delete this object?': 'Bu nesneyi silmek istediğinden emin misin?', @@ -31,6 +37,7 @@ 'cache': 'zula', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'Boş bırakılamaz', 'Change password': 'Parolayı değiştir', @@ -65,6 +72,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Kitap', "Don't know what to do?": 'Neleri nasıl yapacağını bilmiyor musun?', 'done!': 'done!', @@ -93,6 +101,7 @@ 'Hello World ## comment': 'Merhaba Dünya ## yorum ', 'Hello World## comment': 'Merhaba Dünya## yorum ', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Anasayfa', 'How did you get here?': 'Bu sayfayı gƶrüntüleme uğruna neler mi oldu?', 'import': 'import', @@ -132,6 +141,7 @@ 'new record inserted': 'new record inserted', 'next %s rows': 'next %s rows', 'No databases in this application': 'No databases in this application', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Nesne ya da tablo adı', 'Old password': 'Eski parola', 'Online book': 'Online book', @@ -158,6 +168,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Tarifeler', 'Record': 'Record', 'record does not exist': 'record does not exist', diff --git a/applications/welcome/languages/uk.py b/applications/welcome/languages/uk.py index b40fd1dd..eb2678eb 100755 --- a/applications/welcome/languages/uk.py +++ b/applications/welcome/languages/uk.py @@ -15,6 +15,10 @@ '%s selected': 'Вибрано %s %%{запис}', '%Y-%m-%d': '%Y/%m/%d', '%Y-%m-%d %H:%M:%S': '%Y/%m/%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '1 day ago': '1 Гень Ń‚Š¾Š¼Ńƒ', '1 hour ago': '1 гоГину Ń‚Š¾Š¼Ńƒ', '1 minute ago': '1 Ń…Š²ŠøŠ»ŠøŠ½Ńƒ Ń‚Š¾Š¼Ńƒ', @@ -32,11 +36,13 @@ '@markmin\x01Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'ŠžŃ†Ń–Š½ŠŗŠ° ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½Š½Ń: **%(ratio)s%%** (**%(hits)s** %%{ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½Š½Ń(hits)} та **%(misses)s** %%{ŃŃ…ŠøŠ±Š½ŠµŠ½Š½Ń(misses)})', '@markmin\x01Number of entries: **%s**': 'ŠšŃ–Š»ŃŒŠŗŃ–ŃŃ‚ŃŒ Š²Ń…Š¾Š“Š¶ŠµŠ½ŃŒ: ``**%s**``:red', '@markmin\x01RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': "Час Š¶ŠøŃ‚Ń‚Ń об'єктів в ŠžŠ—ŠŸ-ŠšŠ•ŠØŠ† ŃŃŠ³Š°Ń” **%(hours)02d** %%{гоГину(hours)} **%(min)02d** %%{Ń…Š²ŠøŠ»ŠøŠ½Ńƒ(min)} та **%(sec)02d** %%{секунГу(sec)}.", +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'About': 'ŠŸŃ€Š¾ ГоГаток', 'Access Control': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»ŃŒ Š“Š¾ŃŃ‚ŃƒŠæŃƒ', 'admin': 'admin', 'Administrative Interface': 'АГміністративний інтерфейс', 'Ajax Recipes': 'Рецепти Š“Š»Ń Ajax', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ незахищенний канал (HTTP). Appadmin вимкнено', 'Are you sure you want to delete this object?': "Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø цей об'єкт?", 'Available Databases and Tables': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Ń– бази Ганих та таблиці', @@ -45,6 +51,7 @@ 'cache': 'кеш', 'Cache': 'Кеш', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– кешу', 'Cannot be empty': 'ŠŸŠ¾Ń€Š¾Š¶Š½Ń” Š·Š½Š°Ń‡ŠµŠ½Š½Ń Š½ŠµŠæŃ€ŠøŠæŃƒŃŃ‚ŠøŠ¼Šµ', 'Change password': 'Змінити ŠæŠ°Ń€Š¾Š»ŃŒ', @@ -80,6 +87,7 @@ 'DISK': 'Š”Š˜Š”Šš', 'Disk Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– Гискового кешу', 'Disk Cleared': 'Дисковий кеш очищено', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Š”Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†Ń–Ń', "Don't know what to do?": 'ŠŠµ знаєте що робити Галі?', 'done!': 'зроблено!', @@ -107,6 +115,7 @@ 'Groups': 'Š“Ń€ŃƒŠæŠø', 'Hello World': 'ŠŸŃ€ŠøŠ²Ń–Ń‚, світ!', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'ŠŸŠ¾Ń‡Š°Ń‚Š¾Šŗ', 'How did you get here?': 'ŠÆŠŗ Ń†ŃŒŠ¾Š³Š¾ було Š“Š¾ŃŃŠ³Š½ŃƒŃ‚Š¾?', 'import': 'Š†Š¼ŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø', @@ -150,6 +159,7 @@ 'next 100 rows': 'Š½Š°ŃŃ‚ŃƒŠæŠ½Ń– 100 Ń€ŃŠ“ŠŗŃ–Š²', 'No databases in this application': 'Даний ГоГаток не Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” базу Ганих', 'now': 'зараз', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': "ŠžŠ±'єкт або назва таблиці", 'Old password': 'Дтарий ŠæŠ°Ń€Š¾Š»ŃŒ', 'Online book': 'Online book', @@ -181,6 +191,7 @@ 'RAM': "ŠžŠŸŠ•Š ŠŠ¢Š˜Š’ŠŠ ПАМ'ЯТЬ (ŠžŠ—ŠŸ)", 'RAM Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– ŠžŠ—ŠŸ-кешу', 'Ram Cleared': 'ŠžŠ—ŠŸ-кеш очищено', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Рецепти', 'Record': 'запис', 'Record %(id)s updated': 'Запис %(id)s змінено', diff --git a/applications/welcome/languages/zh-cn.py b/applications/welcome/languages/zh-cn.py index 888040c5..916384d5 100755 --- a/applications/welcome/languages/zh-cn.py +++ b/applications/welcome/languages/zh-cn.py @@ -8,8 +8,13 @@ '%s selected': '%s 已选择', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(ę ¼å¼ē±»ä¼¼ "zh-tw")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'ę–°ē‰ˆ web2py å·²ęŽØå‡ŗ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆ web2py å·²ęŽØå‡ŗ: %s', 'about': 'å…³äŗŽ', @@ -24,6 +29,7 @@ 'Administrator Password:': 'ē®”ē†å‘˜åÆ†ē :', 'Ajax Recipes': 'Ajax Recipes', 'An error occured, please %s the page': 'An error occured, please %s the page', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'ē®”ē†ē•Œé¢åœØéžå®‰å…Øé€šé“äø‹č¢«ē¦ē”Ø', 'Are you sure you want to delete file "%s"?': 'ē”®å®šč¦åˆ é™¤ę–‡ä»¶"%s"?', 'Are you sure you want to delete this object?': 'ē”®å®šč¦åˆ é™¤čÆ„åÆ¹č±”ä¹ˆļ¼Ÿ', @@ -39,6 +45,7 @@ 'cache': 'é«˜é€Ÿē¼“å­˜', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '编译失蓄:åŗ”ē”ØēØ‹åŗęœ‰é”™čÆÆļ¼ŒčÆ·ęŽ’é™¤é”™čÆÆåŽå†å°čÆ•ē¼–čÆ‘.', @@ -83,6 +90,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': '完成!', @@ -113,6 +121,7 @@ 'Groups': 'Groups', 'Hello World': 'Hello World', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -164,6 +173,7 @@ 'next 100 rows': '往后 100 笔', 'NO': '否', 'No databases in this application': 'čÆ„åŗ”ē”ØēØ‹åŗäøå«ę•°ę®åŗ“', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Object or table name', 'Online book': 'Online book', 'Online examples': 'ē‚¹å‡»čæ›å…„åœØēŗæä¾‹å­', @@ -190,6 +200,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': '记录', 'record does not exist': 'č®°å½•äøå­˜åœØ', diff --git a/applications/welcome/languages/zh-tw.py b/applications/welcome/languages/zh-tw.py index 33e94daf..2b64c75d 100755 --- a/applications/welcome/languages/zh-tw.py +++ b/applications/welcome/languages/zh-tw.py @@ -8,8 +8,13 @@ '%s selected': '%s 已選擇', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(ę ¼å¼é”žä¼¼ "zh-tw")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ: %s', 'about': 'é—œę–¼', @@ -24,6 +29,7 @@ 'Administrator Password:': '箔理哔密碼:', 'Ajax Recipes': 'Ajax Recipes', 'An error occured, please %s the page': 'An error occured, please %s the page', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'å› ē‚ŗä¾†č‡Ŗéžå®‰å…Øé€šé“,ē®”ē†ä»‹é¢é—œé–‰', 'Are you sure you want to delete file "%s"?': 'ē¢ŗå®šč¦åˆŖé™¤ęŖ”ę”ˆ"%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', @@ -39,6 +45,7 @@ 'cache': 'åæ«å–čØ˜ę†¶é«”', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '焔法編譯:ę‡‰ē”ØēØ‹å¼äø­å«ęœ‰éŒÆčŖ¤ļ¼Œč«‹é™¤éŒÆå¾Œå†č©¦äø€ę¬”.', @@ -83,6 +90,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': '完成!', @@ -113,6 +121,7 @@ 'Groups': 'Groups', 'Hello World': 'å—Ø! äø–ē•Œ', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -164,6 +173,7 @@ 'next 100 rows': '往後 100 ē­†', 'NO': '否', 'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Object or table name': 'Object or table name', 'Online book': 'Online book', 'Online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', @@ -190,6 +200,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'ē“€éŒ„', 'record does not exist': 'ē“€éŒ„äøå­˜åœØ', diff --git a/applications/welcome/languages/zh.py b/applications/welcome/languages/zh.py index e92241a2..97940322 100755 --- a/applications/welcome/languages/zh.py +++ b/applications/welcome/languages/zh.py @@ -8,8 +8,13 @@ '%s selected': '%s 已選擇', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', '(something like "it-it")': '(ę ¼å¼é”žä¼¼ "zh-tw")', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', '?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', 'A new version of web2py is available': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ: %s', 'about': 'é—œę–¼', @@ -23,6 +28,7 @@ 'Administrative interface': 'é»žę­¤č™•é€²å…„ē®”ē†ä»‹é¢', 'Administrator Password:': '箔理哔密碼:', 'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', 'appadmin is disabled because insecure channel': 'å› ē‚ŗä¾†č‡Ŗéžå®‰å…Øé€šé“,ē®”ē†ä»‹é¢é—œé–‰', 'Are you sure you want to delete file "%s"?': 'ē¢ŗå®šč¦åˆŖé™¤ęŖ”ę”ˆ"%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', @@ -38,6 +44,7 @@ 'cache': 'åæ«å–čØ˜ę†¶é«”', 'Cache': 'Cache', 'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Cache Keys': 'Cache Keys', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '焔法編譯:ę‡‰ē”ØēØ‹å¼äø­å«ęœ‰éŒÆčŖ¤ļ¼Œč«‹é™¤éŒÆå¾Œå†č©¦äø€ę¬”.', @@ -80,6 +87,7 @@ 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Documentation': 'Documentation', "Don't know what to do?": "Don't know what to do?", 'done!': '完成!', @@ -108,6 +116,7 @@ 'Groups': 'Groups', 'Hello World': 'å—Ø! äø–ē•Œ', 'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', 'Home': 'Home', 'How did you get here?': 'How did you get here?', 'import': 'import', @@ -155,6 +164,7 @@ 'next 100 rows': '往後 100 ē­†', 'NO': '否', 'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', +'Number of entries: **%s**': 'Number of entries: **%s**', 'Online book': 'Online book', 'Online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', 'or import from csv file': 'ęˆ–ę˜Æå¾žé€—č™Ÿåˆ†éš”ęŖ”(CSV)åŒÆå…„', @@ -180,6 +190,7 @@ 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', 'Recipes': 'Recipes', 'Record': 'ē“€éŒ„', 'record does not exist': 'ē“€éŒ„äøå­˜åœØ', From f9dce481f79e97fa8b64531588a049c9dcfeb624 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 01:09:39 -0700 Subject: [PATCH 093/436] Added encoding to plural files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added ā€œ# -*- coding: utf-8 -*-ā€œ lines to all plural files --- applications/admin/languages/plural-en.py | 1 + applications/admin/languages/plural-ru.py | 1 + applications/admin/languages/plural-uk.py | 1 + applications/welcome/languages/plural-cs.py | 1 + applications/welcome/languages/plural-en.py | 1 + applications/welcome/languages/plural-es.py | 3 ++- applications/welcome/languages/plural-ru.py | 1 + applications/welcome/languages/plural-uk.py | 1 + 8 files changed, 9 insertions(+), 1 deletion(-) diff --git a/applications/admin/languages/plural-en.py b/applications/admin/languages/plural-en.py index 638e23e3..3c711a31 100755 --- a/applications/admin/languages/plural-en.py +++ b/applications/admin/languages/plural-en.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'file': ['files'], diff --git a/applications/admin/languages/plural-ru.py b/applications/admin/languages/plural-ru.py index 4ccd46ad..0900bc91 100755 --- a/applications/admin/languages/plural-ru.py +++ b/applications/admin/languages/plural-ru.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'файл': ['файла','файлов'], diff --git a/applications/admin/languages/plural-uk.py b/applications/admin/languages/plural-uk.py index e2d7a0fb..cbb014ac 100755 --- a/applications/admin/languages/plural-uk.py +++ b/applications/admin/languages/plural-uk.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'останній': ['останні','останніх'], diff --git a/applications/welcome/languages/plural-cs.py b/applications/welcome/languages/plural-cs.py index a58226bf..2a795587 100755 --- a/applications/welcome/languages/plural-cs.py +++ b/applications/welcome/languages/plural-cs.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'vteřina': ['vteřiny', 'vteřin'], diff --git a/applications/welcome/languages/plural-en.py b/applications/welcome/languages/plural-en.py index fecf98ea..5103b462 100755 --- a/applications/welcome/languages/plural-en.py +++ b/applications/welcome/languages/plural-en.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'account': ['accounts'], diff --git a/applications/welcome/languages/plural-es.py b/applications/welcome/languages/plural-es.py index 5e3c9e16..235f1a3a 100755 --- a/applications/welcome/languages/plural-es.py +++ b/applications/welcome/languages/plural-es.py @@ -1,4 +1,5 @@ -# coding: utf-8 +#!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'fila': ['filas'], diff --git a/applications/welcome/languages/plural-ru.py b/applications/welcome/languages/plural-ru.py index 61499183..d9e73a07 100755 --- a/applications/welcome/languages/plural-ru.py +++ b/applications/welcome/languages/plural-ru.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'выбрана': ['выбраны','выбрано'], diff --git a/applications/welcome/languages/plural-uk.py b/applications/welcome/languages/plural-uk.py index c07ebda4..e75ef121 100755 --- a/applications/welcome/languages/plural-uk.py +++ b/applications/welcome/languages/plural-uk.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- { # "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], 'байт': ['байти','байтів'], From 2140eced7950fbd41c4dca906550ea3cfb9b50a2 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 01:26:41 -0700 Subject: [PATCH 094/436] Fixing a consistency error when updating T.M() translations --- gluon/languages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index 98fa4790..afc5b332 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -962,7 +962,7 @@ def findT(path, language=DEFAULT_LANGUAGE): + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): data = to_native(read_locked(filename)) items = regex_translate.findall(data) - items += regex_translate_m.findall(data) + items += ["@markmin\x01%s" %x for x in regex_translate_m.findall(data)] for item in items: try: message = safe_eval(item) @@ -976,7 +976,7 @@ def findT(path, language=DEFAULT_LANGUAGE): if len(tokens) == 2: message = tokens[0].strip() + '##' + tokens[1].strip() if message and not message in sentences: - sentences[message] = message + sentences[message] = message.replace("@markmin\x01", "") if not '!langcode!' in sentences: sentences['!langcode!'] = ( DEFAULT_LANGUAGE if language in ('default', DEFAULT_LANGUAGE) else language) From aee0f8dbc578bd80fa3a01af09e47b0779266ea8 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 9 Sep 2016 01:59:14 -0700 Subject: [PATCH 095/436] Stripped unused strings from translations --- applications/admin/languages/af.py | 553 +++++++++- applications/admin/languages/bg.py | 630 ++++++++--- applications/admin/languages/cs.py | 242 ++++- applications/admin/languages/de.py | 290 ++++- applications/admin/languages/default.py | 184 ---- applications/admin/languages/en.py | 574 ++++++++++ applications/admin/languages/es.py | 13 +- applications/admin/languages/fr.py | 8 +- applications/admin/languages/he.py | 625 ++++++++--- applications/admin/languages/it.py | 295 ++++- applications/admin/languages/ja.py | 386 ++++++- applications/admin/languages/my-mm.py | 394 ++++++- applications/admin/languages/nl.py | 176 ++- applications/admin/languages/pl.py | 625 ++++++++--- applications/admin/languages/pt-br.py | 324 +++++- applications/admin/languages/pt.py | 1078 ++++++++++++------- applications/admin/languages/ro.py | 861 ++++++++++----- applications/admin/languages/ru.py | 342 +++++- applications/admin/languages/sl.py | 735 +++++++++---- applications/admin/languages/sr-cr.py | 391 ++++++- applications/admin/languages/sr-lt.py | 391 ++++++- applications/admin/languages/tr.py | 232 +++- applications/admin/languages/uk.py | 141 ++- applications/admin/languages/zh-tw.py | 690 +++++++++--- applications/admin/languages/zh.py | 669 +++++++++--- applications/welcome/languages/.DS_Store | Bin 0 -> 6148 bytes applications/welcome/languages/ca.py | 2 +- applications/welcome/languages/cs.py | 2 +- applications/welcome/languages/default.py | 122 --- applications/welcome/languages/en.py | 157 +++ applications/welcome/languages/plural-cs.py | 40 +- applications/welcome/languages/plural-en.py | 24 +- applications/welcome/languages/plural-es.py | 10 +- 33 files changed, 9192 insertions(+), 2014 deletions(-) delete mode 100755 applications/admin/languages/default.py create mode 100755 applications/admin/languages/en.py create mode 100644 applications/welcome/languages/.DS_Store delete mode 100755 applications/welcome/languages/default.py create mode 100755 applications/welcome/languages/en.py diff --git a/applications/admin/languages/af.py b/applications/admin/languages/af.py index d1a83621..6351ae09 100755 --- a/applications/admin/languages/af.py +++ b/applications/admin/languages/af.py @@ -2,88 +2,581 @@ { '!langcode!': 'af', '!langname!': 'Afrikaanse', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s rows deleted', '%s %%{row} updated': '%s rows updated', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access)': '(vereis internet toegang)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(iets soos "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Soek: **%s** lĆŖre', +'Abort': 'Abort', 'About': 'oor', 'About application': 'Oor program', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'Additionele kode vir u application', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin disabled because no admin password', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'admin disabled because unable to access password file', +'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', 'Admin language': 'Admin taal', +'Admin versioning page': 'Admin versioning page', +'administrative interface': 'administrative interface', +'Administrator Password:': 'Administrator Password:', +'and rename it:': 'en verander die naam:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', +'Application': 'Application', +'application "%s" uninstalled': 'application "%s" uninstalled', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'application compiled', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'application is compiled and cannot be designed', 'Application name:': 'Program naam:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', +'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', +'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', +'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', +'Are you sure?': 'Are you sure?', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Autocomplete Python Code': 'Autocomplete Python Code', +'Available Databases and Tables': 'Available Databases and Tables', +'back': 'back', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'cache, errors and sessions cleaned', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', +'Cannot be empty': 'Cannot be empty', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'cannot create file', +'cannot upload file "%(filename)s"': 'cannot upload file "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'verander admin wagwoord', +'change editor settings': 'change editor settings', +'Changelog': 'Changelog', +'check all': 'check all', 'Check for upgrades': 'soek vir upgrades', +'Check to delete': 'Check to delete', +'Checking for upgrades...': 'Checking for upgrades...', 'Clean': 'maak skoon', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'collapse/expand all', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'kompileer', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'compiled application removed', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'Beheerders', +'controllers': 'beheerders', +'Count': 'Count', 'Create': 'skep', +'create file with filename:': 'skep lĆŖer met naam:', +'Create/Upload': 'Create/Upload', +'created by': 'geskep deur', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'crontab', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', +'currently running': 'loop tans', +'currently saved or': 'currently saved or', +'data uploaded': 'data uploaded', +'Database': 'Database', +'Database %s select': 'Database %s select', +'Database administration': 'Database administration', +'database administration': 'database administration', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Date and Time', +'db': 'db', +'Debug': 'Debug', +'defines tables': 'defines tables', +'Delete': 'Delete', +'delete': 'delete', +'delete all checked': 'delete all checked', +'delete plugin': 'delete plugin', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', +'Delete:': 'Delete:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'deploy', 'Deploy on Google App Engine': 'Stuur na Google App Engine toe', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', +'design': 'design', +'Detailed traceback description': 'Detailed traceback description', +'details': 'details', +'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', +'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', +'docs': 'docs', +'Docs': 'Docs', +'done!': 'done!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', +'download layouts': 'aflaai layouts', +'Download layouts from repository': 'Download layouts from repository', +'download plugins': 'aflaai plugins', +'Download plugins from repository': 'Download plugins from repository', 'Edit': 'wysig', +'edit all': 'edit all', 'Edit application': 'Wysig program', +'edit controller:': 'edit controller:', +'Edit current record': 'Edit current record', +'edit views:': 'edit views:', +'Editing %s': 'Editing %s', +'Editing Language file': 'Editing Language file', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', +'Error': 'Error', +'Error logs for "%(app)s"': 'Error logs for "%(app)s"', +'Error snapshot': 'Error snapshot', +'Error ticket': 'Error ticket', 'Errors': 'foute', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', +'export as csv file': 'export as csv file', +'Exports:': 'Exports:', +'exposes': 'exposes', +'exposes:': 'exposes:', +'extends': 'extends', +'failed to compile file because:': 'failed to compile file because:', +'failed to reload module because:': 'failed to reload module because:', +'File': 'File', +'file "%(filename)s" created': 'file "%(filename)s" created', +'file "%(filename)s" deleted': 'file "%(filename)s" deleted', +'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', +'file "%s" of %s restored': 'file "%s" of %s restored', +'file changed on disk': 'file changed on disk', +'file does not exist': 'file does not exist', +'file not found': 'file not found', +'file saved on %(time)s': 'file saved on %(time)s', +'file saved on %s': 'file saved on %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', +'filter': 'filter', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'Form has errors': 'Form has errors', +'Frames': 'Frames', +'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Help': 'hulp', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', +'includes': 'includes', +'Indent with tabs': 'Indent with tabs', +'inspect attributes': 'inspect attributes', 'Install': 'installeer', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Geinstalleerde apps', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', +'internal error': 'internal error', +'internal error: %s': 'internal error: %s', +'Internal State': 'Internal State', +'Invalid action': 'Invalid action', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid git repository specified.': 'Invalid git repository specified.', +'invalid password': 'invalid password', +'invalid password.': 'invalid password.', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', +'invalid ticket': 'invalid ticket', +'Key': 'Key', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', +'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', +'Language files (static strings) updated': 'Language files (static strings) updated', +'languages': 'tale', 'Languages': 'Tale', +'Last Revision': 'Last Revision', +'Last saved on:': 'Last saved on:', 'License for': 'Lisensie vir', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', +'loading...': 'laai...', +'Local Apps': 'Local Apps', +'locals': 'locals', +'Locals##debug': 'Locals##debug', +'Login': 'Login', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'Login to the Administrative Interface', +'Login/Register': 'Login/Register', 'Logout': 'logout', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', +'merge': 'merge', 'Models': 'Modelle', +'models': 'modelle', +'Modified On': 'Modified On', 'Modules': 'Modules', +'modules': 'modules', +'Multi User Mode': 'Multi User Mode', +'new application "%s" created': 'new application "%s" created', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', 'New application wizard': 'Nuwe app wizard', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'New Record', +'new record inserted': 'new record inserted', 'New simple application': 'Nuwe eenvoudige app', +'next': 'next', +'next %s rows': 'next %s rows', +'NO': 'NO', +'no changes': 'no changes', +'No databases in this application': 'No databases in this application', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', +'or import from csv file': 'or import from csv file', +'Original/Translation': 'Original/Translation', +'Overview': 'Overview', 'Overwrite installed app': 'skryf oor geinstalleerde program', 'Pack all': 'pack alles', +'Pack compiled': 'Pack compiled', +'Pack custom': 'Pack custom', +'pack plugin': 'pack plugin', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': 'Peeking at file', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', +'plugins': 'plugins', 'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Aangedryf deur', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'Query:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', +'Reload routes': 'Reload routes', +'Remove compiled': 'Remove compiled', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', +'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': 'Resolve Conflict file', +'response': 'response', +'restart': 'restart', +'restore': 'restore', +'return': 'return', +'Revert': 'Revert', +'revert': 'revert', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'Running on %s', +'Save': 'Save', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Save via Ajax', +'Saved file hash:': 'Saved file hash:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', +'session': 'session', +'session expired': 'session expired', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', +'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'site', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', +'some files could not be removed': 'some files could not be removed', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', 'Start wizard': 'start wizard', +'state': 'state', +'Static': 'Static', +'static': 'static', 'Static files': 'Static files', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', +'submit': 'submit', +'Submit': 'Submit', +'successful': 'successful', 'Sure you want to delete this object?': 'Is jy seker jy will hierde object verwyder?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', +'Table': 'Table', +'Temporary': 'Temporary', +'test': 'toets', +'Testing application': 'Testing application', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', +'Theme': 'Theme', +'There are no controllers': 'There are no controllers', +'There are no models': 'There are no models', +'There are no modules': 'There are no modules', 'There are no plugins': 'Daar is geen plugins', +'There are no private files': 'There are no private files', +'There are no static files': 'There are no static files', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'There are no translators, only default language is supported', +'There are no views': 'There are no views', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'Hierdie lĆŖre is sonder veranderinge geserved, jou images gaan hier', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Ticket', +'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'to previous version.': 'to previous version.', 'To create a plugin, name a file/folder plugin_[name]': 'Om n plugin te skep, noem n lĆŖer/gids plugin_[name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Traceback', 'Translation strings for the application': 'Vertaling woorde vir die program', +'try something like': 'try something like', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Unable to check for upgrades', +'unable to create application "%s"': 'unable to create application "%s"', +'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', +'unable to parse csv file': 'unable to parse csv file', +'unable to uninstall "%s"': 'unable to uninstall "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', +'uncheck all': 'uncheck all', 'Uninstall': 'verwyder', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', +'update': 'update', +'update all languages': 'update all languages', +'Update:': 'Update:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', +'upload': 'oplaai', +'Upload': 'Upload', 'Upload & install packed application': 'Oplaai & install gepakte program', 'Upload a package:': 'Oplaai n package:', -'Use an url:': 'Gebruik n url:', -'Views': 'Views', -'administrative interface': 'administrative interface', -'and rename it:': 'en verander die naam:', -'collapse/expand all': 'collapse/expand all', -'controllers': 'beheerders', -'create file with filename:': 'skep lĆŖer met naam:', -'created by': 'geskep deur', -'crontab': 'crontab', -'currently running': 'loop tans', -'database administration': 'database administration', -'direction: ltr': 'direction: ltr', -'download layouts': 'aflaai layouts', -'download plugins': 'aflaai plugins', -'exposes': 'exposes', -'extends': 'extends', -'filter': 'filter', -'includes': 'includes', -'languages': 'tale', -'loading...': 'laai...', -'models': 'modelle', -'modules': 'modules', -'plugins': 'plugins', -'shell': 'shell', -'static': 'static', -'test': 'toets', -'update all languages': 'update all languages', -'upload': 'oplaai', +'Upload and install packed application': 'Upload and install packed application', 'upload file:': 'oplaai lĆŖer:', 'upload plugin file:': 'upload plugin lĆŖer:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'Use an url:': 'Gebruik n url:', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', +'variables': 'variables', +'Version': 'Version', +'Versioning': 'Versioning', 'versioning': 'versioning', +'Views': 'Views', 'views': 'views', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', +'web2py is up to date': 'web2py is up to date', +'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'web2py Onlangse Tweets', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': 'YES', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/bg.py b/applications/admin/languages/bg.py index c8d02f87..c841bec6 100755 --- a/applications/admin/languages/bg.py +++ b/applications/admin/languages/bg.py @@ -3,203 +3,237 @@ '!langcode!': 'bg', '!langname!': 'Š‘ŃŠŠ»Š³Š°Ń€ŃŠŗŠø', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s записите Š±ŃŃ…а изтрити', '%s %%{row} updated': '%s записите Š±ŃŃ…а обновени', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access)': '(requires internet access)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(something like "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', 'A new version of web2py is available': 'A new version of web2py is available', 'A new version of web2py is available: %s': 'A new version of web2py is available: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', -'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Abort': 'Abort', 'About': 'about', 'About application': 'About application', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', +'additional code for your application': 'additional code for your application', 'Additional code for your application': 'Additional code for your application', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin disabled because no admin password', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google apps engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'admin disabled because unable to access password file', 'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', 'Admin is disabled because unsecure channel': 'Admin is disabled because unsecure channel', 'Admin language': 'Admin language', +'Admin versioning page': 'Admin versioning page', +'administrative interface': 'administrative interface', 'Administrator Password:': 'Administrator Password:', +'and rename it (required):': 'and rename it (required):', +'and rename it:': 'and rename it:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', +'Application': 'Application', +'application "%s" uninstalled': 'application "%s" uninstalled', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'application compiled', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'application is compiled and cannot be designed', 'Application name:': 'Application name:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', 'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Are you sure you want to uninstall application "%s"': 'Are you sure you want to uninstall application "%s"', 'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', 'Are you sure you want to upgrade web2py now?': 'Are you sure you want to upgrade web2py now?', +'Are you sure?': 'Are you sure?', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Available databases and tables', +'Available Databases and Tables': 'Available Databases and Tables', +'back': 'back', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'cache, errors and sessions cleaned', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'Cannot be empty', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.', 'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'cannot create file', +'cannot upload file "%(filename)s"': 'cannot upload file "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'change admin password', +'change editor settings': 'change editor settings', +'Changelog': 'Changelog', +'check all': 'check all', 'Check for upgrades': 'check for upgrades', 'Check to delete': 'Check to delete', 'Checking for upgrades...': 'Checking for upgrades...', 'Clean': 'clean', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'click here for online examples': 'щракни Ń‚ŃƒŠŗ за онлайн примери', +'click here for the administrative interface': 'щракни Ń‚ŃƒŠŗ за Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń‚ŠøŠ²Š½ŠøŃ интерфейс', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', +'click to check for upgrades': 'click to check for upgrades', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'collapse/expand all', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'compile', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'compiled application removed', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'Controllers', +'controllers': 'controllers', +'Count': 'Count', 'Create': 'create', +'create file with filename:': 'create file with filename:', +'create new application:': 'create new application:', 'Create new simple application': 'Create new simple application', +'Create/Upload': 'Create/Upload', +'created by': 'created by', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'crontab', 'Current request': 'Current request', 'Current response': 'Current response', 'Current session': 'Current session', -'DESIGN': 'DESIGN', -'Date and Time': 'Date and Time', -'Debug': 'Debug', -'Delete': 'Delete', -'Delete:': 'Delete:', -'Deploy': 'deploy', -'Deploy on Google App Engine': 'Deploy on Google App Engine', -'Deploy to OpenShift': 'Deploy to OpenShift', -'Design for': 'Design for', -'Disable': 'Disable', -'EDIT': 'EDIT', -'Edit': 'edit', -'Edit application': 'Edit application', -'Edit current record': 'Edit current record', -'Editing Language file': 'Editing Language file', -'Editing file': 'Editing file', -'Editing file "%s"': 'Editing file "%s"', -'Enterprise Web Framework': 'Enterprise Web Framework', -'Error logs for "%(app)s"': 'Error logs for "%(app)s"', -'Errors': 'errors', -'Exception instance attributes': 'Exception instance attributes', -'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', -'Get from URL:': 'Get from URL:', -'Hello World': 'ЗГравей, ŃŠ²ŃŃ‚', -'Help': 'help', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', -'Import/Export': 'Import/Export', -'Install': 'install', -'Installed applications': 'Installed applications', -'Internal State': 'Internal State', -'Invalid Query': 'ŠŠµŠ²Š°Š»ŠøŠ“Š½Š° Š·Š°ŃŠ²ŠŗŠ°', -'Invalid action': 'Invalid action', -'Language files (static strings) updated': 'Language files (static strings) updated', -'Languages': 'Languages', -'Last saved on:': 'Last saved on:', -'License for': 'License for', -'Login': 'Login', -'Login to the Administrative Interface': 'Login to the Administrative Interface', -'Logout': 'logout', -'Models': 'Models', -'Modules': 'Modules', -'NO': 'NO', -'New Record': 'New Record', -'New application wizard': 'New application wizard', -'New simple application': 'New simple application', -'No databases in this application': 'No databases in this application', -'Original/Translation': 'Original/Translation', -'Overwrite installed app': 'overwrite installed app', -'PAM authenticated user, cannot change password here': 'PAM authenticated user, cannot change password here', -'Pack all': 'pack all', -'Pack compiled': 'pack compiled', -'Peeking at file': 'Peeking at file', -'Plugin "%s" in application': 'Plugin "%s" in application', -'Plugins': 'Plugins', -'Powered by': 'Powered by', -'Query:': 'Query:', -'Reload routes': 'Reload routes', -'Remove compiled': 'remove compiled', -'Resolve Conflict file': 'Resolve Conflict file', -'Rows in table': 'Rows in table', -'Rows selected': 'Rows selected', -'Running on %s': 'Running on %s', -'Saved file hash:': 'Saved file hash:', -'Site': 'site', -'Start wizard': 'start wizard', -'Static files': 'Static files', -'Sure you want to delete this object?': 'Š”ŠøŠ³ŃƒŃ€ŠµŠ½ ли си, че искаш Га ŠøŠ·Ń‚Ń€ŠøŠµŃˆ този обект?', -'TM': 'TM', -'Testing application': 'Testing application', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', -'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', -'The data representation, define database tables and sets': 'The data representation, define database tables and sets', -'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', -'There are no controllers': 'There are no controllers', -'There are no models': 'There are no models', -'There are no modules': 'There are no modules', -'There are no plugins': 'There are no plugins', -'There are no static files': 'There are no static files', -'There are no translators, only default language is supported': 'There are no translators, only default language is supported', -'There are no views': 'There are no views', -'These files are served without processing, your images go here': 'These files are served without processing, your images go here', -'This is the %(filename)s template': 'This is the %(filename)s template', -'Ticket': 'Ticket', -'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', -'Translation strings for the application': 'Translation strings for the application', -'Unable to check for upgrades': 'Unable to check for upgrades', -'Unable to download': 'Unable to download', -'Unable to download app because:': 'Unable to download app because:', -'Unable to download because': 'Unable to download because', -'Uninstall': 'uninstall', -'Update:': 'Update:', -'Upload & install packed application': 'Upload & install packed application', -'Upload a package:': 'Upload a package:', -'Upload and install packed application': 'Upload and install packed application', -'Upload existing application': 'Upload existing application', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'Use an url:': 'Use an url:', -'Version': 'Version', -'Views': 'Views', -'Web Framework': 'Web Framework', -'Welcome to web2py': 'Добре Гошъл в web2py', -'YES': 'YES', -'additional code for your application': 'additional code for your application', -'admin disabled because no admin password': 'admin disabled because no admin password', -'admin disabled because not supported on google app engine': 'admin disabled because not supported on google apps engine', -'admin disabled because unable to access password file': 'admin disabled because unable to access password file', -'administrative interface': 'administrative interface', -'and rename it (required):': 'and rename it (required):', -'and rename it:': 'and rename it:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', -'application "%s" uninstalled': 'application "%s" uninstalled', -'application compiled': 'application compiled', -'application is compiled and cannot be designed': 'application is compiled and cannot be designed', -'arguments': 'arguments', -'back': 'back', -'cache': 'cache', -'cache, errors and sessions cleaned': 'cache, errors and sessions cleaned', -'cannot create file': 'cannot create file', -'cannot upload file "%(filename)s"': 'cannot upload file "%(filename)s"', -'check all': 'check all', -'click here for online examples': 'щракни Ń‚ŃƒŠŗ за онлайн примери', -'click here for the administrative interface': 'щракни Ń‚ŃƒŠŗ за Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń‚ŠøŠ²Š½ŠøŃ интерфейс', -'click to check for upgrades': 'click to check for upgrades', -'code': 'code', -'collapse/expand all': 'collapse/expand all', -'compiled application removed': 'compiled application removed', -'controllers': 'controllers', -'create file with filename:': 'create file with filename:', -'create new application:': 'create new application:', -'created by': 'created by', -'crontab': 'crontab', 'currently running': 'currently running', 'currently saved or': 'currently saved or', 'data uploaded': 'Ганните Š±ŃŃ…а качени', +'Database': 'Database', 'database': 'database', +'Database %s select': 'Database %s select', 'database %s select': 'database %s select', +'Database administration': 'Database administration', 'database administration': 'database administration', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Date and Time', 'db': 'Гб', +'Debug': 'Debug', 'defines tables': 'defines tables', +'Delete': 'Delete', 'delete': 'delete', 'delete all checked': 'delete all checked', 'delete plugin': 'delete plugin', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', +'Delete:': 'Delete:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', +'Deploy': 'deploy', +'Deploy on Google App Engine': 'Deploy on Google App Engine', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', 'design': 'Гизайн', +'DESIGN': 'DESIGN', +'Design for': 'Design for', +'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', +'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', +'docs': 'docs', +'Docs': 'Docs', 'done!': 'готово!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'download layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'download plugins', +'Download plugins from repository': 'Download plugins from repository', +'EDIT': 'EDIT', +'Edit': 'edit', +'edit all': 'edit all', +'Edit application': 'Edit application', 'edit controller': 'edit controller', +'edit controller:': 'edit controller:', +'Edit current record': 'Edit current record', 'edit views:': 'edit views:', +'Editing %s': 'Editing %s', +'Editing file': 'Editing file', +'Editing file "%s"': 'Editing file "%s"', +'Editing Language file': 'Editing Language file', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', +'Enterprise Web Framework': 'Enterprise Web Framework', +'Error': 'Error', +'Error logs for "%(app)s"': 'Error logs for "%(app)s"', +'Error snapshot': 'Error snapshot', +'Error ticket': 'Error ticket', +'Errors': 'errors', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'export as csv file', +'Exports:': 'Exports:', 'exposes': 'exposes', +'exposes:': 'exposes:', 'extends': 'extends', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'failed to reload module', 'failed to reload module because:': 'failed to reload module because:', +'File': 'File', 'file "%(filename)s" created': 'file "%(filename)s" created', 'file "%(filename)s" deleted': 'file "%(filename)s" deleted', 'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', @@ -207,80 +241,400 @@ 'file "%s" of %s restored': 'file "%s" of %s restored', 'file changed on disk': 'file changed on disk', 'file does not exist': 'file does not exist', +'file not found': 'file not found', 'file saved on %(time)s': 'file saved on %(time)s', 'file saved on %s': 'file saved on %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'filter', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'Form has errors': 'Form has errors', +'Frames': 'Frames', +'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Get from URL:': 'Get from URL:', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', +'Hello World': 'ЗГравей, ŃŠ²ŃŃ‚', +'Help': 'help', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'htmledit', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'includes', +'Indent with tabs': 'Indent with tabs', 'insert new': 'insert new', 'insert new %s': 'insert new %s', +'inspect attributes': 'inspect attributes', +'Install': 'install', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'Installed applications', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'internal error', +'internal error: %s': 'internal error: %s', +'Internal State': 'Internal State', +'Invalid action': 'Invalid action', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'invalid password', +'invalid password.': 'invalid password.', +'Invalid Query': 'ŠŠµŠ²Š°Š»ŠøŠ“Š½Š° Š·Š°ŃŠ²ŠŗŠ°', 'invalid request': 'невалиГна Š·Š°ŃŠ²ŠŗŠ°', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'invalid ticket', +'Key': 'Key', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', +'Language files (static strings) updated': 'Language files (static strings) updated', 'languages': 'languages', +'Languages': 'Languages', 'languages updated': 'languages updated', +'Last Revision': 'Last Revision', +'Last saved on:': 'Last saved on:', +'License for': 'License for', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'loading...', +'Local Apps': 'Local Apps', +'locals': 'locals', +'Locals##debug': 'Locals##debug', +'Login': 'Login', 'login': 'login', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'Login to the Administrative Interface', +'Login/Register': 'Login/Register', +'Logout': 'logout', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', 'merge': 'merge', +'Models': 'Models', 'models': 'models', +'Modified On': 'Modified On', +'Modules': 'Modules', 'modules': 'modules', +'Multi User Mode': 'Multi User Mode', 'new application "%s" created': 'new application "%s" created', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', +'New application wizard': 'New application wizard', 'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'New Record', 'new record inserted': 'Š½Š¾Š²ŠøŃŃ‚ запис беше Гобавен', +'New simple application': 'New simple application', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'next 100 rows', +'NO': 'NO', +'no changes': 'no changes', +'No databases in this application': 'No databases in this application', +'No Interaction yet': 'No Interaction yet', 'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'or import from csv file', 'or provide app url:': 'or provide app url:', 'or provide application url:': 'or provide application url:', +'Original/Translation': 'Original/Translation', +'Overview': 'Overview', +'Overwrite installed app': 'overwrite installed app', +'Pack all': 'pack all', +'Pack compiled': 'pack compiled', +'Pack custom': 'Pack custom', 'pack plugin': 'pack plugin', +'PAM authenticated user, cannot change password here': 'PAM authenticated user, cannot change password here', 'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': 'Peeking at file', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugins', +'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': 'Powered by', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'previous 100 rows', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'Query:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'record', 'record does not exist': 'Š·Š°ŠæŠøŃŃŠŃ‚ не ŃŃŠŃ‰ŠµŃŃ‚Š²ŃƒŠ²Š°', 'record id': 'record id', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', +'Reload routes': 'Reload routes', +'Remove compiled': 'remove compiled', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', +'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': 'Resolve Conflict file', +'response': 'response', +'restart': 'restart', 'restore': 'restore', +'return': 'return', +'Revert': 'Revert', 'revert': 'revert', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in table': 'Rows in table', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'Running on %s', +'Save': 'Save', 'save': 'save', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Save via Ajax', +'Saved file hash:': 'Saved file hash:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': 'selected', +'session': 'session', 'session expired': 'session expired', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', +'Site': 'site', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'some files could not be removed', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', +'Start wizard': 'start wizard', 'state': 'ŃŃŠŃŃ‚Š¾ŃŠ½ŠøŠµ', +'Static': 'Static', 'static': 'static', +'Static files': 'Static files', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'submit': 'submit', +'Submit': 'Submit', +'successful': 'successful', +'Sure you want to delete this object?': 'Š”ŠøŠ³ŃƒŃ€ŠµŠ½ ли си, че искаш Га ŠøŠ·Ń‚Ń€ŠøŠµŃˆ този обект?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'table', +'Table': 'Table', +'Temporary': 'Temporary', 'test': 'test', +'Testing application': 'Testing application', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'the application logic, each URL path is mapped in one exposed function in the controller', +'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'the data representation, define database tables and sets': 'the data representation, define database tables and sets', +'The data representation, define database tables and sets': 'The data representation, define database tables and sets', +'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', 'the presentations layer, views are also known as templates': 'the presentations layer, views are also known as templates', +'Theme': 'Theme', +'There are no controllers': 'There are no controllers', +'There are no models': 'There are no models', +'There are no modules': 'There are no modules', +'There are no plugins': 'There are no plugins', +'There are no private files': 'There are no private files', +'There are no static files': 'There are no static files', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'There are no translators, only default language is supported', +'There are no views': 'There are no views', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'These files are served without processing, your images go here', 'these files are served without processing, your images go here': 'these files are served without processing, your images go here', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'This is the %(filename)s template', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Ticket', +'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'TM': 'TM', 'to previous version.': 'to previous version.', +'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Traceback', 'translation strings for the application': 'translation strings for the application', +'Translation strings for the application': 'Translation strings for the application', 'try': 'try', 'try something like': 'try something like', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Unable to check for upgrades', 'unable to create application "%s"': 'unable to create application "%s"', 'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', 'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': 'Unable to download', +'Unable to download app because:': 'Unable to download app because:', +'Unable to download because': 'Unable to download because', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'не е възможна обработката на csv файла', 'unable to uninstall "%s"': 'unable to uninstall "%s"', 'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'uncheck all', +'Uninstall': 'uninstall', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'update', 'update all languages': 'update all languages', +'Update:': 'Update:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'upgrade web2py now', 'upload': 'upload', +'Upload': 'Upload', +'Upload & install packed application': 'Upload & install packed application', +'Upload a package:': 'Upload a package:', +'Upload and install packed application': 'Upload and install packed application', 'upload application:': 'upload application:', +'Upload existing application': 'Upload existing application', 'upload file:': 'upload file:', 'upload plugin file:': 'upload plugin file:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'Use an url:': 'Use an url:', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', +'Version': 'Version', +'Versioning': 'Versioning', 'versioning': 'versioning', 'view': 'view', +'Views': 'Views', 'views': 'views', -'web2py Recent Tweets': 'web2py Recent Tweets', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py is up to date', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'web2py Recent Tweets', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Welcome to web2py': 'Добре Гошъл в web2py', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': 'YES', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/cs.py b/applications/admin/languages/cs.py index 1b9481ef..e4667c24 100755 --- a/applications/admin/languages/cs.py +++ b/applications/admin/languages/cs.py @@ -3,26 +3,39 @@ '!langcode!': 'cs-cz', '!langname!': 'čeÅ”tina', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': 'Kolonka "Upravit" je nepovinný výraz, například "pole1=\'novĆ”hodnota\'". Výsledky databĆ”zovĆ©ho JOINu nemůžete mazat ani upravovat.', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', '"User Exception" debug mode. An error ticket could be issued!': '"User Exception" debug mode. An error ticket could be issued!', '%%{Row} in Table': '%%{řÔdek} v tabulce', '%%{Row} selected': 'označených %%{řÔdek}', +'%s': '%s', '%s %%{row} deleted': '%s smazaných %%{zĆ”znam}', '%s %%{row} updated': '%s upravených %%{zĆ”znam}', '%s selected': '%s označených', +'%s students registered': '%s students registered', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', '(requires internet access)': '(vyžaduje připojenĆ­ k internetu)', '(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(například "cs-cs")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(soubor **gluon/contrib/plural_rules/%s.py** nenalezen)', '@markmin\x01Searching: **%s** %%{file}': 'HledĆ”nĆ­: **%s** %%{soubor}', +'Abort': 'Abort', 'About': 'O programu', 'About application': 'O aplikaci', +'Accept Terms': 'Accept Terms', 'Access Control': 'ŘízenĆ­ přístupu', 'Add breakpoint': 'Přidat bod přeruÅ”enĆ­', 'Additional code for your application': 'DalŔí kód pro VaÅ”i aplikaci', 'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin disabled because no admin password', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'admin disabled because unable to access password file', +'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', 'Admin language': 'jazyk rozhranĆ­', +'Admin versioning page': 'Admin versioning page', 'Administrative interface': 'pro administrĆ”torskĆ© rozhranĆ­ klikněte sem', 'Administrative Interface': 'AdministrĆ”torskĆ© rozhranĆ­', 'administrative interface': 'rozhranĆ­ pro sprĆ”vu', @@ -30,46 +43,67 @@ 'Ajax Recipes': 'Recepty s ajaxem', 'An error occured, please %s the page': 'An error occured, please %s the page', 'and rename it:': 'a přejmenovat na:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'appadmin je zakĆ”zanĆ” bez zabezpečenĆ©ho spojenĆ­', 'Application': 'Application', 'application "%s" uninstalled': 'application "%s" odinstalovĆ”na', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', 'application compiled': 'aplikace zkompilovĆ”na', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'application is compiled and cannot be designed', 'Application name:': 'NĆ”zev aplikace:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'nepoužita', 'are not used yet': 'jeÅ”tě nepoužita', +'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Opravdu chcete odstranit tento objekt?', 'Are you sure you want to uninstall application "%s"?': 'Opravdu chcete odinstalovat aplikaci "%s"?', +'Are you sure?': 'Are you sure?', 'arguments': 'arguments', 'at char %s': 'at char %s', 'at line %s': 'at line %s', 'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available Databases and Tables': 'DostupnĆ© databĆ”ze a tabulky', 'back': 'zpět', +'Back to the plugins list': 'Back to the plugins list', 'Back to wizard': 'Back to wizard', 'Basics': 'Basics', 'Begin': 'Začƭt', 'breakpoint': 'bod přeruÅ”enĆ­', 'Breakpoints': 'Body přeruÅ”enĆ­', 'breakpoints': 'body přeruÅ”enĆ­', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', 'Buy this book': 'Koupit web2py knihu', 'Cache': 'Cache', 'cache': 'cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Klƭče cache', 'cache, errors and sessions cleaned': 'cache, chyby a relace byly pročiÅ”těny', 'can be a git repo': 'může to být git repo', 'Cancel': 'Storno', 'Cannot be empty': 'Nemůže být prĆ”zdnĆ©', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'cannot create file', +'cannot upload file "%(filename)s"': 'cannot upload file "%(filename)s"', 'Change Admin Password': 'Změnit heslo pro sprĆ”vu', 'Change admin password': 'Změnit heslo pro sprĆ”vu aplikacĆ­', +'change editor settings': 'change editor settings', 'Change password': 'Změna hesla', +'Changelog': 'Changelog', 'check all': 'vÅ”e označit', 'Check for upgrades': 'Zkusit aktualizovat', 'Check to delete': 'Označit ke smazĆ”nĆ­', 'Check to delete:': 'Označit ke smazĆ”nĆ­:', 'Checking for upgrades...': 'ZjiŔńuji, zda jsou k dispozici aktualizace...', 'Clean': 'Pročistit', +'Clear': 'Clear', 'Clear CACHE?': 'Vymazat CACHE?', 'Clear DISK': 'Vymazat DISK', 'Clear RAM': 'Vymazat RAM', @@ -79,8 +113,15 @@ 'code': 'code', 'Code listing': 'Code listing', 'collapse/expand all': 'vÅ”e sbalit/rozbalit', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Community': 'Komunita', 'Compile': 'Zkompilovat', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'zkompilovanĆ” aplikace smazĆ”na', 'Components and Plugins': 'Komponenty a zĆ”suvnĆ© moduly', 'Condition': 'PodmĆ­nka', @@ -92,9 +133,12 @@ 'Count': 'Počet', 'Create': 'Vytvořit', 'create file with filename:': 'vytvořit soubor s nĆ”zvem:', +'Create/Upload': 'Create/Upload', 'created by': 'vytvořil', 'Created By': 'Vytvořeno - kým', +'Created by:': 'Created by:', 'Created On': 'Vytvořeno - kdy', +'Created on:': 'Created on:', 'crontab': 'crontab', 'Current request': 'AktuĆ”lnĆ­ požadavek', 'Current response': 'AktuĆ”lnĆ­ odpověď', @@ -107,6 +151,7 @@ 'Database %s select': 'databĆ”ze %s výběr', 'Database administration': 'Database administration', 'database administration': 'sprĆ”va databĆ”ze', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Datum a čas', 'day': 'den', 'db': 'db', @@ -124,36 +169,60 @@ 'Deploy': 'NahrĆ”t', 'Deploy on Google App Engine': 'NahrĆ”t na Google App Engine', 'Deploy to OpenShift': 'NahrĆ”t na OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', 'Deployment Recipes': 'Postupy pro deployment', 'Description': 'Popis', +'Description:': 'Description:', 'design': 'nĆ”vrh', 'Detailed traceback description': 'Podrobný výpis prostředĆ­', 'details': 'podrobnosti', 'direction: ltr': 'směr: ltr', +'directory not found': 'directory not found', 'Disable': 'Zablokovat', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', 'DISK': 'DISK', 'Disk Cache Keys': 'Klƭče diskovĆ© cache', 'Disk Cleared': 'Disk smazĆ”n', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'dokumentace', +'Docs': 'Docs', 'Documentation': 'Dokumentace', "Don't know what to do?": 'NevĆ­te kudy kam?', 'done!': 'hotovo!', +'Downgrade': 'Downgrade', 'Download': 'StĆ”hnout', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'stĆ”hnout moduly rozvrženĆ­ strĆ”nky', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'stĆ”hnout zĆ”suvnĆ© moduly', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'E-mail', 'Edit': 'Upravit', 'edit all': 'edit all', 'Edit application': 'SprĆ”va aplikace', 'edit controller': 'edit controller', +'edit controller:': 'edit controller:', 'Edit current record': 'Upravit aktuĆ”lnĆ­ zĆ”znam', 'Edit Profile': 'Upravit profil', 'edit views:': 'upravit pohled:', +'Editing %s': 'Editing %s', 'Editing file "%s"': 'Úprava souboru "%s"', 'Editing Language file': 'Úprava jazykovĆ©ho souboru', 'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', 'Email and SMS': 'Email a SMS', 'Enable': 'Odblokovat', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'enter a number between %(min)g and %(max)g': 'zadejte čƭslo mezi %(min)g a %(max)g', 'enter an integer between %(min)g and %(max)g': 'zadejte celĆ© čƭslo mezi %(min)g a %(max)g', 'Error': 'Chyba', @@ -164,124 +233,199 @@ 'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', 'Exception %s': 'Exception %s', 'Exception instance attributes': 'Prvky instance výjimky', +'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Expand Abbreviation', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'exportovat do .csv souboru', +'Exports:': 'Exports:', 'exposes': 'vystavuje', 'exposes:': 'vystavuje funkce:', 'extends': 'rozÅ”iřuje', 'failed to compile file because:': 'soubor se nepodařilo zkompilovat, protože:', +'failed to reload module because:': 'failed to reload module because:', 'FAQ': 'Často kladenĆ© dotazy', 'File': 'Soubor', 'file': 'soubor', 'file "%(filename)s" created': 'file "%(filename)s" created', +'file "%(filename)s" deleted': 'file "%(filename)s" deleted', +'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', +'file "%s" of %s restored': 'file "%s" of %s restored', +'file changed on disk': 'file changed on disk', +'file does not exist': 'file does not exist', +'file not found': 'file not found', 'file saved on %(time)s': 'soubor uložen %(time)s', 'file saved on %s': 'soubor uložen %s', +'filename': 'filename', 'Filename': 'NĆ”zev souboru', +'Files added': 'Files added', 'filter': 'filtr', 'Find Next': 'NajĆ­t dalŔí', 'Find Previous': 'NajĆ­t předchozĆ­', 'First name': 'KřestnĆ­ jmĆ©no', 'Forgot username?': 'Zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', 'forgot username?': 'zapomněl jste svoje přihlaÅ”ovacĆ­ jmĆ©no?', +'Form has errors': 'Form has errors', 'Forms and Validators': 'FormulÔře a validĆ”tory', 'Frames': 'Frames', 'Free Applications': 'Aplikace zdarma', 'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', 'Generate': 'Vytvořit', 'Get from URL:': 'StĆ”hnout z internetu:', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', 'Globals##debug': 'GlobĆ”lnĆ­ proměnnĆ©', 'go!': 'OK!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', 'Goto': 'Goto', 'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Group %(group_id)s created': 'Skupina %(group_id)s vytvořena', 'Group ID': 'ID skupiny', 'Groups': 'Skupiny', 'Hello World': 'Ahoj světe', 'Help': 'NĆ”pověda', +'here': 'here', 'Hide/Show Translated strings': 'Skrýt/Zobrazit přeloženĆ© texty', +'Highlight current line': 'Highlight current line', 'Hits': 'KolikrĆ”t dosaženo', 'Home': 'DomovskĆ” strĆ”nka', 'honored only if the expression evaluates to true': 'brĆ”t v potaz jen když se tato podmĆ­nka vyhodnotĆ­ kladně', 'How did you get here?': 'Jak jste se sem vlastně dostal?', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', 'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', 'import': 'import', 'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'zahrnuje', +'Indent with tabs': 'Indent with tabs', 'Index': 'Index', 'insert new': 'vložit nový zĆ”znam ', 'insert new %s': 'vložit nový zĆ”znam %s', 'inspect attributes': 'inspect attributes', 'Install': 'Instalovat', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'NainstalovanĆ© aplikace', 'Interaction at %s line %s': 'Interakce v %s, na řÔdce %s', 'Interactive console': 'InteraktivnĆ­ příkazovĆ” řÔdka', +'internal error': 'internal error', +'internal error: %s': 'internal error: %s', 'Internal State': 'VnitřnĆ­ stav', 'Introduction': 'Úvod', +'Invalid action': 'Invalid action', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', 'Invalid email': 'Neplatný email', +'Invalid git repository specified.': 'Invalid git repository specified.', 'Invalid password': 'NesprĆ”vnĆ© heslo', +'invalid password': 'invalid password', 'invalid password.': 'neplatnĆ© heslo', 'Invalid Query': 'Neplatný dotaz', 'invalid request': 'Neplatný požadavek', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', +'invalid ticket': 'invalid ticket', 'Is Active': 'Je aktivnĆ­', 'It is %s %%{day} today.': 'Dnes je to %s %%{den}.', 'Key': 'Klƭč', 'Key bindings': 'Vazby klĆ­ÄÅÆ', 'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', +'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', +'Language files (static strings) updated': 'Language files (static strings) updated', 'languages': 'jazyky', 'Languages': 'Jazyky', 'Last name': 'PříjmenĆ­', +'Last Revision': 'Last Revision', 'Last saved on:': 'Naposledy uloženo:', 'Layout': 'RozvrženĆ­ strĆ”nky (layout)', 'Layout Plugins': 'Moduly rozvrženĆ­ strĆ”nky (Layout Plugins)', 'Layouts': 'RozvrženĆ­ strĆ”nek', 'License for': 'Licence pro', +'License:': 'License:', +'Line Nr': 'Line Nr', 'Line number': 'Číslo řÔdku', 'LineNo': 'Č.řÔdku', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', 'Live Chat': 'Online pokec', +'Loading...': 'Loading...', 'loading...': 'nahrĆ”vĆ”m...', +'Local Apps': 'Local Apps', 'locals': 'locals', 'Locals##debug': 'LokĆ”lnĆ­ proměnnĆ©', 'Logged in': 'PřihlÔŔenĆ­ proběhlo ĆŗspěŔně', 'Logged out': 'OdhlÔŔenĆ­ proběhlo ĆŗspěŔně', -'Login': 'PřihlĆ”sit se', 'login': 'přihlĆ”sit se', +'Login': 'PřihlĆ”sit se', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'PřihlĆ”sit se do SprĆ”vce aplikacĆ­', +'Login/Register': 'Login/Register', 'logout': 'odhlĆ”sit se', 'Logout': 'OdhlĆ”sit se', +'lost password': 'lost password', 'Lost Password': 'Zapomněl jste heslo', 'Lost password?': 'Zapomněl jste heslo?', 'lost password?': 'zapomněl jste heslo?', +'Main Menu': 'Main Menu', 'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', 'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', 'Menu Model': 'Model rozbalovacĆ­ nabĆ­dky', +'merge': 'merge', 'Models': 'Modely', 'models': 'modely', 'Modified By': 'Změněno - kým', 'Modified On': 'Změněno - kdy', 'Modules': 'Moduly', 'modules': 'moduly', +'Multi User Mode': 'Multi User Mode', 'My Sites': 'SprĆ”va aplikacĆ­', 'Name': 'JmĆ©no', 'new application "%s" created': 'novĆ” aplikace "%s" vytvořena', +'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'Nový prÅÆvodce aplikacĆ­', 'New application wizard': 'Nový prÅÆvodce aplikacĆ­', 'New password': 'NovĆ© heslo', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', 'New Record': 'Nový zĆ”znam', 'new record inserted': 'nový zĆ”znam byl založen', 'New simple application': 'Vytvořit primitivnĆ­ aplikaci', 'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'dalŔích 100 řÔdkÅÆ', +'NO': 'NO', +'no changes': 'no changes', 'No databases in this application': 'V tĆ©to aplikaci nejsou žÔdnĆ© databĆ”ze', 'No Interaction yet': 'JeÅ”tě žÔdnĆ” interakce nenastala', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': 'Soubor ticket_storage.txt v adresÔři /private nenalezen', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', 'Object or table name': 'Objekt či tabulka', 'Old password': 'PÅÆvodnĆ­ heslo', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'online nĆ”vrhÔř', 'Online examples': 'Příklady online', 'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', 'or alternatively': 'or alternatively', 'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'nebo importovat z .csv souboru', @@ -293,30 +437,53 @@ 'Overwrite installed app': 'Přepsat instalovanou aplikaci', 'Pack all': 'Zabalit', 'Pack compiled': 'Zabalit zkompilovanĆ©', +'Pack custom': 'Pack custom', 'pack plugin': 'pack plugin', -'password': 'heslo', 'Password': 'Heslo', +'password': 'heslo', +'password changed': 'password changed', "Password fields don't match": 'Hesla se neshodujĆ­', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'Peeking at file', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Please': 'ProsĆ­m', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', 'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'zĆ”suvnĆ© moduly', 'Plugins': 'ZĆ”suvnĆ© moduly', 'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'MnožnĆ” čƭsla:', 'Powered by': 'PohĆ”něno', 'Preface': 'Předmluva', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'předchozĆ­ch 100 řÔdkÅÆ', 'Private files': 'SoukromĆ© soubory', 'private files': 'soukromĆ© soubory', 'profile': 'profil', 'Project Progress': 'Vývoj projektu', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', 'Python': 'Python', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Dotaz:', 'Quick Examples': 'KrĆ”tkĆ© příklady', 'RAM': 'RAM', 'RAM Cache Keys': 'Klƭče RAM Cache', 'Ram Cleared': 'RAM smazĆ”na', +'Rapid Search': 'Rapid Search', 'Readme': 'NĆ”pověda', 'Recipes': 'Postupy jak na to', 'Record': 'ZĆ”znam', @@ -335,35 +502,58 @@ 'Removed Breakpoint on %s at line %s': 'Bod přeruÅ”enĆ­ smazĆ”n - soubor %s na řÔdce %s', 'Replace': 'Zaměnit', 'Replace All': 'Zaměnit vÅ”e', +'Repository (%s)': 'Repository (%s)', 'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', 'Reset Password key': 'Reset registračnĆ­ho klƭče', +'Resolve Conflict file': 'Resolve Conflict file', 'response': 'response', 'restart': 'restart', 'restore': 'obnovit', 'Retrieve username': 'ZĆ­skat přihlaÅ”ovacĆ­ jmĆ©no', 'return': 'return', +'Revert': 'Revert', 'revert': 'vrĆ”tit se k pÅÆvodnĆ­mu', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', 'Role': 'Role', +'Roles': 'Roles', 'Rows in Table': 'ZĆ”znamy v tabulce', 'Rows selected': 'ZĆ”znamÅÆ zobrazeno', 'rules are not defined': 'pravidla nejsou definovĆ”na', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "SpustĆ­ testy v tomto souboru (ke spuÅ”těnĆ­ vÅ”ech testÅÆ, použijte tlačƭtko 'test')", 'Running on %s': 'Běží na %s', 'Save': 'Uložit', 'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Uložit pomocĆ­ Ajaxu', 'Saved file hash:': 'hash uloženĆ©ho souboru:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'Semantic': 'Modul semantic', 'Services': 'Služby', 'session': 'session', 'session expired': 'session expired', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Bod přeruÅ”enĆ­ nastaven v souboru %s na řÔdce %s: %s', 'shell': 'příkazovĆ” řÔdka', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', 'Singular Form': 'Singular Form', 'Site': 'SprĆ”va aplikacĆ­', 'Size of cache:': 'Velikost cache:', 'skip to generate': 'skip to generate', +'some files could not be removed': 'some files could not be removed', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', 'Sorry, could not find mercurial installed': 'Bohužel mercurial nenĆ­ nainstalovĆ”n.', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', 'Start a new app': 'Vytvořit novou aplikaci', 'Start searching': 'Začƭt hledĆ”nĆ­', 'Start wizard': 'Spustit prÅÆvodce', @@ -381,34 +571,49 @@ 'successful': 'ĆŗspěŔně', 'Support': 'Podpora', 'Sure you want to delete this object?': 'Opravdu chcete smazat tento objekt?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'Table': 'tabulka', 'Table name': 'NĆ”zev tabulky', 'Temporary': 'Dočasný', 'test': 'test', 'Testing application': 'Testing application', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Dotaz" je podmĆ­nka, například "db.tabulka1.pole1==\'hodnota\'". PodmĆ­nka "db.tabulka1.pole1==db.tabulka2.pole2" pak vytvoří SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika aplikace: každĆ” URL je mapovĆ”na na funkci vystavovanou kontrolĆ©rem.', 'The Core': 'JĆ”dro (The Core)', 'The data representation, define database tables and sets': 'Reprezentace dat: definovat tabulky databĆ”ze a zĆ”znamy', 'The output of the file is a dictionary that was rendered by the view %s': 'Výstup ze souboru je slovnĆ­k, který se zobrazil v pohledu %s.', 'The presentations layer, views are also known as templates': 'PrezentačnĆ­ vrstva: pohledy či templaty (Å”ablony)', 'The Views': 'Pohledy (The Views)', +'Theme': 'Theme', 'There are no controllers': 'There are no controllers', +'There are no models': 'There are no models', 'There are no modules': 'There are no modules', 'There are no plugins': 'ŽÔdnĆ© moduly nejsou instalovĆ”ny.', 'There are no private files': 'ŽÔdnĆ© soukromĆ© soubory neexistujĆ­.', 'There are no static files': 'There are no static files', +'There are no translators': 'There are no translators', 'There are no translators, only default language is supported': 'There are no translators, only default language is supported', 'There are no views': 'There are no views', 'These files are not served, they are only available from within your app': 'Tyto soubory jsou klientÅÆm nepřístupnĆ©. K dispozici jsou pouze v rĆ”mci aplikace.', 'These files are served without processing, your images go here': 'Tyto soubory jsou servĆ­rovĆ”ny bez přídavnĆ© logiky, sem patří např. obrĆ”zky.', 'This App': 'Tato aplikace', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", 'This is a copy of the scaffolding application': 'Toto je kopie aplikace skelet.', +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', 'This is the %(filename)s template': 'This is the %(filename)s template', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', 'this page to see if a breakpoint was hit and debug interaction is required.': 'tuto strĆ”nku, abyste uviděli, zda se dosĆ”hlo bodu přeruÅ”enĆ­.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', 'Time in Cache (h:m:s)': 'Čas v Cache (h:m:s)', 'Timestamp': 'ČasovĆ© razĆ­tko', 'to previous version.': 'k předchozĆ­ verzi.', @@ -416,6 +621,7 @@ 'To emulate a breakpoint programatically, write:': 'K nastavenĆ­ bodu přeruÅ”enĆ­ v kódu programu, napiÅ”te:', 'to use the debugger!': ', abyste mohli ladĆ­cĆ­ program používat!', 'toggle breakpoint': 'vyp./zap. bod přeruÅ”enĆ­', +'Toggle comment': 'Toggle comment', 'Toggle Fullscreen': 'Na celou obrazovku a zpět', 'too short': 'PříliÅ” krĆ”tkĆ©', 'Traceback': 'Traceback', @@ -424,12 +630,25 @@ 'Try the mobile interface': 'Zkuste rozhranĆ­ pro mobilnĆ­ zařízenĆ­', 'try view': 'try view', 'Twitter': 'Twitter', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', 'Type python statement in here and hit Return (Enter) to execute it.': 'Type python statement in here and hit Return (Enter) to execute it.', 'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', 'Unable to check for upgrades': 'Unable to check for upgrades', +'unable to create application "%s"': 'unable to create application "%s"', +'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'csv soubor nedĆ” sa zpracovat', +'unable to uninstall "%s"': 'unable to uninstall "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'vÅ”e odznačit', 'Uninstall': 'Odinstalovat', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'aktualizovat', 'update all languages': 'aktualizovat vÅ”echny jazyky', 'Update:': 'Upravit:', @@ -443,6 +662,7 @@ 'upload file:': 'nahrĆ”t soubor:', 'upload plugin file:': 'nahrĆ”t soubor modulu:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Použijte (...)&(...) pro AND, (...)|(...) pro OR a ~(...) pro NOT pro sestavenĆ­ složitějŔích dotazÅÆ.', +'User': 'User', 'User %(id)s Logged-in': 'Uživatel %(id)s přihlÔŔen', 'User %(id)s Logged-out': 'Uživatel %(id)s odhlÔŔen', 'User %(id)s Password changed': 'Uživatel %(id)s změnil heslo', @@ -451,6 +671,8 @@ 'User %(id)s Username retrieved': 'Uživatel %(id)s si nachal zaslat přihlaÅ”ovacĆ­ jmĆ©no', 'User ID': 'ID uživatele', 'Username': 'PřihlaÅ”ovacĆ­ jmĆ©no', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', 'Verify Password': 'Zopakujte heslo', 'Version': 'Verze', @@ -460,7 +682,14 @@ 'View': 'Pohled (View)', 'Views': 'Pohledy', 'views': 'pohledy', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'MĆ”te aktuĆ”lnĆ­ verzi web2py.', 'web2py online debugger': 'LadĆ­cĆ­ online web2py program', 'web2py Recent Tweets': 'Å těbetĆ”nĆ­ na Twitteru o web2py', @@ -470,11 +699,18 @@ 'Welcome to web2py': 'Vitejte ve web2py', 'Welcome to web2py!': 'VĆ­tejte ve web2py!', 'Which called the function %s located in the file %s': 'kterĆ” zavolala funkci %s v souboru (kontrolĆ©ru) %s.', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': 'YES', +'Yes': 'Yes', 'You are successfully running web2py': 'ÚspěŔně jste spustili web2py.', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'Nastavovat a mazat body přeruÅ”enĆ­ je též možno v rĆ”mci editovĆ”nĆ­ zdrojovĆ©ho souboru přes tlačƭtko Vyp./Zap. bod přeruÅ”enĆ­', +'You can inspect variables using the console bellow': 'Níže pomocĆ­ příkazovĆ© řÔdky si můžete prohlĆ©dnout proměnnĆ©', +'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You can modify this application and adapt it to your needs': 'Tuto aplikaci si můžete upravit a přizpÅÆsobit ji svým potřebĆ”m.', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', 'You need to set up and reach a': 'Je třeba nejprve nastavit a dojĆ­t až na', +'You only need these if you have already registered': 'You only need these if you have already registered', 'You visited the url %s': 'NavÅ”tĆ­vili jste strĆ”nku %s,', 'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Aplikace bude blokovĆ”na než se klikne na jedno z tlačƭtek (dalŔí, krok, pokračovat, atd.)', -'You can inspect variables using the console bellow': 'Níže pomocĆ­ příkazovĆ© řÔdky si můžete prohlĆ©dnout proměnnĆ©', } diff --git a/applications/admin/languages/de.py b/applications/admin/languages/de.py index 654aa43c..da4fa1a9 100755 --- a/applications/admin/languages/de.py +++ b/applications/admin/languages/de.py @@ -3,13 +3,19 @@ '!langcode!': 'de', '!langname!': 'Deutsch', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "Feld1 = \'newvalue". JOIN Ergebnisse kƶnnen nicht aktualisiert oder gelƶscht werden', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s %%{row} Zeilen gelƶscht', '%s %%{row} updated': '%s %%{row} Zeilen aktualisiert', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%d.%m.%Y', '%Y-%m-%d %H:%M:%S': '%d.%m.%Y %H:%M:%S', '(requires internet access)': '(Benƶtigt Internetzugang)', '(requires internet access, experimental)': '(Benƶtigt Internetzugang)', '(something like "it-it")': '(so etwas wie "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(Datei **gluon/contrib/plural_rules/%s.py** wurde nicht gefunden)', '@markmin\x01An error occured, please [[reload %s]] the page': 'Ein Fehler ist aufgetreten, bitte [[reload %s]] sie die Seite erneut', '@markmin\x01Searching: **%s** %%{file}': '@markmin\x01Suche: **%s** Dateien', @@ -18,35 +24,48 @@ 'Abort': 'Abbrechen', 'About': 'Über', 'About application': 'Über die Anwendung', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'ZusƤtzlicher Code für Ihre Anwendung', +'Admin design page': 'Admin design page', 'admin disabled because no admin password': 'admin ist deaktiviert, weil kein Admin-Passwort gesetzt ist', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', 'admin disabled because not supported on google apps engine': 'admin ist deaktiviert, es existiert dafür keine Unterstützung auf der google apps engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'admin ist deaktiviert, weil kein Zugriff auf die Passwortdatei besteht', 'Admin is disabled because insecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals', 'Admin is disabled because unsecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals', 'Admin language': 'Admin-Sprache', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'Administrative Schnittstelle', 'Administrator Password:': 'Administrator Passwort:', 'An error occured, please %s the page': 'Ein Fehler ist aufgetereten, bitte %s die Seite', 'and rename it (required):': 'und benenne sie um (erforderlich):', 'and rename it:': ' und benenne sie um:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals', 'Application': 'Anwendung', 'application "%s" uninstalled': 'Anwendung "%s" deinstalliert', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', 'application compiled': 'Anwendung kompiliert', +'Application exists already': 'Application exists already', 'application is compiled and cannot be designed': 'Die Anwendung ist kompiliert und kann deswegen nicht mehr geƤndert werden', 'Application name:': 'Name der Anwendung:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'werden nicht verwendet', 'are not used yet': 'werden bisher nicht verwendet', 'Are you sure you want to delete file "%s"?': 'Sind Sie sich sicher, dass Sie diese Datei lƶschen wollen "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Sind Sie sich sicher, dass Sie dieses Objekt lƶschen wollen?', 'Are you sure you want to uninstall application "%s"': 'Sind Sie sich sicher, dass Sie diese Anwendung deinstallieren wollen "%s"', 'Are you sure you want to uninstall application "%s"?': 'Sind Sie sich sicher, dass Sie diese Anwendung deinstallieren wollen "%s"?', 'Are you sure you want to upgrade web2py now?': 'Sind Sie sich sicher, dass Sie web2py jetzt upgraden mƶchten?', +'Are you sure?': 'Are you sure?', 'arguments': 'arguments', 'at char %s': 'bei Zeichen %s', 'at line %s': 'in Linie %s', +'ATTENTION:': 'ATTENTION:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ACHTUNG: Die Einwahl benƶtigt eine sichere (HTTPS) Verbindung. Es sei denn sie lƤuft Lokal (localhost).', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ACHTUNG: Testen ist nicht threadsicher. Führen Sie also nicht mehrere Tests gleichzeitig aus.', 'ATTENTION: This is an experimental feature and it needs more testing.': 'ACHTUNG: Dies ist eine experimentelle Funktion und benƶtigt noch weitere Tests.', @@ -54,35 +73,66 @@ 'Authentication': 'Authentifizierung', 'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Verfügbare Datenbanken und Tabellen', +'Available Databases and Tables': 'Available Databases and Tables', 'back': 'Zurück', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', 'beautify': 'Verschƶnern', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', 'cache': 'Zwischenspeicher', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'Zwischenspeicher (cache), Fehler und Sitzungen (sessions) gelƶscht', 'call': 'Aufruf', 'can be a git repo': 'kann ein git Repository sein', 'Cancel': 'Abbrechen', 'Cannot be empty': 'Darf nicht leer sein', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Nicht Kompilierbar: Es sind Fehler in der Anwendung. Beseitigen Sie die Fehler und versuchen Sie es erneut.', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', 'cannot create file': 'Kann Datei nicht erstellen', 'cannot upload file "%(filename)s"': 'Kann Datei nicht Hochladen "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Administrator-Passwort Ƥndern', 'change editor settings': 'Editoreinstellungen Ƥndern', 'Change Password': 'Passwort Ƥndern', 'change password': 'Passwort Ƥndern', +'Changelog': 'Changelog', 'check all': 'alles auswƤhlen', 'Check for upgrades': 'Versionsüberprüfung', 'Check to delete': 'Markiere zum lƶschen', 'Checking for upgrades...': 'Überprüfe auf Updates...', 'Clean': 'Leeren', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'click here for online examples': 'hier klicken für online Beispiele', 'click here for the administrative interface': 'hier klicken für die AdministrationsoberflƤche ', 'Click row to expand traceback': 'Klicke auf die Zeile für Fehlerverfolgung', +'Click row to view a ticket': 'Click row to view a ticket', 'click to check for upgrades': 'hier klicken um nach Upgrades zu suchen', 'Client IP': 'Client IP', 'code': 'code', +'Code listing': 'Code listing', 'collapse/expand all': 'alles zu- bzw. aufklappen', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'kompilieren', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'kompilierte Anwendung gelƶscht', +'Condition': 'Condition', +'continue': 'continue', 'Controller': 'Controller', 'Controllers': 'Controllers', 'controllers': 'controllers', @@ -92,7 +142,11 @@ 'create file with filename:': 'Erzeuge Datei mit Dateinamen:', 'create new application:': 'Erzeuge neue Anwendung:', 'Create new simple application': 'Erzeuge neue Anwendung', +'Create/Upload': 'Create/Upload', 'created by': 'Erstellt von', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'crontab', 'Current request': 'Aktuelle Anfrage (request)', 'Current response': 'Aktuelle Antwort (response)', @@ -103,8 +157,11 @@ 'data uploaded': 'Daten hochgeladen', 'Database': 'Datenbank', 'database': 'Datenbank', +'Database %s select': 'Database %s select', 'database %s select': 'Datenbank %s ausgewƤhlt', +'Database administration': 'Database administration', 'database administration': 'Datenbankadministration', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Datum und Uhrzeit', 'db': 'db', 'DB Model': 'DB Modell', @@ -116,25 +173,49 @@ 'delete plugin': 'Plugin lƶschen', 'Delete this file (you will be asked to confirm deletion)': 'Diese Datei lƶschen (mit BestƤtigungsdialog)', 'Delete:': 'Lƶschen:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'Installieren', 'Deploy on Google App Engine': 'Auf Google App Engine installieren', 'Deploy to OpenShift': 'Auf OpenShift installieren', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', 'Description': 'Beschreibung', +'Description:': 'Description:', 'design': 'design', 'DESIGN': 'DESIGN', 'Design for': 'Design für', 'Detailed traceback description': 'Detaillierte traceback Beschreibung', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', 'Disable': 'Deaktivieren', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'docs', +'Docs': 'Docs', 'documentation': 'Dokumentation', 'done!': 'fertig!', +'Downgrade': 'Downgrade', 'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'Layouts herunterladen', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'Plugins herunterladen', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'E-mail', 'EDIT': 'BEARBEITEN', 'Edit': 'Bearbeiten', +'edit all': 'edit all', 'Edit application': 'Bearbeite Anwendung', 'edit controller': 'Bearbeite Controller', 'edit controller:': 'bearbeite Controller:', @@ -147,7 +228,12 @@ 'Editing file': 'Bearbeite Datei', 'Editing file "%s"': 'Bearbeite Datei "%s"', 'Editing Language file': 'Sprachdatei bearbeiten', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', 'Enable': 'Aktivieren', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Enterprise Web Framework': 'Enterprise Web Framework', 'Error': 'Fehler', 'Error logs for "%(app)s"': 'Fehlerprotokoll für "%(app)s"', @@ -155,16 +241,20 @@ 'Error ticket': 'Error ticket', 'Errors': 'Fehlermeldungen', 'escape': 'escape', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Attribute der Ausnahmeinstanz', 'Exit Fullscreen': 'Vollbild beenden', 'Expand Abbreviation': 'Kürzel erweitern', 'Expand Abbreviation (html files only)': 'Abkürzungen ausschreiben (nur HTML Dateien)', 'export as csv file': 'Exportieren als CSV-Datei', +'Exports:': 'Exports:', 'exposes': 'stellt zur Verfügung', 'exposes:': 'stellt folgendes zur Verfügung:', 'extends': 'erweitert', 'failed to compile file because:': 'Datei konnte nicht kompiliert werden, da:', 'failed to reload module': 'neu laden des Moduls fehlgeschlagen', +'failed to reload module because:': 'failed to reload module because:', 'File': 'Datei', 'file "%(filename)s" created': 'Datei "%(filename)s" erstellt', 'file "%(filename)s" deleted': 'Datei "%(filename)s" gelƶscht', @@ -173,93 +263,166 @@ 'file "%s" of %s restored': 'Datei "%s" von %s wiederhergestellt', 'file changed on disk': 'Datei auf Festplatte geƤndert', 'file does not exist': 'Datei existiert nicht', +'file not found': 'file not found', 'file saved on %(time)s': 'Datei gespeichert am %(time)s', 'file saved on %s': 'Datei gespeichert auf %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'Filter', 'Find Next': 'NƤchster', 'Find Previous': 'Vorheriger', 'First name': 'Vorname', +'Form has errors': 'Form has errors', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Funktionen ohne doctests erzeugen [passed] in Tests', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', 'Get from URL:': 'Get from URL:', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'Gehe zum übereinstimmenden Paar', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', 'Goto': 'Goto', 'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Group ID': 'Gruppen ID', 'Hello World': 'Hallo Welt', 'Help': 'Hilfe', +'here': 'here', 'Hide/Show Translated strings': 'Zeige/Verstecke übersetzte Strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', 'Home': 'Startseite', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'htmledit', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Falls der obere Test eine Fehler-Ticketnummer enthƤlt deutet das auf einen Fehler in der Ausführung des Controllers hin, noch bevor der Doctest ausgeführt werden konnte. Gewƶhnlich Führen fehlerhafte Einrückungen oder fehlerhafter Code ausserhalb der Funktion zu solchen Fehlern. Ein grüner Titel deutet darauf hin, dass alle Test(wenn sie vorhanden sind) erfolgreich durchlaufen wurden. In diesem Fall werden die Testresultate nicht angezeigt.', 'If you answer "yes", be patient, it may take a while to download': 'Wenn Sie mit "Ja" antworten, seien Sie bitte geduldig. Der Download kƶnnte eine Weile dauern.', 'If you answer yes, be patient, it may take a while to download': 'Wenn Sie mit Ja antworten, seien Sie bitte geduldig. Der Download kƶnnte eine Weile dauern.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', 'Import/Export': 'Importieren/Exportieren', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'Einfügen', +'Indent with tabs': 'Indent with tabs', 'Index': 'Index', 'index': 'index', 'insert new': 'neu Einfügen', 'insert new %s': 'neu Einfügen %s', 'inspect attributes': 'Attribute inspizieren', 'Install': 'Installieren', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Installierte Anwendungen', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'Interner Fehler', +'internal error: %s': 'internal error: %s', 'Internal State': 'Interner Status', 'Invalid action': 'Ungültige Aktion', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', 'Invalid email': 'Ungültige Email', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'Ungültiges Passwort', +'invalid password.': 'invalid password.', 'Invalid Query': 'Ungültige Abfrage', 'invalid request': 'Ungültige Anfrage', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'Ungültiges Ticket', +'Key': 'Key', 'Key bindings': 'Tastenbelegungen', 'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', 'Key bindings for ZenConding Plugin': 'Tastenbelegungen für das ZenConding Plugin', 'Keyboard shortcuts': 'Tastenkombination', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'Sprachdatei "%(filename)s" erstellt/aktualisiert', 'Language files (static strings) updated': 'Sprachdatei (statisch Strings) aktualisiert', 'languages': 'Sprachen', 'Languages': 'Sprachen', 'languages updated': 'Sprachen aktualisiert', 'Last name': 'Nachname', +'Last Revision': 'Last Revision', 'Last saved on:': 'Zuletzt gespeichert am:', 'Layout': 'Layout', 'License for': 'Lizenz für', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', 'lists by ticket': 'nach Ticket aufgelistet', +'Loading...': 'Loading...', 'loading...': 'lade...', +'Local Apps': 'Local Apps', 'locals': 'locals', +'Locals##debug': 'Locals##debug', 'located in the file': 'befindet sich in der Datei', -'Login': 'Anmelden', 'login': 'anmelden', +'Login': 'Anmelden', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'An das Administrations-Interface anmelden', +'Login/Register': 'Login/Register', 'Logout': 'Abmelden', +'lost password': 'lost password', 'Lost Password': 'Passwort vergessen', 'lost password?': 'Passwort vergessen?', 'Main Menu': 'Hauptmenü', 'Manage': 'Verwalten', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', 'Match Pair': 'Paare finden', +'Memberships': 'Memberships', 'Menu Model': 'Menü Modell', 'merge': 'verbinden', 'Merge Lines': 'Zeilen zusammenfügen', 'Models': 'Modelle', 'models': 'Modelle', +'Modified On': 'Modified On', 'Modules': 'Module', 'modules': 'Module', +'Multi User Mode': 'Multi User Mode', 'Name': 'Name', 'new application "%s" created': 'neue Anwendung "%s" erzeugt', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', 'New application wizard': 'Neue Anwendung per Assistent', 'new plugin installed': 'Neues Plugin wurde installiert', +'New plugin installed: %s': 'New plugin installed: %s', 'New Record': 'Neuer Datensatz', 'new record inserted': 'Neuer wurde Datensatz eingefügt', 'New simple application': 'Neue einfache Anwendung', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'nƤchsten 100 Zeilen', 'Next Edit Point': 'nƤchster Bearbeitungsschritt', 'NO': 'NEIN', +'no changes': 'no changes', 'No databases in this application': 'Keine Datenbank in dieser Anwendung', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', 'no package selected': 'Kein Paket ausgewƤhlt', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': 'Kein ticket_storage.txt unter /private folder gefunden', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'Online Designer', +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', 'or alternatively': 'oder Alternativ', 'Or Get from URL:': 'oder von folgender URL herunterladen:', 'or import from csv file': 'oder importieren von cvs Datei', @@ -267,77 +430,144 @@ 'or provide application url:': 'oder geben Sie eine Anwendungs-URL an:', 'Origin': 'Herkunft', 'Original/Translation': 'Original/übersetzung', +'Overview': 'Overview', 'Overwrite installed app': 'Installierte Anwendungen überschreiben', 'Pack all': 'Verpacke alles', 'Pack compiled': 'Verpacke kompiliert', 'Pack custom': 'Verpacke individuell', 'pack plugin': 'Plugin verpacken', 'Password': 'Passwort', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'Dateiansicht', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', 'please wait!': 'Bitte warten!', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', 'Plugin "%s" in application': 'Plugin "%s" in Anwendung', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugins', 'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Unterstützt von', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'vorherige 100 zeilen', 'Previous Edit Point': 'vorheriger Bearbeitungsschritt', 'Private files': 'Private files', 'private files': 'private files', 'Project Progress': 'Projekt Fortschritt', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Abfrage:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', 'Rapid Search': 'Schnelle Suche', +'Record': 'Record', 'record': 'Datensatz', 'record does not exist': 'Datensatz existiert nicht', 'record id': 'Datensatz id', 'Record ID': 'Datensatz ID', -'register': 'Registrierung', +'Record id': 'Record id', +'refresh': 'refresh', 'Register': 'Registrieren', +'register': 'Registrierung', 'Registration key': 'Registrierungsschlüssel', 'reload': 'Neu laden', 'Reload routes': 'Routen neu laden', 'Remove compiled': 'Bytecode lƶschen', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Replace': 'Ersetzen', 'Replace All': 'Alle Ersetzen', +'Repository (%s)': 'Repository (%s)', 'request': 'Anfrage', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', 'Reset Password key': 'Passwortschlüssel zurücksetzen', 'Resolve Conflict file': 'bereinige Konflikt-Datei', 'response': 'Antwort', +'restart': 'restart', 'restore': 'wiederherstellen', +'return': 'return', +'Revert': 'Revert', 'revert': 'zurückkehren', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', 'Role': 'Rolle', +'Roles': 'Roles', 'Rows in table': 'Zeilen in Tabelle', +'Rows in Table': 'Rows in Table', 'Rows selected': 'Zeilen ausgewƤhlt', 'rules are not defined': 'Regeln sind nicht definiert', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Tests in dieser Datei ausführen (um alle Dateien auszuführen, kann auch der Button 'test' genutzt werden)", 'Running on %s': 'lƤuft auf %s', 'Save': 'Speichern', 'save': 'Speichern', 'Save file:': 'Speichere Datei:', 'Save file: %s': 'Speichere Datei: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'via Ajax speichern', 'Saved file hash:': 'Gespeicherter Datei-Hash:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', 'Select Files to Package': 'Dateien zum Paketieren wƤhlen', 'selected': 'ausgewƤhlt(e)', 'session': 'Sitzung', 'session expired': 'Sitzung abgelaufen', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'Shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'Seite', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'einige Dateien konnten nicht gelƶscht werden', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', 'source : filesystem': 'Quelle : Dateisystem', +'Start a new app': 'Start a new app', 'Start searching': 'Suche beginnen', 'Start wizard': 'Assistent starten', 'state': 'Status', 'Static': 'Statisch', 'static': 'statische Dateien', 'Static files': 'statische Dateien', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'Stylesheet': 'Stylesheet', -'Submit': 'Submit', 'submit': 'Absenden', +'Submit': 'Submit', +'successful': 'successful', 'Sure you want to delete this object?': 'Wollen Sie das Objekt wirklich lƶschen?', 'switch to : db': 'wechsel zu : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'Tabelle', +'Table': 'Table', 'Table name': 'Tabellen Name', +'Temporary': 'Temporary', 'test': 'Test', 'test_def': 'test_def', 'test_for': 'test_for', @@ -346,6 +576,8 @@ 'Testing application': 'Teste die Anwendung', 'Testing controller': 'Teste Controller', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'Die "query" ist eine Bedingung wie "db.table1.field1 == \'Wert\'". Etwas wie "db.table1.field1 db.table2.field2 ==" führt zu einem SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'Die Logik der Anwendung, jeder URL-Pfad wird auf eine Funktion abgebildet die der Controller zur Verfügung stellt', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Die Logik der Anwendung, jeder URL-Pfad wird auf eine Funktion abgebildet die der Controller zur Verfügung stellt', 'the data representation, define database tables and sets': 'Die DatenreprƤsentation definiert Mengen von Tabellen und Datenbanken', @@ -353,25 +585,39 @@ 'The output of the file is a dictionary that was rendered by the view': 'Die Ausgabe der Datei ist ein "dictionary" und wurde vom "view" gerendert', 'The presentations layer, views are also known as templates': 'Die PrƤsentationsschicht, Views sind auch bekannt als Vorlagen/Templates', 'the presentations layer, views are also known as templates': 'Die PrƤsentationsschicht, Views sind auch bekannt als Vorlagen/Templates', +'Theme': 'Theme', 'There are no controllers': 'Keine Controller vorhanden', 'There are no models': 'Keine Modelle vorhanden', 'There are no modules': 'Keine Module vorhanden', 'There are no plugins': 'Keine Plugins vorhanden', 'There are no private files': 'Keine privaten Dateien vorhanden', 'There are no static files': 'Keine statischen Dateien vorhanden', +'There are no translators': 'There are no translators', 'There are no translators, only default language is supported': 'Keine übersetzungen vorhanden, nur die voreingestellte Sprache wird Unterstützt', 'There are no views': 'Keine Views vorhanden', 'These files are not served, they are only available from within your app': 'Diese Dateien werden nicht ausgeliefert, sie sind nur innerhalb Ihrer App verfügbar', 'These files are served without processing, your images go here': 'Diese Dateien werden ohne Verarbeitung ausgeliefert. Beispielsweise Bilder kommen hier hin.', 'these files are served without processing, your images go here': 'Diese Dateien werden ohne Verarbeitung ausgeliefert. Beispielsweise Bilder kommen hier hin.', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", 'This is a copy of the scaffolding application': 'Dies ist eine Kopie einer Grundgerüst-Anwendung', +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', 'This is the %(filename)s template': 'Dies ist das Template %(filename)s', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Zeitstempel', 'TM': 'TM', 'to previous version.': 'zu einer früheren Version.', 'To create a plugin, name a file/folder plugin_[name]': 'Um ein Plugin zu erstellen benennen Sie eine(n) Datei/Ordner plugin_[Name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'toggle breakpoint': 'Breakpoint aktivieren/deaktivieren', 'Toggle comment': ' Kommentar ein-/ausblenden', 'Toggle Fullscreen': 'Vollbild ein-/ausschalten', @@ -382,18 +628,31 @@ 'try something like': 'Versuchen Sie so etwas wie', 'Try the mobile interface': 'Testen Sie das Interface für Handys', 'try view': 'Versuche view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', 'Unable to check for upgrades': 'Überprüfen von Upgrades nicht mƶglich', 'unable to create application "%s"': 'Erzeugen von Anwendung "%s" nicht mƶglich', 'unable to delete file "%(filename)s"': 'Lƶschen von Dateien "%(filename)s" nicht mƶglich', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', 'Unable to download': 'Herunterladen nicht mƶglich', 'Unable to download app': 'Herunterladen der Anwendung nicht mƶglich', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'Analysieren der cvs Datei nicht mƶglich', 'unable to uninstall "%s"': 'Deinstallieren von "%s" nicht mƶglich', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'Auswahl entfernen', 'Uninstall': 'Deinstallieren', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'Aktualisieren', 'update all languages': 'Aktualisiere alle Sprachen', 'Update:': 'Aktualisiere:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'web2py jetzt upgraden', 'upload': 'upload', 'Upload': 'Upload', @@ -407,26 +666,49 @@ 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Benutze (...)&(...) für AND, (...)|(...) für OR, und ~(...) für NOT, um komplexe Abfragen zu erstellen.', 'Use an url:': 'Verwende URL:', 'user': 'Nutzer', +'User': 'User', 'User ID': 'Benutzer ID', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'Variablen', 'Version': 'Version', 'Version %s.%s.%s (%s) %s': 'Version %s.%s.%s (%s) %s', -'versioning': 'Versionsverwaltung', 'Versioning': 'Versionsverwaltung', +'versioning': 'Versionsverwaltung', 'View': 'Ansicht', 'view': 'Ansicht', 'Views': 'Ansichten', 'views': 'Ansichten', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py ist aktuell', +'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'Neuste Tweets von web2py', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', 'Welcome %s': 'Willkommen %s', 'Welcome to web2py': 'Willkommen zu web2py', 'Which called the function': 'welche die Funktion aufrief', +'Working...': 'Working...', 'Wrap with Abbreviation': 'mit Kürzel einhüllen', +'WSGI reference name': 'WSGI reference name', 'xml': 'xml', 'YES': 'JA', +'Yes': 'Yes', 'You are successfully running web2py': 'web2by wird erfolgreich ausgeführt', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You can modify this application and adapt it to your needs': 'Sie kƶnnen diese Anwendung verƤndern und Ihren Bedürfnissen anpassen', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', 'You visited the url': 'Sie besuchten die URL', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/default.py b/applications/admin/languages/default.py deleted file mode 100755 index 779145a8..00000000 --- a/applications/admin/languages/default.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- coding: utf-8 -*- -{ -'!langcode!': 'en-us', -'!langname!': 'English (US)', -'%Y-%m-%d': '%m-%d-%Y', -'%Y-%m-%d %H:%M:%S': '%m-%d-%Y %H:%M:%S', -'(requires internet access)': '(requires internet access)', -'(something like "it-it")': '(something like "it-it")', -'About': 'About', -'Additional code for your application': 'Additional code for your application', -'admin disabled because unable to access password file': 'admin disabled because unable to access password file', -'Admin language': 'Admin language', -'administrative interface': 'administrative interface', -'Administrator Password:': 'Administrator Password:', -'and rename it:': 'and rename it:', -'Application name:': 'Application name:', -'are not used yet': 'are not used yet', -'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', -'arguments': 'arguments', -'at char %s': 'at char %s', -'at line %s': 'at line %s', -'back': '<': '>', '>=': '>=', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Căutare: **%s** fișiere', 'A new version of web2py is available': 'O nouă versiune de web2py este disponibilă', 'A new version of web2py is available: %s': 'O nouă versiune de web2py este disponibilă: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENȚIE: Nu vă puteți conecta decĆ¢t utilizĆ¢nd o conexiune securizată (HTTPS) sau rulĆ¢nd aplicația pe computerul local.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENȚIE: Nu puteți efectua mai multe teste o dată deoarece lansarea Ć®n execuție a mai multor subpocese nu este sigură.', -'ATTENTION: you cannot edit the running application!': 'ATENȚIE: nu puteți edita o aplicație Ć®n curs de execuție!', 'Abort': 'Anulează', 'About': 'Despre', +'about': 'despre', 'About application': 'Despre aplicație', +'Accept Terms': 'Accept Terms', 'Access Control': 'Control acces', 'Add': 'Adaugă', +'Add breakpoint': 'Add breakpoint', +'additional code for your application': 'cod suplimentar pentru aplicația dvs.', 'Additional code for your application': 'Cod suplimentar pentru aplicație', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'administrare dezactivată deoarece parola de administrator nu a fost furnizată', +'admin disabled because not supported on google app engine': 'administrare dezactivată deoarece funcționalitatea nu e suportat pe Google App Engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'administrare dezactivată deoarece nu există acces la fișierul cu parole', 'Admin is disabled because insecure channel': 'Adminstrarea este dezactivată deoarece conexiunea nu este sigură', 'Admin is disabled because unsecure channel': 'Administrarea este dezactivată deoarece conexiunea nu este securizată', 'Admin language': 'Limba de administrare', +'Admin versioning page': 'Admin versioning page', 'Administration': 'Administrare', +'administrative interface': 'interfață de administrare', 'Administrative Interface': 'Interfață administrare', 'Administrator Password:': 'Parolă administrator:', 'Ajax Recipes': 'Rețete Ajax', 'And': 'Și', +'and rename it (required):': 'și renumește (obligatoriu):', +'and rename it:': ' și renumește:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin dezactivat deoarece conexiunea nu e sigură', +'Application': 'Application', +'application "%s" uninstalled': 'aplicația "%s" a fost dezinstalată', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'aplicația a fost compilată', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'aplicația este compilată și nu poate fi editată', 'Application name:': 'Nume aplicație:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'Sigur ștergeți fișierul "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Sigur ștergeți acest obiect?', 'Are you sure you want to uninstall application "%s"': 'Sigur dezinstalați aplicația "%s"', 'Are you sure you want to uninstall application "%s"?': 'Sigur dezinstalați aplicația "%s"?', +'Are you sure?': 'Are you sure?', +'arguments': 'argumente', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENȚIE: Nu vă puteți conecta decĆ¢t utilizĆ¢nd o conexiune securizată (HTTPS) sau rulĆ¢nd aplicația pe computerul local.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENȚIE: Nu puteți efectua mai multe teste o dată deoarece lansarea Ć®n execuție a mai multor subpocese nu este sigură.', +'ATTENTION: you cannot edit the running application!': 'ATENȚIE: nu puteți edita o aplicație Ć®n curs de execuție!', 'Authentication': 'Autentificare', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Baze de date și tabele disponibile', +'Available Databases and Tables': 'Available Databases and Tables', 'Back': 'Ǝnapoi', +'back': 'Ć®napoi', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', 'Begin': 'Ǝnceput', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', 'Buy this book': 'Cumpără această carte', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Chei cache', +'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'Nu poate fi vid', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Compilare imposibilă: aplicația conține erori. Debogați aplicația și Ć®ncercați din nou.', -'Change Password': 'Schimbare parolă', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'fișier imposibil de creat', +'cannot upload file "%(filename)s"': 'imposibil de Ć®ncărcat fișierul "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Schimbă parola de administrare', +'change editor settings': 'change editor settings', +'Change Password': 'Schimbare parolă', 'Change password': 'Schimbare parolă', +'change password': 'schimbare parolă', +'Changelog': 'Changelog', +'check all': 'coșează tot', 'Check for upgrades': 'Verifică dacă există upgrade-uri', 'Check to delete': 'Coșați pentru a șterge', 'Checking for upgrades...': 'Verifică dacă există actualizări...', +'clean': 'golire', 'Clean': 'Curăță', 'Clear': 'Golește', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Click row to expand traceback': 'Clic pe linie pentru a extinde mesajul de trasabilitate', 'Click row to view a ticket': 'Click row to view a ticket', +'click to check for upgrades': 'Clic pentru a verifica dacă există upgrade-uri', 'Client IP': 'IP client', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'restrĆ¢nge/extinde tot', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Community': 'Comunitate', 'Compile': 'Compilează', +'compile': 'compilare', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'aplicația compilată a fost ștearsă', 'Components and Plugins': 'Componente și plugin-uri', +'Condition': 'Condition', +'contains': 'conține', +'continue': 'continue', 'Controller': 'Controlor', 'Controllers': 'Controlori', +'controllers': 'controlori', 'Copyright': 'Drepturi de autor', 'Count': 'Număr', 'Create': 'Crează', +'create file with filename:': 'crează fișier cu numele:', 'Create new application': 'Creați aplicație nouă', +'create new application:': 'crează aplicație nouă:', +'Create/Upload': 'Create/Upload', +'created by': 'creat de', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'crontab', 'Current request': 'Cerere curentă', 'Current response': 'Răspuns curent', 'Current session': 'Sesiune curentă', -'DB Model': 'Model bază de date', -'DESIGN': 'DESIGN', -'Database': 'Baza de date', -'Date and Time': 'Data și ora', -'Debug': 'Debogare', -'Delete': 'Șterge', -'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', -'Delete:': 'Șterge:', -'Demo': 'Demo', -'Deploy': 'Instalare', -'Deploy on Google App Engine': 'Instalare pe Google App Engine', -'Deployment Recipes': 'Rețete de instalare', -'Description': 'Descriere', -'Design for': 'Design pentru', -'Detailed traceback description': 'Descriere detaliată a mesajului de trasabilitate', -'Disable': 'Dezactivează', -'Disk Cache Keys': 'Chei cache de disc', -'Documentation': 'Documentație', -"Don't know what to do?": 'Nu știți ce să faceți?', -'Download': 'Descărcare', -'E-mail': 'E-mail', -'E-mail invalid': 'E-mail invalid', -'EDIT': 'EDITARE', -'Edit': 'Editare', -'Edit Profile': 'Editare profil', -'Edit This App': 'Editați această aplicație', -'Edit application': 'Editare aplicație', -'Edit current record': 'Editare Ć®nregistrare curentă', -'Editing file': 'Editare fișier', -'Editing file "%s"': 'Editare fișier "%s"', -'Email and SMS': 'E-mail și SMS', -'Error': 'Eroare', -'Error logs for "%(app)s"': 'Log erori pentru "%(app)s"', -'Error snapshot': 'Instantaneu eroare', -'Error ticket': 'Tichet eroare', -'Errors': 'Erori', -'Exception instance attributes': 'Exception instance attributes', -'Expand Abbreviation': 'Extinde abreviația', -'Export': 'Export', -'FAQ': 'Ǝntrebări frecvente', -'False': 'Neadevărat', -'File': 'Fișier', -'First name': 'Prenume', -'Forbidden': 'Interzis', -'Forms and Validators': 'Formulare și validatori', -'Frames': 'Frames', -'Free Applications': 'Aplicații gratuite', -'Functions with no doctests will result in [passed] tests.': 'Funcțiile fără doctests vor genera teste [trecute].', -'Get from URL:': 'Obține de la adresa:', -'Go to Matching Pair': 'Mergi la perechea care se potrivește', -'Group %(group_id)s created': 'Grup %(group_id)s creat', -'Group ID': 'ID grup', -'Group uniquely assigned to user %(id)s': 'Grup asociat Ć®n mod unic utilizatorului %(id)s', -'Groups': 'Grupuri', -'Hello World': 'Salutare lume', -'Help': 'Ajutor', -'Home': 'Acasă', -'How did you get here?': 'Cum ați ajuns aici?', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\r\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Dacă raportul de deasupra conține un număr de tichet, asta indică un eșec Ć®n executarea controlorului, Ć®nainte de orice Ć®ncercare de a executa doctests-urile. Aceasta se Ć®ntĆ¢mplă de obicei din cauza unei erori de identare sau a unei erori din afara corpului funcției.\r\nUn titlu Ć®n verde arată ca toate testele (dacă definite) au fost trecute. Ǝn acest caz rezultatele testelor nu sunt arătate.', -'Import/Export': 'Import/Export', -'Index': 'Index', -'Install': 'Instalează', -'Installed applications': 'Aplicații instalate', -'Internal State': 'Stare internă', -'Introduction': 'Introducere', -'Invalid Query': 'Interogare invalidă', -'Invalid action': 'Acțiune invalidă', -'Invalid email': 'E-mail invalid', -'Invalid password': 'Parolă invalidă', -'Key bindings': 'Combinație taste', -'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', -'Language files (static strings) updated': 'Fișierele de limbă (șirurile statice de caractere) actualizate', -'Languages': 'Limbi', -'Last name': 'Nume', -'Last saved on:': 'Ultima salvare:', -'Layout': 'Șablon', -'Layout Plugins': 'Șablon plugin-uri', -'Layouts': 'Șabloane', -'License for': 'Licență pentru', -'Live Chat': 'Chat live', -'Logged in': 'Logat', -'Logged out': 'Delogat', -'Login': 'Autentificare', -'Login to the Administrative Interface': 'Logare interfață de administrare', -'Logout': 'Ieșire', -'Lost Password': 'Parolă pierdută', -'Lost password?': 'Parolă pierdută?', -'Main Menu': 'Meniu principal', -'Match Pair': 'Potrivește pereche', -'Menu Model': 'Model meniu', -'Merge Lines': 'Unește linii', -'Models': 'Modele', -'Modules': 'Module', -'My Sites': 'Site-urile mele', -'NO': 'NU', -'Name': 'Nume', -'New': 'Nou', -'New Application Wizard': 'New Application Wizard', -'New Record': 'Ǝnregistrare nouă', -'New application wizard': 'Magician aplicație nouă', -'New password': 'Parola nouă', -'New simple application': 'O nouă aplicație simplă', -'Next Edit Point': 'Următorul punct de editare', -'No databases in this application': 'Aplicație fără bază de date', -'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', -'OR': 'SAU', -'Object or table name': 'Obiect sau nume de tabel', -'Old password': 'Parola veche', -'Online examples': 'Exemple online', -'Or': 'Sau', -'Origin': 'Origine', -'Original/Translation': 'Original/Traducere', -'Other Plugins': 'Alte plugin-uri', -'Other Recipes': 'Alte rețete', -'Overview': 'Prezentare de ansamblu', -'Overwrite installed app': 'Suprascrie aplicație instalată', -'Pack all': 'Ǝmpachetează tot', -'Password': 'Parola', -"Password fields don't match": 'CĆ¢mpurile de parolă nu se potrivesc', -'Peeking at file': 'Vizualizare fișier', -'Plugins': 'Plugin-uri', -'Powered by': 'Pus Ć®n mișcare de', -'Preface': 'Prefață', -'Previous Edit Point': 'Punct de editare anterior', -'Profile': 'Profil', -'Project Progress': 'Progres proiect', -'Python': 'Python', -'Query': 'Interogare', -'Query:': 'Interogare:', -'Quick Examples': 'Exemple rapide', -'RAM Cache Keys': 'Chei cache RAM', -'Recipes': 'Rețete', -'Record ID': 'ID Ć®nregistrare', -'Register': 'Ǝnregistrare', -'Registration identifier': 'Identificator de autentificare', -'Registration key': 'Cheie Ć®nregistrare', -'Registration successful': 'Autentificare reușită', -'Reload routes': 'ReĆ®ncarcare rute', -'Remember me (for 30 days)': 'Ține-mă minte (timp de 30 de zile)', -'Request reset password': 'Cerere resetare parolă', -'Reset Password key': 'Cheie resetare parolă', -'Resolve Conflict file': 'Rezolvă conflict fișier', -'Role': 'Rol', -'Rows in table': 'Linii Ć®n tabel', -'Rows selected': 'Linii selectate', -"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", -'Save': 'Salvează', -'Save profile': 'Salvează profil', -'Save via Ajax': 'Salvează utilizĆ¢nd Ajax', -'Saved file hash:': 'Hash fișier salvat:', -'Search': 'Căutare', -'Semantic': 'Semantică', -'Services': 'Servicii', -'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', -'Site': 'Site', -'Sorry, could not find mercurial installed': 'Scuze, dar n-am gasit unde este instalat mercurial', -'Start a new app': 'Ǝncepe o nouă aplicație', -'Start wizard': 'Startează magician', -'Static files': 'Fișiere statice', -'Stylesheet': 'Foaie de stiluri', -'Submit': 'Ǝnregistrează', -'Support': 'Suport', -'Sure you want to delete this object?': 'Sigur ștergeți acest obiect?', -'Table name': 'Nume tabel', -'Testing application': 'Testare aplicație', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Interogarea (query)" este o condiție de tipul "db.tabel1.cĆ¢mp1==\'valoare\'". Ceva de genul "db.tabel1.cĆ¢mp1==db.tabel2.cĆ¢mp2" generează un JOIN SQL.', -'The Core': 'Nucleul', -'The Views': 'Vederile', -'The application logic, each URL path is mapped in one exposed function in the controller': 'Logica aplicației, fiecare rută URL este mapată Ć®ntr-o funcție expusă de controlor', -'The data representation, define database tables and sets': 'Reprezentarea datelor, definește tabele și seturi de date', -'The output of the file is a dictionary that was rendered by the view': 'Fișierul produce un dicționar care a fost prelucrat de vederea', -'The presentations layer, views are also known as templates': 'Nivelul de prezentare, vederile sunt cunoscute de asemenea ca șabloane', -'There are no controllers': 'Nu există controlori', -'There are no models': 'Nu există modele', -'There are no modules': 'Nu există module', -'There are no plugins': 'Nu există plugin-uri', -'There are no static files': 'Nu există fișiere statice', -'There are no translators, only default language is supported': 'Nu există traduceri, doar limba implicită este suportată', -'There are no views': 'Nu există vederi', -'These files are served without processing, your images go here': 'Aceste fișiere sunt servite fără procesare, fișierele imagine se pun aici', -'This App': 'Această aplicație', -'This is a copy of the scaffolding application': 'Aceasta este o copie a aplicației schelet', -'This is the %(filename)s template': 'Aceasta este șablonul fișierului %(filename)s', -'Ticket': 'Tichet', -'Ticket ID': 'ID tichet', -'Timestamp': 'Moment Ć®n timp (timestamp)', -'To create a plugin, name a file/folder plugin_[name]': 'Pentru a crea un plugin, numește un fișier/director plugin_[nume]', -'Traceback': 'Trasabilitate', -'Translation strings for the application': 'Șiruri de traducere pentru aplicație', -'True': 'Adevărat', -'Twitter': 'Twitter', -'Unable to check for upgrades': 'Imposibil de verificat dacă există actualizări', -'Unable to download': 'Imposibil de descărcat', -'Unable to download app': 'Imposibil de descărcat aplicația', -'Uninstall': 'Dezinstalează', -'Update:': 'Actualizare:', -'Upload a package:': 'Ǝncarcă un pachet:', -'Upload and install packed application': 'Ǝncarcă și instalează aplicație Ć®mpachetată', -'Upload existing application': 'Ǝncarcă aplicația existentă', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Folosiți (...)&(...) pentru ȘI, (...)|(...) pentru SAU, și ~(...) pentru NEGARE, pentru a crea interogări complexe.', -'User %(id)s Logged-in': 'Utilizator %(id)s autentificat', -'User %(id)s Logged-out': 'Utilizator %(id)s delogat', -'User %(id)s Password changed': 'Parola utilizatorului %(id)s a fost schimbată', -'User %(id)s Password reset': 'Resetare parola utilizator %(id)s', -'User %(id)s Profile updated': 'Profil utilizator %(id)s actualizat', -'User %(id)s Registered': 'Utilizator %(id)s Ć®nregistrat', -'User ID': 'ID utilizator', -'VERSION': 'VERSIUNE', -'Verify Password': 'Verifică parola', -'Version': 'Version', -'Version %s.%s.%s (%s) %s': 'Versiune %s.%s.%s (%s) %s', -'Versioning': 'Versiuni', -'Videos': 'Video-uri', -'View': 'Vedere', -'Views': 'Vederi', -'Web Framework': 'Framework web', -'Welcome': 'Bine ați venit', -'Welcome %s': 'Bine ați venit %s', -'Welcome to web2py': 'Bun venit la web2py', -'Welcome to web2py!': 'Bun venit la web2py!', -'Which called the function': 'Care a apelat funcția', -'Wrap with Abbreviation': 'Ǝncadrează cu abreviația', -'YES': 'DA', -'You are successfully running web2py': 'Rulați cu succes web2py', -'You can modify this application and adapt it to your needs': 'Puteți modifica și adapta aplicația nevoilor dvs.', -'You visited the url': 'Ați vizitat adresa', -'about': 'despre', -'additional code for your application': 'cod suplimentar pentru aplicația dvs.', -'admin disabled because no admin password': 'administrare dezactivată deoarece parola de administrator nu a fost furnizată', -'admin disabled because not supported on google app engine': 'administrare dezactivată deoarece funcționalitatea nu e suportat pe Google App Engine', -'admin disabled because unable to access password file': 'administrare dezactivată deoarece nu există acces la fișierul cu parole', -'administrative interface': 'interfață de administrare', -'and rename it (required):': 'și renumește (obligatoriu):', -'and rename it:': ' și renumește:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin dezactivat deoarece conexiunea nu e sigură', -'application "%s" uninstalled': 'aplicația "%s" a fost dezinstalată', -'application compiled': 'aplicația a fost compilată', -'application is compiled and cannot be designed': 'aplicația este compilată și nu poate fi editată', -'arguments': 'argumente', -'back': 'Ć®napoi', -'cache': 'cache', -'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', -'cannot create file': 'fișier imposibil de creat', -'cannot upload file "%(filename)s"': 'imposibil de Ć®ncărcat fișierul "%(filename)s"', -'change password': 'schimbare parolă', -'check all': 'coșează tot', -'clean': 'golire', -'click to check for upgrades': 'Clic pentru a verifica dacă există upgrade-uri', -'code': 'code', -'collapse/expand all': 'restrĆ¢nge/extinde tot', -'compile': 'compilare', -'compiled application removed': 'aplicația compilată a fost ștearsă', -'contains': 'conține', -'controllers': 'controlori', -'create file with filename:': 'crează fișier cu numele:', -'create new application:': 'crează aplicație nouă:', -'created by': 'creat de', -'crontab': 'crontab', 'currently running': 'acum Ć®n funcționare', 'currently saved or': 'Ć®n prezent salvat sau', 'customize me!': 'Personalizează-mă!', 'data uploaded': 'date Ć®ncărcate', +'Database': 'Baza de date', 'database': 'bază de date', +'Database %s select': 'Database %s select', 'database %s select': 'selectare bază de date %s', +'Database administration': 'Database administration', 'database administration': 'administrare bază de date', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Data și ora', 'db': 'db', +'DB Model': 'Model bază de date', +'Debug': 'Debogare', 'defines tables': 'definire tabele', +'Delete': 'Șterge', 'delete': 'șterge', 'delete all checked': 'șterge tot ce e coșat', +'delete plugin': 'delete plugin', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', +'Delete:': 'Șterge:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', +'Deploy': 'Instalare', +'Deploy on Google App Engine': 'Instalare pe Google App Engine', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Deployment Recipes': 'Rețete de instalare', +'Description': 'Descriere', +'Description:': 'Description:', 'design': 'design', +'DESIGN': 'DESIGN', +'Design for': 'Design pentru', +'Detailed traceback description': 'Descriere detaliată a mesajului de trasabilitate', 'details': 'detalii', 'direction: ltr': 'direcție: stĆ¢nga-sus-dreapta', +'directory not found': 'directory not found', +'Disable': 'Dezactivează', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Chei cache de disc', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'documentație', +'Docs': 'Docs', +'Documentation': 'Documentație', +"Don't know what to do?": 'Nu știți ce să faceți?', 'done!': 'gata!', +'Downgrade': 'Downgrade', +'Download': 'Descărcare', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'descărcare șabloane', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'descărcare plugin-uri', +'Download plugins from repository': 'Download plugins from repository', +'E-mail': 'E-mail', +'E-mail invalid': 'E-mail invalid', 'edit': 'editare', +'EDIT': 'EDITARE', +'Edit': 'Editare', +'edit all': 'edit all', +'Edit application': 'Editare aplicație', 'edit controller': 'editare controlor', +'edit controller:': 'edit controller:', +'Edit current record': 'Editare Ć®nregistrare curentă', +'Edit Profile': 'Editare profil', 'edit profile': 'editare profil', +'Edit This App': 'Editați această aplicație', 'edit views:': 'editează vederi:', +'Editing %s': 'Editing %s', +'Editing file': 'Editare fișier', +'Editing file "%s"': 'Editare fișier "%s"', +'Editing Language file': 'Editing Language file', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Email and SMS': 'E-mail și SMS', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'enter a number between %(min)g and %(max)g': 'introduceți un număr Ć®ntre %(min)g și %(max)g', 'enter an integer between %(min)g and %(max)g': 'introduceți un Ć®ntreg Ć®ntre %(min)g și %(max)g', +'Error': 'Eroare', +'Error logs for "%(app)s"': 'Log erori pentru "%(app)s"', +'Error snapshot': 'Instantaneu eroare', +'Error ticket': 'Tichet eroare', 'errors': 'erori', +'Errors': 'Erori', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation': 'Extinde abreviația', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', +'Export': 'Export', 'export as csv file': 'exportă ca fișier csv', +'Exports:': 'Exports:', 'exposes': 'expune', 'exposes:': 'expune:', 'extends': 'extinde', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'reĆ®ncarcare modul nereușită', +'failed to reload module because:': 'failed to reload module because:', +'False': 'Neadevărat', +'FAQ': 'Ǝntrebări frecvente', +'File': 'Fișier', 'file "%(filename)s" created': 'fișier "%(filename)s" creat', 'file "%(filename)s" deleted': 'fișier "%(filename)s" șters', 'file "%(filename)s" uploaded': 'fișier "%(filename)s" Ć®ncărcat', @@ -368,92 +286,503 @@ 'file "%s" of %s restored': 'fișier "%s" de %s restaurat', 'file changed on disk': 'fișier modificat pe disc', 'file does not exist': 'fișier inexistent', +'file not found': 'file not found', 'file saved on %(time)s': 'fișier salvat %(time)s', 'file saved on %s': 'fișier salvat pe %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'filtru', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'First name': 'Prenume', +'Forbidden': 'Interzis', +'Form has errors': 'Form has errors', +'Forms and Validators': 'Formulare și validatori', +'Frames': 'Frames', +'Free Applications': 'Aplicații gratuite', +'Functions with no doctests will result in [passed] tests.': 'Funcțiile fără doctests vor genera teste [trecute].', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Get from URL:': 'Obține de la adresa:', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'Go to Matching Pair': 'Mergi la perechea care se potrivește', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', +'Group %(group_id)s created': 'Grup %(group_id)s creat', +'Group ID': 'ID grup', +'Group uniquely assigned to user %(id)s': 'Grup asociat Ć®n mod unic utilizatorului %(id)s', +'Groups': 'Grupuri', +'Hello World': 'Salutare lume', 'help': 'ajutor', +'Help': 'Ajutor', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Acasă', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', +'How did you get here?': 'Cum ați ajuns aici?', 'htmledit': 'editare html', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\r\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Dacă raportul de deasupra conține un număr de tichet, asta indică un eșec Ć®n executarea controlorului, Ć®nainte de orice Ć®ncercare de a executa doctests-urile. Aceasta se Ć®ntĆ¢mplă de obicei din cauza unei erori de identare sau a unei erori din afara corpului funcției.\r\nUn titlu Ć®n verde arată ca toate testele (dacă definite) au fost trecute. Ǝn acest caz rezultatele testelor nu sunt arătate.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'include', +'Indent with tabs': 'Indent with tabs', +'Index': 'Index', 'index': 'index', 'insert new': 'adaugă nou', 'insert new %s': 'adaugă nou %s', 'inspect attributes': 'inspectare atribute', +'Install': 'Instalează', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'Aplicații instalate', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'eroare internă', +'internal error: %s': 'internal error: %s', +'Internal State': 'Stare internă', +'Introduction': 'Introducere', +'Invalid action': 'Acțiune invalidă', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid email': 'E-mail invalid', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'parolă invalidă', +'Invalid password': 'Parolă invalidă', +'invalid password.': 'invalid password.', +'Invalid Query': 'Interogare invalidă', 'invalid request': 'cerere invalidă', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'tichet invalid', +'Key': 'Key', +'Key bindings': 'Combinație taste', +'Key bindings for ZenCoding Plugin': 'Key bindings for ZenCoding Plugin', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'fișier de limbă "%(filename)s" creat/actualizat', +'Language files (static strings) updated': 'Fișierele de limbă (șirurile statice de caractere) actualizate', 'languages': 'limbi', +'Languages': 'Limbi', 'languages updated': 'limbi actualizate', +'Last name': 'Nume', +'Last Revision': 'Last Revision', +'Last saved on:': 'Ultima salvare:', +'Layout': 'Șablon', +'Layout Plugins': 'Șablon plugin-uri', +'Layouts': 'Șabloane', +'License for': 'Licență pentru', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Live Chat': 'Chat live', +'Loading...': 'Loading...', 'loading...': 'Ć®ncarc...', +'Local Apps': 'Local Apps', 'locals': 'localizare', +'Locals##debug': 'Locals##debug', 'located in the file': 'prezentă Ć®n fișierul', +'Logged in': 'Logat', +'Logged out': 'Delogat', 'login': 'autentificare', +'Login': 'Autentificare', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'Logare interfață de administrare', +'Login/Register': 'Login/Register', 'logout': 'ieșire', +'Logout': 'Ieșire', +'Lost Password': 'Parolă pierdută', +'lost password': 'lost password', +'Lost password?': 'Parolă pierdută?', +'Main Menu': 'Meniu principal', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Match Pair': 'Potrivește pereche', +'Memberships': 'Memberships', +'Menu Model': 'Model meniu', 'merge': 'unește', +'Merge Lines': 'Unește linii', +'Models': 'Modele', 'models': 'modele', +'Modified On': 'Modified On', +'Modules': 'Module', 'modules': 'module', +'Multi User Mode': 'Multi User Mode', +'My Sites': 'Site-urile mele', +'Name': 'Nume', +'New': 'Nou', 'new application "%s" created': 'aplicația nouă "%s" a fost creată', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', +'New application wizard': 'Magician aplicație nouă', +'New password': 'Parola nouă', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'Ǝnregistrare nouă', 'new record inserted': 'Ć®nregistrare nouă adăugată', +'New simple application': 'O nouă aplicație simplă', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'următoarele 100 de linii', +'Next Edit Point': 'Următorul punct de editare', +'NO': 'NU', +'no changes': 'no changes', +'No databases in this application': 'Aplicație fără bază de date', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +'Object or table name': 'Obiect sau nume de tabel', +'Old password': 'Parola veche', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'designer online', +'Online examples': 'Exemple online', +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'Or': 'Sau', +'OR': 'SAU', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'sau importă din fișier csv', 'or provide application url:': 'sau furnizează adresă url:', +'Origin': 'Origine', +'Original/Translation': 'Original/Traducere', +'Other Plugins': 'Alte plugin-uri', +'Other Recipes': 'Alte rețete', +'Overview': 'Prezentare de ansamblu', +'Overwrite installed app': 'Suprascrie aplicație instalată', 'pack all': 'Ć®mpachetează toate', +'Pack all': 'Ǝmpachetează tot', +'Pack compiled': 'Pack compiled', 'pack compiled': 'Ć®mpachetează ce e compilat', +'Pack custom': 'Pack custom', +'pack plugin': 'pack plugin', +'Password': 'Parola', +'password changed': 'password changed', +"Password fields don't match": 'CĆ¢mpurile de parolă nu se potrivesc', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': 'Vizualizare fișier', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', 'please input your password again': 'introduceți parola din nou', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugin-uri', +'Plugins': 'Plugin-uri', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': 'Pus Ć®n mișcare de', +'Preface': 'Prefață', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '100 de linii anterioare', +'Previous Edit Point': 'Punct de editare anterior', +'Private files': 'Private files', +'private files': 'private files', +'Profile': 'Profil', +'Project Progress': 'Progres proiect', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'Python': 'Python', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query': 'Interogare', +'Query:': 'Interogare:', +'Quick Examples': 'Exemple rapide', +'RAM': 'RAM', +'RAM Cache Keys': 'Chei cache RAM', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Recipes': 'Rețete', +'Record': 'Record', 'record': 'Ć®nregistrare', 'record does not exist': 'Ć®nregistrare inexistentă', 'record id': 'id Ć®nregistrare', +'Record ID': 'ID Ć®nregistrare', +'Record id': 'Record id', +'refresh': 'refresh', 'register': 'Ć®nregistrare', +'Register': 'Ǝnregistrare', +'Registration identifier': 'Identificator de autentificare', +'Registration key': 'Cheie Ć®nregistrare', +'Registration successful': 'Autentificare reușită', +'Reload routes': 'ReĆ®ncarcare rute', +'Remember me (for 30 days)': 'Ține-mă minte (timp de 30 de zile)', +'Remove compiled': 'Remove compiled', 'remove compiled': 'șterge compilate', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', 'request': 'cerere', +'Request reset password': 'Cerere resetare parolă', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Reset Password key': 'Cheie resetare parolă', +'Resolve Conflict file': 'Rezolvă conflict fișier', 'response': 'răspuns', +'restart': 'restart', 'restore': 'restaurare', +'return': 'return', +'Revert': 'Revert', 'revert': 'revenire', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Rol', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows in table': 'Linii Ć®n tabel', +'Rows selected': 'Linii selectate', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'Running on %s', +'Save': 'Salvează', 'save': 'salvare', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save profile': 'Salvează profil', +'Save via Ajax': 'Salvează utilizĆ¢nd Ajax', +'Saved file hash:': 'Hash fișier salvat:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Căutare', +'Select Files to Package': 'Select Files to Package', 'selected': 'selectat', +'Semantic': 'Semantică', +'Services': 'Servicii', 'session': 'sesiune', 'session expired': 'sesiune expirată', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'line de commandă', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'site': 'site', +'Site': 'Site', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'anumite fișiere n-au putut fi șterse', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Scuze, dar n-am gasit unde este instalat mercurial', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Ǝncepe o nouă aplicație', +'Start searching': 'Start searching', +'Start wizard': 'Startează magician', 'starts with': 'Ć®ncepe cu', 'state': 'stare', +'Static': 'Static', 'static': 'static', +'Static files': 'Fișiere statice', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', +'Stylesheet': 'Foaie de stiluri', +'submit': 'submit', +'Submit': 'Ǝnregistrează', 'successful': 'cu succes', +'Support': 'Suport', +'Sure you want to delete this object?': 'Sigur ștergeți acest obiect?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'tabel', +'Table': 'Table', +'Table name': 'Nume tabel', +'Temporary': 'Temporary', 'test': 'test', +'Testing application': 'Testare aplicație', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Interogarea (query)" este o condiție de tipul "db.tabel1.cĆ¢mp1==\'valoare\'". Ceva de genul "db.tabel1.cĆ¢mp1==db.tabel2.cĆ¢mp2" generează un JOIN SQL.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'logica aplicației, fiecare rută URL este mapată Ć®ntr-o funcție expusă de controlor', +'The application logic, each URL path is mapped in one exposed function in the controller': 'Logica aplicației, fiecare rută URL este mapată Ć®ntr-o funcție expusă de controlor', +'The Core': 'Nucleul', 'the data representation, define database tables and sets': 'reprezentarea datelor, definește tabelele bazei de date și seturile (de date)', +'The data representation, define database tables and sets': 'Reprezentarea datelor, definește tabele și seturi de date', +'The output of the file is a dictionary that was rendered by the view': 'Fișierul produce un dicționar care a fost prelucrat de vederea', +'The presentations layer, views are also known as templates': 'Nivelul de prezentare, vederile sunt cunoscute de asemenea ca șabloane', 'the presentations layer, views are also known as templates': 'nivelul de prezentare, vederile sunt de asemenea numite și șabloane', +'The Views': 'Vederile', +'Theme': 'Theme', +'There are no controllers': 'Nu există controlori', +'There are no models': 'Nu există modele', +'There are no modules': 'Nu există module', +'There are no plugins': 'Nu există plugin-uri', +'There are no private files': 'There are no private files', +'There are no static files': 'Nu există fișiere statice', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'Nu există traduceri, doar limba implicită este suportată', +'There are no views': 'Nu există vederi', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'Aceste fișiere sunt servite fără procesare, fișierele imagine se pun aici', 'these files are served without processing, your images go here': 'aceste fișiere sunt servite fără procesare, imaginea se plasează acolo', +'This App': 'Această aplicație', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is a copy of the scaffolding application': 'Aceasta este o copie a aplicației schelet', +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'Aceasta este șablonul fișierului %(filename)s', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Tichet', +'Ticket ID': 'ID tichet', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Timestamp': 'Moment Ć®n timp (timestamp)', 'to previous version.': 'la versiunea anterioară.', +'To create a plugin, name a file/folder plugin_[name]': 'Pentru a crea un plugin, numește un fișier/director plugin_[nume]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'toggle breakpoint': 'comutare breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', 'too short': 'prea scurt', +'Traceback': 'Trasabilitate', 'translation strings for the application': 'șiruri de caractere folosite la traducerea aplicației', +'Translation strings for the application': 'Șiruri de traducere pentru aplicație', +'True': 'Adevărat', 'try': 'Ć®ncearcă', 'try something like': 'Ć®ncearcă ceva de genul', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Twitter': 'Twitter', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Imposibil de verificat dacă există actualizări', 'unable to create application "%s"': 'imposibil de creat aplicația "%s"', 'unable to delete file "%(filename)s"': 'imposibil de șters fișierul "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': 'Imposibil de descărcat', +'Unable to download app': 'Imposibil de descărcat aplicația', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'imposibil de analizat fișierul csv', 'unable to uninstall "%s"': 'imposibil de dezinstalat "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'decoșează tot', 'uninstall': 'dezinstalează', +'Uninstall': 'Dezinstalează', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'actualizează', 'update all languages': 'actualizează toate limbile', +'Update:': 'Actualizare:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upload': 'Ć®ncarcă', +'Upload': 'Upload', +'Upload a package:': 'Ǝncarcă un pachet:', +'Upload and install packed application': 'Ǝncarcă și instalează aplicație Ć®mpachetată', 'upload application:': 'incarcă aplicația:', +'Upload existing application': 'Ǝncarcă aplicația existentă', 'upload file:': 'Ć®ncărcă fișier:', 'upload plugin file:': 'Ć®ncarcă fișier plugin:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Folosiți (...)&(...) pentru ȘI, (...)|(...) pentru SAU, și ~(...) pentru NEGARE, pentru a crea interogări complexe.', 'user': 'utilizator', +'User': 'User', +'User %(id)s Logged-in': 'Utilizator %(id)s autentificat', +'User %(id)s Logged-out': 'Utilizator %(id)s delogat', +'User %(id)s Password changed': 'Parola utilizatorului %(id)s a fost schimbată', +'User %(id)s Password reset': 'Resetare parola utilizator %(id)s', +'User %(id)s Profile updated': 'Profil utilizator %(id)s actualizat', +'User %(id)s Registered': 'Utilizator %(id)s Ć®nregistrat', +'User ID': 'ID utilizator', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'value already in database or empty': 'Valoare existentă Ć®n baza de date sau vidă', 'variables': 'variables', +'Verify Password': 'Verifică parola', +'VERSION': 'VERSIUNE', +'Version': 'Version', +'Version %s.%s.%s (%s) %s': 'Versiune %s.%s.%s (%s) %s', +'Versioning': 'Versiuni', 'versioning': 'versiuni', +'Videos': 'Video-uri', +'View': 'Vedere', 'view': 'vedere', +'Views': 'Vederi', 'views': 'vederi', -'web2py Recent Tweets': 'Ultimele tweet-uri web2py', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Framework web', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py este la zi', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'Ultimele tweet-uri web2py', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Welcome': 'Bine ați venit', +'Welcome %s': 'Bine ați venit %s', +'Welcome to web2py': 'Bun venit la web2py', +'Welcome to web2py!': 'Bun venit la web2py!', +'Which called the function': 'Care a apelat funcția', +'Working...': 'Working...', +'Wrap with Abbreviation': 'Ǝncadrează cu abreviația', +'WSGI reference name': 'WSGI reference name', +'YES': 'DA', +'Yes': 'Yes', +'You are successfully running web2py': 'Rulați cu succes web2py', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You can modify this application and adapt it to your needs': 'Puteți modifica și adapta aplicația nevoilor dvs.', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'You visited the url': 'Ați vizitat adresa', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/ru.py b/applications/admin/languages/ru.py index 04ad7ede..88f9d703 100755 --- a/applications/admin/languages/ru.py +++ b/applications/admin/languages/ru.py @@ -3,84 +3,148 @@ '!langcode!': 'ru', '!langname!': 'Русский', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "Feld1 = \'newvalue". JOIN Ergebnisse kƶnnen nicht aktualisiert oder gelƶscht werden', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s %%{строкa} %%{уГаленa}', '%s %%{row} updated': '%s %%{строкa} %%{обновленa}', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%Y-%m-%d', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access)': '(Ń‚Ń€ŠµŠ±ŃƒŠµŃ‚ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ Šŗ ŠøŠ½Ń‚ŠµŃ€Š½ŠµŃ‚Ńƒ)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(напоГобие "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'ŠŠ°Š¹Š“ŠµŠ½Š¾: **%s** %%{файл}', 'A new version of web2py is available': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Š° Š½Š¾Š²Š°Ń Š²ŠµŃ€ŃŠøŃ web2py', 'A new version of web2py is available: %s': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Š° Š½Š¾Š²Š°Ń Š²ŠµŃ€ŃŠøŃ web2py: %s', 'Abort': 'ŠžŃ‚Š¼ŠµŠ½Š°', 'About': 'Šž', 'About application': 'Šž приложении', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'additional code for your application': 'Гобавочный коГ Š“Š»Ń вашего ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'Additional code for your application': 'Š”Š¾ŠæŠ¾Š»ŃŒŠ½ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Š¹ коГ Š“Š»Ń вашего ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', +'Admin design page': 'Admin design page', 'admin disabled because no admin password': 'аГминка Š¾Ń‚ŠŗŠ»ŃŽŃ‡ŠµŠ½Š°, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что Š¾Ń‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŠµŃ‚ ŠæŠ°Ń€Š¾Š»ŃŒ аГминистратора', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', 'admin disabled because not supported on google apps engine': 'аГминка Š¾Ń‚ŠŗŠ»ŃŽŃ‡ŠµŠ½Š°, т.Šŗ. не ŠæŠ¾Š“Š“ŠµŃ€Š¶ŠøŠ²Š°ŠµŃ‚ŃŃ на google app engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'аГминка Š¾Ń‚ŠŗŠ»ŃŽŃ‡ŠµŠ½Š°, т.Šŗ. невозможно ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚ŃŒ Š“Š¾ŃŃ‚ŃƒŠæ Šŗ Ń„Š°Š¹Š»Ńƒ с ŠæŠ°Ń€Š¾Š»ŃŠ¼Šø ', 'Admin is disabled because insecure channel': 'АГминпанель Š²Ń‹ŠŗŠ»ŃŽŃ‡ŠµŠ½Š° ŠøŠ·-за небезопасного ŃŠ¾ŠµŠ“ŠøŠ½ŠµŠ½ŠøŃ', 'Admin is disabled because unsecure channel': 'АГминпанель Š²Ń‹ŠŗŠ»ŃŽŃ‡ŠµŠ½ ŠøŠ·-за небезопасного ŃŠ¾ŠµŠ“ŠøŠ½ŠµŠ½ŠøŃ', 'Admin language': 'Язык аГминпанели', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'интерфейс аГминистратора', 'Administrator Password:': 'ŠŸŠ°Ń€Š¾Š»ŃŒ аГминистратора:', 'and rename it (required):': 'Šø ŠæŠµŃ€ŠµŠøŠ¼ŠµŠ½ŃƒŠ¹Ń‚Šµ его (необхоГимо):', 'and rename it:': ' Šø ŠæŠµŃ€ŠµŠøŠ¼ŠµŠ½ŃƒŠ¹Ń‚Šµ его:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'Appadmin Š¾Ń‚ŠŗŠ»ŃŽŃ‡ŠµŠ½, т.Šŗ. соеГинение не безопасно', +'Application': 'Application', 'application "%s" uninstalled': 'ŠŸŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŠµ "%s" уГалено', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', 'application compiled': 'ŠŸŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŠµ скомпилировано', +'Application exists already': 'Application exists already', 'application is compiled and cannot be designed': 'ŠŸŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŠµ скомпилировано Šø Гизайн не может Š±Ń‹Ń‚ŃŒ изменен', 'Application name:': 'ŠŠ°Š·Š²Š°Š½ŠøŠµ ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'are not used', 'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'Š’Ń‹ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ файл "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Are you sure you want to uninstall application "%s"': 'Š’Ń‹ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ приложение "%s"', 'Are you sure you want to uninstall application "%s"?': 'Š’Ń‹ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ приложение "%s"?', 'Are you sure you want to upgrade web2py now?': 'Š’Ń‹ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ хотите Š¾Š±Š½Š¾Š²ŠøŃ‚ŃŒ web2py сейчас?', +'Are you sure?': 'Are you sure?', 'arguments': 'Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń‹', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'Š’ŠŠ˜ŠœŠŠŠ˜Š•: Š”Š»Ń вхоГа Ń‚Ń€ŠµŠ±ŃƒŠµŃ‚ŃŃ бесопасное (HTTPS) соеГинение либо запуск на localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'Š’ŠŠ˜ŠœŠŠŠ˜Š•: Тестирование не потокобезопасно, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ не Š·Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚е несколько тестов ŠæŠ°Ń€Š°Š»Š»ŠµŠ»ŃŒŠ½Š¾.', 'ATTENTION: This is an experimental feature and it needs more testing.': 'Š’ŠŠ˜ŠœŠŠŠ˜Š•: Это ŃŠŗŃŠæŠµŃ€ŠøŠ¼ŠµŠ½Ń‚Š°Š»ŃŒŠ½Š°Ń Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŒ Šø Ń‚Ń€ŠµŠ±ŃƒŠµŃ‚ Ń‚ŠµŃŃ‚ŠøŃ€Š¾Š²Š°Š½ŠøŃ.', 'ATTENTION: you cannot edit the running application!': 'Š’ŠŠ˜ŠœŠŠŠ˜Š•: Š’Ń‹ не можете Ń€ŠµŠ“Š°ŠŗŃ‚ŠøŃ€Š¾Š²Š°Ń‚ŃŒ Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠµŠµ приложение!', 'Authentication': 'ŠŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Ń‹Šµ базы Ганных Šø таблицы', +'Available Databases and Tables': 'Available Databases and Tables', 'back': 'назаГ', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', 'beautify': 'Š Š°ŃŠŗŃ€Š°ŃŠøŃ‚ŃŒ', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', 'cache': 'ŠŗŃŃˆ', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'ŠšŃŃˆ, ошибки Šø сессии очищены', 'call': 'вызов', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'ŠŠµ может Š±Ń‹Ń‚ŃŒ ŠæŃƒŃŃ‚Ń‹Š¼', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ ŠŗŠ¾Š¼ŠæŠøŠ»ŠøŃ€Š¾Š²Š°Ń‚ŃŒ: в приложении ŠæŃ€ŠøŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ ошибки. ŠžŃ‚Š»Š°Š“ŃŒŃ‚Šµ его, ŠøŃŠæŃ€Š°Š²ŃŒŃ‚Šµ ошибки Šø ŠæŠ¾ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ заново.', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', 'cannot create file': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ ŃŠ¾Š·Š“Š°Ń‚ŃŒ файл', 'cannot upload file "%(filename)s"': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ файл "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Š˜Š·Š¼ŠµŠ½ŠøŃ‚ŃŒ ŠæŠ°Ń€Š¾Š»ŃŒ аГминистратора', +'change editor settings': 'change editor settings', 'Change Password': 'Š˜Š·Š¼ŠµŠ½ŠøŃ‚ŃŒ ŠæŠ°Ń€Š¾Š»ŃŒ', 'change password': 'ŠøŠ·Š¼ŠµŠ½ŠøŃ‚ŃŒ ŠæŠ°Ń€Š¾Š»ŃŒ', +'Changelog': 'Changelog', 'check all': 'ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ все', 'Check for upgrades': 'ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ Š¾Š±Š½Š¾Š²Š»ŠµŠ½ŠøŃ', 'Check to delete': 'ŠŸŠ¾ŃŃ‚Š°Š²ŃŒŃ‚Šµ Š“Š»Ń ŃƒŠ“Š°Š»ŠµŠ½ŠøŃ', 'Checking for upgrades...': 'ŠŸŃ€Š¾Š²ŠµŃ€ŠŗŠ° обновлений...', 'Clean': 'ŠžŃ‡ŠøŃŃ‚ŠøŃ‚ŃŒ', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'click here for online examples': 'нажмите зГесь Š“Š»Ń онлайн примеров', 'click here for the administrative interface': 'нажмите зГесь Š“Š»Ń интерфейса аГминистратора', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', 'click to check for upgrades': 'нажмите Š“Š»Ń проверки Š¾Š±Š½Š¾Š²Š»ŠµŠ½ŠøŃ', 'Client IP': 'IP клиента', 'code': 'коГ', +'Code listing': 'Code listing', 'collapse/expand all': 'ŃŠ²ŠµŃ€Š½ŃƒŃ‚ŃŒ/Ń€Š°Š·Š²ŠµŃ€Š½ŃƒŃ‚ŃŒ все', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'ŠšŠ¾Š¼ŠæŠøŠ»ŠøŃ€Š¾Š²Š°Ń‚ŃŒ', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'скомпилированное приложение уГалено', +'Condition': 'Condition', +'continue': 'continue', 'Controller': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»Š»ŠµŃ€', 'Controllers': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»Š»ŠµŃ€Ń‹', 'controllers': 'контроллеры', 'Copyright': 'Copyright', +'Count': 'Count', 'Create': 'Š”Š¾Š·Š“Š°Ń‚ŃŒ', 'create file with filename:': 'Š”Š¾Š·Š“Š°Ń‚ŃŒ файл с названием:', 'create new application:': 'ŃŠ¾Š·Š“Š°Ń‚ŃŒ новое приложение:', 'Create new simple application': 'Š”Š¾Š·Š“Š°Ń‚ŃŒ новое простое приложение', +'Create/Upload': 'Create/Upload', 'created by': 'созГано', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'crontab', 'Current request': 'Š¢ŠµŠŗŃƒŃ‰ŠøŠ¹ запрос', 'Current response': 'Š¢ŠµŠŗŃƒŃ‰ŠøŠ¹ ответ', @@ -91,8 +155,11 @@ 'data uploaded': 'Гата Š·Š°Š³Ń€ŃƒŠ·ŠŗŠø', 'Database': 'База Ганных', 'database': 'база Ганных', +'Database %s select': 'Database %s select', 'database %s select': 'Выбор базы Ганных %s ', +'Database administration': 'Database administration', 'database administration': 'аГминистрирование базы Ганных', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Дата Šø Š²Ń€ŠµŠ¼Ń', 'db': 'бГ', 'DB Model': 'МоГель БД', @@ -104,47 +171,89 @@ 'delete plugin': 'ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ плагин', 'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', 'Delete:': 'Š£Š“Š°Š»ŠøŃ‚ŃŒ:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'Š Š°Š·Š²ŠµŃ€Š½ŃƒŃ‚ŃŒ', 'Deploy on Google App Engine': 'Š Š°Š·Š²ŠµŃ€Š½ŃƒŃ‚ŃŒ на Google App Engine', 'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', 'Description': 'ŠžŠæŠøŃŠ°Š½ŠøŠµ', +'Description:': 'Description:', 'design': 'Гизайн', 'DESIGN': 'Š”Š˜Š—ŠŠ™Š', 'Design for': 'Дизайн Š“Š»Ń', 'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'направление: ltr', +'directory not found': 'directory not found', 'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'docs', +'Docs': 'Docs', 'documentation': 'Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŃ', 'done!': 'выполнено!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ ŃˆŠ°Š±Š»Š¾Š½Ń‹', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ плагины', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'E-mail', 'EDIT': 'ŠŸŠ ŠŠ’ŠšŠ', 'Edit': 'ŠŸŃ€Š°Š²ŠŗŠ°', +'edit all': 'edit all', 'Edit application': 'ŠŸŃ€Š°Š²ŠŗŠ° ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'edit controller': 'правка контроллера', +'edit controller:': 'edit controller:', 'Edit current record': 'ŠŸŃ€Š°Š²ŠŗŠ° Ń‚ŠµŠŗŃƒŃ‰ŠµŠ¹ записи', 'Edit Profile': 'ŠŸŃ€Š°Š²ŠŗŠ° ŠæŃ€Š¾Ń„ŠøŠ»Ń', 'edit profile': 'правка ŠæŃ€Š¾Ń„ŠøŠ»Ń', 'Edit This App': 'ŠŸŃ€Š°Š²ŠŗŠ° Ганного ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'edit views:': 'правка виГов:', +'Editing %s': 'Editing %s', 'Editing file': 'ŠŸŃ€Š°Š²ŠŗŠ° файла', 'Editing file "%s"': 'ŠŸŃ€Š°Š²ŠŗŠ° файла "%s"', 'Editing Language file': 'ŠŸŃ€Š°Š²ŠŗŠ° ŃŠ·Ń‹ŠŗŠ¾Š²Š¾Š³Š¾ файла', 'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Enterprise Web Framework': 'Enterprise Web Framework', +'Error': 'Error', 'Error logs for "%(app)s"': 'Š–ŃƒŃ€Š½Š°Š» ошибок Š“Š»Ń "%(app)s"', 'Error snapshot': 'Error snapshot', 'Error ticket': 'Error ticket', 'Errors': 'ŠžŃˆŠøŠ±ŠŗŠ°', 'escape': 'escape', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'ŠŃ‚Ń€ŠøŠ±ŃƒŃ‚Ń‹ Š¾Š±ŃŠŠµŠŗŃ‚Š° ŠøŃŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ', +'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Š Š°ŃŠŗŃ€Ń‹Ń‚ŃŒ Š°Š±Š±Ń€ŠµŠ²ŠøŠ°Ń‚ŃƒŃ€Ńƒ', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'Экспорт в CSV', +'Exports:': 'Exports:', 'exposes': 'открывает', +'exposes:': 'exposes:', 'extends': 'Ń€Š°ŃŃˆŠøŃ€ŃŠµŃ‚', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'невозможно Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ моГуль', +'failed to reload module because:': 'failed to reload module because:', +'File': 'File', 'file "%(filename)s" created': 'файл "%(filename)s" созГан', 'file "%(filename)s" deleted': 'файл "%(filename)s" уГален', 'file "%(filename)s" uploaded': 'файл "%(filename)s" Š·Š°Š³Ń€ŃƒŠ¶ŠµŠ½', @@ -152,140 +261,308 @@ 'file "%s" of %s restored': 'файл "%s" ŠøŠ· %s восстановлен', 'file changed on disk': 'файл ŠøŠ·Š¼ŠµŠ½ŠøŠ»ŃŃ на Гиске', 'file does not exist': 'файл не ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚', +'file not found': 'file not found', 'file saved on %(time)s': 'файл сохранен %(time)s', 'file saved on %s': 'файл сохранен %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'Ń„ŠøŠ»ŃŒŃ‚Ń€', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', 'First name': 'Š˜Š¼Ń', +'Form has errors': 'Form has errors', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Š¤ŃƒŠ½ŠŗŃ†ŠøŠø без doctest Š±ŃƒŠ“ŃƒŃ‚ Š“Š°Š²Š°Ń‚ŃŒ [ŠæŃ€Š¾ŃˆŠµŠ»] в тестах.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', 'Get from URL:': 'Get from URL:', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'К ŠæŠ¾Š“Ń…Š¾Š“ŃŃ‰ŠµŠ¹ паре', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Group ID': 'ID Š³Ń€ŃƒŠæŠæŃ‹', 'Hello World': 'ŠŸŃ€ŠøŠ²ŠµŃ‚, ŠœŠøŃ€', 'Help': 'ŠŸŠ¾Š¼Š¾Ń‰ŃŒ', +'here': 'here', 'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'htmledit', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Если отчет Š²Ń‹ŃˆŠµ соГержит номер ошибки, ŃŃ‚Š¾ ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ на ошибку при работе контроллера, Го попытки Š²Ń‹ŠæŠ¾Š»Š½ŠøŃ‚ŃŒ doctest. ŠŸŃ€ŠøŃ‡ŠøŠ½Š¾Š¹ чаще ŃŠ²Š»ŃŠµŃ‚ŃŃ неверные Š¾Ń‚ŃŃ‚ŃƒŠæŃ‹ или ошибки в коГе вне Ń„ŃƒŠ½ŠŗŃ†ŠøŠø. \nЗеленый заголовок ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ на успешное выполнение всех тестов. Š’ ŃŃ‚Š¾Š¼ ŃŠ»ŃƒŃ‡Š°Šµ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń‹ тестов не ŠæŠ¾ŠŗŠ°Š·Ń‹Š²Š°ŃŽŃ‚ŃŃ.', 'If you answer "yes", be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, Š·Š°Š³Ń€ŃƒŠ·ŠŗŠ° может ŠæŠ¾Ń‚Ń€ŠµŠ±Š¾Š²Š°Ń‚ŃŒ времени', 'If you answer yes, be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, Š·Š°Š³Ń€ŃƒŠ·ŠŗŠ° может ŠæŠ¾Ń‚Ń€ŠµŠ±Š¾Š²Š°Ń‚ŃŒ времени', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', 'Import/Export': 'Š˜Š¼ŠæŠ¾Ń€Ń‚/Экспорт', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'Š²ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚', +'Indent with tabs': 'Indent with tabs', 'Index': 'ИнГекс', 'index': 'index', 'insert new': 'Š²ŃŃ‚Š°Š²ŠøŃ‚ŃŒ новый', 'insert new %s': 'Š²ŃŃ‚Š°Š²ŠøŃ‚ŃŒ новый %s', 'inspect attributes': 'inspect attributes', 'Install': 'Š£ŃŃ‚Š°Š½Š¾Š²ŠøŃ‚ŃŒ', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Установленные ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'Š²Š½ŃƒŃ‚Ń€ŠµŠ½Š½ŃŃ ошибка', +'internal error: %s': 'internal error: %s', 'Internal State': 'Š’Š½ŃƒŃ‚Ń€ŠµŠ½Š½ŠøŠ¹ ŃŃ‚Š°Ń‚ŃƒŃ', 'Invalid action': 'ŠŠµŠ²ŠµŃ€Š½Š¾Šµ Гействие', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', 'Invalid email': 'ŠŠµŠ²ŠµŃ€Š½Ń‹Š¹ email', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'неверный ŠæŠ°Ń€Š¾Š»ŃŒ', +'invalid password.': 'invalid password.', 'Invalid Query': 'ŠŠµŠ²ŠµŃ€Š½Ń‹Š¹ запрос', 'invalid request': 'неверный запрос', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'неверный тикет', +'Key': 'Key', 'Key bindings': 'ŠšŠ¾Š¼Š±ŠøŠ½Š°Ń†ŠøŠø клавиш', 'Key bindings for ZenConding Plugin': 'ŠšŠ¾Š¼Š±ŠøŠ½Š°Ń†ŠøŠø клавиш Š“Š»Ń плагина ZenConding', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'Языковой файл "%(filename)s" созГан/обновлен', 'Language files (static strings) updated': 'Языковые файлы (статичные строки) обновлены', 'languages': 'ŃŠ·Ń‹ŠŗŠø', 'Languages': 'Языки', 'languages updated': 'ŃŠ·Ń‹ŠŗŠø обновлены', 'Last name': 'Š¤Š°Š¼ŠøŠ»ŠøŃ', +'Last Revision': 'Last Revision', 'Last saved on:': 'ПослеГнее сохранение:', 'Layout': 'Верстка', 'License for': 'Š›ŠøŃ†ŠµŠ½Š·ŠøŃ Š“Š»Ń', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'Š·Š°Š³Ń€ŃƒŠ·ŠŗŠ°...', +'Local Apps': 'Local Apps', 'locals': 'locals', +'Locals##debug': 'Locals##debug', 'located in the file': 'расположенный в файле', -'Login': 'Логин', 'login': 'логин', +'Login': 'Логин', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'ВхоГ в интерфейс аГминистратора', +'Login/Register': 'Login/Register', 'Logout': 'выхоГ', 'Lost Password': 'Забыли ŠæŠ°Ń€Š¾Š»ŃŒ', +'lost password': 'lost password', 'lost password?': 'ŠŸŠ°Ń€Š¾Š»ŃŒ ŃƒŃ‚ŠµŃ€ŃŠ½?', 'Main Menu': 'Главное Š¼ŠµŠ½ŃŽ', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', 'Match Pair': 'ŠŠ°Š¹Ń‚Šø ŠæŠ°Ń€Ńƒ', +'Memberships': 'Memberships', 'Menu Model': 'МоГель Š¼ŠµŠ½ŃŽ', 'merge': 'Š¾Š±ŃŠŠµŠ“ŠøŠ½ŠøŃ‚ŃŒ', 'Merge Lines': 'ŠžŠ±ŃŠŠµŠ“ŠøŠ½ŠøŃ‚ŃŒ линии', 'Models': 'МоГели', 'models': 'моГели', +'Modified On': 'Modified On', 'Modules': 'МоГули', 'modules': 'моГули', +'Multi User Mode': 'Multi User Mode', 'Name': 'ŠŠ°Š·Š²Š°Š½ŠøŠµ', 'new application "%s" created': 'новое приложение "%s" созГано', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', 'New application wizard': 'ŠœŠ°ŃŃ‚ŠµŃ€ нового ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', 'New Record': 'ŠŠ¾Š²Š°Ń запись', 'new record inserted': 'Š½Š¾Š²Š°Ń запись вставлена', 'New simple application': 'ŠŠ¾Š²Š¾Šµ простое приложение', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'ŃŠ»ŠµŠ“ŃƒŃŽŃ‰ŠøŠµ 100 строк', 'Next Edit Point': 'Š”Š»ŠµŠ“ŃƒŃŽŃ‰ŠµŠµ место правки', 'NO': 'ŠŠ•Š¢', +'no changes': 'no changes', 'No databases in this application': 'Š’ приложении нет базы Ганных', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'или испорт ŠøŠ· cvs файла', 'or provide app url:': 'или URL ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ:', 'or provide application url:': 'или URL ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ:', 'Origin': 'ŠžŃ€ŠøŠ³ŠøŠ½Š°Š»', 'Original/Translation': 'ŠžŃ€ŠøŠ³ŠøŠ½Š°Š»/ŠŸŠµŃ€ŠµŠ²Š¾Š“', +'Overview': 'Overview', 'Overwrite installed app': 'ŠŸŠµŃ€ŠµŠæŠøŃŠ°Ń‚ŃŒ на ŃƒŃŃ‚Š°Š½Š¾Š²Š»ŠµŠ½Š½Š¾Šµ приложение', 'Pack all': 'ŃƒŠæŠ°ŠŗŠ¾Š²Š°Ń‚ŃŒ все', 'Pack compiled': 'Архив скомпилирован', +'Pack custom': 'Pack custom', 'pack plugin': 'Š£ŠæŠ°ŠŗŠ¾Š²Š°Ń‚ŃŒ плагин', 'Password': 'ŠŸŠ°Ń€Š¾Š»ŃŒ', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'ŠŸŃ€Š¾ŃŠ¼Š¾Ń‚Ń€', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', 'please wait!': 'поГожГите, ŠæŠ¾Š¶Š°Š»ŃƒŠ¹ŃŃ‚Š°!', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', 'Plugin "%s" in application': 'Плагин "%s" в приложении', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'плагины', 'Plugins': 'ŠŸŠ»Š°Š³ŠøŠ½Ń‹', 'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'ŠžŠ±ŠµŃŠæŠµŃ‡ŠµŠ½', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'ŠæŃ€ŠµŠ“Ń‹Š“ŃƒŃ‰ŠøŠµ 100 строк', 'Previous Edit Point': 'ŠŸŃ€ŠµŠ“Ń‹Š“ŃƒŃ‰ŠµŠµ место правки', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Запрос:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'запись', 'record does not exist': 'запись не ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚', 'record id': 'id записи', 'Record ID': 'ID записи', -'register': 'Š·Š°Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€Š¾Š²Š°Ń‚ŃŒŃŃ', +'Record id': 'Record id', +'refresh': 'refresh', 'Register': 'Š—Š°Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€Š¾Š²Š°Ń‚ŃŒŃŃ', +'register': 'Š·Š°Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€Š¾Š²Š°Ń‚ŃŒŃŃ', 'Registration key': 'ŠšŠ»ŃŽŃ‡ регистрации', 'Reload routes': 'Reload routes', 'Remove compiled': 'Š£Š“Š°Š»ŠøŃ‚ŃŒ скомпилированное', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', 'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', 'Reset Password key': 'Дброс ŠæŠ°Ń€Š¾Š»Ń', 'Resolve Conflict file': 'Š ŠµŃˆŠøŃ‚ŃŒ конфликт в файле', 'response': 'response', +'restart': 'restart', 'restore': 'Š²Š¾ŃŃŃ‚Š°Š½Š¾Š²ŠøŃ‚ŃŒ', +'return': 'return', +'Revert': 'Revert', 'revert': 'Š¾Ń‚ŠŗŠ°Ń‚ŠøŃ‚ŃŒŃŃ', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', 'Role': 'Роль', +'Roles': 'Roles', 'Rows in table': 'Дтрок в таблице', +'Rows in Table': 'Rows in Table', 'Rows selected': 'Выбрано строк', +'rules are not defined': 'rules are not defined', 'rules:': 'rules:', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", 'Running on %s': 'Running on %s', 'Save': 'Save', 'save': 'ŃŠ¾Ń…Ń€Š°Š½ŠøŃ‚ŃŒ', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Š”Š¾Ń…Ń€Š°Š½ŠøŃ‚ŃŒ через Ajax', 'Saved file hash:': 'Š„ŃŃˆ сохраненного файла:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': 'выбрано', 'session': 'session', 'session expired': 'ŃŠµŃŃŠøŃ истекла', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', 'Singular Form': 'Singular Form', 'Site': 'сайт', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'некоторые файлы Š½ŠµŠ»ŃŒŠ·Ń ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', 'Start wizard': 'Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚ŃŒ мастер', 'state': 'ŃŃ‚Š°Ń‚ŃƒŃ', +'Static': 'Static', 'static': 'статичные файлы', 'Static files': 'Дтатические файлы', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'Stylesheet': 'Таблицы стилей', 'submit': 'ŠžŃ‚ŠæŃ€Š°Š²ŠøŃ‚ŃŒ', +'Submit': 'Submit', +'successful': 'successful', 'Sure you want to delete this object?': 'Š”ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ хотите ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ Ганный Š¾Š±ŃŠŠµŠŗŃ‚?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'таблица', +'Table': 'Table', 'Table name': 'ŠŠ°Š·Š²Š°Š½ŠøŠµ таблицы', +'Temporary': 'Temporary', 'test': 'тест', 'test_def': 'test_def', 'test_for': 'test_for', @@ -294,6 +571,8 @@ 'Testing application': 'Тест ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'Testing controller': 'Тест контроллера', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"query" ŃŠ²Š»ŃŠµŃ‚ŃŃ условием виГа "db.table1.field1 == \'значение\'". Что-либо типа "db.table1.field1 db.table2.field2 ==" веГет Šŗ SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'Логика ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ, кажГый URL Š¾Ń‚Š¾Š±Ń€Š°Š¶Š°ŠµŃ‚ŃŃ в Š¾Ń‚ŠŗŃ€Ń‹Ń‚ŃƒŃŽ Ń„ŃƒŠ½ŠŗŃ†ŠøŃŽ в контроллере', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Логика ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ, кажГый URL Š¾Ń‚Š¾Š±Ń€Š°Š¶Š°ŠµŃ‚ŃŃ Šŗ оГной Ń„ŃƒŠ½ŠŗŃ†ŠøŠø в контроллере', 'the data representation, define database tables and sets': 'преГставление Ганных, Š¾ŠæŃ€ŠµŠ“ŠµŠ»ŠøŃ‚ŃŒ таблицы Šø наборы', @@ -301,43 +580,77 @@ 'The output of the file is a dictionary that was rendered by the view': 'ВывоГом файла ŃŠ²Š»ŃŠµŃ‚ŃŃ ŃŠ»Š¾Š²Š°Ń€ŃŒ, который созГан в виГе', 'The presentations layer, views are also known as templates': 'Длой презентации, виГы так же известны, как ŃˆŠ°Š±Š»Š¾Š½Ń‹', 'the presentations layer, views are also known as templates': 'слой ŠæŃ€ŠµŠ“ŃŃ‚Š°Š²Š»ŠµŠ½ŠøŃ, виГы известные так же как ŃˆŠ°Š±Š»Š¾Š½Ń‹', +'Theme': 'Theme', 'There are no controllers': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ контроллеры', 'There are no models': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ моГели', 'There are no modules': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ моГули', 'There are no plugins': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ плагины', +'There are no private files': 'There are no private files', 'There are no static files': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ статичные файлы', +'There are no translators': 'There are no translators', 'There are no translators, only default language is supported': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ перевоГчики, ŠæŠ¾Š“Š“ŠµŃ€Š¶ŠøŠ²Š°ŠµŃ‚ŃŃ Ń‚Š¾Š»ŃŒŠŗŠ¾ станГартный ŃŠ·Ń‹Šŗ', 'There are no views': 'ŠžŃ‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‚ виГы', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'Эти файлы Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŃŽŃ‚ŃŃ без обработки, ваши ŠøŠ·Š¾Š±Ń€Š°Š¶ŠµŠ½ŠøŃ ŠæŠ¾ŠæŠ°Š“ŃƒŃ‚ ŃŃŽŠ“Š°', 'these files are served without processing, your images go here': 'Эти файлы Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŃŽŃ‚ŃŃ без обработки, ваши ŠøŠ·Š¾Š±Ń€Š°Š¶ŠµŠ½ŠøŃ ŠæŠ¾ŠæŠ°Š“ŃƒŃ‚ ŃŃŽŠ“Š°', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", 'This is a copy of the scaffolding application': 'Это ŠŗŠ¾ŠæŠøŃ сгенерированного ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', 'This is the %(filename)s template': 'Это шаблон %(filename)s', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'Ticket': 'Тикет', 'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'Timestamp': 'Š’Ń€ŠµŠ¼Ń', 'TM': 'TM', 'to previous version.': 'на ŠæŃ€ŠµŠ“Ń‹Š“ŃƒŃ‰ŃƒŃŽ Š²ŠµŃ€ŃŠøŃŽ.', 'To create a plugin, name a file/folder plugin_[name]': 'Š”Š»Ń ŃŠ¾Š·Š“Š°Š½ŠøŃ плагина назовите файл/папку plugin_[название]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'translation strings for the application': 'строки перевоГа Š“Š»Ń ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'Translation strings for the application': 'Дтроки перевоГа Š“Š»Ń ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ', 'try': 'try', 'try something like': 'ŠæŠ¾ŠæŃ€Š¾Š±Š¾Š²Š°Ń‚ŃŒ что-либо виГа', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', 'Unable to check for upgrades': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ Š¾Š±Š½Š¾Š²Š»ŠµŠ½ŠøŃ', 'unable to create application "%s"': 'невозможно ŃŠ¾Š·Š“Š°Ń‚ŃŒ приложение "%s" nicht mƶglich', 'unable to delete file "%(filename)s"': 'невозможно ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ файл "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', 'Unable to download': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ', 'Unable to download app': 'ŠŠµŠ²Š¾Š·Š¼Š¾Š¶Š½Š¾ Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'невозможно Ń€Š°Š·Š¾Š±Ń€Š°Ń‚ŃŒ файл csv', 'unable to uninstall "%s"': 'невозможно ŃƒŠ“Š°Š»ŠøŃ‚ŃŒ "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'ŃŠ½ŃŃ‚ŃŒ выбор всего', 'Uninstall': 'Š£Š“Š°Š»ŠøŃ‚ŃŒ', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'Š¾Š±Š½Š¾Š²ŠøŃ‚ŃŒ', 'update all languages': 'Š¾Š±Š½Š¾Š²ŠøŃ‚ŃŒ все ŃŠ·Ń‹ŠŗŠø', 'Update:': 'ŠžŠ±Š½Š¾Š²ŠøŃ‚ŃŒ:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'Š¾Š±Š½Š¾Š²ŠøŃ‚ŃŒ web2py сейчас', 'upload': 'Š·Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ', +'Upload': 'Upload', 'Upload & install packed application': 'Š—Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ Šø ŃƒŃŃ‚Š°Š½Š¾Š²ŠøŃ‚ŃŒ приложение в архиве', 'Upload a package:': 'Š—Š°Š³Ń€ŃƒŠ·ŠøŃ‚ŃŒ пакет:', 'Upload and install packed application': 'Upload and install packed application', @@ -348,25 +661,48 @@ 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Š˜ŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ (...)&(...) Š“Š»Ń AND, (...)|(...) Š“Š»Ń OR, Šø ~(...) Š“Š»Ń NOT при созГании сложных запросов.', 'Use an url:': 'Š˜ŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ url:', 'user': 'ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ', +'User': 'User', 'User ID': 'ID ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'переменные', 'Version': 'Š’ŠµŃ€ŃŠøŃ', -'versioning': 'версии', 'Versioning': 'Versioning', +'versioning': 'версии', 'View': 'Š’ŠøŠ“', 'view': 'виГ', 'Views': 'ВиГы', 'views': 'виГы', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py обновлен', +'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'послеГние твиты по web2py', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', 'Welcome %s': 'Добро ŠæŠ¾Š¶Š°Š»Š¾Š²Š°Ń‚ŃŒ, %s', 'Welcome to web2py': 'Добро ŠæŠ¾Š¶Š°Š»Š¾Š²Š°Ń‚ŃŒ в web2py', 'Which called the function': 'ŠšŠ¾Ń‚Š¾Ń€Ń‹Š¹ вызвал Ń„ŃƒŠ½ŠŗŃ†ŠøŃŽ', +'Working...': 'Working...', 'Wrap with Abbreviation': 'Š—Š°ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŒ в Š°Š±Š±Ń€ŠµŠ²ŠøŠ°Ń‚ŃƒŃ€Ńƒ', +'WSGI reference name': 'WSGI reference name', 'xml': 'xml', 'YES': 'ДА', +'Yes': 'Yes', 'You are successfully running web2py': 'Š’Ń‹ успешно Š·Š°ŠæŃƒŃŃ‚или web2by', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You can modify this application and adapt it to your needs': 'Š’Ń‹ можете ŠøŠ·Š¼ŠµŠ½ŠøŃ‚ŃŒ ŃŃ‚Š¾ приложение Šø ŠæŠ¾Š“Š¾Š³Š½Š°Ń‚ŃŒ поГ свои Š½ŃƒŠ¶Š“Ń‹', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', 'You visited the url': 'Š’Ń‹ посетили URL', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/sl.py b/applications/admin/languages/sl.py index e4a1c1c6..c6767b97 100755 --- a/applications/admin/languages/sl.py +++ b/applications/admin/languages/sl.py @@ -3,276 +3,257 @@ '!langcode!': 'sl', '!langname!': 'Slovenski', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Popravi" je izbirni izraz kot npr.: "stolpec1 = \'novavrednost\'". Rezultatov JOIN operacije ne morete popravljati ali brisati', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s vrstic izbrisanih', '%s %%{row} updated': '%s vrstic popravljeno', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access)': '(zahteva internetni dostop)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(nekaj kot "sl-SI" ali samo "sl")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Iskanje: **%s** datoteke', 'A new version of web2py is available': 'Nova različica web2py je na voljo', 'A new version of web2py is available: %s': 'Nova različica web2py je na voljo: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'POZOR: Prijava zahteva varno povezavo (HTTPS) ali lokalni (localhost) dostop.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'POZOR: Testiranje ni večopravilno, zato ne poganjajte več testov hkrati.', -'ATTENTION: This is an experimental feature and it needs more testing.': 'POZOR: To je preizkusni fazi in potrebuje več testiranja.', -'ATTENTION: you cannot edit the running application!': 'POZOR: Ne morete urejati aplikacije, ki že teče!', 'Abort': 'Preklic', 'About': 'Vizitka', 'About application': 'O aplikaciji', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', +'additional code for your application': 'dodatna koda za vaÅ”o aplikacijo', 'Additional code for your application': 'Dodatna koda za vaÅ”o aplikacijo', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'skrbnik izključen, ker ni skrbniÅ”kega gesla', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', +'admin disabled because not supported on google apps engine': 'skrbnik izključen, ker ni podprt na Google App sistemu', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'skrbnik izključen, ker ne morem dostopati do datoteke z gesli', 'Admin is disabled because insecure channel': 'Skrbnik je izključen zaradi nezavarovane povezave', 'Admin is disabled because unsecure channel': 'Skrbnik je izključen zaradi nezavarovane povezave', 'Admin language': 'SkrbniÅ”ki jezik', +'Admin versioning page': 'Admin versioning page', +'administrative interface': 'skrbniÅ”ki vmesnik', 'Administrator Password:': 'SkrbniÅ”ko geslo:', +'and rename it (required):': 'in jo preimenujte (obvezno):', +'and rename it:': ' in jo preimenujte:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'Appadmin izključen, ker zahteva varno (HTTPS) povezavo', +'Application': 'Application', +'application "%s" uninstalled': 'Aplikacija "%s" odstranjena', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'aplikacija prevedena', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'aplikacija je prevedena in je ne morete popravljati', 'Application name:': 'Ime aplikacije:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'Ali res želite pobrisati datoteko "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Ali res želite izbrisati ta predmet?', 'Are you sure you want to uninstall application "%s"': 'Ali res želite odstraniti program "%s"', 'Are you sure you want to uninstall application "%s"?': 'Ali res želite odstraniti program "%s"?', 'Are you sure you want to upgrade web2py now?': 'Ali želite sedaj nadgraditi web2py?', +'Are you sure?': 'Are you sure?', +'arguments': 'argumenti', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'POZOR: Prijava zahteva varno povezavo (HTTPS) ali lokalni (localhost) dostop.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'POZOR: Testiranje ni večopravilno, zato ne poganjajte več testov hkrati.', +'ATTENTION: This is an experimental feature and it needs more testing.': 'POZOR: To je preizkusni fazi in potrebuje več testiranja.', +'ATTENTION: you cannot edit the running application!': 'POZOR: Ne morete urejati aplikacije, ki že teče!', 'Authentication': 'Avtentikacija', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'Podatkovne baze in tabele', +'Available Databases and Tables': 'Available Databases and Tables', +'back': 'nazaj', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'beautify': 'olepÅ”aj', 'Begin': 'Začni', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'predpomnilnik', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'Predpomnilnik, napake in seja so očiŔčeni', +'call': 'kliči', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'Ne sme biti prazno', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Nemogoče prevajanje: napake v programu. Odpravite napake in poskusite ponovno.', -'Change Password': 'Spremeni geslo', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'ne morem ustvariti datoteke', +'cannot upload file "%(filename)s"': 'ne morem naložiti datoteke "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Spremenite skrbniÅ”ko geslo', +'change editor settings': 'change editor settings', +'Change Password': 'Spremeni geslo', +'change password': 'spremeni geslo', +'Changelog': 'Changelog', +'check all': 'označi vse', 'Check for upgrades': 'Preveri za posodobitve', 'Check to delete': 'Odkljukajte, če želite izbrisati', 'Checking for upgrades...': 'Preverjam posodobitve...', 'Clean': 'Počisti', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'click here for online examples': 'kliknite za spletne primere', +'click here for the administrative interface': 'kliknite za skrbniÅ”ki vmesnik', 'Click row to expand traceback': 'Kliknite vrstico da razÅ”irite sledenje', 'Click row to view a ticket': 'Kliknite vrstico za ogled listka', +'click to check for upgrades': 'kliknite za preverjanje nadgradenj', 'Client IP': 'IP klienta', +'code': 'koda', +'Code listing': 'Code listing', +'collapse/expand all': 'zapri/odpri vse', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'Prevedi', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'prevedena aplikacija je odstranjena', +'Condition': 'Condition', +'continue': 'continue', 'Controller': 'Krmilnik', 'Controllers': 'Krmilniki', +'controllers': 'krmilniki', 'Copyright': 'Copyright', 'Count': 'Å tevilo', 'Create': 'Ustvari', +'create file with filename:': 'Ustvari datoteko z imenom:', +'create new application:': 'Ustvari novo aplikacijo:', 'Create new simple application': 'Ustvari novo enostavno aplikacijo', +'Create/Upload': 'Create/Upload', +'created by': 'ustvaril', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'crontab', 'Current request': 'Trenutna zahteva', 'Current response': 'Trenutni odgovor', 'Current session': 'Trenutna seja', -'DB Model': 'Podatkovni model', -'DESIGN': 'Izgled', +'currently running': 'trenutno teče', +'currently saved or': 'trenutno shranjeno ali', +'customize me!': 'Spremeni me!', +'data uploaded': 'podatki naloženi', 'Database': 'Podatkovna baza', +'database': 'podatkovna baza', +'Database %s select': 'Database %s select', +'database %s select': 'izberi podatkovno bazo %s ', +'Database administration': 'Database administration', +'database administration': 'upravljanje s podatkovno bazo', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Datum in čas', +'db': 'db', +'DB Model': 'Podatkovni model', +'Debug': 'Debug', +'defines tables': 'definiraj tabele', 'Delete': 'IzbriÅ”i', +'delete': 'izbriÅ”i', +'delete all checked': 'izbriÅ”i vse označene', +'delete plugin': 'izbriÅ”i vtičnik', 'Delete this file (you will be asked to confirm deletion)': 'IzbriÅ”i to datoteko (izbris boste morali potrditi)', 'Delete:': 'IzbriÅ”i:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'Namesti', 'Deploy on Google App Engine': 'Prenesi na Google App sistem', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', 'Description': 'Opis', +'Description:': 'Description:', +'design': 'oblikuj', +'DESIGN': 'Izgled', 'Design for': 'Oblikuj za', 'Detailed traceback description': 'Natačen opis sledenja', +'details': 'podrobnosti', +'direction: ltr': 'smer: ltr', +'directory not found': 'directory not found', 'Disable': 'Izključi', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', +'docs': 'docs', +'Docs': 'Docs', +'documentation': 'dokumentacija', +'done!': 'Narejeno!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', +'download layouts': 'prenesi postavitev', +'Download layouts from repository': 'Download layouts from repository', +'download plugins': 'prenesi vtičnik', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'E-mail', 'EDIT': 'UREDI', 'Edit': 'Uredi', -'Edit Profile': 'Uredi profil', -'Edit This App': 'Uredi ta program', +'edit all': 'edit all', 'Edit application': 'Uredi program', +'edit controller': 'uredi krmilnik', +'edit controller:': 'edit controller:', 'Edit current record': 'Uredi trenutni zapis', -'Editing Language file': 'Uredi jezikovno datoteko', +'Edit Profile': 'Uredi profil', +'edit profile': 'uredi profil', +'Edit This App': 'Uredi ta program', +'edit views:': 'urejaj poglede:', +'Editing %s': 'Editing %s', 'Editing file': 'Urejanje datoteke', 'Editing file "%s"': 'Urejanje datoteke "%s"', +'Editing Language file': 'Uredi jezikovno datoteko', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Enterprise Web Framework': 'Enterprise Web Framework', 'Error': 'Napaka', 'Error logs for "%(app)s"': 'Dnevnik napak za "%(app)s"', 'Error snapshot': 'Posnetek napake', 'Error ticket': 'Listek napake', 'Errors': 'Napake', -'Exception instance attributes': 'Lastnosti instance izjeme', -'Expand Abbreviation': 'RazÅ”iri okrajÅ”ave', -'File': 'Datoteka', -'First name': 'Ime', -'Frames': 'Okvirji', -'Functions with no doctests will result in [passed] tests.': 'Funkcije brez doctest bodo opravile teste brez preverjanja.', -'Get from URL:': 'Naloži iz URL:', -'Go to Matching Pair': 'Pojdi k ujemalnemu paru', -'Group ID': 'ID skupine', -'Hello World': 'Pozdravljen, Svet!', -'Help': 'Pomoč', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Če poročilo zgoraj vsebuje Å”tevilko listka to pomeni napako pri izvajanju krmilnika, Å”e preden so se izvedli doctesti. To ponavadi pomeni napako pri zamiku programske vrstice ali izven funkcije.\nZelen naslov označuje, da so vsi testi opravljeni. V tem primeru testi niso prikazani.', -'If you answer "yes", be patient, it may take a while to download': 'Če odgovorite z "DA", potrpite. Prenos traja nekaj časa', -'If you answer yes, be patient, it may take a while to download': 'Če odgovorite z "DA", potrpite. Prenos traja nekaj časa', -'Import/Export': 'Uvoz/Izvoz', -'Index': 'Indeks', -'Install': 'Namesti', -'Installed applications': 'NameŔčene aplikacije', -'Internal State': 'Notranje stanje', -'Invalid Query': 'Napačno povpraÅ”evanje', -'Invalid action': 'Napačno dejanje', -'Invalid email': 'Napačen e-naslov', -'Key bindings': 'Povezave tipk', -'Key bindings for ZenConding Plugin': 'Povezave tipk za ZenConding vtičnik', -'Language files (static strings) updated': 'Jezikovna datoteka (statično besedilo) posodobljeno', -'Languages': 'Jeziki', -'Last name': 'Priimek', -'Last saved on:': 'Zadnjič shranjeno:', -'Layout': 'Postavitev', -'License for': 'Licenca za', -'Login': 'Prijava', -'Login to the Administrative Interface': 'Prijava v skrbniÅ”ki vmesnik', -'Logout': 'Odjava', -'Lost Password': 'Izgubljeno geslo', -'Main Menu': 'Glavni meni', -'Match Pair': 'Ujemalni par', -'Menu Model': 'Model menija', -'Merge Lines': 'Združi vrstice', -'Models': 'Modeli', -'Modules': 'Moduli', -'NO': 'NE', -'Name': 'Ime', -'New Application Wizard': 'Čarovnik za novo aplikacijo', -'New Record': 'Nov zapis', -'New application wizard': 'Čarovnik za novo aplikacijo', -'New simple application': 'Nova enostavna aplikacija', -'Next Edit Point': 'Naslednja točka urejanja', -'No databases in this application': 'V tem programu ni podatkovnih baz', -'Origin': 'Izvor', -'Original/Translation': 'Izvor/Prevod', -'Overwrite installed app': 'PrepiÅ”i obstoječi program', -'Pack all': 'Zapakiraj vse', -'Pack compiled': 'Paket preveden', -'Password': 'Geslo', -'Peeking at file': 'Vpogled v datoteko', -'Plugin "%s" in application': 'Vtičnik "%s" v aplikaciji', -'Plugins': 'Vtičniki', -'Powered by': 'Narejeno z', -'Previous Edit Point': 'PrejÅ”nja točka urejanja', -'Query:': 'VpraÅ”anje:', -'Record ID': 'ID zapisa', -'Register': 'Registracija', -'Registration key': 'Registracijski ključ', -'Reload routes': 'Ponovno naloži preusmeritve', -'Remove compiled': 'Odstrani prevedeno', -'Reset Password key': 'Ponastavi ključ gesla', -'Resolve Conflict file': 'Datoteka za razreÅ”itev problemov', -'Role': 'Vloga', -'Rows in table': 'Vrstic v tabeli', -'Rows selected': 'Izbranih vrstic', -"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Poženi teste v tej datoteki (za vse teste uporabite gumb 'test')", -'Save via Ajax': 'Shrani preko AJAX', -'Saved file hash:': 'Koda shranjene datoteke:', -'Site': 'SpletiŔče', -'Sorry, could not find mercurial installed': 'Oprostite. Mercurial ni nameŔčen.', -'Start a new app': 'Začnite z novo aplikacijo', -'Start wizard': 'Zaženi čarovnika', -'Static files': 'Statične datoteke', -'Stylesheet': 'CSS datoteka', -'Sure you want to delete this object?': 'Ali res želite izbrisati ta predmet?', -'TM': 'TM', -'Table name': 'Ime tabele', -'Testing application': 'Testiranje programa', -'Testing controller': 'Testiranje krmilnika', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"vpraÅ”anje" je pogoj kot npr. "db.table1.field1 == \'vrednost\'". Nekaj kot "db.table1.field1 == db.table2.field2" izvede SQL operacijo JOIN.', -'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika delovanja aplikacije. Vsak URL je povezan z eno objavljeno funkcijo v krmilniku', -'The data representation, define database tables and sets': 'Predstavitev podatkov. Definirajte podatkovne tabele in množice', -'The output of the file is a dictionary that was rendered by the view': 'Rezultat datoteke je slovar, ki ga predela pogled', -'The presentations layer, views are also known as templates': 'Predstavitveni nivo. Pogledi so znani tudi kot predloge', -'There are no controllers': 'Ni krmilnikov', -'There are no models': 'Ni modelov', -'There are no modules': 'Ni modulov', -'There are no plugins': 'Ni vtičnikov', -'There are no static files': 'Ni statičnih datotek', -'There are no translators, only default language is supported': 'Ni prevodov. Podprt je samo privzeti jezik', -'There are no views': 'Ni pogledov', -'These files are served without processing, your images go here': 'Te datoteke so poslane brez obdelave. VaÅ”e slike shranite tu.', -'This is a copy of the scaffolding application': 'To je kopija okvirne aplikacije', -'This is the %(filename)s template': 'To je predloga %(filename)s', -'Ticket': 'Listek', -'Ticket ID': 'ID listka', -'Timestamp': 'Časovni žig', -'To create a plugin, name a file/folder plugin_[name]': 'Če želite ustvariti vtičnik, poimenujte datoteko/mapo plugin_[ime]', -'Traceback': 'Sledljivost', -'Translation strings for the application': 'Prevajalna besedila za aplikacijo', -'Unable to check for upgrades': 'Ne morem preveriti posodobitev', -'Unable to download': 'Ne morem prenesti datoteke', -'Unable to download app': 'Ne morem prenesti programa', -'Uninstall': 'Odstrani', -'Update:': 'Posodobitev:', -'Upload & install packed application': 'Naloži in namesti pakirano aplikacijo', -'Upload a package:': 'Naloži paket:', -'Upload and install packed application': 'Naloži in namesti pakirano aplikacijo', -'Upload existing application': 'Naloži obstoječo aplikacijo', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Uporabie (...)&(...) za AND, (...)|(...) za OR, in ~(...) za NOT pri gradnji kompleksnih povpraÅ”evanj.', -'Use an url:': 'Uporabite URL:', -'User ID': 'ID uporabnika', -'Version': 'Različica', -'Versioning': 'Versioning', -'View': 'Pogled', -'Views': 'Pogledi', -'Web Framework': 'Web Framework', -'Welcome %s': 'DobrodoÅ”li, %s', -'Welcome to web2py': 'DobrodoÅ”li v web2py', -'Which called the function': 'Ki je klical funkcijo', -'Wrap with Abbreviation': 'Ovij z okrajÅ”avo', -'YES': 'DA', -'You are successfully running web2py': 'UspeÅ”no ste pognali web2py', -'You can modify this application and adapt it to your needs': 'Lahko spremenite to aplikacijo in jo prilagodite vaÅ”im potrebam', -'You visited the url': 'Obiskali ste URL', -'additional code for your application': 'dodatna koda za vaÅ”o aplikacijo', -'admin disabled because no admin password': 'skrbnik izključen, ker ni skrbniÅ”kega gesla', -'admin disabled because not supported on google apps engine': 'skrbnik izključen, ker ni podprt na Google App sistemu', -'admin disabled because unable to access password file': 'skrbnik izključen, ker ne morem dostopati do datoteke z gesli', -'administrative interface': 'skrbniÅ”ki vmesnik', -'and rename it (required):': 'in jo preimenujte (obvezno):', -'and rename it:': ' in jo preimenujte:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'Appadmin izključen, ker zahteva varno (HTTPS) povezavo', -'application "%s" uninstalled': 'Aplikacija "%s" odstranjena', -'application compiled': 'aplikacija prevedena', -'application is compiled and cannot be designed': 'aplikacija je prevedena in je ne morete popravljati', -'arguments': 'argumenti', -'back': 'nazaj', -'beautify': 'olepÅ”aj', -'cache': 'predpomnilnik', -'cache, errors and sessions cleaned': 'Predpomnilnik, napake in seja so očiŔčeni', -'call': 'kliči', -'cannot create file': 'ne morem ustvariti datoteke', -'cannot upload file "%(filename)s"': 'ne morem naložiti datoteke "%(filename)s"', -'change password': 'spremeni geslo', -'check all': 'označi vse', -'click here for online examples': 'kliknite za spletne primere', -'click here for the administrative interface': 'kliknite za skrbniÅ”ki vmesnik', -'click to check for upgrades': 'kliknite za preverjanje nadgradenj', -'code': 'koda', -'collapse/expand all': 'zapri/odpri vse', -'compiled application removed': 'prevedena aplikacija je odstranjena', -'controllers': 'krmilniki', -'create file with filename:': 'Ustvari datoteko z imenom:', -'create new application:': 'Ustvari novo aplikacijo:', -'created by': 'ustvaril', -'crontab': 'crontab', -'currently running': 'trenutno teče', -'currently saved or': 'trenutno shranjeno ali', -'customize me!': 'Spremeni me!', -'data uploaded': 'podatki naloženi', -'database': 'podatkovna baza', -'database %s select': 'izberi podatkovno bazo %s ', -'database administration': 'upravljanje s podatkovno bazo', -'db': 'db', -'defines tables': 'definiraj tabele', -'delete': 'izbriÅ”i', -'delete all checked': 'izbriÅ”i vse označene', -'delete plugin': 'izbriÅ”i vtičnik', -'design': 'oblikuj', -'details': 'podrobnosti', -'direction: ltr': 'smer: ltr', -'documentation': 'dokumentacija', -'done!': 'Narejeno!', -'download layouts': 'prenesi postavitev', -'download plugins': 'prenesi vtičnik', -'edit controller': 'uredi krmilnik', -'edit profile': 'uredi profil', -'edit views:': 'urejaj poglede:', 'escape': 'escape', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Lastnosti instance izjeme', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation': 'RazÅ”iri okrajÅ”ave', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'izvozi kot CSV', +'Exports:': 'Exports:', 'exposes': 'objavlja', +'exposes:': 'exposes:', 'extends': 'razÅ”irja', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'nisem uspel ponovno naložiti modula', +'failed to reload module because:': 'failed to reload module because:', +'File': 'Datoteka', 'file "%(filename)s" created': 'datoteka "%(filename)s" ustvarjena', 'file "%(filename)s" deleted': 'datoteka "%(filename)s" izbrisana', 'file "%(filename)s" uploaded': 'datoteka "%(filename)s" preneÅ”ena', @@ -280,89 +261,447 @@ 'file "%s" of %s restored': 'datoteka "%s" od %s obnovljena', 'file changed on disk': 'datoteka je bila spremenjena', 'file does not exist': 'datoteka ne obstaja', +'file not found': 'file not found', 'file saved on %(time)s': 'datoteka shranjena %(time)s', 'file saved on %s': 'datoteka shranjena %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'fiter', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'First name': 'Ime', +'Form has errors': 'Form has errors', +'Frames': 'Okvirji', +'Functions with no doctests will result in [passed] tests.': 'Funkcije brez doctest bodo opravile teste brez preverjanja.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Get from URL:': 'Naloži iz URL:', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'Go to Matching Pair': 'Pojdi k ujemalnemu paru', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', +'Group ID': 'ID skupine', +'Hello World': 'Pozdravljen, Svet!', +'Help': 'Pomoč', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'htmledit', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Če poročilo zgoraj vsebuje Å”tevilko listka to pomeni napako pri izvajanju krmilnika, Å”e preden so se izvedli doctesti. To ponavadi pomeni napako pri zamiku programske vrstice ali izven funkcije.\nZelen naslov označuje, da so vsi testi opravljeni. V tem primeru testi niso prikazani.', +'If you answer "yes", be patient, it may take a while to download': 'Če odgovorite z "DA", potrpite. Prenos traja nekaj časa', +'If you answer yes, be patient, it may take a while to download': 'Če odgovorite z "DA", potrpite. Prenos traja nekaj časa', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Uvoz/Izvoz', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'vključuje', +'Indent with tabs': 'Indent with tabs', +'Index': 'Indeks', 'index': 'indeks', 'insert new': 'vstavi nov', 'insert new %s': 'vstavi nov %s', 'inspect attributes': 'pregled lastnosti', +'Install': 'Namesti', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'NameŔčene aplikacije', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'notranja napaka', +'internal error: %s': 'internal error: %s', +'Internal State': 'Notranje stanje', +'Invalid action': 'Napačno dejanje', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid email': 'Napačen e-naslov', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'napačno geslo', +'invalid password.': 'invalid password.', +'Invalid Query': 'Napačno povpraÅ”evanje', 'invalid request': 'napačna zahteva', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'napačen kartonček', +'Key': 'Key', +'Key bindings': 'Povezave tipk', +'Key bindings for ZenConding Plugin': 'Povezave tipk za ZenConding vtičnik', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'jezikovna datoteka "%(filename)s" ustvarjena/posodobljena', +'Language files (static strings) updated': 'Jezikovna datoteka (statično besedilo) posodobljeno', 'languages': 'jeziki', +'Languages': 'Jeziki', 'languages updated': 'jeziki posodobljeni', +'Last name': 'Priimek', +'Last Revision': 'Last Revision', +'Last saved on:': 'Zadnjič shranjeno:', +'Layout': 'Postavitev', +'License for': 'Licenca za', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'nalaganje...', +'Local Apps': 'Local Apps', 'locals': 'locals', +'Locals##debug': 'Locals##debug', 'located in the file': 'se nahaja v datoteki', 'login': 'prijava', +'Login': 'Prijava', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'Prijava v skrbniÅ”ki vmesnik', +'Login/Register': 'Login/Register', +'Logout': 'Odjava', +'Lost Password': 'Izgubljeno geslo', +'lost password': 'lost password', 'lost password?': 'Izgubljeno geslo?', +'Main Menu': 'Glavni meni', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Match Pair': 'Ujemalni par', +'Memberships': 'Memberships', +'Menu Model': 'Model menija', 'merge': 'združi', +'Merge Lines': 'Združi vrstice', +'Models': 'Modeli', 'models': 'modeli', +'Modified On': 'Modified On', +'Modules': 'Moduli', 'modules': 'moduli', +'Multi User Mode': 'Multi User Mode', +'Name': 'Ime', 'new application "%s" created': 'ustvarjen nov program "%s"', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'Čarovnik za novo aplikacijo', +'New application wizard': 'Čarovnik za novo aplikacijo', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'Nov zapis', 'new record inserted': 'vstavljen nov zapis', +'New simple application': 'Nova enostavna aplikacija', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'naslednjih 100 zapisov', +'Next Edit Point': 'Naslednja točka urejanja', +'NO': 'NE', +'no changes': 'no changes', +'No databases in this application': 'V tem programu ni podatkovnih baz', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'ali uvozi iz CSV datoteke', 'or provide app url:': 'ali vpiÅ”ite URL programa:', 'or provide application url:': 'ali vpiÅ”ite URL programa:', +'Origin': 'Izvor', +'Original/Translation': 'Izvor/Prevod', +'Overview': 'Overview', +'Overwrite installed app': 'PrepiÅ”i obstoječi program', +'Pack all': 'Zapakiraj vse', +'Pack compiled': 'Paket preveden', +'Pack custom': 'Pack custom', 'pack plugin': 'zapakiraj vtičnik', +'Password': 'Geslo', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': 'Vpogled v datoteko', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', 'please wait!': 'Prosim počakajte!', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Vtičnik "%s" v aplikaciji', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'vtičniki', +'Plugins': 'Vtičniki', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': 'Narejeno z', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'prejÅ”njih 100 zapisov', +'Previous Edit Point': 'PrejÅ”nja točka urejanja', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'VpraÅ”anje:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'zapis', 'record does not exist': 'zapis ne obstaja', 'record id': 'id zapisa', +'Record ID': 'ID zapisa', +'Record id': 'Record id', +'refresh': 'refresh', +'Register': 'Registracija', 'register': 'registracija', +'Registration key': 'Registracijski ključ', +'Reload routes': 'Ponovno naloži preusmeritve', +'Remove compiled': 'Odstrani prevedeno', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', 'request': 'zahteva', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Reset Password key': 'Ponastavi ključ gesla', +'Resolve Conflict file': 'Datoteka za razreÅ”itev problemov', 'response': 'odgovor', +'restart': 'restart', 'restore': 'obnovi', +'return': 'return', +'Revert': 'Revert', 'revert': 'povrni', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Vloga', +'Roles': 'Roles', +'Rows in table': 'Vrstic v tabeli', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Izbranih vrstic', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Poženi teste v tej datoteki (za vse teste uporabite gumb 'test')", +'Running on %s': 'Running on %s', +'Save': 'Save', 'save': 'shrani', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Shrani preko AJAX', +'Saved file hash:': 'Koda shranjene datoteke:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': 'izbrano', 'session': 'seja', 'session expired': 'seja pretekla', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'lupina', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', +'Site': 'SpletiŔče', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'nekaterih datotek se ni dalo izbrisati', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Oprostite. Mercurial ni nameŔčen.', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Začnite z novo aplikacijo', +'Start searching': 'Start searching', +'Start wizard': 'Zaženi čarovnika', 'state': 'stanje', +'Static': 'Static', 'static': 'statično', +'Static files': 'Statične datoteke', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', +'Stylesheet': 'CSS datoteka', 'submit': 'poÅ”lji', +'Submit': 'Submit', +'successful': 'successful', +'Sure you want to delete this object?': 'Ali res želite izbrisati ta predmet?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'tabela', +'Table': 'Table', +'Table name': 'Ime tabele', +'Temporary': 'Temporary', 'test': 'test', 'test_def': 'test_def', 'test_for': 'test_for', 'test_if': 'test_if', 'test_try': 'test_try', +'Testing application': 'Testiranje programa', +'Testing controller': 'Testiranje krmilnika', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"vpraÅ”anje" je pogoj kot npr. "db.table1.field1 == \'vrednost\'". Nekaj kot "db.table1.field1 == db.table2.field2" izvede SQL operacijo JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'krmilna logika, vsaka URL pot je preslikana v eno funkcijo v krmilniku', +'The application logic, each URL path is mapped in one exposed function in the controller': 'Logika delovanja aplikacije. Vsak URL je povezan z eno objavljeno funkcijo v krmilniku', 'the data representation, define database tables and sets': 'podatkovna predstavitev, definirajte tabele in množice', +'The data representation, define database tables and sets': 'Predstavitev podatkov. Definirajte podatkovne tabele in množice', +'The output of the file is a dictionary that was rendered by the view': 'Rezultat datoteke je slovar, ki ga predela pogled', +'The presentations layer, views are also known as templates': 'Predstavitveni nivo. Pogledi so znani tudi kot predloge', 'the presentations layer, views are also known as templates': 'predstavitveni nivo, pogledi so tudi znani kot predloge', +'Theme': 'Theme', +'There are no controllers': 'Ni krmilnikov', +'There are no models': 'Ni modelov', +'There are no modules': 'Ni modulov', +'There are no plugins': 'Ni vtičnikov', +'There are no private files': 'There are no private files', +'There are no static files': 'Ni statičnih datotek', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'Ni prevodov. Podprt je samo privzeti jezik', +'There are no views': 'Ni pogledov', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'Te datoteke so poslane brez obdelave. VaÅ”e slike shranite tu.', 'these files are served without processing, your images go here': 'te datoteke so poslane brez posredovanja in obdelave, svoje slike shranite tu', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is a copy of the scaffolding application': 'To je kopija okvirne aplikacije', +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'To je predloga %(filename)s', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Listek', +'Ticket ID': 'ID listka', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Timestamp': 'Časovni žig', +'TM': 'TM', 'to previous version.': 'na prejÅ”njo različico.', +'To create a plugin, name a file/folder plugin_[name]': 'Če želite ustvariti vtičnik, poimenujte datoteko/mapo plugin_[ime]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Sledljivost', 'translation strings for the application': 'prevodna besedila za program', +'Translation strings for the application': 'Prevajalna besedila za aplikacijo', 'try': 'poskusi', 'try something like': 'poskusite na primer', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Ne morem preveriti posodobitev', 'unable to create application "%s"': 'ne morem ustvariti programa "%s" ', 'unable to delete file "%(filename)s"': 'ne morem izbrisati datoteke "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': 'Ne morem prenesti datoteke', +'Unable to download app': 'Ne morem prenesti programa', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'ne morem obdelati csv datoteke', 'unable to uninstall "%s"': 'ne morem odstraniti "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'odznači vse', +'Uninstall': 'Odstrani', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'posodobi', 'update all languages': 'posodobi vse jezike', +'Update:': 'Posodobitev:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'posodobi web2py', 'upload': 'naloži', +'Upload': 'Upload', +'Upload & install packed application': 'Naloži in namesti pakirano aplikacijo', +'Upload a package:': 'Naloži paket:', +'Upload and install packed application': 'Naloži in namesti pakirano aplikacijo', 'upload application:': 'naloži program:', +'Upload existing application': 'Naloži obstoječo aplikacijo', 'upload file:': 'naloži datoteko:', 'upload plugin file:': 'naloži vtičnik:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Uporabie (...)&(...) za AND, (...)|(...) za OR, in ~(...) za NOT pri gradnji kompleksnih povpraÅ”evanj.', +'Use an url:': 'Uporabite URL:', 'user': 'uporabnik', +'User': 'User', +'User ID': 'ID uporabnika', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'spremenljivke', +'Version': 'Različica', +'Versioning': 'Versioning', 'versioning': 'različice', +'View': 'Pogled', 'view': 'pogled', +'Views': 'Pogledi', 'views': 'pogledi', -'web2py Recent Tweets': 'zadnji tviti na web2py', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py je ažuren', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'zadnji tviti na web2py', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Welcome %s': 'DobrodoÅ”li, %s', +'Welcome to web2py': 'DobrodoÅ”li v web2py', +'Which called the function': 'Ki je klical funkcijo', +'Working...': 'Working...', +'Wrap with Abbreviation': 'Ovij z okrajÅ”avo', +'WSGI reference name': 'WSGI reference name', 'xml': 'xml', +'YES': 'DA', +'Yes': 'Yes', +'You are successfully running web2py': 'UspeÅ”no ste pognali web2py', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You can modify this application and adapt it to your needs': 'Lahko spremenite to aplikacijo in jo prilagodite vaÅ”im potrebam', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'You visited the url': 'Obiskali ste URL', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/sr-cr.py b/applications/admin/languages/sr-cr.py index 04ff676c..56a9a384 100755 --- a/applications/admin/languages/sr-cr.py +++ b/applications/admin/languages/sr-cr.py @@ -2,211 +2,594 @@ { '!langcode!': 'sr-cr', '!langname!': 'Дрпски (Ћирилица)', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', +'%s %%{row} deleted': '%s %%{row} deleted', +'%s %%{row} updated': '%s %%{row} updated', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', '(requires internet access)': '(Š·Š°Ń…Ń‚ŠøŃ˜ŠµŠ²Š° ŠæŃ€ŠøŃŃ‚ŃƒŠæ ŠøŠ½Ń‚ŠµŃ€Š½ŠµŃ‚Ńƒ)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(Š½ŠµŃˆŃ‚Š¾ као "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(Гатотека **gluon/contrib/plural_rules/%s.py** није пронађена)', +'Abort': 'Abort', 'About': 'Š˜Š½Ń„Š¾Ń€Š¼Š°Ń†ŠøŃ˜Šµ', 'About application': 'Šž Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'ДоГатни коГ за Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin disabled because no admin password', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° Ń˜ŠµŃ€ не могу ŠæŃ€ŠøŃŃ‚ŃƒŠæŠøŃ‚Šø Гатотеци са лозинком', +'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', 'Admin language': 'Језик аГминистратора', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'аГминистративни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', 'Administrator Password:': 'Лозинка аГминистратора:', 'and rename it:': 'Šø ŠæŃ€ŠµŠøŠ¼ŠµŠ½ŃƒŃ˜ у:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', +'Application': 'Application', +'application "%s" uninstalled': 'application "%s" uninstalled', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'application compiled', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'application is compiled and cannot be designed', 'Application name:': 'ŠŠ°Š·ŠøŠ² Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'није ŠŗŠ¾Ń€ŠøŃˆŃ‚ŠµŠ½Š¾', 'are not used yet': 'није још ŠŗŠ¾Ń€ŠøŃˆŃ‚ŠµŠ½Š¾', +'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø Га желите обрисати?', +'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', +'Are you sure?': 'Are you sure?', 'arguments': 'arguments', 'at char %s': 'коГ слова %s', 'at line %s': 'на линији %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Autocomplete Python Code': 'Autocomplete Python Code', +'Available Databases and Tables': 'Available Databases and Tables', 'back': 'назаГ', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', 'Basics': 'ŠžŃŠ½Š¾Š²Šµ', 'Begin': 'ŠŸŠ¾Ń‡ŠµŃ‚Š°Šŗ', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'кеш, Š³Ń€ŠµŃˆŠŗŠµ Šø сесије су обрисани', 'can be a git repo': 'може бити git repo', +'Cancel': 'Cancel', +'Cannot be empty': 'Cannot be empty', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'cannot create file', 'cannot upload file "%(filename)s"': 'не Š¼Š¾Ń„Ńƒ отпремити Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'ŠŸŃ€Š¾Š¼ŠøŃ˜ŠµŠ½Šø лзинку аГминистратора', +'change editor settings': 'change editor settings', +'Changelog': 'Changelog', 'check all': 'check all', 'Check for upgrades': 'ŠŸŃ€Š¾Š²Ń˜ŠµŃ€Šø Š¼Š¾Š³ŃƒŃ›Š½Š¾ŃŃ‚ Š½Š°Š“Š¾Š³Ń€Š°Š“ŃšŠµ', +'Check to delete': 'Check to delete', 'Checking for upgrades...': 'ŠŸŃ€Š¾Š²Ń˜ŠµŃ€Š°Š²Š°Š¼ Š¼Š¾Š³ŃƒŃ›Š½Š¾ŃŃ‚ Š½Š°Š“Š¾Š³Ń€Š°Š“ŃšŠµ...', 'Clean': 'ŠŸŃ€Š¾Ń‡ŠøŃŃ‚Šø', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', 'code': 'коГ', +'Code listing': 'Code listing', 'collapse/expand all': 'сакрити/приказати све', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'ŠšŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Ń˜', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'compiled application removed', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»ŠµŃ€Šø', 'controllers': 'контролери', 'Count': 'Count', 'Create': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜', 'create file with filename:': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜ Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ поГ називом:', 'Create rules': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜ правила', +'Create/Upload': 'Create/Upload', 'created by': 'израГио', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'crontab', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'currently running': 'Ń‚Ń€ŠµŠ½ŃƒŃ‚Š½Š¾ ŠæŠ¾ŠŗŃ€ŠµŠ½ŃƒŃ‚', 'currently saved or': 'Ń‚Ń€ŠµŠ½ŃƒŃ‚Š½Š¾ ŃŠ°Ń‡ŃƒŠ²Š°Š½Š¾ или', +'data uploaded': 'data uploaded', +'Database': 'Database', +'Database %s select': 'Database %s select', +'Database administration': 'Database administration', 'database administration': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° базе поГатака', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Date and Time', +'db': 'db', 'Debug': 'Debug', 'defines tables': 'Š“ŠµŃ„ŠøŠ½ŠøŃˆŠµ табеле', -'delete': 'Š¾Š±Ń€ŠøŃˆŠø', 'Delete': 'ŠžŠ±Ń€ŠøŃˆŠø', +'delete': 'Š¾Š±Ń€ŠøŃˆŠø', 'delete all checked': 'delete all checked', +'delete plugin': 'delete plugin', 'Delete this file (you will be asked to confirm deletion)': 'ŠžŠ±Ń€ŠøŃˆŠø ову Š“Š°Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ (бићете ŃƒŠæŠøŃ‚Š°Š½Šø за ŠæŠ¾Ń‚Š²Ń€Š“Ńƒ Š±Ń€ŠøŃŠ°ŃšŠ°)', +'Delete:': 'Delete:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø', 'Deploy on Google App Engine': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на Google App Engine', 'Deploy to OpenShift': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', +'design': 'design', 'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', 'Disable': 'Š˜ŃŠŗŃ™ŃƒŃ‡Šø', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŃ˜Š°', +'Docs': 'Docs', +'done!': 'done!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø plugins', +'Download plugins from repository': 'Download plugins from repository', 'Edit': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ', 'edit all': 'ŃƒŃ€ŠµŠ“Šø све', 'Edit application': 'УреГи Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'edit controller': 'ŃƒŃ€ŠµŠ“Šø контролер', +'edit controller:': 'edit controller:', +'Edit current record': 'Edit current record', 'edit views:': 'ŃƒŃ€ŠµŠ“Šø views:', +'Editing %s': 'Editing %s', 'Editing file "%s"': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ Гатотеке "%s"', 'Editing Language file': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ Ń˜ŠµŠ·ŠøŃ‡ŠŗŠµ Гатотеке', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Error': 'Š“Ń€ŠµŃˆŠŗŠ°', 'Error logs for "%(app)s"': 'ŠŸŃ€ŠµŠ³Š»ŠµŠ“ Š³Ń€ŠµŃˆŠ°ŠŗŠ° за "%(app)s"', 'Error snapshot': 'Error snapshot', 'Error ticket': 'Error ticket', 'Errors': 'Š“Ń€ŠµŃˆŠŗŠµ', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Expand Abbreviation', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', +'export as csv file': 'export as csv file', +'Exports:': 'Exports:', 'exposes': 'exposes', 'exposes:': 'exposes:', 'extends': 'ŠæŃ€Š¾ŃˆŠøŃ€ŃƒŃ˜Šµ', 'failed to compile file because:': 'нисам могао Га ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š¼ због:', +'failed to reload module because:': 'failed to reload module because:', 'File': 'Датотека', +'file "%(filename)s" created': 'file "%(filename)s" created', +'file "%(filename)s" deleted': 'file "%(filename)s" deleted', +'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', +'file "%s" of %s restored': 'file "%s" of %s restored', +'file changed on disk': 'file changed on disk', 'file does not exist': 'Гатотека не ŠæŠ¾ŃŃ‚Š¾Ń˜Šø', +'file not found': 'file not found', +'file saved on %(time)s': 'file saved on %(time)s', 'file saved on %s': 'Гатотека ŃŠ°Ń‡ŃƒŠ²Š°Š½Š° на %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'филтер', 'Find Next': 'ŠŸŃ€Š¾Š½Š°Ń’Šø сљеГећи', 'Find Previous': 'ŠŸŃ€Š¾Š½Š°Ń’Šø претхоГни', +'Form has errors': 'Form has errors', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', 'Generate': 'Generate', 'Get from URL:': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø са странице:', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'Go to Matching Pair', 'go!': 'крени!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Help': 'ŠŸŠ¾Š¼Š¾Ń›', +'here': 'here', 'Hide/Show Translated strings': 'Дакрити/ŠŸŃ€ŠøŠŗŠ°Š·Š°Ń‚Šø превеГене Ń€ŠøŃ˜ŠµŃ‡Šø', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'ŃƒŠŗŃ™ŃƒŃ‡ŃƒŃ˜Šµ', +'Indent with tabs': 'Indent with tabs', 'inspect attributes': 'inspect attributes', 'Install': 'Š˜Š½ŃŃ‚Š°Š»ŠøŃ€Š°Ń˜', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Š˜Š½ŃŃ‚Š°Š»ŠøŃ€Š°Š½Šµ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', +'internal error': 'internal error', +'internal error: %s': 'internal error: %s', +'Internal State': 'Internal State', +'Invalid action': 'Invalid action', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid git repository specified.': 'Invalid git repository specified.', +'invalid password': 'invalid password', 'invalid password.': 'ŠæŠ¾Š³Ń€ŠµŃˆŠ½Š° лозинка.', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', +'invalid ticket': 'invalid ticket', +'Key': 'Key', 'Key bindings': 'ŠŸŃ€ŠµŃ‡ŠøŃ†Šµ', 'Key bindings for ZenCoding Plugin': 'ŠŸŃ€ŠµŃ‡ŠøŃ†Šµ за ZenCoding Plugin', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', +'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', 'Language files (static strings) updated': 'ŠˆŠµŠ·ŠøŃ‡ŠŗŠµ Гатотеке су Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½Šµ', 'languages': 'Ń˜ŠµŠ·ŠøŃ†Šø', 'Languages': 'ŠˆŠµŠ·ŠøŃ†Šø', +'Last Revision': 'Last Revision', 'Last saved on:': 'ŠŸŠ¾ŃŃ™ŠµŠ“ŃšŠ° измјена:', 'License for': 'Лиценца за', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'ŠæŃ€ŠµŃƒŠ·ŠøŠ¼Š°Š¼...', +'Local Apps': 'Local Apps', 'locals': 'locals', +'Locals##debug': 'Locals##debug', 'Login': 'ŠŸŃ€ŠøŃ˜Š°Š²Š°', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'ŠŸŃ€ŠøŃ˜Š°Š²Š° за аГминистративни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', +'Login/Register': 'Login/Register', 'Logout': 'Излаз', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', 'Match Pair': 'Match Pair', +'Memberships': 'Memberships', +'merge': 'merge', 'Merge Lines': 'Дпоји линије', -'models': 'models', 'Models': 'Models', +'models': 'models', +'Modified On': 'Modified On', 'Modules': 'Modules', 'modules': 'modules', +'Multi User Mode': 'Multi User Mode', +'new application "%s" created': 'new application "%s" created', +'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'Š§Š°Ń€Š¾Š±ŃšŠ°Šŗ за нове Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', 'New application wizard': 'Š§Š°Ń€Š¾Š±ŃšŠ°Šŗ за нове Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'New Record', +'new record inserted': 'new record inserted', 'New simple application': 'ŠŠ¾Š²Š° Ń˜ŠµŠ“Š½Š¾ŃŃ‚Š°Š²Š½Š° Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š°', +'next': 'next', +'next %s rows': 'next %s rows', 'Next Edit Point': 'Next Edit Point', +'NO': 'NO', +'no changes': 'no changes', +'No databases in this application': 'No databases in this application', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'онлајн Š“ŠøŠ·Š°Ń˜Š½ŠµŃ€', +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', +'or import from csv file': 'or import from csv file', 'Original/Translation': 'ŠžŃ€Š³ŠøŠ½Š°Š»/ŠŸŃ€ŠµŠ²Š¾Š“', +'Overview': 'Overview', 'Overwrite installed app': 'ŠŸŃ€ŠµŠ±Ń€ŠøŃˆŠø ŠæŠ¾ŃŃ‚Š¾Ń˜ŠµŃ›Ńƒ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'Pack all': 'Š—Š°ŠæŠ°ŠŗŃƒŃ˜ све', +'Pack compiled': 'Pack compiled', +'Pack custom': 'Pack custom', +'pack plugin': 'pack plugin', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'Peeking at file', -'Plugins': 'Plugins', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugins', +'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'ŠžŠ¼Š¾Š³ŃƒŃ›ŠøŠ¾', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'Previous Edit Point': 'Previous Edit Point', 'Private files': 'Private files', 'private files': 'private files', 'Project Progress': 'ŠŠ°ŠæŃ€ŠµŠ“Š°Šŗ ŠæŃ€Š¾Ń˜ŠµŠŗŃ‚Š°', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'Query:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', 'Reload routes': 'ŠžŠ±Š½Š¾Š²Šø ŠæŃ€ŠµŃƒŃŠ¼Ń˜ŠµŃ€ŠµŃšŠ°', +'Remove compiled': 'Remove compiled', 'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Replace': 'Š—Š°Š¼ŠøŃ˜ŠµŠ½Šø', 'Replace All': 'Š—Š°Š¼ŠøŃ˜ŠµŠ½Šø све', +'Repository (%s)': 'Repository (%s)', 'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': 'Resolve Conflict file', 'response': 'response', 'restart': 'restart', 'restore': 'restore', +'return': 'return', +'Revert': 'Revert', 'revert': 'revert', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', 'rules are not defined': 'правила нису Гефинисана', 'rules:': 'правила:', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", 'Running on %s': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½ŃƒŃ‚Šø на %s', 'Save': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'ŃŠ°Ń‡ŃƒŠ²Š°Ń˜ via Ajax', 'Saved file hash:': 'Š”Š°Ń‡ŃƒŠ²Š°Š½Š¾ као Ń…Š°Ńˆ:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'session': 'сесија', 'session expired': 'сесија истекла', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'Š”Š°Ń˜Ń‚', +'Size of cache:': 'Size of cache:', 'skip to generate': 'skip to generate', +'some files could not be removed': 'some files could not be removed', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', 'Start a new app': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø нову Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'Start searching': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø ŠæŃ€ŠµŃ‚Ń€Š°Š³Ńƒ', 'Start wizard': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø Ń‡Š°Ń€Š¾Š±ŃšŠ°ŠŗŠ°', +'state': 'state', +'Static': 'Static', 'static': 'static', 'Static files': 'Static files', +'Statistics': 'Statistics', 'Step': 'ŠšŠ¾Ń€Š°Šŗ', +'step': 'step', +'stop': 'stop', +'submit': 'submit', 'Submit': 'ŠŸŃ€ŠøŃ…Š²Š°Ń‚Šø', 'successful': 'успјешан', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', +'Table': 'Table', +'Temporary': 'Temporary', 'test': 'тест', 'Testing application': 'Š¢ŠµŃŃ‚ŠøŃ€Š°ŃšŠµ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', +'Theme': 'Theme', +'There are no controllers': 'There are no controllers', 'There are no models': 'There are no models', +'There are no modules': 'There are no modules', 'There are no plugins': 'There are no plugins', 'There are no private files': 'There are no private files', +'There are no static files': 'There are no static files', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'There are no translators, only default language is supported', +'There are no views': 'There are no views', 'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'These files are served without processing, your images go here', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', 'Ticket Missing': 'Ticket nedostaje', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'to previous version.': 'на ŠæŃ€ŠµŃ‚Ń…Š¾Š“Š½Ńƒ Š²ŠµŃ€Š·ŠøŃ˜Ńƒ.', 'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', 'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'Translation strings for the application': 'Š ŠøŃ˜ŠµŃ‡Šø у Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø које треба превести', +'try something like': 'try something like', 'Try the mobile interface': 'ŠŸŃ€Š¾Š±Š°Ń˜ мобилни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', 'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Unable to check for upgrades', +'unable to create application "%s"': 'unable to create application "%s"', +'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', +'unable to parse csv file': 'unable to parse csv file', +'unable to uninstall "%s"': 'unable to uninstall "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'uncheck all', 'Uninstall': 'Š”ŠµŠøŠ½ŃŃ‚Š°Š»ŠøŃ€Š°Ń˜', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'Š°Š¶ŃƒŃ€ŠøŃ€Š°Ń˜', 'update all languages': 'Š°Š¶ŃƒŃ€ŠøŃ€Š°Ń˜ све језике', +'Update:': 'Update:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upload': 'ŠžŃ‚ŠæŃ€ŠµŠ¼Šø', +'Upload': 'Upload', 'Upload a package:': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø пакет:', 'Upload and install packed application': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø Šø ŠøŠ½ŃŃ‚Š°Š»ŠøŃ€Š°Ń˜ запаковану Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'upload file:': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ:', 'upload plugin file:': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø плагин Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', 'Version': 'Š’ŠµŃ€Š·ŠøŃ˜Š°', 'Version %s.%s.%s (%s) %s': 'Š’ŠµŃ€Š·ŠøŃ˜Š° %s.%s.%s (%s) %s', 'Versioning': 'Versioning', -'views': 'views', 'Views': 'Views', +'views': 'views', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py је Š°Š¶ŃƒŃ€Š°Š½', +'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'web2py Recent Tweets', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Working...': 'Working...', 'Wrap with Abbreviation': 'Wrap with Abbreviation', +'WSGI reference name': 'WSGI reference name', +'YES': 'YES', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/sr-lt.py b/applications/admin/languages/sr-lt.py index 64f8eb7f..659cb84e 100755 --- a/applications/admin/languages/sr-lt.py +++ b/applications/admin/languages/sr-lt.py @@ -2,211 +2,594 @@ { '!langcode!': 'sr-lt', '!langname!': 'Srpski (Latinica)', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', +'%s %%{row} deleted': '%s %%{row} deleted', +'%s %%{row} updated': '%s %%{row} updated', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', '(requires internet access)': '(zahtijeva pristup internetu)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(neÅ”to kao "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(datoteka **gluon/contrib/plural_rules/%s.py** nije pronađena)', +'Abort': 'Abort', 'About': 'Informacije', 'About application': 'O aplikaciji', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'Dodatni kod za aplikaciju', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'admin disabled because no admin password', +'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'administracija onemogućena jer ne mogu pristupiti datoteci sa lozinkom', +'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', 'Admin language': 'Jezik administratora', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'administrativni interfejs', 'Administrator Password:': 'Lozinka administratora:', 'and rename it:': 'i preimenuj u:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', +'Application': 'Application', +'application "%s" uninstalled': 'application "%s" uninstalled', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'application compiled', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'application is compiled and cannot be designed', 'Application name:': 'Naziv aplikacije:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'nije koriÅ”teno', 'are not used yet': 'nije joÅ” koriÅ”teno', +'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Da li ste sigurni da želite obrisati?', +'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', +'Are you sure?': 'Are you sure?', 'arguments': 'arguments', 'at char %s': 'kod slova %s', 'at line %s': 'na liniji %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'Autocomplete Python Code': 'Autocomplete Python Code', +'Available Databases and Tables': 'Available Databases and Tables', 'back': 'nazad', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', 'Basics': 'Osnove', 'Begin': 'Početak', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'keÅ”, greÅ”ke i sesije su obrisani', 'can be a git repo': 'može biti git repo', +'Cancel': 'Cancel', +'Cannot be empty': 'Cannot be empty', +'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', +'cannot create file': 'cannot create file', 'cannot upload file "%(filename)s"': 'ne mogu otpremiti datoteku "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Promijeni lozinku administratora', +'change editor settings': 'change editor settings', +'Changelog': 'Changelog', 'check all': 'check all', 'Check for upgrades': 'Provjeri mogućnost nadogradnje', +'Check to delete': 'Check to delete', 'Checking for upgrades...': 'Provjeravam mogućnost nadogradnje...', 'Clean': 'Pročisti', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', 'code': 'kod', +'Code listing': 'Code listing', 'collapse/expand all': 'sakriti/prikazati sve', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'Kompajliraj', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'compiled application removed', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'Kontroleri', 'controllers': 'kontroleri', 'Count': 'Count', 'Create': 'Kreiraj', 'create file with filename:': 'Kreiraj datoteku pod nazivom:', 'Create rules': 'Kreiraj pravila', +'Create/Upload': 'Create/Upload', 'created by': 'izradio', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'crontab', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', 'currently running': 'trenutno pokrenut', 'currently saved or': 'trenutno sačuvano ili', +'data uploaded': 'data uploaded', +'Database': 'Database', +'Database %s select': 'Database %s select', +'Database administration': 'Database administration', 'database administration': 'administracija baze podataka', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'Date and Time', +'db': 'db', 'Debug': 'Debug', 'defines tables': 'definiÅ”e tabele', -'delete': 'obriÅ”i', 'Delete': 'ObriÅ”i', +'delete': 'obriÅ”i', 'delete all checked': 'delete all checked', +'delete plugin': 'delete plugin', 'Delete this file (you will be asked to confirm deletion)': 'ObriÅ”i ovu datoteku (bićete upitani za potvrdu brisanja)', +'Delete:': 'Delete:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', 'Deploy': 'Postavi', 'Deploy on Google App Engine': 'Postavi na Google App Engine', 'Deploy to OpenShift': 'Postavi na OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', +'design': 'design', 'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', 'Disable': 'Isključi', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'dokumentacija', +'Docs': 'Docs', +'done!': 'done!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'preuzmi layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'preuzmi plugins', +'Download plugins from repository': 'Download plugins from repository', 'Edit': 'Uređivanje', 'edit all': 'uredi sve', 'Edit application': 'Uredi aplikaciju', 'edit controller': 'uredi controller', +'edit controller:': 'edit controller:', +'Edit current record': 'Edit current record', 'edit views:': 'uredi views:', +'Editing %s': 'Editing %s', 'Editing file "%s"': 'Uređivanje datoteke "%s"', 'Editing Language file': 'Uređivanje jezičke datoteke', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'Error': 'GreÅ”ka', 'Error logs for "%(app)s"': 'Pregled greÅ”aka za "%(app)s"', 'Error snapshot': 'Error snapshot', 'Error ticket': 'Error ticket', 'Errors': 'GreÅ”ke', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Expand Abbreviation', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', +'export as csv file': 'export as csv file', +'Exports:': 'Exports:', 'exposes': 'exposes', 'exposes:': 'exposes:', 'extends': 'proÅ”iruje', 'failed to compile file because:': 'nisam mogao da kompajliram zbog:', +'failed to reload module because:': 'failed to reload module because:', 'File': 'Datoteka', +'file "%(filename)s" created': 'file "%(filename)s" created', +'file "%(filename)s" deleted': 'file "%(filename)s" deleted', +'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', +'file "%s" of %s restored': 'file "%s" of %s restored', +'file changed on disk': 'file changed on disk', 'file does not exist': 'datoteka ne postoji', +'file not found': 'file not found', +'file saved on %(time)s': 'file saved on %(time)s', 'file saved on %s': 'datoteka sačuvana na %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', 'filter': 'filter', 'Find Next': 'Pronađi sljedeći', 'Find Previous': 'Pronađi prethodni', +'Form has errors': 'Form has errors', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', 'Generate': 'Generate', 'Get from URL:': 'Preuzmi sa stranice:', 'Git Pull': 'Git Pull', 'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'Go to Matching Pair', 'go!': 'kreni!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', 'Help': 'Pomoć', +'here': 'here', 'Hide/Show Translated strings': 'Sakriti/Prikazati prevedene riječi', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'Import/Export', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'uključuje', +'Indent with tabs': 'Indent with tabs', 'inspect attributes': 'inspect attributes', 'Install': 'Instaliraj', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Instalirane aplikacije', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', +'internal error': 'internal error', +'internal error: %s': 'internal error: %s', +'Internal State': 'Internal State', +'Invalid action': 'Invalid action', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid git repository specified.': 'Invalid git repository specified.', +'invalid password': 'invalid password', 'invalid password.': 'pogreÅ”na lozinka.', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', +'invalid ticket': 'invalid ticket', +'Key': 'Key', 'Key bindings': 'Prečice', 'Key bindings for ZenCoding Plugin': 'Prečice za for ZenCoding Plugin', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', +'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', 'Language files (static strings) updated': 'Jezičke datoteke su ažurirane', 'languages': 'jezici', 'Languages': 'Jezici', +'Last Revision': 'Last Revision', 'Last saved on:': 'Posljednja izmjena:', 'License for': 'Licenca za', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': 'preuzimam...', +'Local Apps': 'Local Apps', 'locals': 'locals', +'Locals##debug': 'Locals##debug', 'Login': 'Prijava', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'Prijava za administrativni interfejs', +'Login/Register': 'Login/Register', 'Logout': 'Izlaz', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', 'Match Pair': 'Match Pair', +'Memberships': 'Memberships', +'merge': 'merge', 'Merge Lines': 'Spoji linije', -'models': 'models', 'Models': 'Models', +'models': 'models', +'Modified On': 'Modified On', 'Modules': 'Modules', 'modules': 'modules', +'Multi User Mode': 'Multi User Mode', +'new application "%s" created': 'new application "%s" created', +'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'Čarobnjak za nove aplikacije', 'New application wizard': 'Čarobnjak za nove aplikacije', +'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'New Record', +'new record inserted': 'new record inserted', 'New simple application': 'Nova jednostavna aplikacija', +'next': 'next', +'next %s rows': 'next %s rows', 'Next Edit Point': 'Next Edit Point', +'NO': 'NO', +'no changes': 'no changes', +'No databases in this application': 'No databases in this application', +'No Interaction yet': 'No Interaction yet', +'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'onlajn dizajner', +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', +'or import from csv file': 'or import from csv file', 'Original/Translation': 'Original/Prevod', +'Overview': 'Overview', 'Overwrite installed app': 'PrebriÅ”i postojeću aplikaciju', 'Pack all': 'Zapakuj sve', +'Pack compiled': 'Pack compiled', +'Pack custom': 'Pack custom', +'pack plugin': 'pack plugin', +'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'Peeking at file': 'Peeking at file', -'Plugins': 'Plugins', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', +'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'plugins', +'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Omogućio', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'Previous Edit Point': 'Previous Edit Point', 'Private files': 'Private files', 'private files': 'private files', 'Project Progress': 'Napredak projekta', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': 'Query:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', 'Reload routes': 'Obnovi preusmjerenja', +'Remove compiled': 'Remove compiled', 'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Replace': 'Zamijeni', 'Replace All': 'Zamijeni sve', +'Repository (%s)': 'Repository (%s)', 'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': 'Resolve Conflict file', 'response': 'response', 'restart': 'restart', 'restore': 'restore', +'return': 'return', +'Revert': 'Revert', 'revert': 'revert', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', 'rules are not defined': 'pravila nisu definisana', 'rules:': 'pravila:', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", 'Running on %s': 'Pokrenuto na %s', 'Save': 'Sačuvaj', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Sačuvaj via Ajax', 'Saved file hash:': 'Sačuvano kao haÅ”:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'session': 'sesija', 'session expired': 'sesija istekla', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'Sajt', +'Size of cache:': 'Size of cache:', 'skip to generate': 'skip to generate', +'some files could not be removed': 'some files could not be removed', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', 'Start a new app': 'Pokreni novu aplikaciju', 'Start searching': 'Pokreni pretragu', 'Start wizard': 'Pokreni čarobnjaka', +'state': 'state', +'Static': 'Static', 'static': 'static', 'Static files': 'Static files', +'Statistics': 'Statistics', 'Step': 'Korak', +'step': 'step', +'stop': 'stop', +'submit': 'submit', 'Submit': 'Prihvati', 'successful': 'uspjeÅ”an', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', +'Table': 'Table', +'Temporary': 'Temporary', 'test': 'test', 'Testing application': 'Testing application', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', +'Theme': 'Theme', +'There are no controllers': 'There are no controllers', 'There are no models': 'There are no models', +'There are no modules': 'There are no modules', 'There are no plugins': 'There are no plugins', 'There are no private files': 'There are no private files', +'There are no static files': 'There are no static files', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'There are no translators, only default language is supported', +'There are no views': 'There are no views', 'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'These files are served without processing, your images go here', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', 'Ticket Missing': 'Ticket nedostaje', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'to previous version.': 'na prethodnu verziju.', 'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', 'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'Translation strings for the application': 'Riječi u aplikaciji koje treba prevesti', +'try something like': 'try something like', 'Try the mobile interface': 'Probaj mobilni interfejs', 'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'Unable to check for upgrades', +'unable to create application "%s"': 'unable to create application "%s"', +'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download app because:': 'Unable to download app because:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', +'unable to parse csv file': 'unable to parse csv file', +'unable to uninstall "%s"': 'unable to uninstall "%s"', +'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'uncheck all', 'Uninstall': 'Deinstaliraj', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'ažuriraj', 'update all languages': 'ažuriraj sve jezike', +'Update:': 'Update:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upload': 'Otpremi', +'Upload': 'Upload', 'Upload a package:': 'Preuzmi paket:', 'Upload and install packed application': 'Preuzmi i instaliraj zapakovanu aplikaciju', 'upload file:': 'preuzmi datoteku:', 'upload plugin file:': 'preuzmi plugin datoteku:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', 'Version': 'Verzija', 'Version %s.%s.%s (%s) %s': 'Verzija %s.%s.%s (%s) %s', 'Versioning': 'Versioning', -'views': 'views', 'Views': 'Views', +'views': 'views', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py je ažuran', +'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'web2py Recent Tweets', +'web2py upgrade': 'web2py upgrade', +'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Working...': 'Working...', 'Wrap with Abbreviation': 'Wrap with Abbreviation', +'WSGI reference name': 'WSGI reference name', +'YES': 'YES', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/tr.py b/applications/admin/languages/tr.py index b31e25cd..50e52d8e 100755 --- a/applications/admin/languages/tr.py +++ b/applications/admin/languages/tr.py @@ -1,29 +1,38 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- { '!langcode!': 'tr', '!langname!': 'TürkƧe', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"güncelleme" ("update") "field1 = \'yenideğer\'" gibi seƧeneğe bağlı bir ifadedir. JOIN sonuƧlarını silemez veya silemezsiniz.', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s %%{row} silindi', '%s %%{row} updated': '%s %%{row} güncellendi', +'%s selected': '%s selected', +'%s students registered': '%s students registered', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', '(requires internet access)': '(Internet erişimi gerekir)', '(requires internet access, experimental)': '(internet erişimi gerekir, deneysel)', '(something like "it-it")': '("it-it" şeklinde birşeyler) ', +'(version %s)': '(version %s)', '1: Setting Parameters': '1: Parametrelerin Yapılandırılması', +'?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': '@markmin\x01Bir hata oluştu, lütfen sayfayı [[reload %s]]', "@markmin\x01Mercurial Version Control System Interface[[NEWLINE]]for application '%s'": "'%s' uygulaması iƧin[[NEWLINE]]Mercurial Sürüm Kontrol Sistemi Arayüzü", '@markmin\x01Searching: **%s** %%{file}': '@markmin\x01Aranıyor: **%s** %%{file}', 'A new version of web2py is available: %s': "web2py'nin yeni sürümü mevcut: %s ", 'A new version of web2py is available: Version 1.68.2 (2009-10-21 09:59:29)\n': "web2py'nin yeni sürümü mevcut: Sürüm 1.68.2 (2009-10-21 09:59:29)\r\n", +'Abort': 'Abort', 'About': 'Hakkında', 'About application': 'Uygulama hakkında', +'Accept Terms': 'Accept Terms', 'Add breakpoint': 'Kesme noktası ekle', 'additional code for your application': 'uygulamanız iƧin fazladan kod', 'Additional code for your application': 'Uygulamanız iƧin fazladan kod', 'Admin design page': 'Yƶnetici tasarım sayfası', 'admin disabled because no admin password': 'yƶnetici parolası olmadığından admin etkinsiz', 'admin disabled because not supported on google app engine': 'Google App Motoru tafaından desteklenmediğinden admin etkinsizleştirildi', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', 'admin disabled because unable to access password file': 'parola dosyasına erişielemdiğinden admin etkinsizleştirildi', 'Admin is disabled because insecure channel': 'güvenzis kanal olduğundan admin etkinsizleştirildi', 'Admin language': 'Admin dilleri', @@ -32,14 +41,18 @@ 'Administrator Password:': 'Yƶnetici Parolası:', 'and rename it (required):': 've yeniden adlandır (gerekli):', 'and rename it:': 'yeniden adlandır:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'appadmin': 'appadmin', 'appadmin is disabled because insecure channel': 'güvenzis kanal olduğundan admin etkinsizleştirildi', 'Application': 'Uygulama', 'application "%s" uninstalled': '"%s" uygulaması kaldırıldı', 'application %(appname)s installed with md5sum: %(digest)s': '%(appname)s uygulaması md5sum %(digest)s ile kuruldu', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', 'application compiled': 'uygulama derlendi', +'Application exists already': 'Application exists already', 'application is compiled and cannot be designed': 'uygulama derlenmiş ve tasarlanamaz', 'Application name:': 'Uygulama adı:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'kullanılamıyor', 'are not used yet': 'şimdilik kullanılamıyor', 'Are you sure you want to delete file "%s"?': 'Ā«%sĀ» dosyasını silmek istediğinize emin misiniz?', @@ -47,20 +60,31 @@ 'Are you sure you want to delete this object?': 'Bu nesneyi silmek istediğinizden emin misiniz?', 'Are you sure you want to uninstall application "%s"?': 'Ā«%sĀ» uygulamasını kaldırmak istediğinizden emin misiniz?', 'Are you sure you want to upgrade web2py now?': "web2py'yi güncellemek istediğinizden emin misiniz?", +'Are you sure?': 'Are you sure?', 'arguments': 'argümanlar', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'UYARI: Giriş günceli bağlantı (HTTPS) gerektirmekte veya yerel makinada Ƨalışılmalıdır.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'UYARI: ƇOKLU TEST GÜVENLİ DEĞİL. BİRDEN ƇOK TESTİ AYNI ANDA YAPMAYIN.', 'ATTENTION: you cannot edit the running application!': 'UYARI: Ƨalışan uygulamayı düzenleyemezsiniz!', 'Autocomplete Python Code': 'Python Kodlarını Otomatik Tamamla', 'Available databases and tables': 'Kullanılabilir veritabanları ve tablolar', +'Available Databases and Tables': 'Available Databases and Tables', 'back': 'geri', +'Back to the plugins list': 'Back to the plugins list', 'Back to wizard': 'Sihirbaza geri dƶn', 'Basics': 'Temeller', 'Begin': 'Başla', 'breakpoint': 'kesme noktası', -'Breakpoints': 'Kesme Noktaları', 'breakpoints': 'kesme noktaları', +'Breakpoints': 'Kesme Noktaları', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', 'cache': 'zula', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'zula, hatalar ve oturumlar temizlendi', 'can be a git repo': 'git deposu olabilir', 'Cancel': 'VazgeƧ', @@ -69,23 +93,36 @@ 'Cannot compile: there are errors in your app:': 'Derlenemiyor: uygulamanızda hata(lar) var:', 'cannot create file': 'dosya oluşturulamıyor', 'cannot upload file "%(filename)s"': '"%(filename)s" dosyalarını yükleyemiyor', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'admin parolasını değiştir', 'change editor settings': 'düzenleyici ayarlarını değiştir', +'Changelog': 'Changelog', 'check all': 'tümünü kontrol et', 'Check for upgrades': 'Güncellemeleri kontrol et', 'Check to delete': 'Silmek iƧin kontrok et', 'Checking for upgrades...': 'Güncellemeler denetleniyor ... ', 'Clean': 'Temizle', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', 'Click row to expand traceback': 'Takibi genişletmek iƧin satır üzerine tıkla', +'Click row to view a ticket': 'Click row to view a ticket', 'click to check for upgrades': 'güncellemeleri denetlemek iƧin tıklayın', 'code': 'kod', +'Code listing': 'Code listing', 'collapse/expand all': 'sıkıştır/tümünü aƧ', +'Command': 'Command', 'Comment:': 'Yorum:', 'Commit': 'Ɩneri', 'Commit form': 'Tarafından ƶneri', +'Committed files': 'Committed files', 'Compile': 'Derle', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'derlenmiş uygulama kaldırıldı', 'Condition': 'Durum', +'continue': 'continue', 'Controllers': 'DenetƧiler', 'controllers': 'denetƧiler', 'Count': 'Sayı', @@ -95,6 +132,9 @@ 'Create new simple application': 'Yeni basit uygulama oluştur', 'Create/Upload': 'Oluştur/Yükle', 'created by': 'yazan:', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', 'crontab': 'krontab', 'Current request': 'Şimdiki istek', 'Current response': 'Şimdiki yanıt', @@ -102,10 +142,13 @@ 'currently running': 'şimdiki Ƨalışan', 'currently saved or': 'şimdiki kaydedilen veya', 'data uploaded': 'veri yüklendi', +'Database': 'Database', 'database': 'veritabı', +'Database %s select': 'Database %s select', 'database %s select': '%s veritabanı seƧildi', 'Database administration': 'Veritabanı yƶnetimi', 'database administration': 'veritabı yƶnetimi', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Tarih ve Zaman', 'db': 'db', 'Debug': 'Hata Ayıkla', @@ -117,19 +160,40 @@ 'Delete this file (you will be asked to confirm deletion)': 'Bu dosyayı sil (silmek iƧin onay istenecek)', 'Delete:': 'Sil:', 'deleted after first hit': 'ilk vuruşta silinir', +'Demo': 'Demo', 'Deploy': 'Yayımla', 'Deploy on Google App Engine': 'Google App Motorunda Yayınla', 'Deploy to OpenShift': "OpenShift'e Yayınla ", +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', 'Deployment form': 'Yayınlama formu', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', 'design': 'tadarla', 'Detailed traceback description': 'Ayrıntılı nedenin bulma tanımı', +'details': 'details', 'direction: ltr': 'yƶn: ltr', +'directory not found': 'directory not found', 'Disable': 'Devre Dışı', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'dokümanlar', +'Docs': 'Docs', 'done!': 'bitti!', +'Downgrade': 'Downgrade', 'Download .w2p': '.w2p İndir', +'Download as .exe': 'Download as .exe', 'download layouts': 'düzenleri indir', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'eklentileri indir', +'Download plugins from repository': 'Download plugins from repository', 'EDIT': 'DÜZENLE', 'Edit': 'Düzenle', 'edit all': 'tümünü düzenle', @@ -142,7 +206,12 @@ 'Editing file': 'Dosya düzenleniyor', 'Editing file "%s"': '"%s" dosyası düzenleniyor ', 'Editing Language file': 'Dil dosyası düzenleniyor', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', 'Enable': 'Etkinleştir', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'enter a valid email address': 'geƧerli e-posta adresi girin', 'enter a value': 'bir değer girin', 'enter only letters, numbers, and underscore': 'sadece harf, sayı ve alt Ƨizgi giriniz', @@ -152,13 +221,18 @@ 'Error snapshot': 'Hata resmi', 'Error ticket': 'Hata bileti', 'Errors': 'Hatalar', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', 'Exception instance attributes': 'Hata durumu ƶzellikleri', 'Exit Fullscreen': 'Tam Ekrandan Ƈık', 'Expand Abbreviation': 'Kısıtlamayı AƧ', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'CSV olarak dışa ver', +'Exports:': 'Exports:', 'exposes': 'sergileniyor', 'exposes:': 'sergile:', 'extends': 'genişlet', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'modül yüklenemedi', 'failed to reload module because:': 'modü yüklenemedi çünkü:', 'File': 'Dosya', @@ -171,13 +245,17 @@ 'file not found': 'dosya bulanamadı', 'file saved on %(time)s': 'dosyası %(time)s zamanında kaydedildi', 'file saved on %s': 'dosyası kaydedildi: %s', +'filename': 'filename', 'Filename': 'Dosya adı', +'Files added': 'Files added', 'filter': 'filtre', 'Find Next': 'Sonrakini Bul', 'Find Previous': 'Ɩncekini Bul', +'Form has errors': 'Form has errors', 'Frames': 'ƇerƧeveler', 'Functions with no doctests will result in [passed] tests.': '[passed] testlerdeki işlevlerde doctest yok', 'GAE Email': 'GAE E-postası', +'GAE Output': 'GAE Output', 'GAE Password': 'GAE Parolası', 'Generate': 'Oluştur', 'Git Pull': 'Git Ƈek', @@ -188,79 +266,129 @@ 'Google Application Id': 'Google Uygulama Id', 'Goto': 'Git', 'graph model': 'grafik modeli', +'Graph Model': 'Graph Model', 'Help': 'Yardım', +'here': 'here', 'Hide/Show Translated strings': 'Ƈevrilmiş cümleleri Gizle/Gƶrüntüle', +'Highlight current line': 'Highlight current line', 'Hits': 'Vuruşlar', 'Home': 'Anasayfa', 'honored only if the expression evaluates to true': 'sadece deyim doğru sonucunu verirse', 'htmledit': 'html dğzenleyici', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Doküman testi yapılmadan ƶnce, eğer yukarıdaki rapor bir bilet numarası iƧeriyorsa bu denetleyici Ƨalıştırılırken bir hata oluşturğunu gƶsterir. Bu genellikle girinti/Ƨıkıntı (indentation) hatası veya işlev dışındaki bir hatadan kaynaklanır.\nYeşil başlık geƧilen tüm testleri (eğer tanımlanmışsa) gƶsterir. Bu durumda test sonuƧları gƶrüntülenmez.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', 'Import/Export': 'İƧe/Dışa Aktarıcı', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'iƧerir', +'Indent with tabs': 'Indent with tabs', 'index': 'indeks', 'insert new': 'yeni ekle', 'insert new %s': '%s yeni ekle', 'inspect attributes': 'ƶzellikleri denetleyin', 'Install': 'Kurucu', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Kurulu uygulamalar', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'dahili hata', +'internal error: %s': 'internal error: %s', 'Internal State': 'Dahili Durum', 'Invalid action': 'GeƧersiz eylem', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', 'invalid expression': 'geƧersiz ifade', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'Parola geƧersiz', 'invalid password.': 'geƧersiz parola.', 'Invalid Query': 'GeƧersiz Sorgu', 'invalid request': 'geƧersiz istek', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'geƧersiz bilet', +'Key': 'Key', 'Key bindings': 'Anahtarlar', 'Key bindings for ZenCoding Plugin': 'ZenCoding Eklentisi iƧin anahtarlar', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': '"%(filename)s" dil dosyası/dosyaları güncellendi', 'Language files (static strings) updated': 'Dil dosyası (statik cümleler) güncellendi', 'languages': 'diller', 'Languages': 'Diller', +'Last Revision': 'Last Revision', 'Last saved on:': 'Son kaydedilme:', 'License for': 'iƧin lisanslanmış', +'License:': 'License:', 'Line Nr': 'Satır Nr', 'Line number': 'Satır numarası', 'lists by exception': 'istisnaya gƶre sırala', 'lists by ticket': 'bilete gƶre sırala', +'Loading...': 'Loading...', 'loading...': 'yükleniyor ...', +'Local Apps': 'Local Apps', 'locals': 'yereller', 'Locals##debug': 'Yereller', 'Login': 'Giriş', 'login': 'giriş', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'Yƶnetsel Arayüze Giriş\t', +'Login/Register': 'Login/Register', 'Logout': 'Ƈıkış', +'lost password': 'lost password', +'Main Menu': 'Main Menu', 'Manage': 'Yƶnet', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', 'merge': 'birleştir', 'Models': 'Modeller', 'models': 'modeller', +'Modified On': 'Modified On', 'Modules': 'Modüller', 'modules': 'modüller', +'Multi User Mode': 'Multi User Mode', 'new application "%s" created': 'yeni uygulama "%s" oluşturuldu', +'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'Yeni Uygulama Sihirbazı', 'New application wizard': 'Yeni uygulama sihirbazı', 'new plugin installed': 'yeni eklenti kuruldu', +'New plugin installed: %s': 'New plugin installed: %s', 'New Record': 'Yeni Kayıt', 'new record inserted': 'yeni kayıt eklendi', 'New simple application': 'Yeni basit uygulama', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': 'sonraki 100 satır', 'NO': 'HAYIR', +'no changes': 'no changes', 'No databases in this application': 'Bu uygulamada veritabanı yok', 'No Interaction yet': 'Henüz etkileşim yok', 'no match': 'eşlenen yok', 'no package selected': 'hiƧ paket seƧilmemiş', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', 'No ticket_storage.txt found under /private folder': '/private dizininde ticket_storage.txt dosyası bulunamadı', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Not: Eğer github hata kodu 128 durumunu bildiren bir ileti alırsanız, OpenShift hesabınızı ait ssh anahtarının doğru yapılandırıldığından emin olun.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'ƧevirimiƧi tasarlayıcı', 'Open new app in new window': 'Yeni pencerede yeni uygualama aƧ', 'OpenShift Deployment Interface': 'OpenShift Yayınlama Arayüzü', +'OpenShift Output': 'OpenShift Output', 'or alternatively': 'veya seƧenek olarak', 'Or Get from URL:': 'Veya şu URL den alın:', 'or import from csv file': 'veya CSV dsoyasından iƧerin', 'or provide app url:': "veya uygulama URL'si verin:", 'or provide application url:': "veya uygulama URL'si verin:", 'Original/Translation': 'Orjinal / Ƈeviri', +'Overview': 'Overview', 'Overwrite installed app': 'Kurulu uygulama üzerine yaz', 'Pack all': 'Tümünü paketle', 'Pack compiled': 'Derlenenleri paketle', @@ -268,106 +396,171 @@ 'pack plugin': 'eklentiyi paketle', 'PAM authenticated user, cannot change password here': 'PAM onaylı kullanıcı, parola buradan değiştirilemiyor', 'password changed': 'parola değiştirilidi', +'Past revisions': 'Past revisions', 'Path to appcfg.py': 'appcfg.py dosyasının patikası', 'Path to local openshift repo root.': 'Yerel openshift repo kƶkünün patikası.', 'Peeking at file': 'Dosya gƶzetleniyor', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Please': 'Lütfen', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin "%(plugin)s" deleted': '"%(plugin)s" eklentisi silindi', 'Plugin "%s" in application': '"%s" uygulamasında eklenti', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', 'plugins': 'eklentiler', 'Plugins': 'Eklentiler', +'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Ƈoğul-Kipler:', 'Powered by': 'Yazılım Temeli:', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'ƶnceki 100 satır', 'Private files': 'Ɩzel dosyalar', 'private files': 'ƶzel dosyalar', 'Project Progress': 'Proje İlerlemesi', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', 'pygraphviz library not found': 'pygraphviz kütüphanesi yok', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Sorgu: ', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', 'Rapid Search': 'Hızlı Arama', +'Record': 'Record', 'record': 'kayıt', 'record does not exist': 'kayıt bulunamıyor', 'record id': 'kayıt id', +'Record id': 'Record id', 'refresh': 'yeniden yükle', +'register': 'register', 'Reload routes': 'Yƶnelendirmeyi yeniden yükle', 'Remove compiled': 'Derlemeyi kaldır', 'Removed Breakpoint on %s at line %s': ' %s üzerindeki kesme noktası %s satırında değiştrildi.', 'Replace': 'Değiştir', 'Replace All': 'Tümünü değiştir', +'Repository (%s)': 'Repository (%s)', 'request': 'istek', +'requires distutils, but not installed': 'requires distutils, but not installed', 'requires python-git, but not installed': 'python-git gerekyior, fakat kurulu değil', 'Resolve Conflict file': 'Dosyadaki Ƨakışmayı Ƨƶz', 'response': 'tepki', 'restart': 'yeniden başla', 'restore': 'geri al', +'return': 'return', +'Revert': 'Revert', 'revert': 'geri al', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', 'Rows in table': 'Tablosundaki satırlar', 'Rows selected': 'SeƧilen satırlar', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Bu dosyadaki testleri Ƨalıştırır (tüm dosyaları Ƨalıştırmak iƧin, 'test' etiketli düğmeyi kullanabilirsiniz)", 'Running on %s': '%s üzerinde Ƨalışıyor', 'Save': 'Kaydet', 'save': 'kaydet', 'Save file:': 'Dosyayı kaydet:', 'Save file: %s': 'Dosyayı farklı kaydet: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'Ajax üzerinden kaydet', 'Saved file hash:': 'Kaydedilen dosyanın parmak izi:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', 'Select Files to Package': 'Paketlenecek Dosyaları SeƧ', 'selected': 'seƧildi', 'session': 'oturum', 'session expired': 'oturum zamanaşımına uğradı', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': "%s'nin %s satırındaki kesme noktasını: %s yap", 'shell': 'kabuk', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', 'Site': 'Site', +'Size of cache:': 'Size of cache:', 'skip to generate': 'oluşturmak iƧin atla', 'some files could not be removed': 'bazı dosyalar kaldırılamadı', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', 'source : filesystem': 'kaynak : dosyasistemi', 'Start a new app': 'Yeni uygualama başla', 'Start searching': 'Aramaya başla', 'Start wizard': 'Başlatma sihirbazı', 'state': 'durum', -'static': 'statik', 'Static': 'Statik', +'static': 'statik', 'Static files': 'Statik dosyalar', +'Statistics': 'Statistics', 'Step': 'Basamak', -'Submit': 'Gƶnder', +'step': 'step', +'stop': 'stop', 'submit': 'gƶnder', +'Submit': 'Gƶnder', 'successful': 'başarılı', 'Sure you want to delete this object?': 'Bu nesneyi silmek istediğinizden emin misiniz? ', 'switch to : db': 'geƧ : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'tablo', +'Table': 'Table', 'Temporary': 'GeƧici', 'test': 'test', 'Testing application': 'Uygulama test ediliyor', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"sorgulama" "db.table1.field1==\'değer\'" şeklinde bir durumu ifade eder. SQL birleştirmede (JOIN) "db.table1.field1==db.table2.field2" şeklindedir.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'uygulama mantığı: her URL denetleyicideki bir işleve eşlenir', 'The application logic, each URL path is mapped in one exposed function in the controller': 'Uygulama mantığı: her URL denetleyicideki bir işleve eşlenir', 'the data representation, define database tables and sets': 'veri gƶsterimi, veritabanı tablolarını ve setlerini tanımla', 'The data representation, define database tables and sets': 'Veri gƶsterimi, veritabanı tablolarını ve setlerini tanımla', 'The presentations layer, views are also known as templates': 'Sunum katmanı, gƶrünümler şablon olarakda biliniyor.', 'the presentations layer, views are also known as templates': 'sunum katmanı, gƶrünümler şablon olarakda biliniyor.', +'Theme': 'Theme', 'There are no controllers': 'DenetƧiler yok', 'There are no models': 'Modeller yok', 'There are no modules': 'Modüller yok', 'There are no plugins': 'Eklentiler yok', 'There are no private files': 'Ɩzel dosyalar yok', 'There are no static files': 'Statik dosyalar yok', +'There are no translators': 'There are no translators', 'There are no translators, only default language is supported': 'Ƈeviriler, sadeceƶn tanmlı dil destekleniyor.', 'There are no views': 'Gƶrünümler yok', 'These files are not served, they are only available from within your app': 'Bu dosyalar servis yapılmıyor, bunlar sadece uygulamanız iƧerisinden erişilebilir.', 'These files are served without processing, your images go here': 'Bu dosyalarda işlem yapılmadan kaydedildi, resimler buraya gidiyor:', 'these files are served without processing, your images go here': 'bu dosyalarda işlem yapılmadan kaydedildi, resimler buraya gidiyor:', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', 'This is the %(filename)s template': 'Bu şablonlar %(filename)s: ', "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "Bu sayfa uygulamanızı OpenShif uygulama reposuna koyar. Uygulamanızın web2py iskeleti ile oluşturulduğunuz ve web2py dosya sisteminizdeki ropoya erişilebileceği varsayılmıştır. Bu işlev web2py'nin Ƨalıştığı ortamda GitPython'e ihtiyaƧ duyar.", 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'Bu sayfa uygulamanızı Google App Motoru bilişim bulutuna yükleyecektir. İndeksleri yerel olarak oluştırmanız gerektiğini aklınızda tutun, Google uygulama sunucusunu yerel olarak kurup Ƨalıştırın, aksi halde bazı kayıtlarda hatalar olacaktır. Uyarı: yayınlama ağ hızınıza bağlı olarak uzun zaman alabilir. Uyarı: bu sizin app.yaml dosyasını yeniden yazar. LÜTFEN BİRDEN FAZLA GƖNDERMEYİN.', 'this page to see if a breakpoint was hit and debug interaction is required.': 'bu sayfayı, kesme noktasına geldiğini gƶrmek iƧin ve hata ayıklama etkileşmesi gerekiyor..', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'Ticket': 'Bilet', -'Ticket ID': 'Bile ID\'si', +'Ticket ID': "Bile ID'si", +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'TM': 'TM', 'to previous version.': 'ƶnceki sürüme.', 'To create a plugin, name a file/folder plugin_[name]': 'Eklenti oluşturmak iƧin dosyayı dosya/klasƶr plugin_[isim] şeklinde isimlendir. ', 'To emulate a breakpoint programatically, write:': 'Program ile kesme noktasını ƶykünmek iƧin, yazın:', 'to use the debugger!': 'hata ayıklayıcısını kullanmak iƧin!', 'toggle breakpoint': 'kesme noktasını değiştir', +'Toggle comment': 'Toggle comment', 'Toggle Fullscreen': 'Tam Ekrana GeƧ', 'Traceback': 'Nedenin bul', 'translation strings for the application': 'uygulama iƧin Ƨeviri cümleleri', @@ -376,23 +569,33 @@ 'try something like': 'gibi birşey dene', 'Try the mobile interface': 'Mobil arayüzü dene', 'try view': 'gƶrünümü dene', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', 'Unable to check for upgrades': 'Güncellemeler denetlenemiyor', 'unable to create application "%s"': '"%s" uygulaması oluşturulamıyor', 'unable to delete file "%(filename)s"': '"%(filename)s" dosylaları silinemiyor', 'unable to delete file plugin "%(plugin)s"': '"%(plugin)s" eklenti dosyasyaları silenemiyor', +'Unable to determine the line number!': 'Unable to determine the line number!', 'Unable to download': 'İndirilemiyor', 'Unable to download app': 'Uygulamanız indirilemiyor', 'Unable to download app because:': 'Uygulamanız indirilemiyor çünkü:', 'Unable to download because': 'İndirilemiyor çünkü:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': "impossible d'analyser les fichiers CSV", 'unable to uninstall "%s"': 'kladıramıyor çünkü "%s"', 'unable to upgrade because "%s"': 'güncelleyemiyor çünkü "%s"', 'uncheck all': 'tümünü kladır', 'Uninstall': 'Kaldır', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'güncelle', 'update all languages': 'tüm delleri yükle', 'Update:': 'Güncelle:', +'Upgrade': 'Upgrade', 'upgrade now': 'şimdi güncelle', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': "web2py'yi güncelle", 'upload': 'yükle', 'Upload': 'Yükle', @@ -406,21 +609,38 @@ 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Karmaşık sorgularda Ve (AND) iƧin (...)&(...) kullanın, Veya (OR) iƧin (...)|(...) kullanın ve DEĞİL (NOT) iƧin ~(...) kullanın. ', 'Use an url:': "Url'yi kullan:", 'user': 'kullanıcı', +'User': 'User', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'değişkenler', 'Version': 'Sürüm', -'versioning': 'sürümleme', 'Versioning': 'Sürümleme', +'versioning': 'sürümleme', 'view': 'gƶrüntü', 'Views': 'Gƶrüntüler', 'views': 'gƶrüntüler', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Ƈatısı', +'web2py Admin Password': 'web2py Admin Password', 'web2py apps to deploy': 'Yayınlanacak web2py uygulaması', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py güncel', 'web2py online debugger': 'ƧevirimiƧi web2py hata ayıklayıcı', 'web2py Recent Tweets': 'web2py Son Twitler', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'web2py güncellendi, lütfen yeniden başlatın', +'Working...': 'Working...', 'WSGI reference name': 'WSGI referans ismi', 'YES': 'EVET', +'Yes': 'Yes', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': '"kesme noktaları" düğmesini kullanarak düzenleme penceresine geƧebilir ve kesme nokatalarını hem ekeleyebilir hemde kaldırabilirsiniz', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', 'You need to set up and reach a': 'Gelinceye kadar kurmalısınız', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/uk.py b/applications/admin/languages/uk.py index ca54e12c..a45124c6 100755 --- a/applications/admin/languages/uk.py +++ b/applications/admin/languages/uk.py @@ -9,12 +9,15 @@ '%s %%{row} deleted': 'Š’ŠøŠ»ŃƒŃ‡ŠµŠ½Š¾ %s %%{Ń€ŃŠ“Š¾Šŗ}', '%s %%{row} updated': 'Š’ŠøŠ»ŃƒŃ‡ŠµŠ½Š¾ %s %%{Ń€ŃŠ“Š¾Šŗ}', '%s Recent Tweets': '%s %%{останній} %%{твіт}', +'%s selected': '%s selected', '%s students registered': '%s ŃŃ‚ŃƒŠ“ŠµŠ½Ń‚Ń–Š² зареєстровано', '%Y-%m-%d': '%Y/%m/%d', '%Y-%m-%d %H:%M:%S': '%Y/%m/%d %H:%M:%S', '(requires internet access)': '(потрібно мати Š“Š¾ŃŃ‚ŃƒŠæ в інтернет)', '(requires internet access, experimental)': '(потрібно мати Š“Š¾ŃŃ‚ŃƒŠæ в інтернет, ŠµŠŗŃŠæŠµŃ€ŠøŠ¼ŠµŠ½Ń‚Š°Š»ŃŒŠ½Š¾)', '(something like "it-it")': '(Ń‰Š¾ŃŃŒ схоже на "uk-ua")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(не Ń–ŃŠ½ŃƒŃ” Ń„Š°Š¹Š»Ńƒ **gluon/contrib/plural_rules/%s.py**)', '@markmin\x01An error occured, please [[reload %s]] the page': 'Š”Ń‚Š°Š»Š°ŃŃŒ помилка, буГь-ласка [[переватажте %s]] ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ', "@markmin\x01Mercurial Version Control System Interface[[NEWLINE]]for application '%s'": "Інтерфейс системи ŠŗŠ¾Š½Ń‚Ń€Š¾Š»ŃŽ версій Mercurial[[NEWLINE]]Š“Š»Ń Š“Š¾Š“Š°Ń‚ŠŗŃƒ '%s'", @@ -23,17 +26,21 @@ 'About': 'ŠŸŃ€Š¾', 'about': 'про', 'About application': 'ŠŸŃ€Š¾ ГоГаток', +'Accept Terms': 'Accept Terms', 'Add breakpoint': 'ДоГати Ń‚Š¾Ń‡ŠŗŃƒ зупинки', 'Additional code for your application': 'ДоГатковий коГ Š“Š»Ń вашого Š“Š¾Š“Š°Ń‚ŠŗŃƒ', +'Admin design page': 'Admin design page', 'admin disabled because no admin password': 'аГмін.інтерфейс Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾, бо не вказано ŠæŠ°Ń€Š¾Š»ŃŒ аГміністратора', 'admin disabled because not supported on google app engine': 'аГмін.інтерфейс Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ через те, що Google Application Engine його не ŠæŃ–Š“Ń‚Ń€ŠøŠ¼ŃƒŃ”', 'admin disabled because too many invalid login attempts': 'аГмін.інтерфейс заблоковано, бо ŠŗŃ–Š»ŃŒŠŗŃ–ŃŃ‚ŃŒ хибних спроб Š²Ń…Š¾Š“Ńƒ перевищило граничний Ń€Ń–Š²ŠµŠ½ŃŒ', 'admin disabled because unable to access password file': 'аГмін.інтерфейс Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ через Š²Ń–Š“ŃŃƒŃ‚Š½Ń–ŃŃ‚ŃŒ Š“Š¾ŃŃ‚ŃƒŠæŃƒ Го Ń„Š°Š¹Š»Ńƒ паролів', 'Admin is disabled because insecure channel': "АГмін.інтерфейс Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ через Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń ненаГійного каналу Š·Š²Ń'зку", 'Admin language': 'Мова Ń–Š½Ń‚ŠµŃ€Ń„ŠµŠ¹ŃŃƒ:', +'Admin versioning page': 'Admin versioning page', 'administrative interface': 'інтерфейс аГміністратора', 'Administrator Password:': 'ŠŸŠ°Ń€Š¾Š»ŃŒ аГміністратора:', 'and rename it:': 'i змінити назву на:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', 'App does not exist or your are not authorized': 'ДоГаток не Ń–ŃŠ½ŃƒŃ”, або ви не авторизовані', 'appadmin': 'AГм.панель', 'appadmin is disabled because insecure channel': "аГмін.панель Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ через Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń ненаГійного каналу зв'ŃŠ·ŠŗŃƒ", @@ -45,12 +52,14 @@ 'Application exists already': 'ДоГаток вже Ń–ŃŠ½ŃƒŃ”', 'application is compiled and cannot be designed': 'ГоГаток ŃŠŗŠ¾Š¼ŠæŃ–Š»ŃŒŠ¾Š²Š°Š½ŠøŠ¹. Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń Š·Š¼Ń–Š½ŃŽŠ²Š°Ń‚Šø не можна', 'Application name:': 'ŠŠ°Š·Š²Š° Š“Š¾Š“Š°Ń‚ŠŗŃƒ:', +'Application updated via git pull': 'Application updated via git pull', 'are not used': 'не Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‚ŃŒŃŃ', 'are not used yet': 'поки що не Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‚ŃŒŃŃ', 'Are you sure you want to delete file "%s"?': 'Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø файл "%s"?', 'Are you sure you want to delete plugin "%s"?': 'Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø Š²Ń‚ŃƒŠ»ŠŗŃƒ "%s"?"', 'Are you sure you want to delete this object?': "Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø цей об'єкт?", 'Are you sure you want to uninstall application "%s"?': 'Š’Šø впевнені, що хочете Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø (uninstall) ГоГаток "%s"?', +'Are you sure?': 'Are you sure?', 'arguments': 'Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Šø', 'at char %s': 'на символі %s', 'at line %s': 'в Ń€ŃŠ“ŠŗŃƒ %s', @@ -60,7 +69,10 @@ 'ATTENTION: you cannot edit the running application!': 'УВАГА: Š’Šø не можете Ń€ŠµŠ“Š°Š³ŃƒŠ²Š°Ń‚Šø ГоГаток, ŃŠŗŠøŠ¹ зараз Š²ŠøŠŗŠ¾Š½ŃƒŃ”те!', 'Autocomplete Python Code': 'ŠŠ²Ń‚Š¾Š·Š°Š²ŠµŃ€ŃˆŠµŠ½Š½Ń коГу на Python', 'Available databases and tables': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Ń– бази Ганих та таблиці', +'Available Databases and Tables': 'Available Databases and Tables', 'back': '<< назаГ', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', 'bad_resource': 'поганий_Ń€ŠµŃŃƒŃ€Ń', 'Basics': 'ŠŸŠ¾Ń‡Š°Ń‚Š¾Šŗ', 'Begin': 'ŠŸŠ¾Ń‡Š°Ń‚Š¾Šŗ', @@ -69,7 +81,9 @@ 'breakpoints': 'точки зупинок', 'Bulk Register': 'Масова Ń€ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–Ń', 'Bulk Student Registration': 'Масова Ń€ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–Ń ŃŃ‚ŃƒŠ“ŠµŠ½Ń‚Ń–Š²', +'Cache': 'Cache', 'cache': 'кеш', +'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– кешу', 'cache, errors and sessions cleaned': 'кеш, список зареєстрованих помилок та сесії очищенні', 'can be a git repo': 'може Š±ŃƒŃ‚Šø git-репозитарієм', @@ -78,13 +92,16 @@ 'Cannot compile: there are errors in your app:': 'ŠŠµ Š²Š“Š°Ń”Ń‚ŃŒŃŃ ŃŠŗŠ¾Š¼ŠæŃ–Š»ŃŽŠ²Š°Ń‚Šø: є помилки у вашому Š“Š¾Š“Š°Ń‚ŠŗŃƒ:', 'cannot create file': 'не можу створити файл', 'cannot upload file "%(filename)s"': 'не можу завантажити файл "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Змінити ŠæŠ°Ń€Š¾Š»ŃŒ аГміністратора', 'change editor settings': 'змінити Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń реГактора', +'Changelog': 'Changelog', 'check all': 'віГмітити всі', 'Check for upgrades': 'ŠŸŠµŃ€ŠµŠ²Ń–Ń€ŠøŃ‚Šø Š¾Š½Š¾Š²Š»ŠµŠ½Š½Ń', 'Check to delete': 'ŠŸŠ¾Š¼Ń–Ń‚ŠøŃ‚Šø на Š²ŠøŠ»ŃƒŃ‡ŠµŠ½Š½Ń', 'Checking for upgrades...': 'Š’Ń–Š“Š±ŃƒŠ²Š°Ń”Ń‚ŃŒŃŃ пошук оновлень...', 'Clean': 'ŠžŃ‡ŠøŃŃ‚ŠøŃ‚Šø', +'Clear': 'Clear', 'Clear CACHE?': 'ŠžŃ‡ŠøŃŃ‚ŠøŃ‚Šø ВЕДЬ кеш?', 'Clear DISK': 'ŠžŃ‡ŠøŃŃ‚ŠøŃ‚Šø Š”Š˜Š”ŠšŠžŠ’Š˜Š™ ŠšŠ•ŠØ', 'Clear RAM': "ŠžŃ‡ŠøŃŃ‚ŠøŃ‚Šø ŠšŠ•ŠØ Š’ ПАМ'ЯТІ", @@ -97,7 +114,10 @@ 'Comment:': 'ŠŸŠ¾ŃŃŠ½ŠµŠ½Š½Ń:', 'Commit': 'ŠšŠ¾Š¼Š¼Ń–Ń‚', 'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'ŠšŠ¾Š¼ŠæŃ–Š»ŃŽŠ²Š°Ń‚Šø', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', 'compiled application removed': 'ŃŠŗŠ¾Š¼ŠæŃ–Š»ŃŒŠ¾Š²Š°Š½ŠøŠ¹ ГоГаток Š²ŠøŠ»ŃƒŃ‡ŠµŠ½Š¾', 'Condition': 'Умова', 'contact_admin': "зв'ŃŠ·Š°Ń‚ŠøŃŃŒ_Š·_аГміністратором", @@ -112,7 +132,9 @@ 'Create rules': 'Дтворити правила', 'Create/Upload': 'Дтворити/Завантажити', 'created by': 'Автор:', +'Created by:': 'Created by:', 'Created On': 'Дтворено в', +'Created on:': 'Created on:', 'crontab': 'Ń‚Š°Š±Š»ŠøŃ†Ń cron', 'Current request': 'ŠŸŠ¾Ń‚Š¾Ń‡Š½ŠøŠ¹ запит', 'Current response': 'ŠŸŠ¾Ń‚Š¾Ń‡Š½Š° Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ', @@ -120,9 +142,13 @@ 'currently running': 'наразі, активний ГоГаток', 'currently saved or': 'останній збережений, або', 'data uploaded': 'Гані завантажено', +'Database': 'Database', 'database': 'база Ганих', +'Database %s select': 'Database %s select', 'database %s select': 'Вибірка Š· бази Ганих %s', +'Database administration': 'Database administration', 'database administration': 'Š°Š“Š¼Ń–Š½Ń–ŃŃ‚Ń€ŃƒŠ²Š°Š½Š½Ń бази Ганих', +'Database Administration (appadmin)': 'Database Administration (appadmin)', 'Date and Time': 'Дата і час', 'db': 'база Ганих', 'Debug': 'ЛаГнати (Debug)', @@ -134,10 +160,15 @@ 'Delete this file (you will be asked to confirm deletion)': 'Š’ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø цей файл (буГе віГображено запит на ŠæŃ–Š“Ń‚Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń операції)', 'Delete:': 'Š’ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø:', 'deleted after first hit': 'автоматично Š²ŠøŠ»ŃƒŃ‡Š°Ń”Ń‚ŃŒŃŃ ŠæŃ–ŃŠ»Ń ŠæŠµŃ€ŃˆŠ¾Š³Š¾ ŃŠæŃ€Š°Ń†ŃŽŠ²Š°Š½Š½Ń', +'Demo': 'Demo', 'Deploy': 'Š Š¾Š·Š³Š¾Ń€Š½ŃƒŃ‚Šø', 'Deploy on Google App Engine': 'Š Š¾Š·Š³Š¾Ń€Š½ŃƒŃ‚Šø на Google App Engine (GAE)', 'Deploy to OpenShift': 'Š Š¾Š·Š³Š¾Ń€Š½ŃƒŃ‚Šø на OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', 'Deployment form': 'Форма Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń (deployment form)', +'Deployment Interface': 'Deployment Interface', +'Description:': 'Description:', 'design': 'Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń', 'Detailed traceback description': 'Š”ŠµŃ‚Š°Š»ŃŒŠ½ŠøŠ¹ опис ŃŃ‚ŠµŠŗŃƒ викликів (traceback)', 'details': 'Š“ŠµŃ‚Š°Š»ŃŒŠ½Ń–ŃˆŠµ', @@ -146,14 +177,23 @@ 'Disable': 'Š’ŠøŠ¼ŠŗŠ½ŃƒŃ‚Šø', 'Disabled': 'Вимкнено', 'disabled in demo mode': 'Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ в Гемо-режимі', +'disabled in GAE mode': 'disabled in GAE mode', 'disabled in multi user mode': 'Š²Ń–Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ в багато-ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń†ŃŒŠŗŠ¾Š¼Ńƒ режимі', +'DISK': 'DISK', 'Disk Cache Keys': 'ŠšŠ»ŃŽŃ‡Ń– Гискового кешу', 'Disk Cleared': 'Дисковий кеш очищено', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', 'docs': 'Гок.', +'Docs': 'Docs', 'done!': 'зроблено!', 'Downgrade': 'ŠŸŠ¾Š²ŠµŃ€Š½ŃƒŃ‚Šø ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š½ŃŽ Š²ŠµŃ€ŃŃ–ŃŽ', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', 'download layouts': 'завантажити макет (layout)', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'завантажити Š²Ń‚ŃƒŠ»ŠŗŠø', +'Download plugins from repository': 'Download plugins from repository', 'Edit': 'Š ŠµŠ“Š°Š³ŃƒŠ²Š°Ń‚Šø', 'edit all': 'Ń€ŠµŠ“Š°Š³ŃƒŠ²Š°Ń‚Šø всі', 'Edit application': 'ŠŠ°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń Š“Š¾Š“Š°Ń‚ŠŗŃƒ', @@ -165,7 +205,11 @@ 'Editing file "%s"': 'Š ŠµŠ“Š°Š³ŃƒŃ”Ń‚ŃŒŃŃ файл "%s"', 'Editing Language file': 'Š ŠµŠ“Š°Š³ŃƒŃ”Ń‚ŃŒŃŃ файл ŠæŠµŃ€ŠµŠŗŠ»Š°Š“Ńƒ', 'Editing Plural Forms File': 'Š ŠµŠ“Š°Š³ŃƒŃ”Ń‚ŃŒŃŃ файл форм множини', +'Editor': 'Editor', +'Email Address': 'Email Address', 'Enable': 'Š£Š²Ń–Š¼ŠŗŠ½ŃƒŃ‚Šø', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', 'enter a value': 'Š²Š²ŠµŠ“Ń–Ń‚ŃŒ Š·Š½Š°Ń‡ŠµŠ½Š½Ń', 'Error': 'Помилка', 'Error logs for "%(app)s"': 'Дписок зареєстрованих помилок Š“Š¾Š“Š°Ń‚ŠŗŃƒ "%(app)s"', @@ -178,7 +222,9 @@ 'Exception instance attributes': 'ŠŃ‚Ń€ŠøŠ±ŃƒŃ‚Šø примірника класу Exception (Š²ŠøŠŗŠ»ŃŽŃ‡ŠµŠ½Š½Ń)', 'Exit Fullscreen': 'Вийти Š· повноекранного Ń€ŠµŠ¶ŠøŠ¼Ńƒ', 'Expand Abbreviation': 'Š Š¾Š·Š³Š¾Ń€Š½ŃƒŃ‚Šø Š°Š±Ń€ŠµŠ²Ń–Š°Ń‚ŃƒŃ€Ńƒ', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'ŠµŠŗŃŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø ŃŠŗ файл csv', +'Exports:': 'Exports:', 'exposes': 'Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ”', 'exposes:': 'Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ”:', 'extends': 'Ń€Š¾Š·ŃˆŠøŃ€ŃŽŃ”', @@ -195,10 +241,13 @@ 'file not found': 'файл не знайГено', 'file saved on %(time)s': 'файл збережено в %(time)s', 'file saved on %s': 'файл збережено в %s', +'filename': 'filename', 'Filename': "Ім'я Ń„Š°Š¹Š»Ńƒ", +'Files added': 'Files added', 'filter': 'Ń„Ń–Š»ŃŒŃ‚Ń€', 'Find Next': 'ŠØŃƒŠŗŠ°Ń‚Šø Š½Š°ŃŃ‚ŃƒŠæŠ½ŠøŠ¹', 'Find Previous': 'ŠØŃƒŠŗŠ°Ń‚Šø попереГній', +'Form has errors': 'Form has errors', 'Frames': 'Дтек викликів', 'Functions with no doctests will result in [passed] tests.': 'Š¤ŃƒŠ½ŠŗŃ†Ń–Ń—, в ŃŠŗŠøŃ… Š²Ń–Š“ŃŃƒŃ‚Š½Ń– Гок-тести Š²Ń–Š“Š½Š¾ŃŃŃ‚ŃŒŃŃ Го Ń„ŃƒŠ½ŠŗŃ†Ń–Š¹, ŃŠŗŃ– ŃƒŃŠæŃ–ŃˆŠ½Š¾ ŠæŃ€Š¾Š¹ŃˆŠ»Šø тести.', 'GAE Email': 'Ел.ŠæŠ¾ŃˆŃ‚Š° GAE', @@ -215,22 +264,30 @@ 'Google Application Id': 'ІГентифікатор Google Application', 'Goto': 'ŠŸŠµŃ€ŠµŠ¹Ń‚Šø Го', 'graph model': 'графова моГель', +'Graph Model': 'Graph Model', 'Help': 'Допомога', +'here': 'here', 'Hide/Show Translated strings': 'Дховати/показати Š’Š–Š• ŠŸŠ•Š Š•ŠšŠ›ŠŠ”Š•ŠŠ† Ń€ŃŠ“ŠŗŠø', +'Highlight current line': 'Highlight current line', 'Hits': 'Š”ŠæŃ€Š°Ń†ŃŒŠ¾Š²ŃƒŠ²Š°Š½ŃŒ', 'Home': 'Домівка', 'honored only if the expression evaluates to true': 'точка зупинки Š°ŠŗŃ‚ŠøŠ²ŃƒŃ”Ń‚ŃŒŃŃ Ń‚Ń–Š»ŃŒŠŗŠø за істинності умови', 'If start the downgrade, be patient, it may take a while to rollback': 'Š—Š°ŠæŃƒŃŃ‚ŠøŠ²ŃˆŠø ŠæŠ¾Š²ŠµŃ€Š½ŠµŠ½Š½Ń на ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š½ŃŽ Š²ŠµŃ€ŃŃ–ŃŽ, Š±ŃƒŠ“ŃŒŃ‚Šµ Ń‚ŠµŃ€ŠæŠ»ŃŃ‡ŠøŠ¼Šø, це може Š·Š°Š¹Š½ŃŃ‚Šø трохи Ń‡Š°ŃŃƒ', 'If start the upgrade, be patient, it may take a while to download': 'Š—Š°ŠæŃƒŃŃ‚ŠøŠ²ŃˆŠø Š¾Š½Š¾Š²Š»ŠµŠ½Š½Ń, Š±ŃƒŠ“ŃŒŃ‚Šµ Ń‚ŠµŃ€ŠæŠ»ŃŃ‡ŠøŠ¼Šø, потрібен час Š“Š»Ń Š·Š°Š²Š°Š½Ń‚Š°Š¶ŠµŠ½Š½Ń необхіГних Ганих', 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Якщо в наГаному вище звіті ŠæŃ€ŠøŃŃƒŃ‚Š½Ń позначка про помилку (ticket number), то це Š²ŠŗŠ°Š·ŃƒŃ” на збій у виконанні контролера ще Го ŠæŠ¾Ń‡Š°Ń‚ŠŗŃƒ запуску Гок-тестів. Це, зазвичай, ŃŠøŠ³Š½Š°Š»Ń–Š·ŃƒŃ” про помилку Š²ŠøŃ€Ń–Š²Š½ŃŽŠ²Š°Š½Š½Ń Ń‚ŠµŠŗŃŃ‚Ńƒ програми (indention error) або помилку за межами Ń„ŃƒŠ½ŠŗŃ†Ń–Ń— (error outside function code).\n\t\tЗелений заголовок ŃŠøŠ³Š½Š°Š»Ń–Š·ŃƒŃ”, що всі тести (Š· Š½Š°ŃŠ²Š½ŠøŃ…) ŠæŃ€Š¾Š¹ŃˆŠ»Šø ŃƒŃŠæŃ–ŃˆŠ½Š¾. Š’ Ń†ŃŒŠ¾Š¼Ńƒ випаГку Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ тестів показано не буГе.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', 'Import/Export': 'Імпорт/Експорт', 'In development, use the default Rocket webserver that is currently supported by this debugger.': 'ŠŸŃ–Š“ час розробки , Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ вбуГований веб-сервер Rocket, він найкраще Š½Š°Š»Š°ŃˆŃ‚ований на ŃŠæŃ–Š»ŃŒŠ½Ńƒ Ń€Š¾Š±Š¾Ń‚Ńƒ Š· інтерактивним лаГначем.', 'includes': 'Š²ŠŗŠ»ŃŽŃ‡Š°Ń”', +'Indent with tabs': 'Indent with tabs', 'index': 'інГекс', 'insert new': 'вставити новий', 'insert new %s': 'вставити новий %s', 'inspect attributes': 'Ń–Š½ŃŠæŠµŠŗŃ‚ŃƒŠ²Š°Ń‚Šø Š°Ń‚Ń€ŠøŠ±ŃƒŃ‚Šø', 'Install': 'Š’ŃŃ‚Š°Š½Š¾Š²Š»ŠµŠ½Š½Ń', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', 'Installed applications': 'Встановлені ГоГатки (applications)', 'Interaction at %s line %s': 'Š’ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ %s Ń€ŃŠ“Š¾Šŗ %s', 'Interactive console': 'Інтерактивна консоль', @@ -238,41 +295,57 @@ 'internal error: %s': 'Š²Š½ŃƒŃ‚Ń€Ń–ŃˆŠ½Ń помилка: %s', 'Internal State': 'Š’Š½ŃƒŃ‚Ń€Ń–ŃˆŠ½Ń–Š¹ стан', 'Invalid action': 'Помилкова Š“Ń–Ń', +'Invalid application name': 'Invalid application name', 'invalid circual reference': 'помилкове циклічне ŠæŠ¾ŃŠøŠ»Š°Š½Š½Ń', 'invalid circular reference': 'помилкове циклічне ŠæŠ¾ŃŠøŠ»Š°Š½Š½Ń', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½ŠøŠ¹ ŠæŠ°Ń€Š¾Š»ŃŒ', 'invalid password.': 'Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½ŠøŠ¹ ŠæŠ°Ń€Š¾Š»ŃŒ.', 'Invalid Query': 'Помилковий запит', 'invalid request': 'хибний запит', +'Invalid request': 'Invalid request', 'invalid request ': 'Єибний запит', 'invalid table names (auth_* tables already defined)': 'хибна назва таблиці (таблиці auth_* вже оголошено)', 'invalid ticket': 'неГійсна позначка про помилку (ticket)', 'Key': 'ŠšŠ»ŃŽŃ‡', 'Key bindings': 'ŠšŠ»Š°Š²Ń–Š°Ń‚ŃƒŃ€Š½Ń– ŃŠŗŠ¾Ń€Š¾Ń‡ŠµŠ½Š½Ń:', 'Key bindings for ZenCoding Plugin': 'ŠšŠ»Š°Š²Ń–Š°Ń‚ŃƒŃ€Š½Ń– ŃŠŗŠ¾Ń€Š¾Ń‡ŠµŠ½Š½Ń Š“Š»Ń Š²Ń‚ŃƒŠ»ŠŗŠø ZenCoding plugin', +'Keyboard shortcuts': 'Keyboard shortcuts', 'kill process': 'вбити процес', 'language file "%(filename)s" created/updated': 'Файл ŠæŠµŃ€ŠµŠŗŠ»Š°Š“Ńƒ "%(filename)s" створено/оновлено', 'Language files (static strings) updated': 'Файли переклаГів (Ń–Š· статичних Ń€ŃŠ“ŠŗŃ–Š² в ŠæŠµŃ€ŃˆŠ¾Š“Š¶ŠµŃ€ŠµŠ»Š°Ń…) оновлено', 'languages': 'переклаГи', 'Languages': 'ŠŸŠµŃ€ŠµŠŗŠ»Š°Š“Šø', +'Last Revision': 'Last Revision', 'Last saved on:': 'Востаннє збережено:', 'License for': 'Š›Ń–Ń†ŠµŠ½Š·Ń–Ń Š“Š¾Š“Š°Ń‚ŠŗŃƒ', +'License:': 'License:', +'Line Nr': 'Line Nr', 'Line number': 'ā„– Ń€ŃŠ“ŠŗŠ°', 'LineNo': 'ā„– Ń€ŃŠ“ŠŗŠ°', 'lists by exception': 'список Š²ŠøŠŗŠ»ŃŽŃ‡ŠµŠ½ŃŒ (exceptions)', 'lists by ticket': 'список позначок (tickets)', +'Loading...': 'Loading...', 'loading...': 'Š·Š°Š²Š°Š½Ń‚Š°Š¶ŠµŠ½Š½Ń...', +'Local Apps': 'Local Apps', 'locals': 'Š»Š¾ŠŗŠ°Š»ŃŒŠ½Ń–', 'Locals##debug': 'Š›Š¾ŠŗŠ°Š»ŃŒŠ½Ń– змінні', 'Login': 'Š’Ń…Ń–Š“', 'login': 'вхіГ', +'Login successful': 'Login successful', 'Login to the Administrative Interface': 'Š’Ń…Ń–Š“ в аГміністративний інтерфейс', +'Login/Register': 'Login/Register', 'Logout': 'Š’ŠøŃ…Ń–Š“', +'lost password': 'lost password', 'Main Menu': 'ŠžŃŠ½Š¾Š²Š½Šµ Š¼ŠµŠ½ŃŽ', 'Manage': 'ŠšŠµŃ€ŃƒŠ²Š°Ń‚Šø', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', 'Manage Admin Users/Students': 'АГміністратор ŠŗŠµŃ€ŃƒŠ²Š°Š½Š½Ń ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°Š¼Šø/ŃŃ‚ŃƒŠ“ŠµŠ½Ń‚Š°Š¼Šø', +'Manage Cache': 'Manage Cache', 'Manage Students': 'ŠšŠµŃ€ŃƒŠ²Š°Ń‚Šø ŃŃ‚ŃƒŠ“ŠµŠ½Ń‚Š°Š¼Šø', 'Match Pair': 'Знайти ŠæŠ°Ń€Ńƒ', +'Memberships': 'Memberships', 'merge': "Š·'єГнати", 'Merge Lines': "Š—'єГнати Ń€ŃŠ“ŠŗŠø", 'Minimum length is %s': 'ŠœŃ–Š½Ń–Š¼Š°Š»ŃŒŠ½Š° Говжина ŃŃ‚Š°Š½Š¾Š²ŠøŃ‚ŃŒ %s', @@ -281,37 +354,46 @@ 'Modified On': 'Змінено в', 'Modules': 'ŠœŠ¾Š“ŃƒŠ»Ń–', 'modules': 'Š¼Š¾Š“ŃƒŠ»Ń–', +'Multi User Mode': 'Multi User Mode', 'Must include at least %s %s': 'ŠœŠ°Ń” Š²Š¼Ń–Ń‰ŃƒŠ²Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s %s', 'Must include at least %s lower case': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s малих букв', 'Must include at least %s of the following : %s': 'ŠœŠ°Ń” Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø не менше %s таких символів : %s', 'Must include at least %s upper case': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s великих букв', 'new application "%s" created': 'новий ГоГаток "%s" створено', +'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'ŠœŠ°Š¹ŃŃ‚ŠµŃ€ ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń нового Š“Š¾Š“Š°Ń‚ŠŗŃƒ', 'New application wizard': 'ŠœŠ°Š¹ŃŃ‚ŠµŃ€ ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń нового Š“Š¾Š“Š°Ń‚ŠŗŃƒ', 'new plugin installed': 'нова Š²Ń‚ŃƒŠ»ŠŗŠ° (plugin) встановлена', +'New plugin installed: %s': 'New plugin installed: %s', 'New Record': 'ŠŠ¾Š²ŠøŠ¹ запис', 'new record inserted': 'новий Ń€ŃŠ“Š¾Šŗ ГоГано', 'New simple application': 'ŠŠ¾Š²ŠøŠ¹ простий ГоГаток', 'next': 'Š½Š°ŃŃ‚ŃƒŠæŠ½ŠøŠ¹', +'next %s rows': 'next %s rows', 'next 100 rows': 'Š½Š°ŃŃ‚ŃƒŠæŠ½Ń– 100 Ń€ŃŠ“ŠŗŃ–Š²', 'Next Edit Point': 'ŠŠ°ŃŃ‚ŃƒŠæŠ½Šµ місце Ń€ŠµŠ“Š°Š³ŃƒŠ²Š°Š½Š½Ń', 'NO': 'ŠŠ†', +'no changes': 'no changes', 'No databases in this application': 'Даний ГоГаток не Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” бази Ганих', 'No Interaction yet': 'ЛаГнач не активовано', 'no match': 'ŃŠæŃ–Š²ŠæŠ°Š“Š°Š½ŃŒ нема', 'no package selected': 'пакет не вибрано', 'no permission to uninstall "%s"': 'нема прав на Š²ŠøŠ»ŃƒŃ‡ŠµŠ½Š½Ń (uninstall) "%s"', 'No ticket_storage.txt found under /private folder': 'Š’ каталозі /private Š²Ń–Š“ŃŃƒŃ‚Š½Ń–Š¹ файл ticket_storage.txt', +'Node:': 'Node:', 'Not Authorized': 'ŠŠµ Гозволено', +'Not supported': 'Not supported', 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'ŠŸŃ€ŠøŠ¼Ń–Ń‚ŠŗŠ°: Якщо ви отримали ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń про помилку Š· коГом ŃŃ‚Š°Š½Ńƒ github рівним 128, ŠæŠµŃ€ŠµŠŗŠ¾Š½Š°Š¹Ń‚ŠµŃŃŒ, що система та обліковий запис, Š· ŃŠŗŠ¾Š³Š¾ Š²Ń–Š“Š±ŃƒŠ²Š°Ń”Ń‚ŃŒŃŃ Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‚ŃŒ віГповіГний ŠŗŠ»ŃŽŃ‡ ssh, Š½Š°Š»Š°ŃˆŃ‚Š¾Š²Š°Š½ŠøŠ¹ в Š¾Š±Š»Ń–ŠŗŠ¾Š²Š¾Š¼Ńƒ записі openshift.', "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": 'Š£ промисловій ŠµŠŗŃŠæŠ»ŃƒŠ°Ń‚Š°Ń†Ń–Ń—, ви повинні Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Ń‚Šø ваш веб-сервер на Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń оГного ŠæŃ€Š¾Ń†ŠµŃŃƒ та Š±Š°Š³Š°Ń‚ŃŒŠ¾Ń… потоків, ŃŠŗŃ‰Š¾ бажаєте ŃŠŗŠ¾Ń€ŠøŃŃ‚Š°Ń‚ŠøŃŃŒ цим лаГначем.', 'online designer': 'Гизайнер БД', +'Open new app in new window': 'Open new app in new window', 'OpenShift Deployment Interface': 'OpenShift: Інтерфейс Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń', 'OpenShift Output': 'ВивіГ OpenShift', 'or alternatively': 'або Š°Š»ŃŒŃ‚ернативно', 'Or Get from URL:': 'Або ŠžŃ‚римати Š· мережі (ч/Š· URL):', 'or import from csv file': 'або Ń–Š¼ŠæŠ¾Ń€Ń‚ŃƒŠ²Š°Ń‚Šø через csv-файл', 'Original/Translation': 'ŠžŃ€ŠøŠ³Ń–Š½Š°Š»/переклаГ', +'Overview': 'Overview', 'Overwrite installed app': 'ŠŸŠµŃ€ŠµŠ·Š°ŠæŠøŃŠ°Ń‚Šø встановлений ГоГаток', 'Pack all': 'ŠŸŠ°ŠŗŃƒŠ²Š°Ń‚Šø все', 'Pack compiled': 'ŠŸŠ°ŠŗŃƒŠ²Š°Ń‚Šø Š·ŠŗŠ¾Š¼ŠæŃ–Š»ŃŒŠ¾Š²Š°Š½Šµ', @@ -319,42 +401,62 @@ 'pack plugin': 'Š·Š°ŠæŠ°ŠŗŃƒŠ²Š°Ń‚Šø Š²Ń‚ŃƒŠ»ŠŗŃƒ', 'PAM authenticated user, cannot change password here': 'Ввімкнена система іГентифікації ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Ń–Š² PAM. Š”Š»Ń зміни ŠæŠ°Ń€Š¾Š»ŃŽ ŃŠŗŠ¾Ń€ŠøŃŃ‚Š°Š¹Ń‚ŠµŃŃŒ команГами Š²Š°ŃˆŠ¾Ń— ŠžŠ” ', 'password changed': 'ŠæŠ°Ń€Š¾Š»ŃŒ змінено', +'Past revisions': 'Past revisions', 'Path to appcfg.py': 'ŠØŠ»ŃŃ… Го appcfg.py', 'Path to local openshift repo root.': 'ŠØŠ»ŃŃ… Го локального ŠŗŠ¾Ń€Š½Ń Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š°Ń€Ń–Ń openshift.', 'peek': 'Š³Š»ŃŠ½ŃƒŃ‚Šø', 'Peeking at file': 'ŠŸŠµŃ€ŠµŠ³Š»ŃŠ“ Ń„Š°Š¹Š»Ńƒ', +'Permission': 'Permission', +'Permissions': 'Permissions', 'Please': 'Š‘ŃƒŠ“ŃŒ-ласка', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin': 'Š²Ń‚ŃƒŠ»ŠŗŠ°', 'plugin "%(plugin)s" deleted': 'Š²Ń‚ŃƒŠ»ŠŗŃƒ "%(plugin)s" Š²ŠøŠ»ŃƒŃ‡ŠµŠ½Š¾', 'Plugin "%s" in application': 'Š’Ń‚ŃƒŠ»ŠŗŠ° "%s" в Š“Š¾Š“Š°Ń‚ŠŗŃƒ', 'plugin not specified': 'Š²Ń‚ŃƒŠ»ŠŗŃƒ не визначено', +'Plugin page': 'Plugin page', 'plugins': 'Š²Ń‚ŃƒŠ»ŠŗŠø (plugins)', 'Plugins': 'Š’Ń‚ŃƒŠ»ŠŗŠø (Plugins)', 'Plural Form #%s': 'Форма множини #%s', 'Plural-Forms:': 'Форми множини:', 'Powered by': 'ŠŸŃ€Š°Ń†ŃŽŃ” на', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': 'попереГні 100 Ń€ŃŠ“ŠŗŃ–Š²', 'Previous Edit Point': 'ŠŸŠ¾ŠæŠµŃ€ŠµŠ“Š½Ń” місце Ń€ŠµŠ“Š°Š³ŃƒŠ²Š°Š½Š½Ń', 'Private files': 'ŠŸŃ€ŠøŠ²Š°Ń‚Š½Ń– файли', 'private files': 'приватні файли', 'Project Progress': 'ŠŸŠ¾ŃŃ‚ŃƒŠæ ŠæŃ€Š¾ŠµŠŗŃ‚Ńƒ', 'Pull': 'Š’Ń‚ŃŠ³Š½ŃƒŃ‚Šø', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', 'Push': 'ŠŸŃ€Š¾ŃˆŃ‚Š¾Š²Ń…Š½ŃƒŃ‚Šø', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', 'Query:': 'Запит:', +'RAM': 'RAM', 'RAM Cache Keys': 'ŠšŠ»ŃŽŃ‡ ŠžŠ—ŠŸ-кешу (RAM Cache)', 'Ram Cleared': "Кеш в пам'яті очищено", 'Rapid Search': 'ŠœŠøŃ‚Ń‚Ń”Š²ŠøŠ¹ пошук', +'Record': 'Record', 'record': 'запис', 'record does not exist': 'запису не Ń–ŃŠ½ŃƒŃ”', 'record id': 'ІГ.запису', +'Record id': 'Record id', 'refresh': 'Š¾Š½Š¾Š²Ń–Ń‚ŃŒ', +'register': 'register', 'reload': 'перевантажити', 'Reload routes': 'ŠŸŠµŃ€ŠµŠ·Š°Š²Š°Š½Ń‚Š°Š¶ŠøŃ‚Šø Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚Šø', 'Remove compiled': 'Š’ŠøŠ»ŃƒŃ‡.компл', 'Removed Breakpoint on %s at line %s': 'Š’ŠøŠ»ŃƒŃ‡ŠµŠ½Š¾ Ń‚Š¾Ń‡ŠŗŃƒ зупинки у %s в Ń€ŃŠ“ŠŗŃƒ %s', 'Replace': 'Замінити', 'Replace All': 'Замінити все', +'Repository (%s)': 'Repository (%s)', 'request': 'запит', +'requires distutils, but not installed': 'requires distutils, but not installed', 'requires python-git, but not installed': 'Š”Š»Ń Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń необхіГний пакет python-git, але він не встановлений', 'resolve': "розв'ŃŠ·Š°Ń‚Šø", 'Resolve Conflict file': "Файл розв'ŃŠ·ŃƒŠ²Š°Š½Š½Ń ŠŗŠ¾Š½Ń„Š»Ń–ŠŗŃ‚Ńƒ", @@ -362,7 +464,14 @@ 'restart': 'ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šø майстра', 'restore': 'ŠæŠ¾Š²ŠµŃ€Š½ŃƒŃ‚Šø', 'return': 'ŠæŠ¾Š²ŠµŃ€Š½ŃƒŃ‚ŠøŃŃŒ', +'Revert': 'Revert', 'revert': 'Š²Ń–Š“Š½Š¾Š²ŠøŃ‚ŠøŃŃŒ', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', 'Rows in table': 'Š ŃŠ“ŠŗŃ–Š² у таблиці', 'Rows selected': 'Š ŃŠ“ŠŗŃ–Š² вибрано', 'rules are not defined': 'правила не визначені', @@ -376,22 +485,33 @@ 'Save': 'Зберегти', 'Save file:': 'Зберегти файл:', 'Save file: %s': 'Зберегти файл: %s', +'Save model as...': 'Save model as...', 'Save via Ajax': 'зберегти через Ajax', 'Saved file hash:': 'Єеш збереженого Ń„Š°Š¹Š»Ńƒ:', +'Screenshot %s': 'Screenshot %s', 'search': 'пошук', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': 'віГмічено', 'session': 'ŃŠµŃŃ–Ń', 'session expired': 'час Ганої сесії вичерпано', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'ДоГано Ń‚Š¾Ń‡ŠŗŃƒ зупинки в %s на Ń€ŃŠ“Š¾Šŗ %s: %s', 'shell': 'консоль', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', 'signup': 'ŠæŃ–Š“ŠæŠøŃŠ°Ń‚ŠøŃŃŒ', 'signup_requested': 'необхіГна_Ń€ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–Ń', 'Singular Form': 'Форма оГнини', 'site': 'сайт', 'Site': 'Дайт', +'Size of cache:': 'Size of cache:', 'skip to generate': 'перейти Го Š³ŠµŠ½ŠµŃ€ŃƒŠ²Š°Š½Š½Ń Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ńƒ', 'some files could not be removed': 'Š“ŠµŃŠŗŃ– файли не можна Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', 'Sorry, could not find mercurial installed': 'ŠŠµ вГалось Š²ŠøŃŠ²ŠøŃ‚Šø Š²ŃŃ‚Š°Š½Š¾Š²Š»ŠµŠ½Ńƒ ŃŠøŃŃ‚ŠµŠ¼Ńƒ ŠŗŠ¾Š½Ń‚Ń€Š¾Š»ŃŽ версій Mercurial', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', 'Start a new app': 'Š”Ń‚Š²Š¾Ń€ŃŽŃ”Ń‚ŃŒŃŃ новий ГоГаток', 'Start searching': 'Розпочати пошук', 'Start wizard': 'ŠŠŗŃ‚ŠøŠ²ŃƒŠ²Š°Ń‚Šø майстра', @@ -399,6 +519,7 @@ 'Static': 'Дтатичні', 'static': 'статичні', 'Static files': 'Дтатичні файли', +'Statistics': 'Statistics', 'Step': 'ŠšŃ€Š¾Šŗ', 'step': 'крок', 'stop': 'Š·ŃƒŠæŠøŠ½ŠøŃ‚Šø', @@ -406,7 +527,10 @@ 'Submit': 'Š—Š°ŃŃ‚Š¾ŃŃƒŠ²Š°Ń‚Šø', 'successful': 'ŃƒŃŠæŃ–ŃˆŠ½Š¾', 'switch to : db': 'ŠæŠµŃ€ŠµŠ¼ŠŗŠ½ŃƒŃ‚Šø на : БД', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': 'Ń‚Š°Š±Š»ŠøŃ†Ń', +'Table': 'Table', 'tags': 'мітки (tags)', 'Temporary': 'Тимчасово', 'test': 'Ń‚ŠµŃŃ‚ŃƒŠ²Š°Ń‚Šø всі', @@ -417,6 +541,7 @@ 'The application logic, each URL path is mapped in one exposed function in the controller': 'Логіка Š“Š¾Š“Š°Ń‚ŠŗŃƒ, кожний ŃˆŠ»ŃŃ… URL ŠæŃ€Š¾ŠµŠŗŃ‚ŃƒŃ”Ń‚ŃŒŃŃ на оГну Š· Ń„ŃƒŠ½ŠŗŃ†Ń–Š¹ Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń в контролері', 'The data representation, define database tables and sets': 'ŠŸŃ€ŠµŠ“ŃŃ‚Š°Š²Š»ŠµŠ½Š½Ń Ганих, опис Ń‚Š°Š±Š»ŠøŃ†ŃŒ БД та наборів', 'The presentations layer, views are also known as templates': 'ŠŸŃ€ŠµŠ·ŠµŠ½Ń‚Š°Ń†Ń–Š¹Š½ŠøŠ¹ Ń€Ń–Š²ŠµŠ½ŃŒ, Š²Ń–Š“Š¾Š±Ń€Š°Š¶ŠµŠ½Š½Ń, віГомі також ŃŠŗ шаблони', +'Theme': 'Theme', 'There are no controllers': 'ЖоГного контролера, наразі, не Ń–ŃŠ½ŃƒŃ”', 'There are no models': 'МоГелей, наразі, нема', 'There are no modules': 'ŠœŠ¾Š“ŃƒŠ»Ń–Š² поки що нема', @@ -448,6 +573,7 @@ 'To emulate a breakpoint programatically, write:': 'Š”Š»Ń Š²ŃŃ‚Š°Š½Š¾Š²Š»ŠµŠ½Š½Ń точки зупинки програмним чином Š½Š°ŠæŠøŃˆŃ–Ń‚ŃŒ:', 'to use the debugger!': 'щоб Š°ŠŗŃ‚ŠøŠ²ŃƒŠ²Š°Ń‚Šø лаГнач!', 'toggle breakpoint': '+/- Ń‚Š¾Ń‡ŠŗŃƒ зупинки', +'Toggle comment': 'Toggle comment', 'Toggle Fullscreen': 'ŠŸŠµŃ€ŠµŠ¼ŠŗŠ½ŃƒŃ‚Šø на весь екран', 'Traceback': 'Дтек викликів (Traceback)', 'Translation strings for the application': 'ŠŸŠ°Ń€Šø Ń€ŃŠ“ŠŗŃ–Š² <оригінал>:<переклаГ> Š“Š»Ń вибраної мови', @@ -456,6 +582,7 @@ 'try view': 'Š“ŠøŠ²ŠøŃ‚ŠøŃŃŒ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚', 'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Š½Š°Š±ŠµŃ€Ń–Ń‚ŃŒ тут буГь-ŃŠŗŃ– команГи лаГнача PDB і Š½Š°Ń‚ŠøŃŠ½Ń–Ń‚ŃŒ ŠŗŠ»Š°Š²Ń–ŃˆŃƒ [Return] ([Enter]), щоб Š·Š°ŠæŃƒŃŃ‚ити їх на Š²ŠøŠŗŠ¾Š½Š°Š½Š½Ń.', 'Type python statement in here and hit Return (Enter) to execute it.': 'ŠŠ°Š±ŠµŃ€Ń–Ń‚ŃŒ тут буГь-ŃŠŗŃ– вирази Python і Š½Š°Ń‚ŠøŃŠ½Ń–Ń‚ŃŒ ŠŗŠ»Š°Š²Ń–ŃˆŃƒ [Return] ([Enter]), щоб Š·Š°ŠæŃƒŃŃ‚ити їх на Š²ŠøŠŗŠ¾Š½Š°Š½Š½Ń.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', 'Unable to check for upgrades': 'ŠŠµŠ¼Š¾Š¶Š»ŠøŠ²Š¾ перевірити Š¾Š½Š¾Š²Š»ŠµŠ½Š½Ń', 'unable to create application "%s"': 'не можу створити ГоГаток "%s"', 'unable to create application "%s" (it may exist already)': 'не можу створити ГоГаток "%s" (можливо його вже створено)', @@ -466,7 +593,9 @@ 'Unable to download because:': 'ŠŠµŠ¼Š¾Š¶Š»ŠøŠ²Š¾ завантажити через:', 'unable to download layout': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ завантажити макет', 'unable to download plugin: %s': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ завантажити Š²Ń‚ŃƒŠ»ŠŗŃƒ: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', 'unable to install application "%(appname)s"': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ встановити ГоГаток "%(appname)s"', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ розібрати csv-файл', 'unable to uninstall "%s"': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ Š²ŠøŠ»ŃƒŃ‡ŠøŃ‚Šø "%s"', 'unable to upgrade because "%s"': 'не Š²Š“Š°Ń”Ń‚ŃŒŃŃ оновити, Ń‚Š¾Š¼Ńƒ що "%s"', @@ -490,6 +619,9 @@ 'upload plugin file:': 'завантажити файл Š²Ń‚ŃƒŠ»ŠŗŠø:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Š”Š»Ń ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń склаГних запитів Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ (...)&(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ AND, (...)|(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ OR, та ~(...) Š·Š°Š¼Ń–ŃŃ‚ŃŒ NOT.', 'user': 'ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡', +'User': 'User', +'Username': 'Username', +'Users': 'Users', 'Using the shell may lock the database to other users of this app.': 'Š’ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń оболонки може Š·Š°Š±Š»Š¾ŠŗŃƒŠ²Š°Ń‚Šø базу Ганих віГ ŃŃƒŠ¼Ń–ŃŠ½Š¾Š³Š¾ Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń Ń–Š½ŃˆŠøŠ¼Šø ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°Š¼Šø Ń†ŃŒŠ¾Š³Š¾ Š“Š¾Š“Š°Ń‚ŠŗŃƒ.', 'value not allowed': 'Š½ŠµŠ“Š¾ŠæŃƒŃŃ‚ŠøŠ¼Šµ Š·Š½Š°Ń‡ŠµŠ½Š½Ń', 'variables': 'змінні', @@ -499,8 +631,11 @@ 'view': 'ŠæŠµŃ€ŠµŠ³Š»ŃŠ“', 'Views': 'Š’Ń–Š“Š¾Š±Ń€Š°Š¶ŠµŠ½Š½Ń (Views)', 'views': 'Š²Ń–Š“Š¾Š±Ń€Š°Š¶ŠµŠ½Š½Ń', +'Warning!': 'Warning!', 'WARNING:': 'ŠŸŠžŠŸŠ•Š Š•Š”Š–Š•ŠŠŠÆ:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Веб-каркас (Web Framework)', +'web2py Admin Password': 'web2py Admin Password', 'web2py apps to deploy': 'Готові Го Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń ГоГатки web2py', 'web2py Debugger': 'ЛаГнач web2py', 'web2py downgrade': 'ŠæŠ¾Š²ŠµŃ€Š½ŠµŠ½Š½Ń на ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š½ŃŽ Š²ŠµŃ€ŃŃ–ŃŽ web2py', @@ -509,13 +644,17 @@ 'web2py Recent Tweets': 'ŠžŃŃ‚Š°Š½Š½Ń– твіти web2py', 'web2py upgrade': 'Š¾Š½Š¾Š²Š»ŠµŠ½Š½Ń web2py', 'web2py upgraded; please restart it': 'web2py оновлено; буГь-ласка ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ його', +'Working...': 'Working...', 'Wrap with Abbreviation': 'Š—Š°Š³Š¾Ń€Š½ŃƒŃ‚Šø Š· Š°Š±Ń€ŠµŠ²Ń–Š°Ń‚ŃƒŃ€Š¾ŃŽ', 'WSGI reference name': "ім'я ŠæŠ¾ŃŠøŠ»Š°Š½Š½Ń WSGI", 'YES': 'ТАК', +'Yes': 'Yes', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'Š’Šø також можете Š²ŃŃ‚Š°Š½Š¾Š²Š»ŃŽŠ²Š°Ń‚Šø/Š²ŠøŠ»ŃƒŃ‡Š°Ń‚Šø точки зупинок піГ час Ń€ŠµŠ“Š°Š³ŃƒŠ²Š°Š½Š½Ń ŠæŠµŃ€ŃˆŠ¾Š“Š¶ŠµŃ€ŠµŠ» (sources), Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‡Šø кнопку "+/- Ń‚Š¾Ń‡ŠŗŃƒ зупинки"', +'You can inspect variables using the console bellow': 'Š’Šø можете Š“Š¾ŃŠ»Ń–Š“Š¶ŃƒŠ²Š°Ń‚Šø змінні, Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‡Šø Ń–Š½Ń‚ŠµŃ€Š°ŠŗŃ‚ŠøŠ²Š½Ńƒ консоль', +'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You have one more login attempt before you are locked out': 'Š£ вас є ще оГна спроба переГ тим, ŃŠŗ вхіГ буГе заблоковано', 'you must specify a name for the uploaded application': "ви повинні вказати ім'я ГоГатка, переГ ти, ŃŠŗ завантажити його", 'You need to set up and reach a': 'Треба встановити та Š“Š¾ŃŃŠ³Š½ŃƒŃ‚Šø', +'You only need these if you have already registered': 'You only need these if you have already registered', 'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Š’Š°Ńˆ ГоГаток буГе заблоковано, поки ви не клацнете по оГній Š· кнопок ŠŗŠµŃ€ŃƒŠ²Š°Š½Š½Ń ("Š½Š°ŃŃ‚ŃƒŠæŠ½ŠøŠ¹", "крок", "проГовжити", та ін.)', -'You can inspect variables using the console bellow': 'Š’Šø можете Š“Š¾ŃŠ»Ń–Š“Š¶ŃƒŠ²Š°Ń‚Šø змінні, Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‡Šø Ń–Š½Ń‚ŠµŃ€Š°ŠŗŃ‚ŠøŠ²Š½Ńƒ консоль', } diff --git a/applications/admin/languages/zh-tw.py b/applications/admin/languages/zh-tw.py index 79cb4183..b8d564fc 100755 --- a/applications/admin/languages/zh-tw.py +++ b/applications/admin/languages/zh-tw.py @@ -3,300 +3,680 @@ '!langcode!': 'zh-tw', '!langname!': 'å°ē£äø­ę–‡', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"ꛓꖰ" ę˜Æéøę“‡ę€§ēš„ę¢ä»¶å¼, ę ¼å¼å°±åƒ "ę¬„ä½1=\'值\'". ä½†ę˜Æ JOIN ēš„č³‡ę–™äøåÆä»„ä½æē”Ø update ęˆ–ę˜Æ delete"', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': 'å·²åˆŖé™¤ %s ē­†', '%s %%{row} updated': '已曓新 %s ē­†', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(ę ¼å¼é”žä¼¼ "zh-tw")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', 'A new version of web2py is available': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ', 'A new version of web2py is available: %s': 'ę–°ē‰ˆēš„ web2py å·²ē™¼č”Œ: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ę³Øę„: ē™»å…„ē®”ē†åø³č™Ÿéœ€č¦å®‰å…Øé€£ē·š(HTTPS)ęˆ–ę˜ÆåœØęœ¬ę©Ÿé€£ē·š(localhost).', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ę³Øę„: å› ē‚ŗåœØęø¬č©¦ęØ”å¼äøäæč­‰å¤šåŸ·č”Œē·’å®‰å…Øę€§ļ¼Œä¹Ÿå°±ę˜ÆčŖŖäøåÆä»„åŒę™‚åŸ·č”Œå¤šå€‹ęø¬č©¦ę”ˆä¾‹', -'ATTENTION: you cannot edit the running application!': 'ę³Øę„:äøåÆē·Øč¼Æę­£åœØåŸ·č”Œēš„ę‡‰ē”ØēØ‹å¼!', +'Abort': 'Abort', 'About': 'é—œę–¼', 'About application': 'é—œę–¼ęœ¬ę‡‰ē”ØēØ‹å¼', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', +'additional code for your application': 'ę‡‰ē”ØēØ‹å¼é”å¤–ēš„ēØ‹å¼ē¢¼', +'Additional code for your application': 'Additional code for your application', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æę²’ęœ‰čØ­å®šē®”ē†å“”åÆ†ē¢¼ ', +'admin disabled because not supported on google app engine': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æäøę”Æę“åœØgoogle apps engineē’°å¢ƒäø‹é‹ä½œ', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æē„”ę³•å­˜å–åÆ†ē¢¼ęŖ”', 'Admin is disabled because insecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', 'Admin is disabled because unsecure channel': 'ē®”ē†åŠŸčƒ½(Admin)åœØäøå®‰å…Øé€£ē·šē’°å¢ƒäø‹č‡Ŗå‹•é—œé–‰', +'Admin language': 'Admin language', +'Admin versioning page': 'Admin versioning page', +'administrative interface': 'administrative interface', 'Administrator Password:': '箔理哔密碼:', +'amy_ajax': 'amy_ajax', +'and rename it (required):': 'åŒę™‚ę›“åē‚ŗ(åæ…č¦ēš„):', +'and rename it:': 'åŒę™‚ę›“åē‚ŗ:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': 'ę‡‰ē”ØēØ‹å¼ē®”ē†å“”', +'appadmin is disabled because insecure channel': 'ē®”ē†ä»‹é¢é—œé–‰ē†ē”±ę˜Æé€£ē·šę–¹å¼äøå®‰å…Ø', +'Application': 'Application', +'application "%s" uninstalled': 'å·²ē§»é™¤ę‡‰ē”ØēØ‹å¼ "%s"', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'å·²ē·Øč­Æę‡‰ē”ØēØ‹å¼', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'ę‡‰ē”ØēØ‹å¼å·²ē¶“ē·Øč­Æē„”ę³•é‡ę–°čØ­čØˆ', +'Application name:': 'Application name:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'ē¢ŗå®šč¦åˆŖé™¤ęŖ”ę”ˆ"%s"?', 'Are you sure you want to delete plugin "%s"?': 'ē¢ŗå®šč¦åˆŖé™¤ę’ä»¶ "%s"?', +'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Are you sure you want to uninstall application "%s"': 'ē¢ŗå®šč¦ē§»é™¤ę‡‰ē”ØēØ‹å¼ "%s"', 'Are you sure you want to uninstall application "%s"?': 'ē¢ŗå®šč¦ē§»é™¤ę‡‰ē”ØēØ‹å¼ "%s"', 'Are you sure you want to upgrade web2py now?': 'ē¢ŗå®šē¾åœØč¦å‡ē“š web2py?', +'Are you sure?': 'Are you sure?', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ę³Øę„: ē™»å…„ē®”ē†åø³č™Ÿéœ€č¦å®‰å…Øé€£ē·š(HTTPS)ęˆ–ę˜ÆåœØęœ¬ę©Ÿé€£ē·š(localhost).', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ę³Øę„: å› ē‚ŗåœØęø¬č©¦ęØ”å¼äøäæč­‰å¤šåŸ·č”Œē·’å®‰å…Øę€§ļ¼Œä¹Ÿå°±ę˜ÆčŖŖäøåÆä»„åŒę™‚åŸ·č”Œå¤šå€‹ęø¬č©¦ę”ˆä¾‹', +'ATTENTION: you cannot edit the running application!': 'ę³Øę„:äøåÆē·Øč¼Æę­£åœØåŸ·č”Œēš„ę‡‰ē”ØēØ‹å¼!', 'Authentication': 'é©—č­‰', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'åÆęä¾›ēš„č³‡ę–™åŗ«å’Œč³‡ę–™č”Ø', +'Available Databases and Tables': 'Available Databases and Tables', +'back': '回復(back)', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'åæ«å–čØ˜ę†¶é«”', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'åæ«å–čØ˜ę†¶é«”,éŒÆčŖ¤ē“€éŒ„,é€£ē·šē“€éŒ„å·²ęø…é™¤', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'äøåÆē©ŗē™½', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': '焔法編譯:ę‡‰ē”ØēØ‹å¼äø­å«ęœ‰éŒÆčŖ¤ļ¼Œč«‹é™¤éŒÆå¾Œå†č©¦äø€ę¬”.', 'Cannot compile: there are errors in your app:': '焔法編譯: åœØä½ ēš„ę‡‰ē”ØēØ‹å¼å­˜åœØéŒÆčŖ¤:', -'Change Password': 'č®Šę›“åÆ†ē¢¼', +'cannot create file': 'ē„”ę³•å‰µå»ŗęŖ”ę”ˆ', +'cannot upload file "%(filename)s"': 'ē„”ę³•äøŠå‚³ęŖ”ę”ˆ "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'change admin password', +'change editor settings': 'change editor settings', +'Change Password': 'č®Šę›“åÆ†ē¢¼', +'change_password': 'č®Šę›“åÆ†ē¢¼', +'Changelog': 'Changelog', +'check all': '全選', +'Check for upgrades': 'Check for upgrades', 'Check to delete': 'ę‰“å‹¾ä»£č”ØåˆŖé™¤', 'Check to delete:': 'ę‰“å‹¾ä»£č”ØåˆŖé™¤:', 'Checking for upgrades...': 'ęŖ¢ęŸ„ę–°ē‰ˆęœ¬äø­...', 'Clean': '清除', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'click here for online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', +'click here for the administrative interface': 'é»žę­¤č™•é€²å…„ē®”ē†ä»‹é¢', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', +'click to check for upgrades': 'é»žę“Šę‰“å‹¾ä»„ä¾æå‡ē“š', 'Client IP': 'å®¢ęˆ¶ē«Æē¶²å€(IP)', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'collapse/expand all', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': 'ē·Øč­Æ', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': 'å·²ē§»é™¤å·²ē·Øč­Æēš„ę‡‰ē”ØēØ‹å¼', +'Condition': 'Condition', +'continue': 'continue', 'Controller': 'ꎧ件', 'Controllers': 'ꎧ件', +'controllers': 'ꎧ件', 'Copyright': 'ē‰ˆę¬Šę‰€ęœ‰', +'Count': 'Count', 'Create': '創建', +'create file with filename:': 'å‰µå»ŗęŖ”ę”ˆ:', +'create new application:': 'å‰µå»ŗę–°ę‡‰ē”ØēØ‹å¼:', 'Create new simple application': 'å‰µå»ŗę‡‰ē”ØēØ‹å¼', +'Create/Upload': 'Create/Upload', +'created by': '創建自', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'å®šę™‚åŸ·č”Œč”Ø', 'Current request': 'ē›®å‰ē¶²č·Æč³‡ę–™č¦ę±‚(request)', 'Current response': 'ē›®å‰ē¶²č·Æč³‡ę–™å›žę‡‰(response)', 'Current session': 'ē›®å‰ē¶²č·Æé€£ē·šč³‡čØŠ(session)', -'DB Model': '資料庫樔組', -'DESIGN': '設計', -'Database': '資料庫', -'Date and Time': 'ę—„ęœŸå’Œę™‚é–“', -'Delete': 'åˆŖé™¤', -'Delete:': 'åˆŖé™¤:', -'Deploy on Google App Engine': 'é…ē½®åˆ° Google App Engine', -'Description': 'ęčæ°', -'Design for': 'čØ­čØˆē‚ŗäŗ†', -'E-mail': '電子郵件', -'EDIT': '編輯', -'Edit': '編輯', -'Edit Profile': 'ē·Øč¼ÆčØ­å®šęŖ”', -'Edit This App': 'ē·Øč¼Æęœ¬ę‡‰ē”ØēØ‹å¼', -'Edit application': 'ē·Øč¼Æę‡‰ē”ØēØ‹å¼', -'Edit current record': 'ē·Øč¼Æē•¶å‰ē“€éŒ„', -'Editing Language file': 'ē·Øč¼ÆčŖžčØ€ęŖ”ę”ˆ', -'Editing file': 'ē·Øč¼ÆęŖ”ę”ˆ', -'Editing file "%s"': 'ē·Øč¼ÆęŖ”ę”ˆ"%s"', -'Enterprise Web Framework': 'ä¼ę„­ē¶²ē«™å¹³å°', -'Error logs for "%(app)s"': '"%(app)s"ēš„éŒÆčŖ¤ē“€éŒ„', -'Errors': 'éŒÆčŖ¤ē“€éŒ„', -'Exception instance attributes': 'Exception instance attributes', -'First name': '名', -'Functions with no doctests will result in [passed] tests.': 'ę²’ęœ‰ doctests ēš„å‡½å¼ęœƒé”Æē¤ŗ [passed].', -'Group ID': 'ē¾¤ēµ„ē·Øč™Ÿ', -'Hello World': 'å—Ø! äø–ē•Œ', -'Help': 'čŖŖę˜ŽęŖ”', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', -'Import/Export': 'åŒÆå…„/åŒÆå‡ŗ', -'Index': '瓢引', -'Install': 'å®‰č£', -'Installed applications': 'å·²å®‰č£ę‡‰ē”ØēØ‹å¼', -'Internal State': 'å…§éƒØē‹€ę…‹', -'Invalid Query': 'äøåˆę³•ēš„ęŸ„č©¢', -'Invalid action': 'äøåˆę³•ēš„å‹•ä½œ(action)', -'Invalid email': 'äøåˆę³•ēš„é›»å­éƒµä»¶', -'Language files (static strings) updated': 'čŖžčØ€ęŖ”å·²ę›“ę–°', -'Languages': 'å„åœ‹čŖžčØ€', -'Last name': '姓', -'Last saved on:': 'ęœ€å¾Œå„²å­˜ę™‚é–“:', -'Layout': 'ē¶²é é…ē½®', -'License for': 'čØ±åÆč­‰', -'Login': '登兄', -'Login to the Administrative Interface': 'ē™»å…„åˆ°ē®”ē†å“”ä»‹é¢', -'Logout': '登出', -'Lost Password': 'åÆ†ē¢¼éŗåæ˜', -'Main Menu': '主選單', -'Menu Model': '選單樔組(menu)', -'Models': '資料樔組', -'Modules': 'ēØ‹å¼ęØ”ēµ„', -'NO': '否', -'Name': '名字', -'New Record': 'ę–°ē“€éŒ„', -'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', -'Origin': 'åŽŸę–‡', -'Original/Translation': 'åŽŸę–‡/翻譯', -'Overwrite installed app': 'č¦†č“‹å·²å®‰č£ēš„ę‡‰ē”ØēØ‹å¼', -'PAM authenticated user, cannot change password here': 'PAM ęŽˆę¬Šä½æē”Øč€…, ē„”ę³•åœØę­¤č®Šę›“åÆ†ē¢¼', -'Pack all': 'å…ØéƒØę‰“åŒ…', -'Pack compiled': 'ę‰“åŒ…å·²ē·Øč­Æč³‡ę–™', -'Password': '密碼', -"Password fields don't match": 'åÆ†ē¢¼ę¬„äøåŒ¹é…', -'Peeking at file': '選個文件', -'Plugin "%s" in application': 'åœØę‡‰ē”ØēØ‹å¼ēš„ę’ä»¶ "%s"', -'Plugins': 'ę’ä»¶', -'Powered by': 'åŸŗę–¼ä»„äø‹ęŠ€č”“ę§‹å»ŗļ¼š', -'Query:': '柄詢:', -'Record ID': 'ē“€éŒ„ē·Øč™Ÿ', -'Register': 'čØ»å†Š', -'Registration key': 'čØ»å†Šé‡‘é‘°', -'Remember me (for 30 days)': 'čØ˜ä½åø³č™Ÿ(30 天)', -'Remove compiled': 'ē·Øč­ÆęŖ”ę”ˆå·²ē§»é™¤', -'Reset Password key': 'é‡čØ­åÆ†ē¢¼', -'Resolve Conflict file': 'č§£ę±ŗč”ēŖęŖ”ę”ˆ', -'Role': '角色', -'Rows in table': 'åœØč³‡ę–™č”Øč£ēš„č³‡ę–™', -'Rows selected': '筆資料被選擇', -'Saved file hash:': 'ęŖ”ę”ˆé›œę¹Šå€¼å·²ē“€éŒ„:', -'Site': '網站', -'Static files': 'éœę…‹ęŖ”ę”ˆ', -'Stylesheet': '網頁風格檔', -'Submit': '傳送', -'Sure you want to delete this object?': 'ē¢ŗå®šč¦åˆŖé™¤ę­¤ē‰©ä»¶?', -'TM': 'TM', -'Table name': 'č³‡ę–™č”ØåēØ±', -'Testing application': 'ęø¬č©¦äø­ēš„ę‡‰ē”ØēØ‹å¼', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"柄詢"ę˜Æäø€å€‹åƒ "db.蔨1.ę¬„ä½1==\'值\'" ēš„ę¢ä»¶å¼. 仄"db.蔨1.ę¬„ä½1==db.蔨2.ę¬„ä½2"ę–¹å¼å‰‡ē›øē•¶ę–¼åŸ·č”Œ JOIN SQL.', -'There are no controllers': 'ę²’ęœ‰ęŽ§ä»¶(controllers)', -'There are no models': 'ę²’ęœ‰č³‡ę–™åŗ«ęØ”ēµ„(models)', -'There are no modules': 'ę²’ęœ‰ēØ‹å¼ęØ”ēµ„(modules)', -'There are no static files': 'ę²’ęœ‰éœę…‹ęŖ”ę”ˆ', -'There are no translators, only default language is supported': 'ę²’ęœ‰ēæ»č­ÆęŖ”,åŖę”Æę“åŽŸå§‹čŖžčØ€', -'There are no views': 'ę²’ęœ‰č¦–åœ–', -'This is the %(filename)s template': 'é€™ę˜Æ%(filename)sęŖ”ę”ˆēš„ęØ£ęæ(template)', -'Ticket': '問锌單', -'Timestamp': 'ę™‚é–“ęØ™čØ˜', -'To create a plugin, name a file/folder plugin_[name]': 'ęŖ”ę”ˆęˆ–ē›®éŒ„åēØ±ä»„ plugin_é–‹é ­ä¾†å‰µå»ŗę’ä»¶', -'Unable to check for upgrades': 'ē„”ę³•åšå‡ē“šęŖ¢ęŸ„', -'Unable to download': '焔法下載', -'Unable to download app': 'ē„”ę³•äø‹č¼‰ę‡‰ē”ØēØ‹å¼', -'Unable to download app because:': 'ē„”ę³•äø‹č¼‰ę‡‰ē”ØēØ‹å¼å› ē‚ŗ:', -'Unable to download because': 'å› ē‚ŗäø‹åˆ—åŽŸå› ē„”ę³•äø‹č¼‰:', -'Uninstall': 'č§£é™¤å®‰č£', -'Update:': 'ꛓꖰ:', -'Upload & install packed application': 'äøŠå‚³äø¦å®‰č£å·²ę‰“åŒ…ēš„ę‡‰ē”ØēØ‹å¼', -'Upload existing application': 'ę›“ę–°å­˜åœØēš„ę‡‰ē”ØēØ‹å¼', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'ä½æē”Øäø‹åˆ—ę–¹å¼ä¾†ēµ„åˆę›“č¤‡é›œēš„ę¢ä»¶å¼, (...)&(...) ä»£č”ØåŒę™‚å­˜åœØēš„ę¢ä»¶, (...)|(...) ä»£č”Øę“‡äø€ēš„ę¢ä»¶, ~(...)å‰‡ä»£č”Øåå‘ę¢ä»¶.', -'User %(id)s Logged-in': '使用者 %(id)s 已登兄', -'User %(id)s Registered': '使用者 %(id)s å·²čØ»å†Š', -'User ID': 'ä½æē”Øč€…ē·Øč™Ÿ', -'Verify Password': '驗證密碼', -'Version': 'ē‰ˆęœ¬', -'View': 'č¦–åœ–', -'Views': 'č¦–åœ–', -'Welcome %s': 'ę­”čæŽ %s', -'Welcome to web2py': 'ę­”čæŽä½æē”Ø web2py', -'YES': '是', -'additional code for your application': 'ę‡‰ē”ØēØ‹å¼é”å¤–ēš„ēØ‹å¼ē¢¼', -'admin disabled because no admin password': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æę²’ęœ‰čØ­å®šē®”ē†å“”åÆ†ē¢¼ ', -'admin disabled because not supported on google app engine': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æäøę”Æę“åœØgoogle apps engineē’°å¢ƒäø‹é‹ä½œ', -'admin disabled because unable to access password file': 'ē®”ē†ä»‹é¢é—œé–‰åŽŸå› ę˜Æē„”ę³•å­˜å–åÆ†ē¢¼ęŖ”', -'amy_ajax': 'amy_ajax', -'and rename it (required):': 'åŒę™‚ę›“åē‚ŗ(åæ…č¦ēš„):', -'and rename it:': 'åŒę™‚ę›“åē‚ŗ:', -'appadmin': 'ę‡‰ē”ØēØ‹å¼ē®”ē†å“”', -'appadmin is disabled because insecure channel': 'ē®”ē†ä»‹é¢é—œé–‰ē†ē”±ę˜Æé€£ē·šę–¹å¼äøå®‰å…Ø', -'application "%s" uninstalled': 'å·²ē§»é™¤ę‡‰ē”ØēØ‹å¼ "%s"', -'application compiled': 'å·²ē·Øč­Æę‡‰ē”ØēØ‹å¼', -'application is compiled and cannot be designed': 'ę‡‰ē”ØēØ‹å¼å·²ē¶“ē·Øč­Æē„”ę³•é‡ę–°čØ­čØˆ', -'arguments': 'arguments', -'back': '回復(back)', -'cache': 'åæ«å–čØ˜ę†¶é«”', -'cache, errors and sessions cleaned': 'åæ«å–čØ˜ę†¶é«”,éŒÆčŖ¤ē“€éŒ„,é€£ē·šē“€éŒ„å·²ęø…é™¤', -'cannot create file': 'ē„”ę³•å‰µå»ŗęŖ”ę”ˆ', -'cannot upload file "%(filename)s"': 'ē„”ę³•äøŠå‚³ęŖ”ę”ˆ "%(filename)s"', -'change_password': 'č®Šę›“åÆ†ē¢¼', -'check all': '全選', -'click here for online examples': 'é»žę­¤č™•é€²å…„ē·šäøŠēÆ„ä¾‹', -'click here for the administrative interface': 'é»žę­¤č™•é€²å…„ē®”ē†ä»‹é¢', -'click to check for upgrades': 'é»žę“Šę‰“å‹¾ä»„ä¾æå‡ē“š', -'code': 'code', -'compiled application removed': 'å·²ē§»é™¤å·²ē·Øč­Æēš„ę‡‰ē”ØēØ‹å¼', -'controllers': 'ꎧ件', -'create file with filename:': 'å‰µå»ŗęŖ”ę”ˆ:', -'create new application:': 'å‰µå»ŗę–°ę‡‰ē”ØēØ‹å¼:', -'created by': '創建自', -'crontab': 'å®šę™‚åŸ·č”Œč”Ø', +'currently running': 'currently running', 'currently saved or': 'ē¾åœØå­˜ęŖ”ęˆ–', 'customize me!': 'č«‹čŖæę•“ęˆ‘!', 'data uploaded': 'č³‡ę–™å·²äøŠå‚³', +'Database': '資料庫', 'database': '資料庫', +'Database %s select': 'Database %s select', 'database %s select': '已選擇 %s 資料庫', +'Database administration': 'Database administration', 'database administration': '資料庫箔理', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'ę—„ęœŸå’Œę™‚é–“', 'db': 'db', +'DB Model': '資料庫樔組', +'Debug': 'Debug', 'defines tables': 'å®šē¾©č³‡ę–™č”Ø', +'Delete': 'åˆŖé™¤', 'delete': 'åˆŖé™¤', 'delete all checked': 'åˆŖé™¤ę‰€ęœ‰å·²éøę“‡é …ē›®', 'delete plugin': 'åˆŖé™¤ę’ä»¶', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', +'Delete:': 'åˆŖé™¤:', 'delete_plugin': 'åˆŖé™¤ę’ä»¶', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', +'Deploy': 'Deploy', +'Deploy on Google App Engine': 'é…ē½®åˆ° Google App Engine', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description': 'ęčæ°', +'Description:': 'Description:', 'design': '設計', +'DESIGN': '設計', +'Design for': 'čØ­čØˆē‚ŗäŗ†', +'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', +'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', +'docs': 'docs', +'Docs': 'Docs', 'done!': '完成!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', +'download layouts': 'download layouts', +'Download layouts from repository': 'Download layouts from repository', +'download plugins': 'download plugins', +'Download plugins from repository': 'Download plugins from repository', +'E-mail': '電子郵件', +'EDIT': '編輯', +'Edit': '編輯', +'edit all': 'edit all', +'Edit application': 'ē·Øč¼Æę‡‰ē”ØēØ‹å¼', 'edit controller': 'ē·Øč¼ÆęŽ§ä»¶', +'edit controller:': 'edit controller:', +'Edit current record': 'ē·Øč¼Æē•¶å‰ē“€éŒ„', +'Edit Profile': 'ē·Øč¼ÆčØ­å®šęŖ”', +'Edit This App': 'ē·Øč¼Æęœ¬ę‡‰ē”ØēØ‹å¼', 'edit views:': 'ē·Øč¼Æč¦–åœ–', 'edit_language': 'ē·Øč¼ÆčŖžčØ€ęŖ”', +'Editing %s': 'Editing %s', +'Editing file': 'ē·Øč¼ÆęŖ”ę”ˆ', +'Editing file "%s"': 'ē·Øč¼ÆęŖ”ę”ˆ"%s"', +'Editing Language file': 'ē·Øč¼ÆčŖžčØ€ęŖ”ę”ˆ', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', +'Enterprise Web Framework': 'ä¼ę„­ē¶²ē«™å¹³å°', +'Error': 'Error', +'Error logs for "%(app)s"': '"%(app)s"ēš„éŒÆčŖ¤ē“€éŒ„', +'Error snapshot': 'Error snapshot', +'Error ticket': 'Error ticket', +'Errors': 'éŒÆčŖ¤ē“€éŒ„', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': 'ä»„é€—č™Ÿåˆ†éš”ęŖ”(csv)ę ¼å¼åŒÆå‡ŗ', +'Exports:': 'Exports:', 'exposes': '外锯', +'exposes:': 'exposes:', 'extends': '擓展', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module because:': 'å› ē‚ŗäø‹åˆ—åŽŸå› ē„”ę³•é‡ę–°č¼‰å…„ēØ‹å¼ęØ”ēµ„:', +'File': 'File', 'file "%(filename)s" created': 'ęŖ”ę”ˆ "%(filename)s" 已創建', 'file "%(filename)s" deleted': 'ęŖ”ę”ˆ "%(filename)s" å·²åˆŖé™¤', 'file "%(filename)s" uploaded': 'ęŖ”ę”ˆ "%(filename)s" å·²äøŠå‚³', 'file "%s" of %s restored': 'ęŖ”ę”ˆ %s ēš„ "%s" 已回存', 'file changed on disk': 'åœØē£ē¢ŸäøŠęŖ”ę”ˆå·²ę”¹č®Š', 'file does not exist': 'ęŖ”ę”ˆäøå­˜åœØ', +'file not found': 'file not found', 'file saved on %(time)s': 'ęŖ”ę”ˆå·²ę–¼ %(time)s 儲存', 'file saved on %s': 'ęŖ”ę”ˆåœØ %s 已儲存', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', +'filter': 'filter', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'First name': '名', +'Form has errors': 'Form has errors', +'Frames': 'Frames', +'Functions with no doctests will result in [passed] tests.': 'ę²’ęœ‰ doctests ēš„å‡½å¼ęœƒé”Æē¤ŗ [passed].', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', +'Group ID': 'ē¾¤ēµ„ē·Øč™Ÿ', +'Hello World': 'å—Ø! äø–ē•Œ', +'Help': 'čŖŖę˜ŽęŖ”', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'html編輯', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': 'åŒÆå…„/åŒÆå‡ŗ', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': '包含', +'Indent with tabs': 'Indent with tabs', +'Index': '瓢引', 'index': '瓢引', 'insert new': 'ę’å…„ę–°č³‡ę–™', 'insert new %s': 'ę’å…„ę–°č³‡ę–™ %s', +'inspect attributes': 'inspect attributes', +'Install': 'å®‰č£', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'å·²å®‰č£ę‡‰ē”ØēØ‹å¼', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'å…§éƒØéŒÆčŖ¤', +'internal error: %s': 'internal error: %s', +'Internal State': 'å…§éƒØē‹€ę…‹', +'Invalid action': 'äøåˆę³•ēš„å‹•ä½œ(action)', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid email': 'äøåˆę³•ēš„é›»å­éƒµä»¶', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'åÆ†ē¢¼éŒÆčŖ¤', +'invalid password.': 'invalid password.', +'Invalid Query': 'äøåˆę³•ēš„ęŸ„č©¢', 'invalid request': 'äøåˆę³•ēš„ē¶²č·Æč¦ę±‚(request)', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'äøåˆę³•ēš„å•é”Œå–®č™Ÿ', +'Key': 'Key', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': 'čŖžčØ€ęŖ”"%(filename)s"å·²å‰µå»ŗęˆ–ę›“ę–°', +'Language files (static strings) updated': 'čŖžčØ€ęŖ”å·²ę›“ę–°', 'languages': 'čŖžčØ€ęŖ”', +'Languages': 'å„åœ‹čŖžčØ€', +'Last name': '姓', +'Last Revision': 'Last Revision', +'Last saved on:': 'ęœ€å¾Œå„²å­˜ę™‚é–“:', +'Layout': 'ē¶²é é…ē½®', +'License for': 'čØ±åÆč­‰', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': '載兄中...', +'Local Apps': 'Local Apps', +'locals': 'locals', +'Locals##debug': 'Locals##debug', 'login': '登兄', +'Login': '登兄', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'ē™»å…„åˆ°ē®”ē†å“”ä»‹é¢', +'Login/Register': 'Login/Register', +'Logout': '登出', +'Lost Password': 'åÆ†ē¢¼éŗåæ˜', +'lost password': 'lost password', +'Main Menu': '主選單', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', +'Menu Model': '選單樔組(menu)', 'merge': '合併', +'Models': '資料樔組', 'models': '資料庫樔組', +'Modified On': 'Modified On', +'Modules': 'ēØ‹å¼ęØ”ēµ„', 'modules': 'ēØ‹å¼ęØ”ēµ„', +'Multi User Mode': 'Multi User Mode', +'Name': '名字', 'new application "%s" created': 'å·²å‰µå»ŗę–°ēš„ę‡‰ē”ØēØ‹å¼ "%s"', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', +'New application wizard': 'New application wizard', 'new plugin installed': 'å·²å®‰č£ę–°ę’ä»¶', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': 'ę–°ē“€éŒ„', 'new record inserted': 'å·²ę–°å¢žę–°ē“€éŒ„', +'New simple application': 'New simple application', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': '往後 100 ē­†', +'NO': '否', +'no changes': 'no changes', +'No databases in this application': 'é€™ę‡‰ē”ØēØ‹å¼äøå«č³‡ę–™åŗ«', +'No Interaction yet': 'No Interaction yet', 'no match': 'ē„”ę³•åŒ¹é…', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'ęˆ–ę˜Æå¾žé€—č™Ÿåˆ†éš”ęŖ”(CSV)åŒÆå…„', 'or provide app url:': 'ęˆ–ę˜Æęä¾›ę‡‰ē”ØēØ‹å¼ēš„å®‰č£ē¶²å€:', +'Origin': 'åŽŸę–‡', +'Original/Translation': 'åŽŸę–‡/翻譯', +'Overview': 'Overview', +'Overwrite installed app': 'č¦†č“‹å·²å®‰č£ēš„ę‡‰ē”ØēØ‹å¼', +'Pack all': 'å…ØéƒØę‰“åŒ…', +'Pack compiled': 'ę‰“åŒ…å·²ē·Øč­Æč³‡ę–™', +'Pack custom': 'Pack custom', 'pack plugin': 'ę‰“åŒ…ę’ä»¶', +'PAM authenticated user, cannot change password here': 'PAM ęŽˆę¬Šä½æē”Øč€…, ē„”ę³•åœØę­¤č®Šę›“åÆ†ē¢¼', +'Password': '密碼', 'password changed': 'åÆ†ē¢¼å·²č®Šę›“', +"Password fields don't match": 'åÆ†ē¢¼ę¬„äøåŒ¹é…', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', 'peek': 'éøå–', +'Peeking at file': '選個文件', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin': 'ę’ä»¶', 'plugin "%(plugin)s" deleted': 'å·²åˆŖé™¤ę’ä»¶"%(plugin)s"', +'Plugin "%s" in application': 'åœØę‡‰ē”ØēØ‹å¼ēš„ę’ä»¶ "%s"', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', +'plugins': 'plugins', +'Plugins': 'ę’ä»¶', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': 'åŸŗę–¼ä»„äø‹ęŠ€č”“ę§‹å»ŗļ¼š', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '往前 100 ē­†', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': '柄詢:', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'ē“€éŒ„', 'record does not exist': 'ē“€éŒ„äøå­˜åœØ', 'record id': 'ē“€éŒ„ē·Øč™Ÿ', +'Record ID': 'ē“€éŒ„ē·Øč™Ÿ', +'Record id': 'Record id', +'refresh': 'refresh', 'register': 'čØ»å†Š', +'Register': 'čØ»å†Š', +'Registration key': 'čØ»å†Šé‡‘é‘°', +'Reload routes': 'Reload routes', +'Remember me (for 30 days)': 'čØ˜ä½åø³č™Ÿ(30 天)', +'Remove compiled': 'ē·Øč­ÆęŖ”ę”ˆå·²ē§»é™¤', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', +'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Reset Password key': 'é‡čØ­åÆ†ē¢¼', 'resolve': '解決', +'Resolve Conflict file': 'č§£ę±ŗč”ēŖęŖ”ę”ˆ', +'response': 'response', +'restart': 'restart', 'restore': '回存', +'return': 'return', +'Revert': 'Revert', 'revert': 'åå‘ę¢å¾©', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': '角色', +'Roles': 'Roles', +'Rows in table': 'åœØč³‡ę–™č”Øč£ēš„č³‡ę–™', +'Rows in Table': 'Rows in Table', +'Rows selected': '筆資料被選擇', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'Running on %s', +'Save': 'Save', 'save': '儲存', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Save via Ajax', +'Saved file hash:': 'ęŖ”ę”ˆé›œę¹Šå€¼å·²ē“€éŒ„:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': '已選擇', +'session': 'session', 'session expired': 'é€£ē·š(session)å·²éŽę™‚', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'å‘½ä»¤åˆ—ę“ä½œä»‹é¢', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', +'Site': '網站', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'éƒØä»½ęŖ”ę”ˆē„”ę³•ē§»é™¤', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', +'Start wizard': 'Start wizard', 'state': '狀態', +'Static': 'Static', 'static': 'éœę…‹ęŖ”ę”ˆ', +'Static files': 'éœę…‹ęŖ”ę”ˆ', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', +'Stylesheet': '網頁風格檔', 'submit': '傳送', +'Submit': '傳送', +'successful': 'successful', +'Sure you want to delete this object?': 'ē¢ŗå®šč¦åˆŖé™¤ę­¤ē‰©ä»¶?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': '資料蔨', +'Table': 'Table', +'Table name': 'č³‡ę–™č”ØåēØ±', +'Temporary': 'Temporary', 'test': '測試', +'Testing application': 'ęø¬č©¦äø­ēš„ę‡‰ē”ØēØ‹å¼', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"柄詢"ę˜Æäø€å€‹åƒ "db.蔨1.ę¬„ä½1==\'值\'" ēš„ę¢ä»¶å¼. 仄"db.蔨1.ę¬„ä½1==db.蔨2.ę¬„ä½2"ę–¹å¼å‰‡ē›øē•¶ę–¼åŸ·č”Œ JOIN SQL.', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': 'ę‡‰ē”ØēØ‹å¼é‚č¼Æ - ęÆå€‹ē¶²å€č·Æå¾‘å°ę‡‰åˆ°äø€å€‹ęŽ§ä»¶ēš„å‡½å¼', +'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'the data representation, define database tables and sets': 'č³‡ę–™å±•ē¾å±¤ - ē”Øä¾†å®šē¾©č³‡ę–™č”Øå’Œé›†åˆ', +'The data representation, define database tables and sets': 'The data representation, define database tables and sets', +'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', 'the presentations layer, views are also known as templates': 'å¤–č§€å±•ē¾å±¤ - č¦–åœ–ęœ‰ę™‚ä¹Ÿč¢«ēØ±ē‚ŗęØ£ęæ', +'Theme': 'Theme', +'There are no controllers': 'ę²’ęœ‰ęŽ§ä»¶(controllers)', +'There are no models': 'ę²’ęœ‰č³‡ę–™åŗ«ęØ”ēµ„(models)', +'There are no modules': 'ę²’ęœ‰ēØ‹å¼ęØ”ēµ„(modules)', +'There are no plugins': 'There are no plugins', +'There are no private files': 'There are no private files', +'There are no static files': 'ę²’ęœ‰éœę…‹ęŖ”ę”ˆ', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'ę²’ęœ‰ēæ»č­ÆęŖ”,åŖę”Æę“åŽŸå§‹čŖžčØ€', +'There are no views': 'ę²’ęœ‰č¦–åœ–', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'These files are served without processing, your images go here', 'these files are served without processing, your images go here': 'é€™äŗ›ęŖ”ę”ˆäæē•™ęœŖē¶“č™•ē†,ä½ ēš„å½±åƒęŖ”åœØę­¤', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'é€™ę˜Æ%(filename)sęŖ”ę”ˆēš„ęØ£ęæ(template)', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', 'ticket': '問锌單', +'Ticket': '問锌單', +'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Timestamp': 'ę™‚é–“ęØ™čØ˜', +'TM': 'TM', 'to previous version.': 'åˆ°å‰äø€å€‹ē‰ˆęœ¬', +'To create a plugin, name a file/folder plugin_[name]': 'ęŖ”ę”ˆęˆ–ē›®éŒ„åēØ±ä»„ plugin_é–‹é ­ä¾†å‰µå»ŗę’ä»¶', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Traceback', 'translation strings for the application': 'ēæ»č­Æę­¤ę‡‰ē”ØēØ‹å¼ēš„å­—äø²', +'Translation strings for the application': 'Translation strings for the application', 'try': 'å˜—č©¦', 'try something like': 'å˜—č©¦å¦‚', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'ē„”ę³•åšå‡ē“šęŖ¢ęŸ„', 'unable to create application "%s"': 'ē„”ę³•å‰µå»ŗę‡‰ē”ØēØ‹å¼ "%s"', 'unable to delete file "%(filename)s"': 'ē„”ę³•åˆŖé™¤ęŖ”ę”ˆ "%(filename)s"', 'unable to delete file plugin "%(plugin)s"': 'ē„”ę³•åˆŖęŸ„ę’ä»¶ęŖ” "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': '焔法下載', +'Unable to download app': 'ē„”ę³•äø‹č¼‰ę‡‰ē”ØēØ‹å¼', +'Unable to download app because:': 'ē„”ę³•äø‹č¼‰ę‡‰ē”ØēØ‹å¼å› ē‚ŗ:', +'Unable to download because': 'å› ē‚ŗäø‹åˆ—åŽŸå› ē„”ę³•äø‹č¼‰:', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'ē„”ę³•č§£ęžé€—č™Ÿåˆ†éš”ęŖ”(csv)', 'unable to uninstall "%s"': 'ē„”ę³•ē§»é™¤å®‰č£ "%s"', 'unable to upgrade because "%s"': 'ē„”ę³•å‡ē“šå› ē‚ŗ "%s"', 'uncheck all': 'å…Øäøéø', +'Uninstall': 'č§£é™¤å®‰č£', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'ꛓꖰ', 'update all languages': 'å°‡ēØ‹å¼äø­å¾…ēæ»č­ÆčŖžå„ę›“ę–°åˆ°ę‰€ęœ‰ēš„čŖžčØ€ęŖ”', +'Update:': 'ꛓꖰ:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'upgrade web2py now', 'upgrade_web2py': 'å‡ē“š web2py', +'upload': 'upload', +'Upload': 'Upload', +'Upload & install packed application': 'äøŠå‚³äø¦å®‰č£å·²ę‰“åŒ…ēš„ę‡‰ē”ØēØ‹å¼', +'Upload a package:': 'Upload a package:', +'Upload and install packed application': 'Upload and install packed application', 'upload application:': 'äøŠå‚³ę‡‰ē”ØēØ‹å¼:', +'Upload existing application': 'ę›“ę–°å­˜åœØēš„ę‡‰ē”ØēØ‹å¼', 'upload file:': 'äøŠå‚³ęŖ”ę”ˆ:', 'upload plugin file:': 'äøŠå‚³ę’ä»¶ęŖ”:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'ä½æē”Øäø‹åˆ—ę–¹å¼ä¾†ēµ„åˆę›“č¤‡é›œēš„ę¢ä»¶å¼, (...)&(...) ä»£č”ØåŒę™‚å­˜åœØēš„ę¢ä»¶, (...)|(...) ä»£č”Øę“‡äø€ēš„ę¢ä»¶, ~(...)å‰‡ä»£č”Øåå‘ę¢ä»¶.', +'User': 'User', +'User %(id)s Logged-in': '使用者 %(id)s 已登兄', +'User %(id)s Registered': '使用者 %(id)s å·²čØ»å†Š', +'User ID': 'ä½æē”Øč€…ē·Øč™Ÿ', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', +'Verify Password': '驗證密碼', +'Version': 'ē‰ˆęœ¬', +'Versioning': 'Versioning', 'versioning': 'ē‰ˆęœ¬ē®”ē†', +'View': 'č¦–åœ–', 'view': 'č¦–åœ–', +'Views': 'č¦–åœ–', 'views': 'č¦–åœ–', -'web2py Recent Tweets': 'web2py ęœ€čæ‘ēš„ Tweets', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py å·²ē¶“ę˜Æęœ€ę–°ē‰ˆ', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'web2py ęœ€čæ‘ēš„ Tweets', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'å·²å‡ē“š web2py ; č«‹é‡ę–°å•Ÿå‹•', +'Welcome %s': 'ę­”čæŽ %s', +'Welcome to web2py': 'ę­”čæŽä½æē”Ø web2py', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': '是', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/admin/languages/zh.py b/applications/admin/languages/zh.py index d62b0804..7215a477 100755 --- a/applications/admin/languages/zh.py +++ b/applications/admin/languages/zh.py @@ -6,215 +6,243 @@ '"save"': '"äæå­˜"', '"submit"': '"ęäŗ¤"', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"ꛓꖰ"ę˜ÆåÆé…ē½®é”¹ 如 "field1=\'newvalue\'",ä½ ę— ę³•åœØJOIN äø­ę›“ę–°ęˆ–åˆ é™¤', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'"User Exception" debug mode. ': '"User Exception" debug mode. ', +'%s': '%s', '%s %%{row} deleted': '%s 蔌已删', '%s %%{row} updated': '%s č”Œę›“ę–°', +'%s selected': '%s selected', +'%s students registered': '%s students registered', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access)': '(requires internet access)', +'(requires internet access, experimental)': '(requires internet access, experimental)', '(something like "it-it")': '(就酱 "it-it")', +'(version %s)': '(version %s)', +'?': '?', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', 'A new version of web2py is available': 'ę–°ē‰ˆęœ¬web2pyå·²ē»åÆē”Ø', 'A new version of web2py is available: %s': 'ę–°ē‰ˆęœ¬web2pyå·²ē»åÆē”Ø: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': '', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': '', -'ATTENTION: you cannot edit the running application!': 'ę³Øę„: äøčƒ½äæ®ę”¹ę­£åœØčæč”Œäø­ēš„åŗ”ē”Ø!', +'Abort': 'Abort', 'About': 'å…³äŗŽ', 'About application': 'å…³äŗŽčæ™äøŖåŗ”ē”Ø', +'Accept Terms': 'Accept Terms', +'Add breakpoint': 'Add breakpoint', +'additional code for your application': 'ē»™ä½ ēš„åŗ”ē”Øé™„åŠ ä»£ē ', +'Additional code for your application': 'Additional code for your application', +'Admin design page': 'Admin design page', +'admin disabled because no admin password': 'ē®”ē†å‘˜éœ€č¦č®¾å®šåÆ†ē ļ¼Œå¦åˆ™ę— ę³•ē®”ē†', +'admin disabled because not supported on google app engine': 'ęœŖę”ÆęŒGAE,无法箔理', +'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because unable to access password file': 'éœ€č¦åÆä»„ę“ä½œåÆ†ē ę–‡ä»¶ļ¼Œå¦åˆ™ę— ę³•čæ›č”Œē®”ē†', 'Admin is disabled because insecure channel': 'ē®”ē†å› äøå®‰å…Øé€šé“č€Œå…³é—­', 'Admin is disabled because unsecure channel': 'ē®”ē†å› äøēØ³å®šé€šé“č€Œå…³é—­', 'Admin language': 'Admin language', +'Admin versioning page': 'Admin versioning page', +'administrative interface': 'administrative interface', 'Administrator Password:': 'ē®”ē†å‘˜åÆ†ē :', +'and rename it (required):': 'é‡å‘½åäøŗ (åæ…é”»):', +'and rename it:': ' é‡å‘½åäøŗ:', +'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'appadmin': '应用箔理', +'appadmin is disabled because insecure channel': 'åŗ”ē”Øē®”ē†å› éžę³•é€šé“å¤±ę•ˆ', +'Application': 'Application', +'application "%s" uninstalled': '应用"%s" å·²č¢«åøč½½', +'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', +'application compiled': 'åŗ”ē”Øå·²ē¼–čÆ‘å®Œ', +'Application exists already': 'Application exists already', +'application is compiled and cannot be designed': 'åŗ”ē”Øå·²ē¼–čÆ‘å®Œę— ę³•č®¾č®”', 'Application name:': 'Application name:', +'Application updated via git pull': 'Application updated via git pull', +'are not used': 'are not used', +'are not used yet': 'are not used yet', 'Are you sure you want to delete file "%s"?': 'ä½ ēœŸęƒ³åˆ é™¤ę–‡ä»¶"%s"?', 'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', 'Are you sure you want to uninstall application "%s"': 'ä½ ēœŸē”®å®šč¦åøč½½åŗ”ē”Ø "%s"', 'Are you sure you want to uninstall application "%s"?': 'ä½ ēœŸē”®å®šč¦åøč½½åŗ”ē”Ø "%s" ?', 'Are you sure you want to upgrade web2py now?': 'Are you sure you want to upgrade web2py now?', +'Are you sure?': 'Are you sure?', +'arguments': 'arguments', +'at char %s': 'at char %s', +'at line %s': 'at line %s', +'ATTENTION:': 'ATTENTION:', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': '', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': '', +'ATTENTION: you cannot edit the running application!': 'ę³Øę„: äøčƒ½äæ®ę”¹ę­£åœØčæč”Œäø­ēš„åŗ”ē”Ø!', +'Autocomplete Python Code': 'Autocomplete Python Code', 'Available databases and tables': 'åÆē”Øę•°ę®åŗ“/蔨', +'Available Databases and Tables': 'Available Databases and Tables', +'back': 'back', +'Back to the plugins list': 'Back to the plugins list', +'Back to wizard': 'Back to wizard', +'Basics': 'Basics', +'Begin': 'Begin', +'breakpoint': 'breakpoint', +'Breakpoints': 'Breakpoints', +'breakpoints': 'breakpoints', +'Bulk Register': 'Bulk Register', +'Bulk Student Registration': 'Bulk Student Registration', +'Cache': 'Cache', +'cache': 'cache', +'Cache Cleared': 'Cache Cleared', +'Cache Keys': 'Cache Keys', +'cache, errors and sessions cleaned': 'ē¼“å­˜ć€é”™čÆÆć€sesiones已被清空', +'can be a git repo': 'can be a git repo', +'Cancel': 'Cancel', 'Cannot be empty': 'äøčƒ½äøå”«', 'Cannot compile: there are errors in your app. Ā  Ā  Ā  Ā Debug it, correct errors and try again.': '无法编译: åŗ”ē”Øäø­ęœ‰é”™čÆÆ,čÆ·äæ®č®¢åŽå†čÆ•.', 'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', -'Change Password': '修改密码', +'cannot create file': 'ę— ę³•åˆ›å»ŗę–‡ä»¶', +'cannot upload file "%(filename)s"': 'ę— ę³•äøŠä¼ ę–‡ä»¶ "%(filename)s"', +'Change Admin Password': 'Change Admin Password', 'Change admin password': 'change admin password', +'change editor settings': 'change editor settings', +'Change Password': '修改密码', +'Changelog': 'Changelog', +'check all': 'ę£€ęŸ„ę‰€ęœ‰', 'Check for upgrades': 'check for upgrades', 'Check to delete': 'ę£€éŖŒåˆ é™¤', 'Checking for upgrades...': 'ę˜Æå¦ęœ‰å‡ēŗ§ē‰ˆęœ¬ę£€ęŸ„ing...', 'Clean': '清除', +'Clear': 'Clear', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'click here for online examples': 'ēŒ›å‡»ę­¤å¤„ļ¼ŒęŸ„ēœ‹åœØēŗæå®žä¾‹', +'click here for the administrative interface': 'ēŒ›å‡»ę­¤å¤„ļ¼Œčæ›å…„ē®”ē†ē•Œé¢', +'Click row to expand traceback': 'Click row to expand traceback', +'Click row to view a ticket': 'Click row to view a ticket', +'click to check for upgrades': 'ēŒ›å‡»ęŸ„ēœ‹ę˜Æå¦ęœ‰å‡ēŗ§ē‰ˆęœ¬', 'Client IP': '客户端IP', +'code': 'code', +'Code listing': 'Code listing', +'collapse/expand all': 'collapse/expand all', +'Command': 'Command', +'Comment:': 'Comment:', +'Commit': 'Commit', +'Commit form': 'Commit form', +'Committed files': 'Committed files', 'Compile': '编译', +'Compile (all or nothing)': 'Compile (all or nothing)', +'Compile (skip failed views)': 'Compile (skip failed views)', +'compiled application removed': '已编译应用已移走', +'Condition': 'Condition', +'continue': 'continue', 'Controllers': 'ęŽ§åˆ¶å™Øs', +'controllers': 'ęŽ§åˆ¶å™Ø', +'Count': 'Count', 'Create': 'create', +'create file with filename:': 'åˆ›å»ŗę–‡ä»¶ē”Øčæ™å:', +'create new application:': 'åˆ›å»ŗę–°åŗ”ē”Ø:', 'Create new simple application': 'åˆ›å»ŗäø€äøŖę–°åŗ”ē”Ø', +'Create/Upload': 'Create/Upload', +'created by': 'åˆ›å»ŗč‡Ŗ', +'Created by:': 'Created by:', +'Created On': 'Created On', +'Created on:': 'Created on:', +'crontab': 'å®šęœŸäŗ‹åŠ”', 'Current request': 'å½“å‰čÆ·ę±‚', 'Current response': 'å½“å‰čæ”å›ž', 'Current session': 'å½“å‰ä¼ščÆ', -'DESIGN': '设讔', -'Date and Time': 'ę—¶é—“ę—„ęœŸ', -'Debug': 'Debug', -'Delete': '删除', -'Delete:': '删除:', -'Deploy': 'deploy', -'Deploy on Google App Engine': '部署到GAE', -'Deploy to OpenShift': 'Deploy to OpenShift', -'Description': 'ęčæ°', -'Design for': '设讔:', -'Disable': 'Disable', -'E-mail': '邮箱:', -'EDIT': '编辑', -'Edit': '修改', -'Edit Profile': 'äæ®č®¢é…ē½®', -'Edit application': '修订应用', -'Edit current record': 'äæ®č®¢å½“å‰č®°å½•', -'Editing Language file': '修订语言文件', -'Editing file': '修订文件', -'Editing file "%s"': '修订文件 %s', -'Enterprise Web Framework': 'å¼ŗę‚ēš„ē½‘ē»œå¼€å‘ę”†ęž¶', -'Error logs for "%(app)s"': '"%(app)s" ēš„é”™čÆÆę—„åæ—', -'Errors': '错误', -'Exception instance attributes': 'Exception instance attributes', -'First name': '姓', -'Functions with no doctests will result in [passed] tests.': '', -'Get from URL:': 'Get from URL:', -'Group ID': '组ID', -'Hello World': 'é“åÆé“ļ¼Œéžåøøé“ļ¼›ååÆåļ¼Œéžåøøå', -'Help': '帮助', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', -'Import/Export': '导兄/导出', -'Install': 'install', -'Installed applications': 'å·²å®‰č£…ēš„åŗ”ē”Ø', -'Internal State': 'å†…éƒØēŠ¶ę€', -'Invalid Query': 'ę— ę•ˆęŸ„čÆ¢', -'Invalid action': 'ę— ę•ˆåŠØä½œ', -'Invalid email': 'ę— ę•ˆēš„email', -'Language files (static strings) updated': '语言文件(é™ę€éƒØåˆ†)已曓新', -'Languages': '语言', -'Last name': '名', -'Last saved on:': 'ęœ€åŽäæå­˜äŗŽ:', -'License for': 'č®øåÆčÆ', -'Login': '登录', -'Login to the Administrative Interface': 'ē™»å½•åˆ°ē®”ē†å‘˜ē•Œé¢', -'Logout': '注销', -'Lost Password': 'åæ˜č®°åÆ†ē ', -'Models': 'ęØ”åž‹s', -'Modules': 'ęØ”å—s', -'NO': 'äø', -'Name': '姓名', -'New Record': '新记录', -'New application wizard': 'New application wizard', -'New simple application': 'New simple application', -'No databases in this application': 'čæ™åŗ”ē”Øę²”ęœ‰ę•°ę®åŗ“', -'Origin': '起源', -'Original/Translation': 'åŽŸå§‹ę–‡ä»¶/翻译文件', -'Overwrite installed app': 'overwrite installed app', -'PAM authenticated user, cannot change password here': 'PAM authenticated user, cannot change password here', -'Pack all': 'å…ØéƒØę‰“åŒ…', -'Pack compiled': 'åŒ…ē¼–čÆ‘å®Œ', -'Password': '密码', -'Peeking at file': '选个文件', -'Plugin "%s" in application': 'Plugin "%s" in application', -'Plugins': 'Plugins', -'Powered by': '', -'Query:': '柄询', -'Record ID': '记录ID', -'Register': 'ę³Øå†Œ', -'Registration key': 'ę³Øå†ŒåÆ†åŒ™', -'Reload routes': 'Reload routes', -'Remove compiled': '已移除', -'Resolve Conflict file': '解决冲突文件', -'Role': '角色', -'Rows in table': '蔨蔌', -'Rows selected': 'č”Œé€‰ę‹©', -'Running on %s': 'Running on %s', -'Saved file hash:': 'å·²å­˜ę–‡ä»¶Hash:', -'Site': '总站', -'Start wizard': 'start wizard', -'Static files': 'é™ę€ę–‡ä»¶', -'Sure you want to delete this object?': 'ēœŸēš„č¦åˆ é™¤čæ™äøŖåÆ¹č±”?', -'TM': '', -'Table name': 'č”Øå', -'Testing application': '应用测试', -'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '', -'There are no controllers': 'å†‡ęŽ§åˆ¶å™Ø', -'There are no models': 'å†‡ęØ”åž‹s', -'There are no modules': 'å†‡ęØ”å—s', -'There are no static files': 'å†‡é™ę€ę–‡ä»¶', -'There are no translators, only default language is supported': 'ę²”ęœ‰ę‰¾åˆ°ē›øåŗ”ēæ»čÆ‘ļ¼ŒåŖčƒ½ä½æē”Øé»˜č®¤čÆ­čØ€', -'There are no views': '冇视图', -'This is the %(filename)s template': 'čæ™ę˜Æ %(filename)s ęØ”ęæ', -'Ticket': '传焨', -'Timestamp': 'ę—¶é—“ęˆ³', -'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', -'Unable to check for upgrades': 'ę— ę³•ę£€ęŸ„ę˜Æå¦éœ€č¦å‡ēŗ§', -'Unable to download': '无法下载', -'Unable to download app': '无法下载应用', -'Unable to download app because:': 'Unable to download app because:', -'Unable to download because': 'Unable to download because', -'Uninstall': 'åøč½½', -'Update:': 'ꛓꖰ:', -'Upload & install packed application': 'Upload & install packed application', -'Upload a package:': 'Upload a package:', -'Upload and install packed application': 'Upload and install packed application', -'Upload existing application': 'äøŠä¼ å·²ęœ‰åŗ”ē”Ø', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) Ā for NOT to build more complex queries.': '', -'Use an url:': 'Use an url:', -'User ID': 'ē”Øęˆ·ID', -'Version': 'ē‰ˆęœ¬', -'Views': '视图', -'Welcome to web2py': 'ę¬¢čæŽä½æē”Øweb2py', -'YES': '是', -'additional code for your application': 'ē»™ä½ ēš„åŗ”ē”Øé™„åŠ ä»£ē ', -'admin disabled because no admin password': 'ē®”ē†å‘˜éœ€č¦č®¾å®šåÆ†ē ļ¼Œå¦åˆ™ę— ę³•ē®”ē†', -'admin disabled because not supported on google app engine': 'ęœŖę”ÆęŒGAE,无法箔理', -'admin disabled because unable to access password file': 'éœ€č¦åÆä»„ę“ä½œåÆ†ē ę–‡ä»¶ļ¼Œå¦åˆ™ę— ę³•čæ›č”Œē®”ē†', -'administrative interface': 'administrative interface', -'and rename it (required):': 'é‡å‘½åäøŗ (åæ…é”»):', -'and rename it:': ' é‡å‘½åäøŗ:', -'appadmin': '应用箔理', -'appadmin is disabled because insecure channel': 'åŗ”ē”Øē®”ē†å› éžę³•é€šé“å¤±ę•ˆ', -'application "%s" uninstalled': '应用"%s" å·²č¢«åøč½½', -'application compiled': 'åŗ”ē”Øå·²ē¼–čÆ‘å®Œ', -'application is compiled and cannot be designed': 'åŗ”ē”Øå·²ē¼–čÆ‘å®Œę— ę³•č®¾č®”', -'arguments': 'arguments', -'back': 'back', -'cache': 'cache', -'cache, errors and sessions cleaned': 'ē¼“å­˜ć€é”™čÆÆć€sesiones已被清空', -'cannot create file': 'ę— ę³•åˆ›å»ŗę–‡ä»¶', -'cannot upload file "%(filename)s"': 'ę— ę³•äøŠä¼ ę–‡ä»¶ "%(filename)s"', -'check all': 'ę£€ęŸ„ę‰€ęœ‰', -'click here for online examples': 'ēŒ›å‡»ę­¤å¤„ļ¼ŒęŸ„ēœ‹åœØēŗæå®žä¾‹', -'click here for the administrative interface': 'ēŒ›å‡»ę­¤å¤„ļ¼Œčæ›å…„ē®”ē†ē•Œé¢', -'click to check for upgrades': 'ēŒ›å‡»ęŸ„ēœ‹ę˜Æå¦ęœ‰å‡ēŗ§ē‰ˆęœ¬', -'code': 'code', -'compiled application removed': '已编译应用已移走', -'controllers': 'ęŽ§åˆ¶å™Ø', -'create file with filename:': 'åˆ›å»ŗę–‡ä»¶ē”Øčæ™å:', -'create new application:': 'åˆ›å»ŗę–°åŗ”ē”Ø:', -'created by': 'åˆ›å»ŗč‡Ŗ', -'crontab': 'å®šęœŸäŗ‹åŠ”', 'currently running': 'currently running', 'currently saved or': 'äæå­˜å½“å‰ēš„ęˆ–', 'customize me!': '定制俺!', 'data uploaded': 'ę•°ę®å·²äøŠä¼ ', +'Database': 'Database', 'database': 'ę•°ę®åŗ“', +'Database %s select': 'Database %s select', 'database %s select': 'ę•°ę®åŗ“ %s 选ꋩ', +'Database administration': 'Database administration', 'database administration': 'ę•°ę®åŗ“ē®”ē†', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'Date and Time': 'ę—¶é—“ę—„ęœŸ', 'db': '', +'Debug': 'Debug', 'defines tables': 'å·²å®šä¹‰č”Ø', +'Delete': '删除', 'delete': '删除', 'delete all checked': 'åˆ é™¤ę‰€ęœ‰é€‰ę‹©ēš„', 'delete plugin': 'delete plugin', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', +'Delete:': '删除:', +'deleted after first hit': 'deleted after first hit', +'Demo': 'Demo', +'Deploy': 'deploy', +'Deploy on Google App Engine': '部署到GAE', +'Deploy to OpenShift': 'Deploy to OpenShift', +'Deploy to pythonanywhere': 'Deploy to pythonanywhere', +'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deployment form': 'Deployment form', +'Deployment Interface': 'Deployment Interface', +'Description': 'ęčæ°', +'Description:': 'Description:', 'design': '设讔', +'DESIGN': '设讔', +'Design for': '设讔:', +'Detailed traceback description': 'Detailed traceback description', +'details': 'details', 'direction: ltr': 'direction: ltr', +'directory not found': 'directory not found', +'Disable': 'Disable', +'Disabled': 'Disabled', +'disabled in demo mode': 'disabled in demo mode', +'disabled in GAE mode': 'disabled in GAE mode', +'disabled in multi user mode': 'disabled in multi user mode', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'Display line numbers': 'Display line numbers', +'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', +'docs': 'docs', +'Docs': 'Docs', 'done!': 'ęžå®š!', +'Downgrade': 'Downgrade', +'Download .w2p': 'Download .w2p', +'Download as .exe': 'Download as .exe', +'download layouts': 'download layouts', +'Download layouts from repository': 'Download layouts from repository', +'download plugins': 'download plugins', +'Download plugins from repository': 'Download plugins from repository', +'E-mail': '邮箱:', +'EDIT': '编辑', +'Edit': '修改', +'edit all': 'edit all', +'Edit application': '修订应用', 'edit controller': 'äæ®č®¢ęŽ§åˆ¶å™Ø', +'edit controller:': 'edit controller:', +'Edit current record': 'äæ®č®¢å½“å‰č®°å½•', +'Edit Profile': 'äæ®č®¢é…ē½®', 'edit views:': 'edit views:', +'Editing %s': 'Editing %s', +'Editing file': '修订文件', +'Editing file "%s"': '修订文件 %s', +'Editing Language file': '修订语言文件', +'Editing Plural Forms File': 'Editing Plural Forms File', +'Editor': 'Editor', +'Email Address': 'Email Address', +'Enable': 'Enable', +'Enable Close-Tag': 'Enable Close-Tag', +'Enable Code Folding': 'Enable Code Folding', +'Enterprise Web Framework': 'å¼ŗę‚ēš„ē½‘ē»œå¼€å‘ę”†ęž¶', +'Error': 'Error', +'Error logs for "%(app)s"': '"%(app)s" ēš„é”™čÆÆę—„åæ—', +'Error snapshot': 'Error snapshot', +'Error ticket': 'Error ticket', +'Errors': '错误', +'Exception %(extype)s: %(exvalue)s': 'Exception %(extype)s: %(exvalue)s', +'Exception %s': 'Exception %s', +'Exception instance attributes': 'Exception instance attributes', +'Exit Fullscreen': 'Exit Fullscreen', +'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', 'export as csv file': '导出为CSVꖇ件', +'Exports:': 'Exports:', 'exposes': '暓露', +'exposes:': 'exposes:', 'extends': '扩展', +'failed to compile file because:': 'failed to compile file because:', 'failed to reload module': 'é‡ę–°åŠ č½½ęØ”å—å¤±č“„', 'failed to reload module because:': 'failed to reload module because:', +'File': 'File', 'file "%(filename)s" created': 'ꖇ件 "%(filename)s" å·²åˆ›å»ŗ', 'file "%(filename)s" deleted': 'ꖇ件 "%(filename)s" 已删除', 'file "%(filename)s" uploaded': 'ꖇ件 "%(filename)s" 已上传', @@ -222,78 +250,417 @@ 'file "%s" of %s restored': 'ꖇ件"%s" ęœ‰å…³ %s 已重存', 'file changed on disk': 'ē”¬ē›˜äøŠēš„ę–‡ä»¶å·²ē»äæ®ę”¹', 'file does not exist': 'ę–‡ä»¶å¹¶äøå­˜åœØ', +'file not found': 'file not found', 'file saved on %(time)s': 'ę–‡ä»¶äæå­˜äŗŽ %(time)s', 'file saved on %s': 'ę–‡ä»¶äæå­˜åœØ %s', +'filename': 'filename', +'Filename': 'Filename', +'Files added': 'Files added', +'filter': 'filter', +'Find Next': 'Find Next', +'Find Previous': 'Find Previous', +'First name': '姓', +'Form has errors': 'Form has errors', +'Frames': 'Frames', +'Functions with no doctests will result in [passed] tests.': '', +'GAE Email': 'GAE Email', +'GAE Output': 'GAE Output', +'GAE Password': 'GAE Password', +'Generate': 'Generate', +'Get from URL:': 'Get from URL:', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', +'Globals##debug': 'Globals##debug', +'go!': 'go!', +'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google Application Id': 'Google Application Id', +'Goto': 'Goto', +'graph model': 'graph model', +'Graph Model': 'Graph Model', +'Group ID': '组ID', +'Hello World': 'é“åÆé“ļ¼Œéžåøøé“ļ¼›ååÆåļ¼Œéžåøøå', +'Help': '帮助', +'here': 'here', +'Hide/Show Translated strings': 'Hide/Show Translated strings', +'Highlight current line': 'Highlight current line', +'Hits': 'Hits', +'Home': 'Home', +'honored only if the expression evaluates to true': 'honored only if the expression evaluates to true', 'htmledit': 'html编辑', +'If start the downgrade, be patient, it may take a while to rollback': 'If start the downgrade, be patient, it may take a while to rollback', +'If start the upgrade, be patient, it may take a while to download': 'If start the upgrade, be patient, it may take a while to download', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\n\t\tA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', +'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', +'import': 'import', +'Import/Export': '导兄/导出', +'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': '包含', +'Indent with tabs': 'Indent with tabs', 'insert new': 'ę–°ę’å…„', 'insert new %s': 'ę–°ę’å…„ %s', +'inspect attributes': 'inspect attributes', +'Install': 'install', +'Installation of %(plugin)s for %(app)s': 'Installation of %(plugin)s for %(app)s', +'Installed applications': 'å·²å®‰č£…ēš„åŗ”ē”Ø', +'Interaction at %s line %s': 'Interaction at %s line %s', +'Interactive console': 'Interactive console', 'internal error': 'å†…éƒØé”™čÆÆ', +'internal error: %s': 'internal error: %s', +'Internal State': 'å†…éƒØēŠ¶ę€', +'Invalid action': 'ę— ę•ˆåŠØä½œ', +'Invalid application name': 'Invalid application name', +'invalid circular reference': 'invalid circular reference', +'Invalid email': 'ę— ę•ˆēš„email', +'Invalid git repository specified.': 'Invalid git repository specified.', 'invalid password': 'ę— ę•ˆåÆ†ē ', +'invalid password.': 'invalid password.', +'Invalid Query': 'ę— ę•ˆęŸ„čÆ¢', 'invalid request': 'ę— ę•ˆčÆ·ę±‚', +'Invalid request': 'Invalid request', +'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', 'invalid ticket': 'ę— ę•ˆä¼ ē„Ø', +'Key': 'Key', +'Keyboard shortcuts': 'Keyboard shortcuts', +'kill process': 'kill process', 'language file "%(filename)s" created/updated': '语言文件 "%(filename)s"č¢«åˆ›å»ŗ/ꛓꖰ', +'Language files (static strings) updated': '语言文件(é™ę€éƒØåˆ†)已曓新', 'languages': '语言', +'Languages': '语言', 'languages updated': 'čÆ­čØ€å·²č¢«åˆ·ę–°', +'Last name': '名', +'Last Revision': 'Last Revision', +'Last saved on:': 'ęœ€åŽäæå­˜äŗŽ:', +'License for': 'č®øåÆčÆ', +'License:': 'License:', +'Line Nr': 'Line Nr', +'Line number': 'Line number', +'lists by exception': 'lists by exception', +'lists by ticket': 'lists by ticket', +'Loading...': 'Loading...', 'loading...': '载兄中...', +'Local Apps': 'Local Apps', +'locals': 'locals', +'Locals##debug': 'Locals##debug', 'login': '登录', +'Login': '登录', +'Login successful': 'Login successful', +'Login to the Administrative Interface': 'ē™»å½•åˆ°ē®”ē†å‘˜ē•Œé¢', +'Login/Register': 'Login/Register', +'Logout': '注销', +'Lost Password': 'åæ˜č®°åÆ†ē ', +'lost password': 'lost password', +'Main Menu': 'Main Menu', +'Manage': 'Manage', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Admin Users/Students': 'Manage Admin Users/Students', +'Manage Cache': 'Manage Cache', +'Manage Students': 'Manage Students', +'Memberships': 'Memberships', 'merge': '合并', +'Models': 'ęØ”åž‹s', 'models': 'ęØ”åž‹s', +'Modified On': 'Modified On', +'Modules': 'ęØ”å—s', 'modules': 'ęØ”å—s', +'Multi User Mode': 'Multi User Mode', +'Name': '姓名', 'new application "%s" created': '新应用 "%s"å·²č¢«åˆ›å»ŗ', +'new application "%s" imported': 'new application "%s" imported', +'New Application Wizard': 'New Application Wizard', +'New application wizard': 'New application wizard', 'new plugin installed': 'new plugin installed', +'New plugin installed: %s': 'New plugin installed: %s', +'New Record': '新记录', 'new record inserted': 'ę–°č®°å½•č¢«ę’å…„', +'New simple application': 'New simple application', +'next': 'next', +'next %s rows': 'next %s rows', 'next 100 rows': '后100蔌', +'NO': 'äø', +'no changes': 'no changes', +'No databases in this application': 'čæ™åŗ”ē”Øę²”ęœ‰ę•°ę®åŗ“', +'No Interaction yet': 'No Interaction yet', 'no match': 'no match', +'no package selected': 'no package selected', +'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'Node:': 'Node:', +'Not Authorized': 'Not Authorized', +'Not supported': 'Not supported', +'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', +"On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", +'Open new app in new window': 'Open new app in new window', +'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'OpenShift Output': 'OpenShift Output', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Or Get from URL:', 'or import from csv file': 'ęˆ–č€…ļ¼Œä»Žcsv文件导兄', 'or provide app url:': 'or provide app url:', 'or provide application url:': 'ęˆ–č€…ļ¼Œęä¾›åŗ”ē”Øę‰€åœØåœ°å€é“¾ęŽ„:', +'Origin': '起源', +'Original/Translation': 'åŽŸå§‹ę–‡ä»¶/翻译文件', +'Overview': 'Overview', +'Overwrite installed app': 'overwrite installed app', +'Pack all': 'å…ØéƒØę‰“åŒ…', +'Pack compiled': 'åŒ…ē¼–čÆ‘å®Œ', +'Pack custom': 'Pack custom', 'pack plugin': 'pack plugin', +'PAM authenticated user, cannot change password here': 'PAM authenticated user, cannot change password here', +'Password': '密码', 'password changed': 'password changed', +'Past revisions': 'Past revisions', +'Path to appcfg.py': 'Path to appcfg.py', +'Path to local openshift repo root.': 'Path to local openshift repo root.', +'Peeking at file': '选个文件', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Please': 'Please', +'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', +'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin not specified': 'plugin not specified', +'Plugin page': 'Plugin page', +'plugins': 'plugins', +'Plugins': 'Plugins', +'Plural Form #%s': 'Plural Form #%s', +'Plural-Forms:': 'Plural-Forms:', +'Powered by': '', +'Preferences saved correctly': 'Preferences saved correctly', +'Preferences saved on session only': 'Preferences saved on session only', +'previous %s rows': 'previous %s rows', 'previous 100 rows': '前100蔌', +'Private files': 'Private files', +'private files': 'private files', +'Project Progress': 'Project Progress', +'Pull': 'Pull', +'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', +'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', +'Push': 'Push', +'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', +'pygraphviz library not found': 'pygraphviz library not found', +'PythonAnywhere Apps': 'PythonAnywhere Apps', +'PythonAnywhere Password': 'PythonAnywhere Password', +'Query:': '柄询', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'Rapid Search': 'Rapid Search', +'Record': 'Record', 'record': 'record', 'record does not exist': 'č®°å½•å¹¶äøå­˜åœØ', 'record id': '记录ID', +'Record ID': '记录ID', +'Record id': 'Record id', +'refresh': 'refresh', +'register': 'register', +'Register': 'ę³Øå†Œ', +'Registration key': 'ę³Øå†ŒåÆ†åŒ™', +'Reload routes': 'Reload routes', +'Remove compiled': '已移除', +'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', +'Replace': 'Replace', +'Replace All': 'Replace All', +'Repository (%s)': 'Repository (%s)', +'request': 'request', +'requires distutils, but not installed': 'requires distutils, but not installed', +'requires python-git, but not installed': 'requires python-git, but not installed', +'Resolve Conflict file': '解决冲突文件', +'response': 'response', +'restart': 'restart', 'restore': '重存', +'return': 'return', +'Revert': 'Revert', 'revert': 'ę¢å¤', +'reverted to revision %s': 'reverted to revision %s', +'Revision %s': 'Revision %s', +'Revision:': 'Revision:', +'Role': '角色', +'Roles': 'Roles', +'Rows in table': '蔨蔌', +'Rows in Table': 'Rows in Table', +'Rows selected': 'č”Œé€‰ę‹©', +'rules are not defined': 'rules are not defined', +'Run tests': 'Run tests', +'Run tests in this file': 'Run tests in this file', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'Running on %s', +'Save': 'Save', 'save': 'äæå­˜', +'Save file:': 'Save file:', +'Save file: %s': 'Save file: %s', +'Save model as...': 'Save model as...', +'Save via Ajax': 'Save via Ajax', +'Saved file hash:': 'å·²å­˜ę–‡ä»¶Hash:', +'Screenshot %s': 'Screenshot %s', +'Search': 'Search', +'Select Files to Package': 'Select Files to Package', 'selected': '已选', +'session': 'session', 'session expired': 'ä¼ščÆčæ‡ęœŸ', +'Session saved correctly': 'Session saved correctly', +'Session saved on session only': 'Session saved on session only', +'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': '', +'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Singular Form': 'Singular Form', +'Site': '总站', +'Size of cache:': 'Size of cache:', +'skip to generate': 'skip to generate', 'some files could not be removed': 'ęœ‰äŗ›ę–‡ä»¶ę— ę³•č¢«ē§»é™¤', +'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', +'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'source : db': 'source : db', +'source : filesystem': 'source : filesystem', +'Start a new app': 'Start a new app', +'Start searching': 'Start searching', +'Start wizard': 'start wizard', 'state': 'ēŠ¶ę€', +'Static': 'Static', 'static': 'é™ę€ę–‡ä»¶', +'Static files': 'é™ę€ę–‡ä»¶', +'Statistics': 'Statistics', +'Step': 'Step', +'step': 'step', +'stop': 'stop', 'submit': 'ęäŗ¤', +'Submit': 'Submit', +'successful': 'successful', +'Sure you want to delete this object?': 'ēœŸēš„č¦åˆ é™¤čæ™äøŖåÆ¹č±”?', +'switch to : db': 'switch to : db', +'switch to : filesystem': 'switch to : filesystem', +'Tab width (# characters)': 'Tab width (# characters)', 'table': '蔨', +'Table': 'Table', +'Table name': 'č”Øå', +'Temporary': 'Temporary', 'test': '测试', +'Testing application': '应用测试', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '', +'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', +'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'the application logic, each URL path is mapped in one exposed function in the controller': '应用逻辑:ęÆäøŖURLē”±ęŽ§åˆ¶å™Øęš“éœ²ēš„å‡½å¼å®Œęˆę˜ å°„', +'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'the data representation, define database tables and sets': 'ę•°ę®č”Øč¾¾å¼,å®šä¹‰ę•°ę®åŗ“/蔨', +'The data representation, define database tables and sets': 'The data representation, define database tables and sets', +'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', 'the presentations layer, views are also known as templates': 'ęäŗ¤å±‚/č§†å›¾éƒ½åœØęØ”ęæäø­åÆēŸ„', +'Theme': 'Theme', +'There are no controllers': 'å†‡ęŽ§åˆ¶å™Ø', +'There are no models': 'å†‡ęØ”åž‹s', +'There are no modules': 'å†‡ęØ”å—s', +'There are no plugins': 'There are no plugins', +'There are no private files': 'There are no private files', +'There are no static files': 'å†‡é™ę€ę–‡ä»¶', +'There are no translators': 'There are no translators', +'There are no translators, only default language is supported': 'ę²”ęœ‰ę‰¾åˆ°ē›øåŗ”ēæ»čÆ‘ļ¼ŒåŖčƒ½ä½æē”Øé»˜č®¤čÆ­čØ€', +'There are no views': '冇视图', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', +'These files are served without processing, your images go here': 'These files are served without processing, your images go here', 'these files are served without processing, your images go here': '', +"This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", +'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to downgrade you do it at your own risk', +'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk': 'This is an experimental feature and it needs more testing. If you decide to upgrade you do it at your own risk', +'This is the %(filename)s template': 'čæ™ę˜Æ %(filename)s ęØ”ęæ', +"This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.": "This page can commit your changes to an openshift app repo and push them to your cloud instance. This assumes that you've already created the application instance using the web2py skeleton and have that repo somewhere on a filesystem that this web2py instance can access. This functionality requires GitPython installed and on the python path of the runtime that web2py is operating in.", +'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.': 'This page can upload your application to the Google App Engine computing cloud. Mind that you must first create indexes locally and this is done by installing the Google appserver and running the app locally with it once, or there will be errors when selecting records. Attention: deployment may take long time, depending on the network speed. Attention: it will overwrite your app.yaml. DO NOT SUBMIT TWICE.', +'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', +'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', +'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', +'Ticket': '传焨', +'Ticket ID': 'Ticket ID', +'Ticket Missing': 'Ticket Missing', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Timestamp': 'ę—¶é—“ęˆ³', +'TM': '', 'to previous version.': 'to previous version.', +'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', +'To emulate a breakpoint programatically, write:': 'To emulate a breakpoint programatically, write:', +'to use the debugger!': 'to use the debugger!', 'to Ā previous version.': 'é€€å›žå‰äø€ē‰ˆęœ¬', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', +'Traceback': 'Traceback', 'translation strings for the application': 'åŗ”ē”Øēš„ēæ»čÆ‘å­—äø²', +'Translation strings for the application': 'Translation strings for the application', 'try': 'å°čÆ•', 'try something like': 'å°čÆ•', +'Try the mobile interface': 'Try the mobile interface', +'try view': 'try view', +'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', +'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', +'Unable to check for upgrades': 'ę— ę³•ę£€ęŸ„ę˜Æå¦éœ€č¦å‡ēŗ§', 'unable to create application "%s"': 'ę— ę³•åˆ›å»ŗåŗ”ē”Ø "%s"', 'unable to delete file "%(filename)s"': 'ę— ę³•åˆ é™¤ę–‡ä»¶ "%(filename)s"', 'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', +'Unable to determine the line number!': 'Unable to determine the line number!', +'Unable to download': '无法下载', +'Unable to download app': '无法下载应用', +'Unable to download app because:': 'Unable to download app because:', +'Unable to download because': 'Unable to download because', +'unable to download layout': 'unable to download layout', +'unable to download plugin: %s': 'unable to download plugin: %s', +'Unable to download the list of plugins': 'Unable to download the list of plugins', +'unable to install plugin "%s"': 'unable to install plugin "%s"', 'unable to parse csv file': 'ę— ę³•ē”Ÿęˆ cvs', 'unable to uninstall "%s"': 'ę— ę³•åøč½½ "%s"', 'unable to upgrade because "%s"': 'unable to upgrade because "%s"', 'uncheck all': 'åé€‰å…ØéƒØ', +'Uninstall': 'åøč½½', +'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'ꛓꖰ', 'update all languages': 'ę›“ę–°ę‰€ęœ‰čÆ­čØ€', +'Update:': 'ꛓꖰ:', +'Upgrade': 'Upgrade', +'upgrade now to %s': 'upgrade now to %s', 'upgrade web2py now': 'upgrade web2py now', +'upload': 'upload', +'Upload': 'Upload', +'Upload & install packed application': 'Upload & install packed application', +'Upload a package:': 'Upload a package:', +'Upload and install packed application': 'Upload and install packed application', 'upload application:': 'ęäŗ¤å·²ęœ‰ēš„åŗ”ē”Ø:', +'Upload existing application': 'äøŠä¼ å·²ęœ‰åŗ”ē”Ø', 'upload file:': 'ęäŗ¤ę–‡ä»¶:', 'upload plugin file:': 'upload plugin file:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) Ā for NOT to build more complex queries.': '', +'Use an url:': 'Use an url:', +'User': 'User', +'User ID': 'ē”Øęˆ·ID', +'Username': 'Username', +'Users': 'Users', +'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', +'Version': 'ē‰ˆęœ¬', +'Versioning': 'Versioning', 'versioning': 'ē‰ˆęœ¬', 'view': '视图', +'Views': '视图', 'views': '视图s', -'web2py Recent Tweets': 'twitteräøŠēš„web2pyčæ›å±•å®žę’­', +'Warning!': 'Warning!', +'WARNING:': 'WARNING:', +'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Web Framework': 'Web Framework', +'web2py Admin Password': 'web2py Admin Password', +'web2py apps to deploy': 'web2py apps to deploy', +'web2py Debugger': 'web2py Debugger', +'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2pyēŽ°åœØå·²ē»ę˜Æęœ€ę–°ēš„ē‰ˆęœ¬äŗ†', +'web2py online debugger': 'web2py online debugger', +'web2py Recent Tweets': 'twitteräøŠēš„web2pyčæ›å±•å®žę’­', +'web2py upgrade': 'web2py upgrade', 'web2py upgraded; please restart it': 'web2py upgraded; please restart it', +'Welcome to web2py': 'ę¬¢čæŽä½æē”Øweb2py', +'Working...': 'Working...', +'WSGI reference name': 'WSGI reference name', +'YES': '是', +'Yes': 'Yes', +'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', +'You can inspect variables using the console below': 'You can inspect variables using the console below', +'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', +'You need to set up and reach a': 'You need to set up and reach a', +'You only need these if you have already registered': 'You only need these if you have already registered', +'Your application will be blocked until you click an action button (next, step, continue, etc.)': 'Your application will be blocked until you click an action button (next, step, continue, etc.)', } diff --git a/applications/welcome/languages/.DS_Store b/applications/welcome/languages/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0c703e4471f572b9253907ba8dc1db9997cbcff9 GIT binary patch literal 6148 zcmeHKO-sZu5Pi`V7QF1yV}5}}e?eW>i--sP19nwJSlzO15pVtJzWGqLTCcLm1d=zI z%$p<+nlu2?yn8qSW&ozFf=P^#BJR}#ke(I z7+Y}Wit{~@U9mOx_tjpz+FN_rO`&5|p+G1Q3WNeXqX6%0Dbqv4XhVTeAQbpgK+cEE zDp)%9hI(|c(I)_LLbn#$@|F-zb}SuxLyoW{qY{mp{E3l_PJi~e(y=!*I+D)JJU;X1 z<0a|r^k)r6Dh;Cz1ww(L0{hmT$^C!9PiFLyKcqx26bJ?WnF49DxLwS-DZg8PY)|gm v%zDQvMdErfSZEJ^0@#srF@6M8kZ3}IKTzNUXyZ4r literal 0 HcmV?d00001 diff --git a/applications/welcome/languages/ca.py b/applications/welcome/languages/ca.py index 951894dc..ef6ec70f 100755 --- a/applications/welcome/languages/ca.py +++ b/applications/welcome/languages/ca.py @@ -35,8 +35,8 @@ 'Admin is disabled because insecure channel': 'Admin inhabilitat, el canal no Ć©s segur', 'Admin is disabled because unsecure channel': 'Admin inhabilitat, el canal no Ć©s segur', 'Administrative interface': 'InterfĆ­cie administrativa', -'Administrative Interface': 'InterfĆ­cie Administrativa', 'administrative interface': 'interfĆ­cie administrativa', +'Administrative Interface': 'InterfĆ­cie Administrativa', 'Administrator Password:': 'Contrasenya del Administrador:', 'Ajax Recipes': 'Receptes AJAX', 'An error occured, please %s the page': 'Hi ha hagut un error, per favor %s la pĆ gina', diff --git a/applications/welcome/languages/cs.py b/applications/welcome/languages/cs.py index 4a86a697..75add19f 100755 --- a/applications/welcome/languages/cs.py +++ b/applications/welcome/languages/cs.py @@ -31,8 +31,8 @@ 'Admin design page': 'Admin design page', 'Admin language': 'jazyk rozhranĆ­', 'Administrative interface': 'pro administrĆ”torskĆ© rozhranĆ­ klikněte sem', -'Administrative Interface': 'AdministrĆ”torskĆ© rozhranĆ­', 'administrative interface': 'rozhranĆ­ pro sprĆ”vu', +'Administrative Interface': 'AdministrĆ”torskĆ© rozhranĆ­', 'Administrator Password:': 'AdministrĆ”torskĆ© heslo:', 'Ajax Recipes': 'Recepty s ajaxem', 'An error occured, please %s the page': 'An error occured, please %s the page', diff --git a/applications/welcome/languages/default.py b/applications/welcome/languages/default.py deleted file mode 100755 index 5e6e8125..00000000 --- a/applications/welcome/languages/default.py +++ /dev/null @@ -1,122 +0,0 @@ -# coding: utf8 -{ -'!langcode!': 'en-us', -'!langname!': 'English (US)', -'%s %%(shop)': '%s %%(shop)', -'%s %%(shop[0])': '%s %%(shop[0])', -'%s %%{quark[0]}': '%s %%{quark[0]}', -'%s %%{shop[0]}': '%s %%{shop[0]}', -'%s %%{shop}': '%s %%{shop}', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', -'@markmin\x01**Hello World**': '**Hello World**', -'About': 'About', -'Access Control': 'Access Control', -'Administrative Interface': 'Administrative Interface', -'Ajax Recipes': 'Ajax Recipes', -'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', -'Buy this book': 'Buy this book', -'Cannot be empty': 'Cannot be empty', -'Check to delete': 'Check to delete', -'Client IP': 'Client IP', -'Community': 'Community', -'Components and Plugins': 'Components and Plugins', -'Controller': 'Controller', -'Copyright': 'Copyright', -'Created By': 'Created By', -'Created On': 'Created On', -'customize me!': 'customize me!', -'Database': 'Database', -'DB Model': 'DB Model', -'Demo': 'Demo', -'Deployment Recipes': 'Deployment Recipes', -'Description': 'Description', -'Documentation': 'Documentation', -"Don't know what to do?": "Don't know what to do?", -'Download': 'Download', -'E-mail': 'E-mail', -'Email and SMS': 'Email and SMS', -'enter an integer between %(min)g and %(max)g': 'enter an integer between %(min)g and %(max)g', -'enter date and time as %(format)s': 'enter date and time as %(format)s', -'Errors': 'Errors', -'FAQ': 'FAQ', -'First name': 'First name', -'Forms and Validators': 'Forms and Validators', -'Free Applications': 'Free Applications', -'Group %(group_id)s created': 'Group %(group_id)s created', -'Group ID': 'Group ID', -'Group uniquely assigned to user %(id)s': 'Group uniquely assigned to user %(id)s', -'Groups': 'Groups', -'Hello World': 'Hello World', -'Hello World ## comment': 'Hello World ', -'Hello World## comment': 'Hello World', -'Home': 'Home', -'How did you get here?': 'How did you get here?', -'Introduction': 'Introduction', -'Invalid email': 'Invalid email', -'Is Active': 'Is Active', -'Last name': 'Last name', -'Layout': 'Layout', -'Layout Plugins': 'Layout Plugins', -'Layouts': 'Layouts', -'Live Chat': 'Live Chat', -'Logged in': 'Logged in', -'Logged out': 'Logged out', -'Login': 'Login', -'Logout': 'Logout', -'Lost Password': 'Lost Password', -'Lost password?': 'Lost password?', -'Menu Model': 'Menu Model', -'Modified By': 'Modified By', -'Modified On': 'Modified On', -'My Sites': 'My Sites', -'Name': 'Name', -'Object or table name': 'Object or table name', -'Online examples': 'Online examples', -'Origin': 'Origin', -'Other Plugins': 'Other Plugins', -'Other Recipes': 'Other Recipes', -'Overview': 'Overview', -'Password': 'Password', -"Password fields don't match": "Password fields don't match", -'please input your password again': 'please input your password again', -'Plugins': 'Plugins', -'Powered by': 'Powered by', -'Preface': 'Preface', -'Profile': 'Profile', -'Python': 'Python', -'Quick Examples': 'Quick Examples', -'Recipes': 'Recipes', -'Record ID': 'Record ID', -'Register': 'Register', -'Registration identifier': 'Registration identifier', -'Registration key': 'Registration key', -'Registration successful': 'Registration successful', -'Remember me (for 30 days)': 'Remember me (for 30 days)', -'Reset Password key': 'Reset Password key', -'Role': 'Role', -'Semantic': 'Semantic', -'Services': 'Services', -'Stylesheet': 'Stylesheet', -'Support': 'Support', -'The Core': 'The Core', -'The output of the file is a dictionary that was rendered by the view %s': 'The output of the file is a dictionary that was rendered by the view %s', -'The Views': 'The Views', -'This App': 'This App', -'Timestamp': 'Timestamp', -'Twitter': 'Twitter', -'User %(id)s Logged-in': 'User %(id)s Logged-in', -'User %(id)s Logged-out': 'User %(id)s Logged-out', -'User %(id)s Registered': 'User %(id)s Registered', -'User ID': 'User ID', -'value already in database or empty': 'value already in database or empty', -'Verify Password': 'Verify Password', -'Videos': 'Videos', -'View': 'View', -'Welcome': 'Welcome', -'Welcome to web2py!': 'Welcome to web2py!', -'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', -'You are successfully running web2py': 'You are successfully running web2py', -'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', -'You visited the url %s': 'You visited the url %s', -} diff --git a/applications/welcome/languages/en.py b/applications/welcome/languages/en.py new file mode 100755 index 00000000..cb50edaa --- /dev/null +++ b/applications/welcome/languages/en.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +{ +'!langcode!': 'en-us', +'!langname!': 'English (US)', +'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', +'%s %%{row} deleted': '%s %%{row} deleted', +'%s %%{row} updated': '%s %%{row} updated', +'%s selected': '%s selected', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'(**%.0d MB**)': '(**%.0d MB**)', +'**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}', +'**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}', +'**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'?': '?', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'About': 'About', +'Access Control': 'Access Control', +'admin': 'admin', +'Ajax Recipes': 'Ajax Recipes', +'An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', +'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', +'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', +'Available Databases and Tables': 'Available Databases and Tables', +"Buy web2py's book": "Buy web2py's book", +'cache': 'cache', +'Cache': 'Cache', +'Cache Cleared': 'Cache Cleared', +'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Cache Keys': 'Cache Keys', +'Cannot be empty': 'Cannot be empty', +'Check to delete': 'Check to delete', +'Clear CACHE?': 'Clear CACHE?', +'Clear DISK': 'Clear DISK', +'Clear RAM': 'Clear RAM', +'Community': 'Community', +'Components and Plugins': 'Components and Plugins', +'Config.ini': 'Config.ini', +'Controller': 'Controller', +'Copyright': 'Copyright', +'Current request': 'Current request', +'Current response': 'Current response', +'Current session': 'Current session', +'data uploaded': 'data uploaded', +'Database': 'Database', +'Database %s select': 'Database %s select', +'Database Administration (appadmin)': 'Database Administration (appadmin)', +'db': 'db', +'DB Model': 'DB Model', +'Delete:': 'Delete:', +'Demo': 'Demo', +'Deployment Recipes': 'Deployment Recipes', +'design': 'design', +'Design': 'Design', +'DISK': 'DISK', +'Disk Cache Keys': 'Disk Cache Keys', +'Disk Cleared': 'Disk Cleared', +'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Documentation': 'Documentation', +"Don't know what to do?": "Don't know what to do?", +'done!': 'done!', +'Download': 'Download', +'Edit current record': 'Edit current record', +'Email and SMS': 'Email and SMS', +'Errors': 'Errors', +'export as csv file': 'export as csv file', +'FAQ': 'FAQ', +'Forms and Validators': 'Forms and Validators', +'Free Applications': 'Free Applications', +'Graph Model': 'Graph Model', +'Groups': 'Groups', +'Hello World': 'Hello World', +'Helping web2py': 'Helping web2py', +'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})', +'Home': 'Home', +'How did you get here?': 'How did you get here?', +'import': 'import', +'Import/Export': 'Import/Export', +'Internal State': 'Internal State', +'Introduction': 'Introduction', +'Invalid Query': 'Invalid Query', +'invalid request': 'invalid request', +'Key': 'Key', +'Layout': 'Layout', +'Live Chat': 'Live Chat', +'Log In': 'Log In', +'Lost Password': 'Lost Password', +'Manage %(action)s': 'Manage %(action)s', +'Manage Access Control': 'Manage Access Control', +'Manage Cache': 'Manage Cache', +'Memberships': 'Memberships', +'Menu Model': 'Menu Model', +'My Sites': 'My Sites', +'New Record': 'New Record', +'new record inserted': 'new record inserted', +'next %s rows': 'next %s rows', +'No databases in this application': 'No databases in this application', +'Number of entries: **%s**': 'Number of entries: **%s**', +'Online book': 'Online book', +'Online examples': 'Online examples', +'or import from csv file': 'or import from csv file', +'Other Recipes': 'Other Recipes', +'Overview': 'Overview', +'Permission': 'Permission', +'Permissions': 'Permissions', +'Plugins': 'Plugins', +'Powered by': 'Powered by', +'Preface': 'Preface', +'previous %s rows': 'previous %s rows', +'pygraphviz library not found': 'pygraphviz library not found', +'Python': 'Python', +'Query:': 'Query:', +'Quick Examples': 'Quick Examples', +'RAM': 'RAM', +'RAM Cache Keys': 'RAM Cache Keys', +'Ram Cleared': 'Ram Cleared', +'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.', +'Recipes': 'Recipes', +'Record': 'Record', +'record does not exist': 'record does not exist', +'Record id': 'Record id', +'Role': 'Role', +'Roles': 'Roles', +'Rows in Table': 'Rows in Table', +'Rows selected': 'Rows selected', +'Save model as...': 'Save model as...', +'Services': 'Services', +'Sign Up': 'Sign Up', +'Size of cache:': 'Size of cache:', +'state': 'state', +'Statistics': 'Statistics', +'Stylesheet': 'Stylesheet', +'submit': 'submit', +'Support': 'Support', +'Table': 'Table', +'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', +'The Core': 'The Core', +'The output of the file is a dictionary that was rendered by the view %s': 'The output of the file is a dictionary that was rendered by the view %s', +'The Views': 'The Views', +'This App': 'This App', +'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', +'Traceback': 'Traceback', +'Twitter': 'Twitter', +'unable to parse csv file': 'unable to parse csv file', +'Update:': 'Update:', +'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', +'User': 'User', +'Users': 'Users', +'Videos': 'Videos', +'View': 'View', +'Welcome to web2py!': 'Welcome to web2py!', +'Which called the function %s located in the file %s': 'Which called the function %s located in the file %s', +'Working...': 'Working...', +'You are successfully running web2py': 'You are successfully running web2py', +'You can modify this application and adapt it to your needs': 'You can modify this application and adapt it to your needs', +'You visited the url %s': 'You visited the url %s', +} diff --git a/applications/welcome/languages/plural-cs.py b/applications/welcome/languages/plural-cs.py index 2a795587..d418a664 100755 --- a/applications/welcome/languages/plural-cs.py +++ b/applications/welcome/languages/plural-cs.py @@ -1,21 +1,19 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -{ -# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], -'vteřina': ['vteřiny', 'vteřin'], -'vteřinou': ['vteřinami', 'vteřinami'], -'minuta': ['minuty', 'minut'], -'minutou': ['minutami', 'minutami'], -'hodina': ['hodiny','hodin'], -'hodinou': ['hodinami','hodinami'], -'den': ['dny','dnÅÆ'], -'dnem': ['dny','dny'], -'týden': ['týdny','týdnÅÆ'], -'týdnem': ['týdny','týdny'], -'měsĆ­c': ['měsĆ­ce','měsĆ­cÅÆ'], -'měsĆ­cem': ['měsĆ­ci','měsĆ­ci'], -'rok': ['roky','let'], -'rokem': ['roky','lety'], -'zĆ”znam': ['zĆ”znamy', 'zĆ”znamÅÆ'], -'soubor': ['soubory', 'souborÅÆ'] -} +# -*- coding: utf-8 -*- +{ +'den': "['dny', 'dn\\xc5\\xaf']", +'dnem': "['dny', 'dny']", +'hodina': "['hodiny', 'hodin']", +'hodinou': "['hodinami', 'hodinami']", +'minuta': "['minuty', 'minut']", +'minutou': "['minutami', 'minutami']", +'měsĆ­c': "['m\\xc4\\x9bs\\xc3\\xadce', 'm\\xc4\\x9bs\\xc3\\xadc\\xc5\\xaf']", +'měsĆ­cem': "['m\\xc4\\x9bs\\xc3\\xadci', 'm\\xc4\\x9bs\\xc3\\xadci']", +'rok': "['roky', 'let']", +'rokem': "['roky', 'lety']", +'soubor': "['soubory', 'soubor\\xc5\\xaf']", +'týden': "['t\\xc3\\xbddny', 't\\xc3\\xbddn\\xc5\\xaf']", +'týdnem': "['t\\xc3\\xbddny', 't\\xc3\\xbddny']", +'vteřina': "['vte\\xc5\\x99iny', 'vte\\xc5\\x99in']", +'vteřinou': "['vte\\xc5\\x99inami', 'vte\\xc5\\x99inami']", +'zĆ”znam': "['z\\xc3\\xa1znamy', 'z\\xc3\\xa1znam\\xc5\\xaf']", +} diff --git a/applications/welcome/languages/plural-en.py b/applications/welcome/languages/plural-en.py index 5103b462..fb41dc7e 100755 --- a/applications/welcome/languages/plural-en.py +++ b/applications/welcome/languages/plural-en.py @@ -1,16 +1,14 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- { -# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], -'account': ['accounts'], -'book': ['books'], -'is': ['are'], -'man': ['men'], -'miss': ['misses'], -'person': ['people'], -'quark': ['quarks'], -'shop': ['shops'], -'this': ['these'], -'was': ['were'], -'woman': ['women'], +'account': "['accounts']", +'book': "['books']", +'is': "['are']", +'man': "['men']", +'miss': "['misses']", +'person': "['people']", +'quark': "['quarks']", +'shop': "['shops']", +'this': "['these']", +'was': "['were']", +'woman': "['women']", } diff --git a/applications/welcome/languages/plural-es.py b/applications/welcome/languages/plural-es.py index 235f1a3a..72ac5f7b 100755 --- a/applications/welcome/languages/plural-es.py +++ b/applications/welcome/languages/plural-es.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- { -# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], -'fila': ['filas'], -'eliminada': ['eliminadas'], -'actualizada': ['actualizadas'], -'seleccionado': ['seleccionados'], +'actualizada': "['actualizadas']", +'eliminada': "['eliminadas']", +'fila': "['filas']", +'seleccionado': "['seleccionados']", } From 2ec0c0b5352dd4668b7a2b162562d47f9a9cedcd Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 9 Sep 2016 15:57:10 +0200 Subject: [PATCH 096/436] fix py3.5 xmlescape with bytes --- gluon/html.py | 6 +++--- gluon/tests/test_html.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gluon/html.py b/gluon/html.py index da8e2d6e..ceef7bd2 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -20,7 +20,7 @@ import urllib import base64 from gluon import sanitizer, decoder import itertools -from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool +from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type from gluon.utils import local_html_escape import marshal @@ -122,7 +122,7 @@ def xmlescape(data, quote=True): if hasattr(data, 'xml') and callable(data.xml): return to_bytes(data.xml()) - if not(isinstance(data, basestring)): + if not(isinstance(data, (text_type, bytes))): # i.e., integers data=str(data) data = to_bytes(data, 'utf8', 'xmlcharrefreplace') @@ -1040,7 +1040,7 @@ class DIV(XmlComponent): hello >>> a.elements('a[u:v=$]')[0].xml() 'hello' - >>> a=FORM( INPUT(_type='text'), SELECT(range(1)), TEXTAREA() ) + >>> a=FORM( INPUT(_type='text'), SELECT(list(range(1))), TEXTAREA() ) >>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled' >>> a.xml() '
      ' diff --git a/gluon/tests/test_html.py b/gluon/tests/test_html.py index d7a75517..e091d1ce 100644 --- a/gluon/tests/test_html.py +++ b/gluon/tests/test_html.py @@ -253,6 +253,7 @@ class TestBareHelpers(unittest.TestCase): # test .get('attrib') self.assertEqual(DIV('

      Test

      ', _class="class_test").get('_class'), 'class_test') + self.assertEqual(DIV(b'a').xml(), b'
      a
      ') def test_CAT(self): # Empty CAT() From 1623328678ce84b11998f7e2053bec1e83e771bc Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 9 Sep 2016 21:13:27 +0200 Subject: [PATCH 097/436] fix missing to_unicode import, close #1442 --- gluon/sqlhtml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 9ddc34d6..03100e43 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -19,7 +19,7 @@ import urllib import re import os -from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native +from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode from gluon.http import HTTP, redirect from gluon.html import XmlComponent, truncate_string from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG From 9263f8c4c3be7dd3ac0d1e9948c23b52f10db0d0 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Sat, 10 Sep 2016 22:06:14 -0700 Subject: [PATCH 098/436] Fixed test errors Cleaned the damage caused by a faulty script run that created a test error as described in https://travis-ci.org/web2py/web2py/builds/158680015 --- applications/welcome/languages/plural-cs.py | 32 ++++++++++----------- applications/welcome/languages/plural-en.py | 22 +++++++------- applications/welcome/languages/plural-es.py | 8 +++--- applications/welcome/languages/plural-ru.py | 26 ++++++++--------- applications/welcome/languages/plural-uk.py | 28 +++++++++--------- 5 files changed, 56 insertions(+), 60 deletions(-) diff --git a/applications/welcome/languages/plural-cs.py b/applications/welcome/languages/plural-cs.py index d418a664..b994d196 100755 --- a/applications/welcome/languages/plural-cs.py +++ b/applications/welcome/languages/plural-cs.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- { -'den': "['dny', 'dn\\xc5\\xaf']", -'dnem': "['dny', 'dny']", -'hodina': "['hodiny', 'hodin']", -'hodinou': "['hodinami', 'hodinami']", -'minuta': "['minuty', 'minut']", -'minutou': "['minutami', 'minutami']", -'měsĆ­c': "['m\\xc4\\x9bs\\xc3\\xadce', 'm\\xc4\\x9bs\\xc3\\xadc\\xc5\\xaf']", -'měsĆ­cem': "['m\\xc4\\x9bs\\xc3\\xadci', 'm\\xc4\\x9bs\\xc3\\xadci']", -'rok': "['roky', 'let']", -'rokem': "['roky', 'lety']", -'soubor': "['soubory', 'soubor\\xc5\\xaf']", -'týden': "['t\\xc3\\xbddny', 't\\xc3\\xbddn\\xc5\\xaf']", -'týdnem': "['t\\xc3\\xbddny', 't\\xc3\\xbddny']", -'vteřina': "['vte\\xc5\\x99iny', 'vte\\xc5\\x99in']", -'vteřinou': "['vte\\xc5\\x99inami', 'vte\\xc5\\x99inami']", -'zĆ”znam': "['z\\xc3\\xa1znamy', 'z\\xc3\\xa1znam\\xc5\\xaf']", +'den': ['dny', 'dn\\xc5\\xaf'], +'dnem': ['dny', 'dny'], +'hodina': ['hodiny', 'hodin'], +'hodinou': ['hodinami', 'hodinami'], +'minuta': ['minuty', 'minut'], +'minutou': ['minutami', 'minutami'], +'měsĆ­c': ['m\\xc4\\x9bs\\xc3\\xadce', 'm\\xc4\\x9bs\\xc3\\xadc\\xc5\\xaf'], +'měsĆ­cem': ['m\\xc4\\x9bs\\xc3\\xadci', 'm\\xc4\\x9bs\\xc3\\xadci'], +'rok': ['roky', 'let'], +'rokem': ['roky', 'lety'], +'soubor': ['soubory', 'soubor\\xc5\\xaf'], +'týden': ['t\\xc3\\xbddny', 't\\xc3\\xbddn\\xc5\\xaf'], +'týdnem': ['t\\xc3\\xbddny', 't\\xc3\\xbddny'], +'vteřina': ['vte\\xc5\\x99iny', 'vte\\xc5\\x99in'], +'vteřinou': ['vte\\xc5\\x99inami', 'vte\\xc5\\x99inami'], +'zĆ”znam': ['z\\xc3\\xa1znamy', 'z\\xc3\\xa1znam\\xc5\\xaf'], } diff --git a/applications/welcome/languages/plural-en.py b/applications/welcome/languages/plural-en.py index fb41dc7e..cbd345bf 100755 --- a/applications/welcome/languages/plural-en.py +++ b/applications/welcome/languages/plural-en.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- { -'account': "['accounts']", -'book': "['books']", -'is': "['are']", -'man': "['men']", -'miss': "['misses']", -'person': "['people']", -'quark': "['quarks']", -'shop': "['shops']", -'this': "['these']", -'was': "['were']", -'woman': "['women']", +'account': ['accounts'], +'book': ['books'], +'is': ['are'], +'man': ['men'], +'miss': ['misses'], +'person': ['people'], +'quark': ['quarks'], +'shop': ['shops'], +'this': ['these'], +'was': ['were'], +'woman': ['women'], } diff --git a/applications/welcome/languages/plural-es.py b/applications/welcome/languages/plural-es.py index 72ac5f7b..a8264d5c 100755 --- a/applications/welcome/languages/plural-es.py +++ b/applications/welcome/languages/plural-es.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { -'actualizada': "['actualizadas']", -'eliminada': "['eliminadas']", -'fila': "['filas']", -'seleccionado': "['seleccionados']", +'actualizada': ['actualizadas'], +'eliminada': ['eliminadas'], +'fila': ['filas'], +'seleccionado': ['seleccionados'], } diff --git a/applications/welcome/languages/plural-ru.py b/applications/welcome/languages/plural-ru.py index d9e73a07..29699b8c 100755 --- a/applications/welcome/languages/plural-ru.py +++ b/applications/welcome/languages/plural-ru.py @@ -1,17 +1,15 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- { -# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], -'выбрана': ['выбраны','выбрано'], -'запись': ['записи','записей'], -'изменена': ['изменены','изменено'], -'строка': ['строки','строк'], -'уГалена': ['ŃƒŠ“Š°Š»ŠµŠ½Ń‹','уГалено'], -'Гень': ['Š“Š½Ń', 'Гней'], -'Š¼ŠµŃŃŃ†': ['Š¼ŠµŃŃŃ†Š°','Š¼ŠµŃŃŃ†ŠµŠ²'], -'Š½ŠµŠ“ŠµŠ»ŃŽ': ['неГели','неГель'], -'гоГ': ['гоГа','лет'], -'час': ['часа','часов'], -'Š¼ŠøŠ½ŃƒŃ‚Ńƒ': ['Š¼ŠøŠ½ŃƒŃ‚Ń‹','Š¼ŠøŠ½ŃƒŃ‚'], -'секунГу': ['ŃŠµŠŗŃƒŠ½Š“Ń‹','секунГ'], +'выбрана': ['\\xd0\\xb2\\xd1\\x8b\\xd0\\xb1\\xd1\\x80\\xd0\\xb0\\xd0\\xbd\\xd1\\x8b', '\\xd0\\xb2\\xd1\\x8b\\xd0\\xb1\\xd1\\x80\\xd0\\xb0\\xd0\\xbd\\xd0\\xbe'], +'гоГ': ['\\xd0\\xb3\\xd0\\xbe\\xd0\\xb4\\xd0\\xb0', '\\xd0\\xbb\\xd0\\xb5\\xd1\\x82'], +'Гень': ['\\xd0\\xb4\\xd0\\xbd\\xd1\\x8f', '\\xd0\\xb4\\xd0\\xbd\\xd0\\xb5\\xd0\\xb9'], +'запись': ['\\xd0\\xb7\\xd0\\xb0\\xd0\\xbf\\xd0\\xb8\\xd1\\x81\\xd0\\xb8', '\\xd0\\xb7\\xd0\\xb0\\xd0\\xbf\\xd0\\xb8\\xd1\\x81\\xd0\\xb5\\xd0\\xb9'], +'изменена': ['\\xd0\\xb8\\xd0\\xb7\\xd0\\xbc\\xd0\\xb5\\xd0\\xbd\\xd0\\xb5\\xd0\\xbd\\xd1\\x8b', '\\xd0\\xb8\\xd0\\xb7\\xd0\\xbc\\xd0\\xb5\\xd0\\xbd\\xd0\\xb5\\xd0\\xbd\\xd0\\xbe'], +'Š¼ŠµŃŃŃ†': ['\\xd0\\xbc\\xd0\\xb5\\xd1\\x81\\xd1\\x8f\\xd1\\x86\\xd0\\xb0', '\\xd0\\xbc\\xd0\\xb5\\xd1\\x81\\xd1\\x8f\\xd1\\x86\\xd0\\xb5\\xd0\\xb2'], +'Š¼ŠøŠ½ŃƒŃ‚Ńƒ': ['\\xd0\\xbc\\xd0\\xb8\\xd0\\xbd\\xd1\\x83\\xd1\\x82\\xd1\\x8b', '\\xd0\\xbc\\xd0\\xb8\\xd0\\xbd\\xd1\\x83\\xd1\\x82'], +'Š½ŠµŠ“ŠµŠ»ŃŽ': ['\\xd0\\xbd\\xd0\\xb5\\xd0\\xb4\\xd0\\xb5\\xd0\\xbb\\xd0\\xb8', '\\xd0\\xbd\\xd0\\xb5\\xd0\\xb4\\xd0\\xb5\\xd0\\xbb\\xd1\\x8c'], +'секунГу': ['\\xd1\\x81\\xd0\\xb5\\xd0\\xba\\xd1\\x83\\xd0\\xbd\\xd0\\xb4\\xd1\\x8b', '\\xd1\\x81\\xd0\\xb5\\xd0\\xba\\xd1\\x83\\xd0\\xbd\\xd0\\xb4'], +'строка': ['\\xd1\\x81\\xd1\\x82\\xd1\\x80\\xd0\\xbe\\xd0\\xba\\xd0\\xb8', '\\xd1\\x81\\xd1\\x82\\xd1\\x80\\xd0\\xbe\\xd0\\xba'], +'уГалена': ['\\xd1\\x83\\xd0\\xb4\\xd0\\xb0\\xd0\\xbb\\xd0\\xb5\\xd0\\xbd\\xd1\\x8b', '\\xd1\\x83\\xd0\\xb4\\xd0\\xb0\\xd0\\xbb\\xd0\\xb5\\xd0\\xbd\\xd0\\xbe'], +'час': ['\\xd1\\x87\\xd0\\xb0\\xd1\\x81\\xd0\\xb0', '\\xd1\\x87\\xd0\\xb0\\xd1\\x81\\xd0\\xbe\\xd0\\xb2'], } diff --git a/applications/welcome/languages/plural-uk.py b/applications/welcome/languages/plural-uk.py index e75ef121..e6b91466 100755 --- a/applications/welcome/languages/plural-uk.py +++ b/applications/welcome/languages/plural-uk.py @@ -1,18 +1,16 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- { -# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...], -'байт': ['байти','байтів'], -'гоГину': ['гоГини','гоГин'], -'Гень': ['Гні','Гнів'], -'елемент': ['елементи','елементів'], -'запис': ['записи','записів'], -'Š¼Ń–ŃŃŃ†ŃŒ': ['Š¼Ń–ŃŃŃ†Ń–','Š¼Ń–ŃŃŃ†Ń–Š²'], -'ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½Š½Ń': ['ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½Š½Ń','ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½ŃŒ'], -'Ń€ŃŠ“Š¾Šŗ': ['Ń€ŃŠ“ŠŗŠø','Ń€ŃŠ“ŠŗŃ–Š²'], -'рік': ['роки','років'], -'секунГу': ['секунГи','секунГ'], -'ŃŃ…ŠøŠ±Š½ŠµŠ½Š½Ń': ['ŃŃ…ŠøŠ±Š½ŠµŠ½Š½Ń','ŃŃ…ŠøŠ±Š½ŠµŠ½ŃŒ'], -'Ń‚ŠøŠ¶Š“ŠµŠ½ŃŒ': ['тижні','тижнів'], -'Ń…Š²ŠøŠ»ŠøŠ½Ńƒ': ['хвилини','хвилин'], +'байт': ['\\xd0\\xb1\\xd0\\xb0\\xd0\\xb9\\xd1\\x82\\xd0\\xb8', '\\xd0\\xb1\\xd0\\xb0\\xd0\\xb9\\xd1\\x82\\xd1\\x96\\xd0\\xb2'], +'гоГину': ['\\xd0\\xb3\\xd0\\xbe\\xd0\\xb4\\xd0\\xb8\\xd0\\xbd\\xd0\\xb8', '\\xd0\\xb3\\xd0\\xbe\\xd0\\xb4\\xd0\\xb8\\xd0\\xbd'], +'Гень': ['\\xd0\\xb4\\xd0\\xbd\\xd1\\x96', '\\xd0\\xb4\\xd0\\xbd\\xd1\\x96\\xd0\\xb2'], +'елемент': ['\\xd0\\xb5\\xd0\\xbb\\xd0\\xb5\\xd0\\xbc\\xd0\\xb5\\xd0\\xbd\\xd1\\x82\\xd0\\xb8', '\\xd0\\xb5\\xd0\\xbb\\xd0\\xb5\\xd0\\xbc\\xd0\\xb5\\xd0\\xbd\\xd1\\x82\\xd1\\x96\\xd0\\xb2'], +'запис': ['\\xd0\\xb7\\xd0\\xb0\\xd0\\xbf\\xd0\\xb8\\xd1\\x81\\xd0\\xb8', '\\xd0\\xb7\\xd0\\xb0\\xd0\\xbf\\xd0\\xb8\\xd1\\x81\\xd1\\x96\\xd0\\xb2'], +'Š¼Ń–ŃŃŃ†ŃŒ': ['\\xd0\\xbc\\xd1\\x96\\xd1\\x81\\xd1\\x8f\\xd1\\x86\\xd1\\x96', '\\xd0\\xbc\\xd1\\x96\\xd1\\x81\\xd1\\x8f\\xd1\\x86\\xd1\\x96\\xd0\\xb2'], +'ŠæŠ¾Ń†Ń–Š»ŃŽŠ²Š°Š½Š½Ń': ['\\xd0\\xbf\\xd0\\xbe\\xd1\\x86\\xd1\\x96\\xd0\\xbb\\xd1\\x8e\\xd0\\xb2\\xd0\\xb0\\xd0\\xbd\\xd0\\xbd\\xd1\\x8f', '\\xd0\\xbf\\xd0\\xbe\\xd1\\x86\\xd1\\x96\\xd0\\xbb\\xd1\\x8e\\xd0\\xb2\\xd0\\xb0\\xd0\\xbd\\xd1\\x8c'], +'Ń€ŃŠ“Š¾Šŗ': ['\\xd1\\x80\\xd1\\x8f\\xd0\\xb4\\xd0\\xba\\xd0\\xb8', '\\xd1\\x80\\xd1\\x8f\\xd0\\xb4\\xd0\\xba\\xd1\\x96\\xd0\\xb2'], +'рік': ['\\xd1\\x80\\xd0\\xbe\\xd0\\xba\\xd0\\xb8', '\\xd1\\x80\\xd0\\xbe\\xd0\\xba\\xd1\\x96\\xd0\\xb2'], +'секунГу': ['\\xd1\\x81\\xd0\\xb5\\xd0\\xba\\xd1\\x83\\xd0\\xbd\\xd0\\xb4\\xd0\\xb8', '\\xd1\\x81\\xd0\\xb5\\xd0\\xba\\xd1\\x83\\xd0\\xbd\\xd0\\xb4'], +'ŃŃ…ŠøŠ±Š½ŠµŠ½Š½Ń': ['\\xd1\\x81\\xd1\\x85\\xd0\\xb8\\xd0\\xb1\\xd0\\xbd\\xd0\\xb5\\xd0\\xbd\\xd0\\xbd\\xd1\\x8f', '\\xd1\\x81\\xd1\\x85\\xd0\\xb8\\xd0\\xb1\\xd0\\xbd\\xd0\\xb5\\xd0\\xbd\\xd1\\x8c'], +'Ń‚ŠøŠ¶Š“ŠµŠ½ŃŒ': ['\\xd1\\x82\\xd0\\xb8\\xd0\\xb6\\xd0\\xbd\\xd1\\x96', '\\xd1\\x82\\xd0\\xb8\\xd0\\xb6\\xd0\\xbd\\xd1\\x96\\xd0\\xb2'], +'Ń…Š²ŠøŠ»ŠøŠ½Ńƒ': ['\\xd1\\x85\\xd0\\xb2\\xd0\\xb8\\xd0\\xbb\\xd0\\xb8\\xd0\\xbd\\xd0\\xb8', '\\xd1\\x85\\xd0\\xb2\\xd0\\xb8\\xd0\\xbb\\xd0\\xb8\\xd0\\xbd'], } From 808c85fdc395f48731739f6a79c852606865a16b Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Sat, 10 Sep 2016 22:12:48 -0700 Subject: [PATCH 099/436] Update welcome/es.py, Spanish complete! --- applications/welcome/languages/es.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/welcome/languages/es.py b/applications/welcome/languages/es.py index b8c12cf1..490bf470 100755 --- a/applications/welcome/languages/es.py +++ b/applications/welcome/languages/es.py @@ -18,7 +18,7 @@ '?': '?', '@markmin\x01An error occured, please [[reload %s]] the page': 'Ha ocurrido un error, por favor [[recargar %s]] la pĆ”gina', '@markmin\x01Number of entries: **%s**': 'NĆŗmero de entradas: **%s**', -'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)', +'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**no disponible**``:red (Necesita libreria de Python: [[guppy http://pypi.python.org/pypi/guppy/ popup]])', 'A new version of web2py is available': 'Hay una nueva versión de web2py disponible', 'A new version of web2py is available: %s': 'Hay una nueva versión de web2py disponible: %s', 'About': 'Acerca de', @@ -27,7 +27,7 @@ 'Access Control': 'Control de Acceso', 'Add': 'AƱadir', 'additional code for your application': 'código adicional para su aplicación', -'admin': 'admin', +'admin': 'administrar', 'admin disabled because no admin password': 'admin deshabilitado por falta de contraseƱa', 'admin disabled because not supported on google app engine': 'admin deshabilitado, no es soportado en GAE', 'admin disabled because unable to access password file': 'admin deshabilitado, imposible acceder al archivo con la contraseƱa', @@ -170,7 +170,7 @@ 'exposes': 'expone', 'extends': 'extiende', 'failed to reload module': 'la recarga del módulo ha fallado', -'FAQ': 'FAQ', +'FAQ': 'Preguntas frecuentes', 'file "%(filename)s" created': 'archivo "%(filename)s" creado', 'file "%(filename)s" deleted': 'archivo "%(filename)s" eliminado', 'file "%(filename)s" uploaded': 'archivo "%(filename)s" subido', @@ -234,7 +234,7 @@ 'License for': 'Licencia para', 'Live Chat': 'Chat en vivo', 'loading...': 'cargando...', -'Log In': 'Log In', +'Log In': 'Iniciar sesion', 'Logged in': 'Sesión iniciada', 'Logged out': 'Sesión finalizada', 'Login': 'Inicio de sesión', From c0742190f05a26f5ca922da3d12cbe6e8e170583 Mon Sep 17 00:00:00 2001 From: abastardi Date: Mon, 12 Sep 2016 09:55:33 -0400 Subject: [PATCH 100/436] Avoid folder creation race condition --- gluon/admin.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/gluon/admin.py b/gluon/admin.py index 90b50c27..9615956f 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -435,13 +435,20 @@ def add_path_first(path): if not global_settings.web2py_runtime_gae: site.addsitedir(path) +def try_mkdir(path): + if not os.path.exists(path): + try: + os.mkdir(path) + except OSError as e: + if e.strerror == 'File exists': # In case of race condition. + pass + else: + raise e def create_missing_folders(): if not global_settings.web2py_runtime_gae: for path in ('applications', 'deposit', 'site-packages', 'logs'): - path = abspath(path, gluon=True) - if not os.path.exists(path): - os.mkdir(path) + try_mkdir(abspath(path, gluon=True)) """ OLD sys.path dance paths = (global_settings.gluon_parent, abspath( @@ -458,7 +465,5 @@ def create_missing_app_folders(request): for subfolder in ('models', 'views', 'controllers', 'databases', 'modules', 'cron', 'errors', 'sessions', 'languages', 'static', 'private', 'uploads'): - path = os.path.join(request.folder, subfolder) - if not os.path.exists(path): - os.mkdir(path) + try_mkdir(os.path.join(request.folder, subfolder)) global_settings.app_folders.add(request.folder) From 897e2ab95da7cd914cbec558d7adfa745590ca36 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 16 Sep 2016 16:28:20 +0200 Subject: [PATCH 101/436] caching read_pyc for controllers and models --- gluon/compileapp.py | 5 +++-- gluon/restricted.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 0ee9c18c..70f924b1 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -581,7 +581,7 @@ def run_models_in(environment): if not regex.search(fname) and c != 'appadmin': continue elif compiled: - code = read_pyc(model) + code = getcfs(model, model, lambda: read_pyc(model)) elif is_gae: code = getcfs(model, model, lambda: compile2(read_file(model), model)) @@ -614,7 +614,8 @@ def run_controller_in(controller, function, environment): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) - restricted(read_pyc(filename), environment, layer=filename) + code = getcfs(filename, filename, lambda: read_pyc(filename)) + restricted(code, environment, layer=filename) elif function == '_TEST': # TESTING: adjust the path to include site packages from settings import global_settings diff --git a/gluon/restricted.py b/gluon/restricted.py index c67bcbf4..c0725a15 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -199,7 +199,7 @@ class RestrictedError(Exception): def compile2(code, layer): - return compile(code.rstrip(), layer, 'exec') + return compile(code, layer, 'exec') def restricted(code, environment=None, layer='Unknown'): From 352c93bc86347646d8190049fef9ba63f9c0dc0e Mon Sep 17 00:00:00 2001 From: ilvalle Date: Fri, 16 Sep 2016 19:33:07 +0200 Subject: [PATCH 102/436] added getcfs for run_view_in --- gluon/compileapp.py | 2 +- gluon/tests/test_appadmin.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 70f924b1..42794bac 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -707,7 +707,7 @@ def run_view_in(environment): for f in files: compiled = pjoin(path, f) if os.path.exists(compiled): - code = read_pyc(compiled) + code = getcfs(compiled, compiled, lambda: read_pyc(compiled)) restricted(code, environment, layer=compiled) return if not os.path.exists(filename) and allow_generic: diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py index 9d7e0493..77bb36d6 100644 --- a/gluon/tests/test_appadmin.py +++ b/gluon/tests/test_appadmin.py @@ -10,7 +10,7 @@ import sys import unittest -from gluon.compileapp import run_controller_in, run_view_in +from gluon.compileapp import run_controller_in, run_view_in, compile_application, remove_compiled_application from gluon.languages import translator from gluon.storage import Storage, List from gluon import fileutils @@ -76,7 +76,7 @@ class TestAppAdmin(unittest.TestCase): def run_view(self): return run_view_in(self.env) - def test_index(self): + def _test_index(self): result = self.run_function() self.assertTrue('db' in result['databases']) self.env.update(result) @@ -86,6 +86,15 @@ class TestAppAdmin(unittest.TestCase): print(e.message) self.fail('Could not make the view') + def test_index(self): + self._test_index() + + def test_index_compiled(self): + appname_path = os.path.join(os.getcwd(), 'applications', 'welcome') + compile_application(appname_path) + self._test_index() + remove_compiled_application(appname_path) + def test_select(self): request = self.env['request'] request.args = List(['db']) From 790593228f94f39acbff19cd47b1d7324c27c869 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 19 Sep 2016 20:41:41 +0200 Subject: [PATCH 103/436] fixes #1452 --- gluon/scheduler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 11dccd16..a68152b5 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -1499,6 +1499,8 @@ class Scheduler(MetaScheduler): kwargs.update(start_time=start_time, next_run_time=next_run_time) except: pass + if 'start_time' in kwargs and 'next_run_time' not in kwargs: + kwargs.update(next_run_time=kwargs['start_time']) rtn = self.db.scheduler_task.validate_and_insert(**kwargs) if not rtn.errors: rtn.uuid = tuuid From 25e8f4aef1f7e3bcd92ca17a7c21f0d97a1dfd7f Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 19 Sep 2016 21:33:01 +0200 Subject: [PATCH 104/436] fixes #1449 codename "he who late imports by default makes everybody's life a misery for no speedup at all" --- gluon/contrib/login_methods/cas_auth.py | 21 +++++++++------------ gluon/globals.py | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/gluon/contrib/login_methods/cas_auth.py b/gluon/contrib/login_methods/cas_auth.py index 61d99868..1a07c5f1 100644 --- a/gluon/contrib/login_methods/cas_auth.py +++ b/gluon/contrib/login_methods/cas_auth.py @@ -8,9 +8,11 @@ License: LGPL v3 Tinkered by Szabolcs Gyuris < szimszo n @ o regpreshaz dot eu> """ +import xml.dom.minidom as dom +import xml.parsers.expat as expat from gluon import current, redirect, URL - +from gluon._compat import urlopen, to_native class CasAuth(object): """ @@ -59,7 +61,7 @@ class CasAuth(object): # vars commented because of # https://code.google.com/p/web2py/issues/detail?id=1774 self.cas_my_url = URL(args=current.request.args, - #vars=current.request.vars, + #vars=current.request.vars, scheme=True) def login_url(self, next="/"): @@ -86,7 +88,6 @@ class CasAuth(object): exposed as CAS.login(request) returns a token on success, None on failed authentication """ - import urllib self.ticket = current.request.vars.ticket if not current.request.vars.ticket: redirect("%s?service=%s" % (self.cas_login_url, @@ -95,7 +96,7 @@ class CasAuth(object): url = "%s?service=%s&ticket=%s" % (self.cas_check_url, self.cas_my_url, self.ticket) - data = urllib.urlopen(url).read() + data = to_native(urlopen(url).read()) if data.startswith('yes') or data.startswith('no'): data = data.split('\n') if data[0] == 'yes': @@ -108,19 +109,16 @@ class CasAuth(object): a = b = c = data[1] return dict(user=a, email=b, username=c) return None - import xml.dom.minidom as dom - import xml.parsers.expat as expat try: dxml = dom.parseString(data) - envelop = dxml.getElementsByTagName( - "cas:authenticationSuccess") + envelop = dxml.getElementsByTagName("cas:authenticationSuccess") if len(envelop) > 0: res = dict() for x in envelop[0].childNodes: if x.nodeName.startswith('cas:') and len(x.childNodes): - key = x.nodeName[4:].encode('utf8') - value = x.childNodes[0].nodeValue.encode('utf8') - if not key in res: + key = to_native(x.nodeName[4:]) + value = to_native(x.childNodes[0].nodeValue) + if key not in res: res[key] = value else: if not isinstance(res[key], list): @@ -136,5 +134,4 @@ class CasAuth(object): exposed CAS.logout() redirects to the CAS logout page """ - import urllib redirect("%s?service=%s" % (self.cas_logout_url, self.cas_my_url)) diff --git a/gluon/globals.py b/gluon/globals.py index 41ac708e..ae4e0bc9 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,7 @@ Contains the classes for the global used variables: - Session """ -from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT +from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype From 40d6a72b9063e13fb47731bd51b07ee23d2671ab Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 21 Sep 2016 22:35:04 +0200 Subject: [PATCH 105/436] fixes #1455 --- gluon/tools.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index e0838f9f..eb4a61f9 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -12,9 +12,11 @@ Auth, Mail, PluginManager and various utilities import base64 from functools import reduce -from gluon._compat import pickle, thread, urllib2, Cookie, StringIO, configparser, MIMEBase, MIMEMultipart, \ - MIMEText, Encoders, Charset, long, urllib_quote, iteritems, to_bytes, to_native, add_charset, \ - charset_QP, basestring, unicodeT, to_unicode +from gluon._compat import pickle, thread, urllib2, Cookie, StringIO +from gluon._compat import configparser, MIMEBase, MIMEMultipart, MIMEText +from gluon._compat import Encoders, Charset, long, urllib_quote, iteritems +from gluon._compat import to_bytes, to_native, add_charset +from gluon._compat import charset_QP, basestring, unicodeT, to_unicode import datetime import logging import sys @@ -40,8 +42,9 @@ from gluon.utils import web2py_uuid, compare from gluon.fileutils import read_file, check_credentials from gluon import * from gluon.contrib.autolinks import expand_one -from gluon.contrib.markmin.markmin2html import \ - replace_at_urls, replace_autolinks, replace_components +from gluon.contrib.markmin.markmin2html import replace_at_urls +from gluon.contrib.markmin.markmin2html import replace_autolinks +from gluon.contrib.markmin.markmin2html import replace_components from pydal.objects import Row, Set, Query import gluon.serializers as serializers @@ -1578,7 +1581,6 @@ class Auth(object): logged_out='Logged out', registration_successful='Registration successful', invalid_email='Invalid email', - unable_send_email='Unable to send email', invalid_login='Invalid login', invalid_user='Invalid user', invalid_password='Invalid password', @@ -3633,7 +3635,7 @@ class Auth(object): message=self.messages.retrieve_password % dict(password=password)): session.flash = self.messages.email_sent else: - session.flash = self.messages.unable_to_send_email + session.flash = self.messages.unable_send_email self.log_event(log, user) callback(onaccept, form) if not next: @@ -3930,7 +3932,7 @@ class Auth(object): if self.email_reset_password(user): session.flash = self.messages.email_sent else: - session.flash = self.messages.unable_to_send_email + session.flash = self.messages.unable_send_email self.log_event(log, user) callback(onaccept, form) if not next: From 37d2efaab85bcf651556ddc87903f6f5f7591fb9 Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 21 Sep 2016 23:00:55 +0200 Subject: [PATCH 106/436] fixes #1413 --- handlers/wsgihandler.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/handlers/wsgihandler.py b/handlers/wsgihandler.py index 39b66d6c..e4f19c9c 100644 --- a/handlers/wsgihandler.py +++ b/handlers/wsgihandler.py @@ -7,22 +7,16 @@ Copyrighted by Massimo Di Pierro License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) -This is a WSGI handler for Apache -Requires apache+mod_wsgi. - -In httpd.conf put something like: - - LoadModule wsgi_module modules/mod_wsgi.so - WSGIScriptAlias / /path/to/wsgihandler.py - +This is a WSGI handler """ +import sys +import os + # change these parameters as required LOGGING = False SOFTCRON = False -import sys -import os path = os.path.dirname(os.path.abspath(__file__)) os.chdir(path) @@ -32,8 +26,6 @@ if not os.path.isdir('applications'): sys.path = [path] + [p for p in sys.path if not p == path] -sys.stdout = sys.stderr - import gluon.main if LOGGING: From 8ddccd41398e624b24abaa48173e812d301122a7 Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 21 Sep 2016 23:13:45 +0200 Subject: [PATCH 107/436] removed leftover (be careful (BOTH contributors and mergers)) --- applications/welcome/languages/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 applications/welcome/languages/.DS_Store diff --git a/applications/welcome/languages/.DS_Store b/applications/welcome/languages/.DS_Store deleted file mode 100644 index 0c703e4471f572b9253907ba8dc1db9997cbcff9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKO-sZu5Pi`V7QF1yV}5}}e?eW>i--sP19nwJSlzO15pVtJzWGqLTCcLm1d=zI z%$p<+nlu2?yn8qSW&ozFf=P^#BJR}#ke(I z7+Y}Wit{~@U9mOx_tjpz+FN_rO`&5|p+G1Q3WNeXqX6%0Dbqv4XhVTeAQbpgK+cEE zDp)%9hI(|c(I)_LLbn#$@|F-zb}SuxLyoW{qY{mp{E3l_PJi~e(y=!*I+D)JJU;X1 z<0a|r^k)r6Dh;Cz1ww(L0{hmT$^C!9PiFLyKcqx26bJ?WnF49DxLwS-DZg8PY)|gm v%zDQvMdErfSZEJ^0@#srF@6M8kZ3}IKTzNUXyZ4r From e5dcdb074f114e9709fbeea4981c4080d951e412 Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 22 Sep 2016 00:01:22 +0200 Subject: [PATCH 108/436] fixes #1310 and improves upon ideas shared on #1450 --- gluon/compileapp.py | 31 +++++++++++++------------------ gluon/template.py | 27 ++++++++++++++------------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 42794bac..459c2848 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -594,7 +594,7 @@ def run_controller_in(controller, function, environment): """ Runs the controller.function() (for the app specified by the current folder). - It tries pre-compiled controller_function.pyc first before compiling it. + It tries pre-compiled controller.function.pyc first before compiling it. """ # if compiled should run compiled! @@ -606,20 +606,15 @@ def run_controller_in(controller, function, environment): filename = pjoin(path, 'controllers.%s.%s.pyc' % (controller, function)) if not os.path.exists(filename): - ### for backward compatibility - filename = pjoin(path, 'controllers_%s_%s.pyc' - % (controller, function)) - ### end for backward compatibility - if not os.path.exists(filename): - raise HTTP(404, - rewrite.THREAD_LOCAL.routes.error_message % badf, - web2py_error=badf) + raise HTTP(404, + rewrite.THREAD_LOCAL.routes.error_message % badf, + web2py_error=badf) code = getcfs(filename, filename, lambda: read_pyc(filename)) restricted(code, environment, layer=filename) elif function == '_TEST': # TESTING: adjust the path to include site packages - from settings import global_settings - from admin import abspath, add_path_first + from gluon.settings import global_settings + from gluon.admin import abspath, add_path_first paths = (global_settings.gluon_parent, abspath( 'site-packages', gluon=True), abspath('gluon', gluon=True), '') [add_path_first(path) for path in paths] @@ -642,7 +637,7 @@ def run_controller_in(controller, function, environment): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badc, web2py_error=badc) - code = read_file(filename) + code = getcfs(filename, filename, lambda: read_file(filename)) exposed = find_exposed_functions(code) if not function in exposed: raise HTTP(404, @@ -669,7 +664,7 @@ def run_view_in(environment): Executes the view for the requested action. The view is the one specified in `response.view` or determined by the url or `view/generic.extension` - It tries the pre-compiled views_controller_function.pyc before compiling it. + It tries the pre-compiled views.controller.function.pyc before compiling it. """ request = current.request response = current.response @@ -691,18 +686,18 @@ def run_view_in(environment): else: filename = pjoin(folder, 'views', view) if os.path.exists(path): # compiled views - x = view.replace('/', '_') - files = ['views_%s.pyc' % x] + x = view.replace('/', '.') + files = ['views.%s.pyc' % x] is_compiled = os.path.exists(pjoin(path, files[0])) # Don't use a generic view if the non-compiled view exists. if is_compiled or (not is_compiled and not os.path.exists(filename)): if allow_generic: - files.append('views_generic.%s.pyc' % request.extension) + files.append('views.generic.%s.pyc' % request.extension) # for backward compatibility if request.extension == 'html': - files.append('views_%s.pyc' % x[:-5]) + files.append('views.%s.pyc' % x[:-5]) if allow_generic: - files.append('views_generic.pyc') + files.append('views.generic.pyc') # end backward compatibility code for f in files: compiled = pjoin(path, f) diff --git a/gluon/template.py b/gluon/template.py index 2afc29ab..e620df97 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -24,6 +24,9 @@ try: # have web2py from gluon.restricted import RestrictedError from gluon.globals import current + from gluon.cfs import getcfs + from gluon.fileutils import read_file + HAS_CFS = True except ImportError: # do not have web2py current = None @@ -426,7 +429,7 @@ class TemplateParser(object): # Allow Views to include other views dynamically context = self.context - if current and not "response" in context: + if current and "response" not in context: context["response"] = getattr(current, 'response', None) # Get the filename; filename looks like ``"template.html"``. @@ -779,12 +782,15 @@ def parse_template(filename, # First, if we have a str try to open the file if isinstance(filename, str): - try: - fp = open(os.path.join(path, filename), 'rb') - text = fp.read() - fp.close() - except IOError: - raise RestrictedError(filename, '', 'Unable to find the file') + fname = os.path.join(path, filename) + if HAS_CFS: + text = getcfs(fname, fname, lambda: read_file(fname)) + else: + try: + with open(fname, 'rb') as fp: + text = fp.read() + except IOError: + raise RestrictedError(filename, '', 'Unable to find the file') else: text = filename.read() text = to_native(text) @@ -890,7 +896,7 @@ def render(content="hello world", Response = DummyResponse # Add it to the context so we can use it. - if not 'NOESCAPE' in context: + if 'NOESCAPE' not in context: context['NOESCAPE'] = NOESCAPE if isinstance(content, unicodeT): @@ -936,8 +942,3 @@ def render(content="hello world", if old_response_body is not None: context['response'].body = old_response_body return text - - -if __name__ == '__main__': - import doctest - doctest.testmod() From 98d636314e3be3afef460a7507d71213555a214d Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 22 Sep 2016 00:17:17 +0200 Subject: [PATCH 109/436] avoid downloading pip as 2.7 has ensurepip --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 80e86c5d..dde913d4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,8 +17,7 @@ init: - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% install: - - ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py - - python get-pip.py + - python -m ensurepip - pip install codecov - git submodule update --init --recursive - pip install pycrypto From e5355b5b12a7de1ee93bee6ab91fd2b79260706a Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 22 Sep 2016 00:34:24 +0200 Subject: [PATCH 110/436] fixes #1407, encouraging a sane default --- applications/admin/views/web2py_ajax.html | 2 +- applications/examples/views/web2py_ajax.html | 2 +- applications/welcome/views/web2py_ajax.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/admin/views/web2py_ajax.html b/applications/admin/views/web2py_ajax.html index a5de41ea..417859e9 100644 --- a/applications/admin/views/web2py_ajax.html +++ b/applications/admin/views/web2py_ajax.html @@ -4,7 +4,7 @@ var w2p_ajax_date_format = "{{=T('%Y-%m-%d')}}"; var w2p_ajax_datetime_format = "{{=T('%Y-%m-%d %H:%M:%S')}}"; var w2p_ajax_disable_with_message = "{{=T('Working...')}}"; - var ajax_error_500 = '{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}' + var ajax_error_500 = "{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}" //--> {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/examples/views/web2py_ajax.html b/applications/examples/views/web2py_ajax.html index bf554868..a0ba6f43 100644 --- a/applications/examples/views/web2py_ajax.html +++ b/applications/examples/views/web2py_ajax.html @@ -3,7 +3,7 @@ var w2p_ajax_confirm_message = "{{=T('Are you sure you want to delete this object?')}}"; var w2p_ajax_date_format = "{{=T('%Y-%m-%d')}}"; var w2p_ajax_datetime_format = "{{=T('%Y-%m-%d %H:%M:%S')}}"; - var ajax_error_500 = '{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}' + var ajax_error_500 = "{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}" //--> {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/welcome/views/web2py_ajax.html b/applications/welcome/views/web2py_ajax.html index b8e1a113..6fefd3a0 100644 --- a/applications/welcome/views/web2py_ajax.html +++ b/applications/welcome/views/web2py_ajax.html @@ -4,7 +4,7 @@ var w2p_ajax_disable_with_message = "{{=T('Working...')}}"; var w2p_ajax_date_format = "{{=T('%Y-%m-%d')}}"; var w2p_ajax_datetime_format = "{{=T('%Y-%m-%d %H:%M:%S')}}"; - var ajax_error_500 = '{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}' + var ajax_error_500 = "{{=T.M('An error occured, please [[reload %s]] the page') % URL(args=request.args, vars=request.get_vars) }}" //--> {{ response.files.insert(0,URL('static','js/jquery.js')) From dd8b0760b5a012cb97965f2a1e07219e7d57e8c1 Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 22 Sep 2016 01:34:34 +0200 Subject: [PATCH 111/436] fixes #1309 (and sessions2trash.py, too) --- applications/admin/cron/expire_sessions.py | 25 +++++++++++++-------- scripts/sessions2trash.py | 26 +++++++++------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/applications/admin/cron/expire_sessions.py b/applications/admin/cron/expire_sessions.py index 10a428c3..34b16091 100644 --- a/applications/admin/cron/expire_sessions.py +++ b/applications/admin/cron/expire_sessions.py @@ -1,17 +1,20 @@ -EXPIRATION_MINUTES=60 -DIGITS=('0','1','2','3','4','5','6','7','8','9') -import os, time, stat, cPickle, logging -path = os.path.join(request.folder,'sessions') + +import os, time, stat, logging +from gluon._compat import pickle + +EXPIRATION_MINUTES = 60 + +path = os.path.join(request.folder, 'sessions') if not os.path.exists(path): os.mkdir(path) now = time.time() -for filename in os.listdir(path): - fullpath=os.path.join(path,filename) - if os.path.isfile(fullpath) and filename.startswith(DIGITS): +for path, dirs, files in os.walk(path, topdown=False): + for x in files: + fullpath = os.path.join(path, x) try: - filetime = os.stat(fullpath)[stat.ST_MTIME] # get it before our io + filetime = os.stat(fullpath)[stat.ST_MTIME] # get it before our io try: - session_data = cPickle.load(open(fullpath, 'rb+')) + session_data = pickle.load(open(fullpath, 'rb+')) expiration = session_data['auth']['expiration'] except: expiration = EXPIRATION_MINUTES * 60 @@ -19,3 +22,7 @@ for filename in os.listdir(path): os.unlink(fullpath) except: logging.exception('failure to check %s' % fullpath) + for d in dirs: + dd = os.path.join(path, d) + if not os.listdir(dd): + os.rmdir(dd) diff --git a/scripts/sessions2trash.py b/scripts/sessions2trash.py index 65758c87..f338b453 100755 --- a/scripts/sessions2trash.py +++ b/scripts/sessions2trash.py @@ -30,18 +30,14 @@ Typical usage: from __future__ import with_statement -import sys -import os -sys.path.append(os.path.join(*__file__.split(os.sep)[:-2] or ['.'])) - from gluon import current from gluon.storage import Storage +from gluon._compat import pickle from optparse import OptionParser -import cPickle import datetime -import os import stat import time +import os EXPIRATION_MINUTES = 60 SLEEP_MINUTES = 5 @@ -86,12 +82,12 @@ class SessionSet(object): status = 'trashed' if self.verbose > 1: - print 'key: %s' % str(item) - print 'expiration: %s seconds' % self.expiration - print 'last visit: %s' % str(last_visit) - print 'age: %s seconds' % age - print 'status: %s' % status - print '' + print('key: %s' % str(item)) + print('expiration: %s seconds' % self.expiration) + print('last visit: %s' % str(last_visit)) + print('age: %s seconds' % age) + print('status: %s' % status) + print('') elif self.verbose > 0: print('%s %s' % (str(item), status)) @@ -145,7 +141,7 @@ class SessionDb(object): def get(self): session = Storage() - session.update(cPickle.loads(self.row.session_data)) + session.update(pickle.loads(self.row.session_data)) return session def last_visit_default(self): @@ -155,7 +151,7 @@ class SessionDb(object): try: return datetime.datetime.strptime(self.row.modified_datetime, '%Y-%m-%d %H:%M:%S.%f') except: - print 'failed to retrieve last modified time (value: %s)' % self.row.modified_datetime + print('failed to retrieve last modified time (value: %s)' % self.row.modified_datetime) def __str__(self): return self.row.unique_key @@ -248,7 +244,7 @@ def main(): break else: if options.verbose: - print 'Sleeping %s seconds' % (options.sleep) + print('Sleeping %s seconds' % (options.sleep)) time.sleep(options.sleep) if __name__ == '__main__': From 4f0a2eb80ba82d3b66d3be9ea9611868a54cff9d Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 22 Sep 2016 01:55:53 +0200 Subject: [PATCH 112/436] fixes #1190 --- gluon/sqlhtml.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 03100e43..8f8d0d68 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -875,7 +875,7 @@ def formstyle_bootstrap(form, fields): controls.add_class('span4') if isinstance(label, LABEL): - label['_class'] = add_class(label.get('_class'),'control-label') + label['_class'] = add_class(label.get('_class'), 'control-label') if _submit: # submit button has unwrapped label and controls, different class @@ -925,8 +925,11 @@ def formstyle_bootstrap3_stacked(form, fields): for e in controls.elements("input"): e.add_class('form-control') + elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): + controls[0].add_class('form-control') + if isinstance(label, LABEL): - label['_class'] = add_class(label.get('_class'),'control-label') + label['_class'] = add_class(label.get('_class'), 'control-label') parent.append(DIV(label, _controls, _class='form-group', _id=id)) return parent @@ -975,8 +978,10 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3): elif isinstance(controls, UL): for e in controls.elements("input"): e.add_class('form-control') + elif isinstance(controls, CAT) and isinstance(controls[0], INPUT): + controls[0].add_class('form-control') if isinstance(label, LABEL): - label['_class'] = add_class(label.get('_class'),'control-label %s' % label_col_class) + label['_class'] = add_class(label.get('_class'), 'control-label %s' % label_col_class) parent.append(DIV(label, _controls, _class='form-group', _id=id)) return parent From 55f929bab4a30146d9ed714f0067d6668dc280d5 Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 23 Sep 2016 20:52:30 +0200 Subject: [PATCH 113/436] fix launching system_tests without coverage --- gluon/widget.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gluon/widget.py b/gluon/widget.py index af3b7689..131bfc3f 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -55,8 +55,11 @@ def run_system_tests(options): """ import subprocess major_version = sys.version_info[0] - minor_version = sys.version_info[1] call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests'] + if major_version == 2: + sys.stderr.write("Python 2.7\n") + else: + sys.stderr.write("Experimental Python 3.x.\n") if options.with_coverage: has_coverage = False coverage_exec = 'coverage2' if major_version == 2 else 'coverage3' @@ -70,14 +73,12 @@ def run_system_tests(options): coverage_config_file) call_args = [coverage_exec, 'run', '--rcfile=%s' % coverage_config, '-m', 'unittest', '-v', 'gluon.tests'] - if major_version == 2: - sys.stderr.write("Python 2.7\n") - else: - sys.stderr.write("Experimental Python 3.x.\n") if has_coverage: ret = subprocess.call(call_args) else: ret = 256 + else: + ret = subprocess.call(call_args) sys.exit(ret and 1) From 54da251e46c68d3b4b491c511b7af1fc6c257d85 Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 23 Sep 2016 21:17:43 +0200 Subject: [PATCH 114/436] here here --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 77e0d3f3..012dbe3e 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 77e0d3f386e326a2016a7e2139900ce72d7ddada +Subproject commit 012dbe3eed0404bc5d5516018f2bf9b3eb5b17ab From 9978e63621d8f14e5d93ae5feb767b200a77e797 Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 23 Sep 2016 23:45:00 +0200 Subject: [PATCH 115/436] fixes #1069, untangles the js, modernizes the approach plus, avoids hitting the backend for each and every keypress... --- gluon/sqlhtml.py | 135 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 20 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 8f8d0d68..e4291cae 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -657,7 +657,8 @@ class AutocompleteWidget(object): def __init__(self, request, field, id_field=None, db=None, orderby=None, limitby=(0, 10), distinct=False, keyword='_autocomplete_%(tablename)s_%(fieldname)s', - min_length=2, help_fields=None, help_string=None, at_beginning = True): + min_length=2, help_fields=None, help_string=None, + at_beginning=True, default_var='ac'): self.help_fields = help_fields or [] self.help_string = help_string @@ -680,7 +681,9 @@ class AutocompleteWidget(object): else: self.is_reference = False if hasattr(request, 'application'): - self.url = URL(args=request.args) + urlvars = request.vars + urlvars[default_var] = 1 + self.url = URL(args=request.args, vars=urlvars) self.callback() else: self.url = request @@ -688,35 +691,55 @@ class AutocompleteWidget(object): def callback(self): if self.keyword in self.request.vars: field = self.fields[0] + kword = self.request.vars[self.keyword] if isinstance(field, Field.Virtual): records = [] table_rows = self.db(self.db[field.tablename]).select(orderby=self.orderby) count = 0 for row in table_rows: if self.at_beginning: - if row[field.name].lower().startswith(self.request.vars[self.keyword]): + if row[field.name].lower().startswith(kword): count += 1 records.append(row) else: - if self.request.vars[self.keyword] in row[field.name].lower(): + if kword in row[field.name].lower(): count += 1 records.append(row) if count == 10: break - rows = Rows(self.db, records, table_rows.colnames, compact=table_rows.compact) + rows = Rows(self.db, records, table_rows.colnames, + compact=table_rows.compact) elif settings and settings.global_settings.web2py_runtime_gae: - rows = self.db(field.__ge__(self.request.vars[self.keyword]) & field.__lt__(self.request.vars[self.keyword] + u'\ufffd')).select(orderby=self.orderby, limitby=self.limitby, *(self.fields+self.help_fields)) + rows = self.db(field.__ge__(kword) & + field.__lt__(kword + u'\ufffd') + ).select(orderby=self.orderby, + limitby=self.limitby, + *(self.fields + self.help_fields)) elif self.at_beginning: - rows = self.db(field.like(self.request.vars[self.keyword] + '%', case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields)) + rows = self.db(field.like(kword + '%', case_sensitive=False) + ).select(orderby=self.orderby, + limitby=self.limitby, + distinct=self.distinct, + *(self.fields + self.help_fields)) else: - rows = self.db(field.contains(self.request.vars[self.keyword], case_sensitive=False)).select(orderby=self.orderby, limitby=self.limitby, distinct=self.distinct, *(self.fields+self.help_fields)) + rows = self.db(field.contains(kword, case_sensitive=False) + ).select(orderby=self.orderby, + limitby=self.limitby, + distinct=self.distinct, + *(self.fields + self.help_fields)) if rows: if self.is_reference: id_field = self.fields[1] if self.help_fields: - options = [OPTION( - self.help_string % dict([(h.name, s[h.name]) for h in self.fields[:1] + self.help_fields]), - _value=s[id_field.name], _selected=(k == 0)) for k, s in enumerate(rows)] + options = [ + OPTION( + self.help_string % dict( + [(h.name, s[h.name]) for h + in self.fields[:1] + self.help_fields]), + _value=s[id_field.name], + _selected=(k == 0)) + for k, s in enumerate(rows) + ] else: options = [OPTION( s[field.name], _value=s[id_field.name], @@ -763,28 +786,100 @@ class AutocompleteWidget(object): record = self.db( self.fields[1] == value).select(self.fields[0]).first() attr['value'] = record and record[self.fields[0].name] - attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \ + attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \ dict(div_id=div_id, u='F' + self.keyword) - attr['_onkeyup'] = "jQuery('#%(key3)s').val('');var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s :selected').text());jQuery('#%(key3)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+encodeURIComponent(jQuery('#%(id)s').val()),function(data){if(data=='')jQuery('#%(key3)s').val('');else{jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key3)s').val(jQuery('#%(key)s').val());jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);};}); else jQuery('#%(div_id)s').fadeOut('slow');" % \ + js = """ + (function($) { + function doit(e_) { + $('#%(key3)s').val(''); + var e=e_.which?e_.which:e_.keyCode; + function %(u)s(){ + $('#%(id)s').val($('#%(key)s :selected').text()); + $('#%(key3)s').val($('#%(key)s').val()) + }; + if(e==39) %(u)s(); + else if(e==40) { + if($('#%(key)s option:selected').next().length) + $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); + %(u)s(); + } + else if(e==38) { + if($('#%(key)s option:selected').prev().length) + $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); + %(u)s(); + } + else if($('#%(id)s').val().length>=%(min_length)s) + $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()), + function(data){ + if(data=='')$('#%(key3)s').val(''); + else{ + $('#%(id)s').next('.error').hide(); + $('#%(div_id)s').html(data).show().focus(); + $('#%(div_id)s select').css('width',$('#%(id)s').css('width')); + $('#%(key3)s').val($('#%(key)s').val()); + $('#%(key)s').change(%(u)s).click(%(u)s); + }; + }); + else $('#%(div_id)s').fadeOut('slow'); + } + var tmr = null; + $("#%(id)s").on('keyup focus',function(e) { + if (tmr) clearTimeout(tmr); + if($('#%(id)s').val().length>=%(min_length)s) { + tmr = setTimeout(function() { tmr = null; doit(e); }, 300); + } + }); + })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \ dict(url=self.url, min_length=self.min_length, key=self.keyword, id=attr['_id'], key2=key2, key3=key3, name=name, div_id=div_id, u='F' + self.keyword) - if self.min_length == 0: - attr['_onfocus'] = attr['_onkeyup'] return CAT(INPUT(**attr), INPUT(_type='hidden', _id=key3, _value=value, _name=name, requires=field.requires), + SCRIPT(js), DIV(_id=div_id, _style='position:absolute;')) else: attr['_name'] = field.name - attr['_onblur'] = "jQuery('#%(div_id)s').delay(1000).fadeOut('slow');" % \ + attr['_onblur'] = "jQuery('#%(div_id)s').delay(500).fadeOut('slow');" % \ dict(div_id=div_id, u='F' + self.keyword) - attr['_onkeyup'] = "var e=event.which?event.which:event.keyCode; function %(u)s(){jQuery('#%(id)s').val(jQuery('#%(key)s').val())}; if(e==39) %(u)s(); else if(e==40) {if(jQuery('#%(key)s option:selected').next().length)jQuery('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); %(u)s();} else if(e==38) {if(jQuery('#%(key)s option:selected').prev().length)jQuery('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); %(u)s();} else if(jQuery('#%(id)s').val().length>=%(min_length)s) jQuery.get('%(url)s?%(key)s='+encodeURIComponent(jQuery('#%(id)s').val()),function(data){jQuery('#%(id)s').next('.error').hide();jQuery('#%(div_id)s').html(data).show().focus();jQuery('#%(div_id)s select').css('width',jQuery('#%(id)s').css('width'));jQuery('#%(key)s').change(%(u)s);jQuery('#%(key)s').click(%(u)s);}); else jQuery('#%(div_id)s').fadeOut('slow');" % \ + js = """ + (function($) { + function doit(e_) { + var e=e_.which?e_.which:e_.keyCode; + function %(u)s(){ + $('#%(id)s').val($('#%(key)s').val()) + }; + if(e==39) %(u)s(); + else if(e==40) { + if($('#%(key)s option:selected').next().length) + $('#%(key)s option:selected').attr('selected',null).next().attr('selected','selected'); + %(u)s(); + } else if(e==38) { + if($('#%(key)s option:selected').prev().length) + $('#%(key)s option:selected').attr('selected',null).prev().attr('selected','selected'); + %(u)s(); + } else if($('#%(id)s').val().length>=%(min_length)s) + $.get('%(url)s&%(key)s='+encodeURIComponent($('#%(id)s').val()), + function(data){ + $('#%(id)s').next('.error').hide(); + $('#%(div_id)s').html(data).show().focus(); + $('#%(div_id)s select').css('width',$('#%(id)s').css('width')); + $('#%(key)s').change(%(u)s).click(%(u)s); + } + ); + else $('#%(div_id)s').fadeOut('slow'); + } + var tmr = null; + $("#%(id)s").on('keyup focus',function(e) { + if (tmr) clearTimeout(tmr); + if($('#%(id)s').val().length>=%(min_length)s) { + tmr = setTimeout(function() { tmr = null; doit(e); }, 300); + } + }); + })(jQuery)""".replace('\n', '').replace(' ' * 4, '') % \ dict(url=self.url, min_length=self.min_length, key=self.keyword, id=attr['_id'], div_id=div_id, u='F' + self.keyword) - if self.min_length == 0: - attr['_onfocus'] = attr['_onkeyup'] - return CAT(INPUT(**attr), + return CAT(INPUT(**attr), SCRIPT(js), DIV(_id=div_id, _style='position:absolute;')) From 576aaf668d8ddc573cb210616f37d61a3d76d7a3 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 24 Sep 2016 09:02:25 +0200 Subject: [PATCH 116/436] cache (and compile) parse_template in run_view_in, fix #1474 --- applications/welcome/controllers/appadmin.py | 2 +- gluon/compileapp.py | 68 ++++++-------------- gluon/restricted.py | 8 +-- gluon/template.py | 16 ++--- gluon/tests/test_appadmin.py | 7 ++ 5 files changed, 33 insertions(+), 68 deletions(-) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 0b8b227f..4d4e965f 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -409,7 +409,7 @@ def ccache(): import copy import time import math - from gluon import portalocker + from pydal.contrib import portalocker ram = { 'entries': 0, diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 459c2848..b3d9cf61 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -430,13 +430,10 @@ def build_environment(request, response, session, store_current=True): current.T = t current.cache = c - global __builtins__ if is_jython: # jython hack + global __builtins__ __builtins__ = mybuiltin() - elif is_pypy: # apply the same hack to pypy too - __builtins__ = mybuiltin() - elif PY2: - __builtins__['__import__'] = builtin.__import__ # WHY? + environment['request'] = request environment['response'] = response environment['session'] = session @@ -487,7 +484,7 @@ def compile_views(folder, skip_failed_views=False): else: raise Exception("%s in %s" % (e, fname)) else: - filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_') + filename = 'views.%s.py' % fname.replace(os.path.sep, '.') filename = pjoin(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) @@ -503,7 +500,7 @@ def compile_models(folder): path = pjoin(folder, 'models') for fname in listdir(path, '.+\.py$'): data = read_file(pjoin(path, fname)) - modelfile = 'models.'+fname.replace(os.path.sep,'.') + modelfile = 'models.'+fname.replace(os.path.sep, '.') filename = pjoin(folder, 'compiled', modelfile) mktree(filename) write_file(filename, data) @@ -582,11 +579,10 @@ def run_models_in(environment): continue elif compiled: code = getcfs(model, model, lambda: read_pyc(model)) - elif is_gae: + else: code = getcfs(model, model, lambda: compile2(read_file(model), model)) - else: - code = getcfs(model, model, None) + restricted(code, environment, layer=model) @@ -610,7 +606,6 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = getcfs(filename, filename, lambda: read_pyc(filename)) - restricted(code, environment, layer=filename) elif function == '_TEST': # TESTING: adjust the path to include site packages from gluon.settings import global_settings @@ -629,7 +624,6 @@ def run_controller_in(controller, function, environment): environment['__symbols__'] = environment.keys() code = read_file(filename) code += TEST_CODE - restricted(code, environment, layer=filename) else: filename = pjoin(folder, 'controllers/%s.py' % controller) @@ -644,10 +638,10 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function) - if is_gae: - layer = filename + ':' + function - code = getcfs(layer, filename, lambda: compile2(code, layer)) - restricted(code, environment, filename) + layer = filename + ':' + function + code = getcfs(layer, filename, lambda: compile2(code, layer)) + + restricted(code, environment, layer=filename) response = current.response vars = response._vars if response.postprocessing: @@ -682,7 +676,7 @@ def run_view_in(environment): if not isinstance(view, str): ccode = parse_template(view, pjoin(folder, 'views'), context=environment) - restricted(ccode, environment, 'file stream') + layer = 'file stream' else: filename = pjoin(folder, 'views', view) if os.path.exists(path): # compiled views @@ -713,16 +707,13 @@ def run_view_in(environment): rewrite.THREAD_LOCAL.routes.error_message % badv, web2py_error=badv) layer = filename - if is_gae: - ccode = getcfs(layer, filename, - lambda: compile2(parse_template(view, - pjoin(folder, 'views'), - context=environment), layer)) - else: - ccode = parse_template(view, - pjoin(folder, 'views'), - context=environment) - restricted(ccode, environment, layer) + # Cache the compiled template + ccode = getcfs(layer, filename, + lambda: compile2(parse_template(view, + pjoin(folder, 'views'), + context=environment), + layer)) + restricted(ccode, environment, layer=layer) def remove_compiled_application(folder): @@ -748,26 +739,3 @@ def compile_application(folder, skip_failed_views=False): compile_controllers(folder) failed_views = compile_views(folder, skip_failed_views) return failed_views - - -def test(): - """ - Example:: - - >>> import traceback, types - >>> environment={'x':1} - >>> open('a.py', 'w').write('print 1/x') - >>> save_pyc('a.py') - >>> os.unlink('a.py') - >>> if type(read_pyc('a.pyc'))==types.CodeType: print 'code' - code - >>> exec read_pyc('a.pyc') in environment - 1 - """ - - return - - -if __name__ == '__main__': - import doctest - doctest.testmod() diff --git a/gluon/restricted.py b/gluon/restricted.py index c0725a15..84c46b36 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -202,7 +202,7 @@ def compile2(code, layer): return compile(code, layer, 'exec') -def restricted(code, environment=None, layer='Unknown'): +def restricted(ccode, environment=None, layer='Unknown'): """ Runs code in environment and returns the output. If an exception occurs in code it raises a RestrictedError containing the traceback. Layer is @@ -213,10 +213,6 @@ def restricted(code, environment=None, layer='Unknown'): environment['__file__'] = layer environment['__name__'] = '__restricted__' try: - if isinstance(code, types.CodeType): - ccode = code - else: - ccode = compile2(code, layer) exec(ccode, environment) except HTTP: raise @@ -231,7 +227,7 @@ def restricted(code, environment=None, layer='Unknown'): sys.excepthook(etype, evalue, tb) del tb output = "%s %s" % (etype, evalue) - raise RestrictedError(layer, code, output, environment) + raise RestrictedError(layer, ccode, output, environment) def snapshot(info=None, context=5, code=None, environment=None): diff --git a/gluon/template.py b/gluon/template.py index e620df97..41da7f2d 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -24,9 +24,6 @@ try: # have web2py from gluon.restricted import RestrictedError from gluon.globals import current - from gluon.cfs import getcfs - from gluon.fileutils import read_file - HAS_CFS = True except ImportError: # do not have web2py current = None @@ -783,14 +780,11 @@ def parse_template(filename, # First, if we have a str try to open the file if isinstance(filename, str): fname = os.path.join(path, filename) - if HAS_CFS: - text = getcfs(fname, fname, lambda: read_file(fname)) - else: - try: - with open(fname, 'rb') as fp: - text = fp.read() - except IOError: - raise RestrictedError(filename, '', 'Unable to find the file') + try: + with open(fname, 'rb') as fp: + text = fp.read() + except IOError: + raise RestrictedError(filename, '', 'Unable to find the file') else: text = filename.read() text = to_native(text) diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py index 77bb36d6..c5e7402e 100644 --- a/gluon/tests/test_appadmin.py +++ b/gluon/tests/test_appadmin.py @@ -16,6 +16,7 @@ from gluon.storage import Storage, List from gluon import fileutils from gluon.dal import DAL, Field, Table from gluon.http import HTTP +from gluon.fileutils import open_file DEFAULT_URI = os.getenv('DB', 'sqlite:memory') @@ -76,12 +77,18 @@ class TestAppAdmin(unittest.TestCase): def run_view(self): return run_view_in(self.env) + def run_view_file_stream(self): + view_path = os.path.join(self.env['request'].folder, 'views', 'appadmin.html') + self.env['response'].view = open_file(view_path, 'r') + return run_view_in(self.env) + def _test_index(self): result = self.run_function() self.assertTrue('db' in result['databases']) self.env.update(result) try: self.run_view() + self.run_view_file_stream() except Exception as e: print(e.message) self.fail('Could not make the view') From b4acfd0724845633f667a55e593a15576379554d Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 25 Sep 2016 10:11:47 +0200 Subject: [PATCH 117/436] removed unnecessary env clone for _view_environment --- gluon/compileapp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index b3d9cf61..23e66628 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -441,7 +441,6 @@ def build_environment(request, response, session, store_current=True): lambda name, reload=False, app=request.application:\ local_import_aux(name, reload, app) BaseAdapter.set_folder(pjoin(request.folder, 'databases')) - response._view_environment = copy.copy(environment) custom_import_install() return environment From 4468355d013055be44913d1b2da57cadb360fe7f Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 25 Sep 2016 10:43:42 +0200 Subject: [PATCH 118/436] fix py3 xmlrpc imports, close #1473 --- applications/admin/controllers/pythonanywhere.py | 4 +--- gluon/_compat.py | 2 ++ gluon/contrib/simplejsonrpc.py | 11 ++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/applications/admin/controllers/pythonanywhere.py b/applications/admin/controllers/pythonanywhere.py index 33dfda6f..e61b06ab 100644 --- a/applications/admin/controllers/pythonanywhere.py +++ b/applications/admin/controllers/pythonanywhere.py @@ -4,10 +4,8 @@ import os import re import gzip import tarfile -from gluon._compat import StringIO -from xmlrpclib import ProtocolError from gluon.contrib.simplejsonrpc import ServerProxy - +from gluon._compat import StringIO, ProtocolError def deploy(): response.title = T('Deploy to pythonanywhere') diff --git a/gluon/_compat.py b/gluon/_compat.py index f7b53dde..a87a5c94 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -31,6 +31,7 @@ if PY2: from types import ClassType import cgi import cookielib + from xmlrpclib import ProtocolError BytesIO = StringIO reduce = reduce hashlib_md5 = hashlib.md5 @@ -94,6 +95,7 @@ else: from urllib.request import FancyURLopener, urlopen from urllib.parse import quote as urllib_quote, unquote as urllib_unquote, urlencode from http import cookiejar as cookielib + from xmlrpc.client import ProtocolError import html # warning, this is the python3 module and not the web2py html module hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) iterkeys = lambda d: iter(d.keys()) diff --git a/gluon/contrib/simplejsonrpc.py b/gluon/contrib/simplejsonrpc.py index 2a4547f7..f60dc4c7 100644 --- a/gluon/contrib/simplejsonrpc.py +++ b/gluon/contrib/simplejsonrpc.py @@ -17,12 +17,17 @@ __copyright__ = "Copyright (C) 2011 Mariano Reingart" __license__ = "LGPL 3.0" __version__ = "0.05" +import sys +PY2 = sys.version_info[0] == 2 import urllib -from xmlrpclib import Transport, SafeTransport -from cStringIO import StringIO +if PY2: + from xmlrpclib import Transport, SafeTransport + from cStringIO import StringIO +else: + from xmlrpc.client import Transport, SafeTransport + from io import StringIO import random -import sys import json From e9473f570f9427ae168f3603bf7576779cc6bcff Mon Sep 17 00:00:00 2001 From: sasa Date: Sun, 25 Sep 2016 20:03:23 +0200 Subject: [PATCH 119/436] Translation for serbian --- applications/admin/languages/sr-cr.py | 438 ++++++++++++------------ applications/admin/languages/sr-lt.py | 467 +++++++++++++------------- 2 files changed, 453 insertions(+), 452 deletions(-) diff --git a/applications/admin/languages/sr-cr.py b/applications/admin/languages/sr-cr.py index 56a9a384..882819b4 100755 --- a/applications/admin/languages/sr-cr.py +++ b/applications/admin/languages/sr-cr.py @@ -5,64 +5,64 @@ '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', '"User Exception" debug mode. ': '"User Exception" debug mode. ', '%s': '%s', -'%s %%{row} deleted': '%s %%{row} deleted', -'%s %%{row} updated': '%s %%{row} updated', +'%s %%{row} deleted': '%s %%{row} је избрисан', +'%s %%{row} updated': '%s %%{row} је Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½', '%s selected': '%s selected', '%s students registered': '%s students registered', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', '(requires internet access)': '(Š·Š°Ń…Ń‚ŠøŃ˜ŠµŠ²Š° ŠæŃ€ŠøŃŃ‚ŃƒŠæ ŠøŠ½Ń‚ŠµŃ€Š½ŠµŃ‚Ńƒ)', -'(requires internet access, experimental)': '(requires internet access, experimental)', -'(something like "it-it")': '(Š½ŠµŃˆŃ‚Š¾ као "it-it")', +'(requires internet access, experimental)': '(Š·Š°Ń…Ń‚ŠøŃ˜ŠµŠ²Š° ŠæŃ€ŠøŃŃ‚ŃƒŠæ ŠøŠ½Ń‚ŠµŃ€Š½ŠµŃ‚Ńƒ, експериментално)', +'(something like "it-it")': '(на ŠæŃ€ŠøŠ¼Ń˜ŠµŃ€ "it-it")', '(version %s)': '(version %s)', '?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(Гатотека **gluon/contrib/plural_rules/%s.py** није пронађена)', -'Abort': 'Abort', +'Abort': 'ŠžŠ“ŃƒŃŃ‚Š°Š½Šø', 'About': 'Š˜Š½Ń„Š¾Ń€Š¼Š°Ń†ŠøŃ˜Šµ', 'About application': 'Šž Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø', -'Accept Terms': 'Accept Terms', +'Accept Terms': 'ŠŸŃ€ŠøŃ…Š²Š°Ń‚Š°ŃšŠµ услова ŠŗŠ¾Ń€ŠøŃˆŃ›ŠµŃšŠ°', 'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'ДоГатни коГ за Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'Admin design page': 'Admin design page', -'admin disabled because no admin password': 'admin disabled because no admin password', -'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', -'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because no admin password': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° Ń˜ŠµŃ€ нема лозинке', +'admin disabled because not supported on google app engine': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° на google app engine', +'admin disabled because too many invalid login attempts': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° због већег Š±Ń€Š¾Ń˜Š° Š½ŠµŃƒŃŠæŃ˜ŠµŠ»ŠøŃ… покушаја', 'admin disabled because unable to access password file': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° Ń˜ŠµŃ€ не могу ŠæŃ€ŠøŃŃ‚ŃƒŠæŠøŃ‚Šø Гатотеци са лозинком', -'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', +'Admin is disabled because insecure channel': 'ŠŠ“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° због Š½ŠµŃŠøŠ³ŃƒŃ€Š½Šµ везе', 'Admin language': 'Језик аГминистратора', 'Admin versioning page': 'Admin versioning page', -'administrative interface': 'аГминистративни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', +'administrative interface': 'аГминистративно Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', 'Administrator Password:': 'Лозинка аГминистратора:', 'and rename it:': 'Šø ŠæŃ€ŠµŠøŠ¼ŠµŠ½ŃƒŃ˜ у:', -'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'App does not exist or you are not authorized': 'ŠŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° не ŠæŠ¾ŃŃ‚Š¾Ń˜Šø или немате права ŠæŃ€ŠøŃŃ‚ŃƒŠæŠ°', 'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', -'Application': 'Application', -'application "%s" uninstalled': 'application "%s" uninstalled', +'appadmin is disabled because insecure channel': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š° због Š½ŠµŃŠøŠ³ŃƒŃ€Š½Šµ везе', +'Application': 'ŠŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š°', +'application "%s" uninstalled': 'Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° "%s" је Геинсталирана', 'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', -'application compiled': 'application compiled', -'Application exists already': 'Application exists already', -'application is compiled and cannot be designed': 'application is compiled and cannot be designed', +'application compiled': 'Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° је ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š½Š°', +'Application exists already': 'ŠŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° већ ŠæŠ¾ŃŃ‚Š¾Ń˜Šø', +'application is compiled and cannot be designed': 'Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° је ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š½Š° Šø не може се Гаље ŃƒŃ€ŠµŃ’ŠøŠ²Š°Ń‚Šø', 'Application name:': 'ŠŠ°Š·ŠøŠ² Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ:', -'Application updated via git pull': 'Application updated via git pull', +'Application updated via git pull': 'ŠŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½Š° преко git pull', 'are not used': 'није ŠŗŠ¾Ń€ŠøŃˆŃ‚ŠµŠ½Š¾', 'are not used yet': 'није још ŠŗŠ¾Ń€ŠøŃˆŃ‚ŠµŠ½Š¾', -'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', -'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', +'Are you sure you want to delete file "%s"?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø Га желите избрисати Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø Га желите избрисати помоћни моГул "%s"?', 'Are you sure you want to delete this object?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø Га желите обрисати?', -'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', -'Are you sure?': 'Are you sure?', +'Are you sure you want to uninstall application "%s"?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø Га желите Геинсталирати Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ "%s"?', +'Are you sure?': 'Да ли сте ŃŠøŠ³ŃƒŃ€Š½Šø?', 'arguments': 'arguments', 'at char %s': 'коГ слова %s', 'at line %s': 'на линији %s', -'ATTENTION:': 'ATTENTION:', +'ATTENTION:': 'ŠŸŠŠ–ŠŠŠ:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', -'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'ATTENTION: you cannot edit the running application!': 'ŠŸŠŠ–ŠŠŠ: не можете ŃƒŃ€ŠµŃ’ŠøŠ²Š°Ń‚Šø ŠæŠ¾ŠŗŃ€ŠµŠ½ŃƒŃ‚Ńƒ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ!', 'Autocomplete Python Code': 'Autocomplete Python Code', -'Available Databases and Tables': 'Available Databases and Tables', +'Available Databases and Tables': 'Š”Š¾ŃŃ‚ŃƒŠæŠ½Šµ базе поГатака Šø табеле', 'back': 'назаГ', -'Back to the plugins list': 'Back to the plugins list', +'Back to the plugins list': 'ŠŠ°Š·Š°Š“ на Š»ŠøŃŃ‚Ńƒ помоћних моГула', 'Back to wizard': 'Back to wizard', 'Basics': 'ŠžŃŠ½Š¾Š²Šµ', 'Begin': 'ŠŸŠ¾Ń‡ŠµŃ‚Š°Šŗ', @@ -71,19 +71,19 @@ 'breakpoints': 'breakpoints', 'Bulk Register': 'Bulk Register', 'Bulk Student Registration': 'Bulk Student Registration', -'Cache': 'Cache', -'cache': 'cache', +'Cache': 'Кеш', +'cache': 'кеш', 'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'кеш, Š³Ń€ŠµŃˆŠŗŠµ Šø сесије су обрисани', -'can be a git repo': 'може бити git repo', -'Cancel': 'Cancel', -'Cannot be empty': 'Cannot be empty', -'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', -'cannot create file': 'cannot create file', +'can be a git repo': 'може бити git Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€ŠøŃ˜ŃƒŠ¼', +'Cancel': 'ŠžŃ‚ŠŗŠ°Š¶Šø', +'Cannot be empty': 'ŠŠµ може бити празно', +'Cannot compile: there are errors in your app:': 'ŠŠµ могу ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Ń‚Šø: Š³Ń€ŠµŃˆŠŗŠ° у Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø:', +'cannot create file': 'не могу креирати Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ', 'cannot upload file "%(filename)s"': 'не Š¼Š¾Ń„Ńƒ отпремити Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ "%(filename)s"', -'Change Admin Password': 'Change Admin Password', -'Change admin password': 'ŠŸŃ€Š¾Š¼ŠøŃ˜ŠµŠ½Šø лзинку аГминистратора', +'Change Admin Password': 'ŠŸŃ€Š¾Š¼ŠøŃ˜ŠµŠ½Šø лозинку аГминистратора', +'Change admin password': 'ŠŸŃ€Š¾Š¼ŠøŃ˜ŠµŠ½Šø лозинку аГминистратора', 'change editor settings': 'change editor settings', 'Changelog': 'Changelog', 'check all': 'check all', @@ -91,66 +91,66 @@ 'Check to delete': 'Check to delete', 'Checking for upgrades...': 'ŠŸŃ€Š¾Š²Ń˜ŠµŃ€Š°Š²Š°Š¼ Š¼Š¾Š³ŃƒŃ›Š½Š¾ŃŃ‚ Š½Š°Š“Š¾Š³Ń€Š°Š“ŃšŠµ...', 'Clean': 'ŠŸŃ€Š¾Ń‡ŠøŃŃ‚Šø', -'Clear': 'Clear', -'Clear CACHE?': 'Clear CACHE?', -'Clear DISK': 'Clear DISK', -'Clear RAM': 'Clear RAM', +'Clear': 'ŠžŠ±Ń€ŠøŃˆŠø', +'Clear CACHE?': 'ŠžŠ±Ń€ŠøŃˆŠø CACHE?', +'Clear DISK': 'ŠžŠ±Ń€ŠøŃˆŠø DISK', +'Clear RAM': 'ŠžŠ±Ń€ŠøŃˆŠø RAM', 'Click row to expand traceback': 'Click row to expand traceback', 'Click row to view a ticket': 'Click row to view a ticket', 'code': 'коГ', -'Code listing': 'Code listing', +'Code listing': 'ŠŸŃ€ŠøŠŗŠ°Š· коГа', 'collapse/expand all': 'сакрити/приказати све', -'Command': 'Command', -'Comment:': 'Comment:', +'Command': 'ŠŠ°Ń€ŠµŠ“Š±Š°', +'Comment:': 'ŠšŠ¾Š¼ŠµŠ½Ń‚Š°Ń€:', 'Commit': 'Commit', 'Commit form': 'Commit form', 'Committed files': 'Committed files', 'Compile': 'ŠšŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Ń˜', -'Compile (all or nothing)': 'Compile (all or nothing)', -'Compile (skip failed views)': 'Compile (skip failed views)', -'compiled application removed': 'compiled application removed', -'Condition': 'Condition', -'continue': 'continue', +'Compile (all or nothing)': 'ŠšŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Ń˜ (све или Š½ŠøŃˆŃ‚а)', +'Compile (skip failed views)': 'ŠšŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Ń˜ (ŠøŠ³Š½Š¾Ń€ŠøŃˆŠø Š³Ń€ŠµŃˆŠŗŠµ)', +'compiled application removed': 'ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š½Š° Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° је уклоњена', +'Condition': 'Š”Ń‚Š°ŃšŠµ', +'continue': 'настави', 'Controllers': 'ŠšŠ¾Š½Ń‚Ń€Š¾Š»ŠµŃ€Šø', 'controllers': 'контролери', -'Count': 'Count', +'Count': 'РеГни Š±Ń€Š¾Ń˜', 'Create': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜', 'create file with filename:': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜ Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ поГ називом:', 'Create rules': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜ правила', -'Create/Upload': 'Create/Upload', +'Create/Upload': 'ŠšŃ€ŠµŠøŃ€Š°Ń˜/ŠŸŃ€ŠµŃƒŠ·Š¼Šø', 'created by': 'израГио', 'Created by:': 'Created by:', 'Created On': 'Created On', 'Created on:': 'Created on:', 'crontab': 'crontab', -'Current request': 'Current request', -'Current response': 'Current response', -'Current session': 'Current session', +'Current request': 'Š¢Ń€ŠµŠ½ŃƒŃ‚Š½Šø Š·Š°Ń…Ń‚Ń˜ŠµŠ²', +'Current response': 'Š¢Ń€ŠµŠ½ŃƒŃ‚Š½Šø оГговор', +'Current session': 'Š¢Ń€ŠµŠ½ŃƒŃ‚Š½Š° сесија', 'currently running': 'Ń‚Ń€ŠµŠ½ŃƒŃ‚Š½Š¾ ŠæŠ¾ŠŗŃ€ŠµŠ½ŃƒŃ‚', 'currently saved or': 'Ń‚Ń€ŠµŠ½ŃƒŃ‚Š½Š¾ ŃŠ°Ń‡ŃƒŠ²Š°Š½Š¾ или', 'data uploaded': 'data uploaded', -'Database': 'Database', +'Database': 'База поГатака', 'Database %s select': 'Database %s select', -'Database administration': 'Database administration', +'Database administration': 'ŠŠ“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° базе поГатака', 'database administration': 'Š°Š“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° базе поГатака', -'Database Administration (appadmin)': 'Database Administration (appadmin)', -'Date and Time': 'Date and Time', +'Database Administration (appadmin)': 'ŠŠ“Š¼ŠøŠ½ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š° базе поГатака (appadmin)', +'Date and Time': 'Š”Š°Ń‚ŃƒŠ¼ Šø Š²Ń€ŠøŃ˜ŠµŠ¼Šµ', 'db': 'db', -'Debug': 'Debug', +'Debug': 'ŠžŃ‚ŠŗŠ»Š¾Š½Šø Š³Ń€ŠµŃˆŠŗŃƒ', 'defines tables': 'Š“ŠµŃ„ŠøŠ½ŠøŃˆŠµ табеле', 'Delete': 'ŠžŠ±Ń€ŠøŃˆŠø', 'delete': 'Š¾Š±Ń€ŠøŃˆŠø', -'delete all checked': 'delete all checked', -'delete plugin': 'delete plugin', -'Delete this file (you will be asked to confirm deletion)': 'ŠžŠ±Ń€ŠøŃˆŠø ову Š“Š°Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ (бићете ŃƒŠæŠøŃ‚Š°Š½Šø за ŠæŠ¾Ń‚Š²Ń€Š“Ńƒ Š±Ń€ŠøŃŠ°ŃšŠ°)', -'Delete:': 'Delete:', +'delete all checked': 'ŠøŠ·Š±Ń€ŠøŃˆŠø све означено', +'delete plugin': 'ŠøŠ·Š±Ń€ŠøŃˆŠø помоћни моГул', +'Delete this file (you will be asked to confirm deletion)': 'ŠžŠ±Ń€ŠøŃˆŠø ову Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ (бићете ŃƒŠæŠøŃ‚Š°Š½Šø за ŠæŠ¾Ń‚Š²Ń€Š“Ńƒ Š±Ń€ŠøŃŠ°ŃšŠ°)', +'Delete:': 'Š˜Š·Š±Ń€ŠøŃˆŠø:', 'deleted after first hit': 'deleted after first hit', 'Demo': 'Demo', 'Deploy': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø', 'Deploy on Google App Engine': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на Google App Engine', 'Deploy to OpenShift': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на OpenShift', -'Deploy to pythonanywhere': 'Deploy to pythonanywhere', -'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deploy to pythonanywhere': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на pythonanywhere', +'Deploy to PythonAnywhere': 'ŠŸŠ¾ŃŃ‚Š°Š²Šø на PythonAnywhere', 'Deployment form': 'Deployment form', 'Deployment Interface': 'Deployment Interface', 'Description:': 'Description:', @@ -160,10 +160,10 @@ 'direction: ltr': 'direction: ltr', 'directory not found': 'directory not found', 'Disable': 'Š˜ŃŠŗŃ™ŃƒŃ‡Šø', -'Disabled': 'Disabled', -'disabled in demo mode': 'disabled in demo mode', -'disabled in GAE mode': 'disabled in GAE mode', -'disabled in multi user mode': 'disabled in multi user mode', +'Disabled': 'Š˜ŃŠŗŃ™ŃƒŃ‡ŠµŠ½Š¾', +'disabled in demo mode': 'Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š¾ у Гемо моГу', +'disabled in GAE mode': 'Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š¾ у GAE моГу', +'disabled in multi user mode': 'Š¾Š½ŠµŠ¼Š¾Š³ŃƒŃ›ŠµŠ½Š¾ у Š²ŠøŃˆŠµŠŗŠ¾Ń€ŠøŃŠ½ŠøŃ‡ŠŗŠ¾Š¼ моГу', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', @@ -173,20 +173,20 @@ 'Docs': 'Docs', 'done!': 'done!', 'Downgrade': 'Downgrade', -'Download .w2p': 'Download .w2p', -'Download as .exe': 'Download as .exe', +'Download .w2p': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø као .w2p', +'Download as .exe': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø као .exe', 'download layouts': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø layouts', 'Download layouts from repository': 'Download layouts from repository', -'download plugins': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø plugins', -'Download plugins from repository': 'Download plugins from repository', -'Edit': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ', +'download plugins': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø помоћне моГуле', +'Download plugins from repository': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø помоћне моГуле ŠøŠ· Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€ŠøŃ˜ŃƒŠ¼', +'Edit': 'УреГи', 'edit all': 'ŃƒŃ€ŠµŠ“Šø све', 'Edit application': 'УреГи Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'edit controller': 'ŃƒŃ€ŠµŠ“Šø контролер', 'edit controller:': 'edit controller:', 'Edit current record': 'Edit current record', -'edit views:': 'ŃƒŃ€ŠµŠ“Šø views:', -'Editing %s': 'Editing %s', +'edit views:': 'ŃƒŃ€ŠµŠ“Šø приказ:', +'Editing %s': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ %s', 'Editing file "%s"': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ Гатотеке "%s"', 'Editing Language file': 'Š£Ń€ŠµŃ’ŠøŠ²Š°ŃšŠµ Ń˜ŠµŠ·ŠøŃ‡ŠŗŠµ Гатотеке', 'Editing Plural Forms File': 'Editing Plural Forms File', @@ -206,21 +206,21 @@ 'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Expand Abbreviation', 'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', -'export as csv file': 'export as csv file', -'Exports:': 'Exports:', -'exposes': 'exposes', -'exposes:': 'exposes:', +'export as csv file': 'извези као csv Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ', +'Exports:': 'Извози:', +'exposes': 'ŠæŃ€ŠøŠŗŠ°Š·ŃƒŃ˜Šµ', +'exposes:': 'ŠæŃ€ŠøŠŗŠ°Š·ŃƒŃ˜Šµ:', 'extends': 'ŠæŃ€Š¾ŃˆŠøŃ€ŃƒŃ˜Šµ', 'failed to compile file because:': 'нисам могао Га ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š¼ због:', 'failed to reload module because:': 'failed to reload module because:', 'File': 'Датотека', -'file "%(filename)s" created': 'file "%(filename)s" created', -'file "%(filename)s" deleted': 'file "%(filename)s" deleted', -'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', -'file "%s" of %s restored': 'file "%s" of %s restored', +'file "%(filename)s" created': 'Гатотека "%(filename)s" је креирана', +'file "%(filename)s" deleted': 'Гатотека "%(filename)s" је избрисана', +'file "%(filename)s" uploaded': 'Гатотека "%(filename)s" је отпремљена', +'file "%s" of %s restored': 'Гатотека "%s" оГ %s е враћено у претхоГно ŃŃ‚Š°ŃšŠµ', 'file changed on disk': 'file changed on disk', 'file does not exist': 'Гатотека не ŠæŠ¾ŃŃ‚Š¾Ń˜Šø', -'file not found': 'file not found', +'file not found': 'Гатотека није пронађена', 'file saved on %(time)s': 'file saved on %(time)s', 'file saved on %s': 'Гатотека ŃŠ°Ń‡ŃƒŠ²Š°Š½Š° на %s', 'filename': 'filename', @@ -242,7 +242,7 @@ 'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'Go to Matching Pair', 'go!': 'крени!', -'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google App Engine Deployment Interface': 'Google App Engine Dинсталационо Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', 'Google Application Id': 'Google Application Id', 'Goto': 'Goto', 'graph model': 'graph model', @@ -260,7 +260,7 @@ 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', 'import': 'import', -'Import/Export': 'Import/Export', +'Import/Export': 'инсталационо Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', 'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'ŃƒŠŗŃ™ŃƒŃ‡ŃƒŃ˜Šµ', 'Indent with tabs': 'Indent with tabs', @@ -270,49 +270,49 @@ 'Installed applications': 'Š˜Š½ŃŃ‚Š°Š»ŠøŃ€Š°Š½Šµ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', 'Interaction at %s line %s': 'Interaction at %s line %s', 'Interactive console': 'Interactive console', -'internal error': 'internal error', -'internal error: %s': 'internal error: %s', +'internal error': 'ŃƒŠ½ŃƒŃ‚Ń€Š°ŃˆŃšŠ° Š³Ń€ŠµŃˆŠŗŠ°', +'internal error: %s': 'ŃƒŠ½ŃƒŃ‚Ń€Š°ŃˆŃšŠ° Š³Ń€ŠµŃˆŠŗŠ°: %s', 'Internal State': 'Internal State', 'Invalid action': 'Invalid action', 'Invalid application name': 'Invalid application name', 'invalid circular reference': 'invalid circular reference', 'Invalid git repository specified.': 'Invalid git repository specified.', -'invalid password': 'invalid password', -'invalid password.': 'ŠæŠ¾Š³Ń€ŠµŃˆŠ½Š° лозинка.', -'Invalid Query': 'Invalid Query', +'invalid password': 'ŠŠµŠ²Š°Š¶ŠµŃ›Š° лозинка', +'invalid password.': 'неважећа лозинка.', +'Invalid Query': 'ŠŸŠ¾Š³Ń€ŠµŃˆŠ°Š½ ŃƒŠæŠøŃ‚', 'invalid request': 'invalid request', 'Invalid request': 'Invalid request', 'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', -'invalid ticket': 'invalid ticket', +'invalid ticket': 'ŠæŠ¾Š³Ń€ŠµŃˆŠ°Š½ тикет', 'Key': 'Key', 'Key bindings': 'ŠŸŃ€ŠµŃ‡ŠøŃ†Šµ', 'Key bindings for ZenCoding Plugin': 'ŠŸŃ€ŠµŃ‡ŠøŃ†Šµ за ZenCoding Plugin', -'Keyboard shortcuts': 'Keyboard shortcuts', +'Keyboard shortcuts': 'ŠŸŃ€ŠµŃ‡ŠøŃ†Šµ на Ń‚Š°ŃŃ‚Š°Ń‚ŃƒŃ€Šø', 'kill process': 'kill process', -'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', +'language file "%(filename)s" created/updated': 'Ń˜ŠµŠ·ŠøŃ‡ŠŗŠ° Гатотека "%(filename)s" је креирана/Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½Š°', 'Language files (static strings) updated': 'ŠˆŠµŠ·ŠøŃ‡ŠŗŠµ Гатотеке су Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½Šµ', 'languages': 'Ń˜ŠµŠ·ŠøŃ†Šø', 'Languages': 'ŠˆŠµŠ·ŠøŃ†Šø', 'Last Revision': 'Last Revision', 'Last saved on:': 'ŠŸŠ¾ŃŃ™ŠµŠ“ŃšŠ° измјена:', 'License for': 'Лиценца за', -'License:': 'License:', +'License:': 'Лиценца:', 'Line Nr': 'Line Nr', -'Line number': 'Line number', -'lists by exception': 'lists by exception', -'lists by ticket': 'lists by ticket', -'Loading...': 'Loading...', +'Line number': 'Š›ŠøŠ½ŠøŃ˜Š° Š±Ń€Š¾Ń˜', +'lists by exception': 'прикажи Š³Ń€ŠµŃˆŠŗŠµ', +'lists by ticket': 'прикажи тикете', +'Loading...': 'ŠŸŃ€ŠµŃƒŠ·ŠøŠ¼Š°Š¼...', 'loading...': 'ŠæŃ€ŠµŃƒŠ·ŠøŠ¼Š°Š¼...', -'Local Apps': 'Local Apps', +'Local Apps': 'Локалне Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', 'locals': 'locals', 'Locals##debug': 'Locals##debug', 'Login': 'ŠŸŃ€ŠøŃ˜Š°Š²Š°', 'Login successful': 'Login successful', -'Login to the Administrative Interface': 'ŠŸŃ€ŠøŃ˜Š°Š²Š° за аГминистративни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', -'Login/Register': 'Login/Register', -'Logout': 'Излаз', -'lost password': 'lost password', -'Main Menu': 'Main Menu', +'Login to the Administrative Interface': 'ŠŸŃ€ŠøŃ˜Š°Š²Š° за аГминистративно Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', +'Login/Register': 'ŠŸŃ€ŠøŃ˜Š°Š²Š°/Š ŠµŠ³ŠøŃŃ‚Ń€Š°Ń†ŠøŃ˜Š°', +'Logout': 'ŠžŠ“Ń˜Š°Š²Š°', +'lost password': 'ŠøŠ·Š³ŃƒŠ±Ń™ŠµŠ½Š° лозинка', +'Main Menu': 'Главни мени', 'Manage': 'Manage', 'Manage %(action)s': 'Manage %(action)s', 'Manage Access Control': 'Manage Access Control', @@ -321,68 +321,68 @@ 'Manage Students': 'Manage Students', 'Match Pair': 'Match Pair', 'Memberships': 'Memberships', -'merge': 'merge', +'merge': 'споји', 'Merge Lines': 'Дпоји линије', -'Models': 'Models', -'models': 'models', +'Models': 'МоГели', +'models': 'МоГели', 'Modified On': 'Modified On', -'Modules': 'Modules', -'modules': 'modules', -'Multi User Mode': 'Multi User Mode', -'new application "%s" created': 'new application "%s" created', -'new application "%s" imported': 'new application "%s" imported', +'Modules': 'МоГули', +'modules': 'моГули', +'Multi User Mode': 'Š’ŠøŃˆŠµŠŗŠ¾Ń€ŠøŃŠ½ŠøŃ‡ŠŗŠø режим раГа', +'new application "%s" created': 'нова Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° "%s" је креирана', +'new application "%s" imported': 'нова Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° "%s" је увежена', 'New Application Wizard': 'Š§Š°Ń€Š¾Š±ŃšŠ°Šŗ за нове Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', 'New application wizard': 'Š§Š°Ń€Š¾Š±ŃšŠ°Šŗ за нове Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', -'new plugin installed': 'new plugin installed', -'New plugin installed: %s': 'New plugin installed: %s', -'New Record': 'New Record', -'new record inserted': 'new record inserted', +'new plugin installed': 'нови помоћни моГул је инсталиран', +'New plugin installed: %s': 'Š˜Š½ŃŃ‚Š°Š»ŠøŃ€Š°Š½ нови помоћни моГул: %s', +'New Record': 'ŠŠ¾Š²Šø запис', +'new record inserted': 'унешен нови запис', 'New simple application': 'ŠŠ¾Š²Š° Ń˜ŠµŠ“Š½Š¾ŃŃ‚Š°Š²Š½Š° Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š°', -'next': 'next', +'next': 'слеГећи', 'next %s rows': 'next %s rows', 'Next Edit Point': 'Next Edit Point', -'NO': 'NO', -'no changes': 'no changes', -'No databases in this application': 'No databases in this application', -'No Interaction yet': 'No Interaction yet', -'no match': 'no match', -'no package selected': 'no package selected', -'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'NO': 'ŠŠ•', +'no changes': 'нема ŠæŃ€Š¾Š¼Ń˜ŠµŠ½Š°', +'No databases in this application': 'ŠŠµŠ¼Š° базе поГатака у Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø', +'No Interaction yet': 'ŠŠµŠ¼Š° још ŠøŠ½Ń‚ŠµŃ€Š°ŠŗŃ†ŠøŃ˜Šµ', +'no match': 'нема ŠæŠ¾Š“ŃƒŠ“Š°Ń€Š°ŃšŠ°', +'no package selected': 'пакет није оГабран', +'no permission to uninstall "%s"': 'немате Š¾Š²Š»Š°ŃˆŃ‚ŠµŃšŠµ Га Геинсталирате "%s"', 'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', 'Node:': 'Node:', -'Not Authorized': 'Not Authorized', -'Not supported': 'Not supported', +'Not Authorized': 'ŠŠµŠ¼Š°Ń‚Šµ Š¾Š²Š»Š°ŃˆŃ›ŠµŃšŠµ', +'Not supported': 'ŠŠøŃ˜Šµ поГржано', 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'онлајн Š“ŠøŠ·Š°Ń˜Š½ŠµŃ€', -'Open new app in new window': 'Open new app in new window', -'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'Open new app in new window': 'ŠžŃ‚Š²Š¾Ń€Šø нову Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ у новом ŠæŃ€Š¾Š·Š¾Ń€Ńƒ', +'OpenShift Deployment Interface': 'OpenShift инсталационо Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', 'OpenShift Output': 'OpenShift Output', -'or alternatively': 'or alternatively', +'or alternatively': 'или алтернативно', 'Or Get from URL:': 'Or Get from URL:', -'or import from csv file': 'or import from csv file', +'or import from csv file': 'или увези ŠæŠ¾Š¼Š¾Ń›Ńƒ csv Гатотеке', 'Original/Translation': 'ŠžŃ€Š³ŠøŠ½Š°Š»/ŠŸŃ€ŠµŠ²Š¾Š“', -'Overview': 'Overview', -'Overwrite installed app': 'ŠŸŃ€ŠµŠ±Ń€ŠøŃˆŠø ŠæŠ¾ŃŃ‚Š¾Ń˜ŠµŃ›Ńƒ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', +'Overview': 'ŠŸŃ€ŠµŠ³Š»ŠµŠ“', +'Overwrite installed app': 'Š—Š°Š¼Ń˜ŠµŠ½Šø већ ŠæŠ¾ŃŃ‚Š¾Ń˜ŠµŃ›Ńƒ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'Pack all': 'Š—Š°ŠæŠ°ŠŗŃƒŃ˜ све', -'Pack compiled': 'Pack compiled', -'Pack custom': 'Pack custom', -'pack plugin': 'pack plugin', -'password changed': 'password changed', -'Past revisions': 'Past revisions', +'Pack compiled': 'Š—Š°ŠæŠ°ŠŗŃƒŃ˜ ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š½Š¾', +'Pack custom': 'ŠŸŃ€ŠøŠ»Š°Š³Š¾Ń’ŠµŠ½Š¾ паковање', +'pack plugin': 'запакуј помоћни моГул', +'password changed': 'лозинка је ŠæŃ€Š¾Š¼ŠøŃ˜ŠµŃšŠµŠ½Š°', +'Past revisions': 'ŠŸŃ€ŠµŃ‚Ń…Š¾Š“Š½Šµ ŠŗŠ¾Ń€ŠµŠŗŃ†ŠøŃ˜Šµ', 'Path to appcfg.py': 'Path to appcfg.py', 'Path to local openshift repo root.': 'Path to local openshift repo root.', -'Peeking at file': 'Peeking at file', -'Permission': 'Permission', -'Permissions': 'Permissions', -'Please': 'Please', +'Peeking at file': 'ŠŸŃ€ŠµŠ³Š»ŠµŠ“ Гатотеке', +'Permission': 'Дозвола', +'Permissions': 'Дозволе', +'Please': 'Молим', 'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', -'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', -'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin "%(plugin)s" deleted': 'помоћни моГул "%(plugin)s" је избрисан', +'Plugin "%s" in application': 'ŠŸŠ¾Š¼Š¾Ń›Š½Šø моГул "%s" у Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø', 'plugin not specified': 'plugin not specified', -'Plugin page': 'Plugin page', -'plugins': 'plugins', -'Plugins': 'Plugins', +'Plugin page': 'Дтраница помоћних моГула', +'plugins': 'помоћни моГули', +'Plugins': 'ŠŸŠ¾Š¼Š¾Ń›Š½Šø моГули', 'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'ŠžŠ¼Š¾Š³ŃƒŃ›ŠøŠ¾', @@ -390,8 +390,8 @@ 'Preferences saved on session only': 'Preferences saved on session only', 'previous %s rows': 'previous %s rows', 'Previous Edit Point': 'Previous Edit Point', -'Private files': 'Private files', -'private files': 'private files', +'Private files': 'ŠŸŃ€ŠøŠ²Š°Ń‚Š½Šµ Гатотеке', +'private files': 'приватне Гатотеке', 'Project Progress': 'ŠŠ°ŠæŃ€ŠµŠ“Š°Šŗ ŠæŃ€Š¾Ń˜ŠµŠŗŃ‚Š°', 'Pull': 'Pull', 'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', @@ -401,7 +401,7 @@ 'pygraphviz library not found': 'pygraphviz library not found', 'PythonAnywhere Apps': 'PythonAnywhere Apps', 'PythonAnywhere Password': 'PythonAnywhere Password', -'Query:': 'Query:', +'Query:': 'Упит:', 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', @@ -412,11 +412,11 @@ 'refresh': 'refresh', 'register': 'register', 'Reload routes': 'ŠžŠ±Š½Š¾Š²Šø ŠæŃ€ŠµŃƒŃŠ¼Ń˜ŠµŃ€ŠµŃšŠ°', -'Remove compiled': 'Remove compiled', +'Remove compiled': 'Уклони ŠŗŠ¾Š¼ŠæŠ°Ń˜Š»ŠøŃ€Š°Š½Š¾', 'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Replace': 'Š—Š°Š¼ŠøŃ˜ŠµŠ½Šø', 'Replace All': 'Š—Š°Š¼ŠøŃ˜ŠµŠ½Šø све', -'Repository (%s)': 'Repository (%s)', +'Repository (%s)': 'Š ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€ŠøŃ˜ŃƒŠ¼ (%s)', 'request': 'request', 'requires distutils, but not installed': 'requires distutils, but not installed', 'requires python-git, but not installed': 'requires python-git, but not installed', @@ -430,28 +430,28 @@ 'reverted to revision %s': 'reverted to revision %s', 'Revision %s': 'Revision %s', 'Revision:': 'Revision:', -'Role': 'Role', -'Roles': 'Roles', -'Rows in Table': 'Rows in Table', +'Role': 'Улога', +'Roles': 'Улоге', +'Rows in Table': 'Записи у табели', 'Rows selected': 'Rows selected', 'rules are not defined': 'правила нису Гефинисана', 'rules:': 'правила:', -'Run tests': 'Run tests', -'Run tests in this file': 'Run tests in this file', +'Run tests': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø тестове', +'Run tests in this file': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø тестове у Гатотеци', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", 'Running on %s': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½ŃƒŃ‚Šø на %s', 'Save': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜', -'Save file:': 'Save file:', -'Save file: %s': 'Save file: %s', -'Save model as...': 'Save model as...', -'Save via Ajax': 'ŃŠ°Ń‡ŃƒŠ²Š°Ń˜ via Ajax', +'Save file:': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜ Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ:', +'Save file: %s': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜ Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ: %s', +'Save model as...': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜ моГел као...', +'Save via Ajax': 'Š”Š°Ń‡ŃƒŠ²Š°Ń˜ преко Ajax', 'Saved file hash:': 'Š”Š°Ń‡ŃƒŠ²Š°Š½Š¾ као Ń…Š°Ńˆ:', -'Screenshot %s': 'Screenshot %s', -'Search': 'Search', -'Select Files to Package': 'Select Files to Package', +'Screenshot %s': 'Днимак екрана %s', +'Search': 'ŠŸŃ€ŠµŃ‚Ń€Š°Š³Š°', +'Select Files to Package': 'ŠžŠ“Š°Š±ŠµŃ€Šø Гатотеке за паковање', 'session': 'сесија', -'session expired': 'сесија истекла', -'Session saved correctly': 'Session saved correctly', +'session expired': 'сесија је истекла', +'Session saved correctly': 'Десија је ŃƒŃ€ŠµŠ“Š½Š¾ ŃŠ°Ń‡ŃƒŠ²Š°Š½Š°', 'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', @@ -459,10 +459,10 @@ 'Singular Form': 'Singular Form', 'Site': 'Š”Š°Ń˜Ń‚', 'Size of cache:': 'Size of cache:', -'skip to generate': 'skip to generate', -'some files could not be removed': 'some files could not be removed', +'skip to generate': 'прескочи Š³ŠµŠ½ŠµŃ€ŠøŃŠ°ŃšŠµ', +'some files could not be removed': 'неке Гатотеке не могу бити уклоњене', 'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', -'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'Sorry, could not find mercurial installed': 'Жалим, mercurial није инсталиран', 'source : db': 'source : db', 'source : filesystem': 'source : filesystem', 'Start a new app': 'ŠŸŠ¾ŠŗŃ€ŠµŠ½Šø нову Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', @@ -474,15 +474,15 @@ 'Static files': 'Static files', 'Statistics': 'Statistics', 'Step': 'ŠšŠ¾Ń€Š°Šŗ', -'step': 'step', +'step': 'kорак', 'stop': 'stop', -'submit': 'submit', +'submit': 'прихвати', 'Submit': 'ŠŸŃ€ŠøŃ…Š²Š°Ń‚Šø', 'successful': 'успјешан', -'switch to : db': 'switch to : db', -'switch to : filesystem': 'switch to : filesystem', +'switch to : db': 'пређи на : db', +'switch to : filesystem': 'пређи на : filesystem', 'Tab width (# characters)': 'Tab width (# characters)', -'Table': 'Table', +'Table': 'Табела', 'Temporary': 'Temporary', 'test': 'тест', 'Testing application': 'Š¢ŠµŃŃ‚ŠøŃ€Š°ŃšŠµ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šµ', @@ -492,16 +492,16 @@ 'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', -'Theme': 'Theme', -'There are no controllers': 'There are no controllers', -'There are no models': 'There are no models', -'There are no modules': 'There are no modules', -'There are no plugins': 'There are no plugins', -'There are no private files': 'There are no private files', -'There are no static files': 'There are no static files', -'There are no translators': 'There are no translators', +'Theme': 'Табела', +'There are no controllers': 'ŠŠµŠ¼Š° контолера', +'There are no models': 'ŠŠµŠ¼Š° моГела', +'There are no modules': 'ŠŠµŠ¼Š° моГула', +'There are no plugins': 'ŠŠµŠ¼Š° помоћних моГула', +'There are no private files': 'ŠŠµŠ¼Š° приватних Гатотека', +'There are no static files': 'ŠŠµŠ¼Š° статичних Гатотека', +'There are no translators': 'ŠŠµŠ¼Š° превоГиоца', 'There are no translators, only default language is supported': 'There are no translators, only default language is supported', -'There are no views': 'There are no views', +'There are no views': 'ŠŠµŠ¼Š° превоГиоца', 'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'These files are served without processing, your images go here', "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", @@ -512,9 +512,9 @@ 'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', 'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', 'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', -'Ticket': 'Ticket', -'Ticket ID': 'Ticket ID', -'Ticket Missing': 'Ticket nedostaje', +'Ticket': 'Тикет', +'Ticket ID': 'Тикет ID', +'Ticket Missing': 'ŠŠµŠ“Š¾ŃŃ‚Š°Ń˜Šµ тикет', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'to previous version.': 'на ŠæŃ€ŠµŃ‚Ń…Š¾Š“Š½Ńƒ Š²ŠµŃ€Š·ŠøŃ˜Ńƒ.', 'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', @@ -525,67 +525,67 @@ 'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'Translation strings for the application': 'Š ŠøŃ˜ŠµŃ‡Šø у Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Šø које треба превести', -'try something like': 'try something like', -'Try the mobile interface': 'ŠŸŃ€Š¾Š±Š°Ń˜ мобилни ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŃ˜Ń', -'try view': 'try view', +'try something like': 'на ŠæŃ€ŠøŠ¼Ń˜ŠµŃ€', +'Try the mobile interface': 'ŠŸŃ€Š¾Š±Š°Ń˜ мобилнo Š¾ŠŗŃ€ŃƒŠ¶ŠµŃšŠµ', +'try view': 'ŠæŃ€Š¾Š±Š°Ń˜ приказ', 'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', 'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', -'Unable to check for upgrades': 'Unable to check for upgrades', +'Unable to check for upgrades': 'ŠŠµ могу Га ŠæŃ€Š¾Š²Ń˜ŠµŃ€ŠøŠ¼ Š¼Š¾Š³ŃƒŃ›Š½Š¾ŃŃ‚ Š½Š°Š“Š¾Š³Ń€Š°Š“ŃšŠµ', 'unable to create application "%s"': 'unable to create application "%s"', -'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', -'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', -'Unable to determine the line number!': 'Unable to determine the line number!', -'Unable to download app because:': 'Unable to download app because:', +'unable to delete file "%(filename)s"': 'не могу избрисати Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'не могу избрисати помоћни моГул "%(plugin)s"', +'Unable to determine the line number!': 'ŠŠµ могу Га ŃƒŃ‚Š²Ń€Š“ŠøŠ¼ Š±Ń€Š¾Ń˜ реГа!', +'Unable to download app because:': 'ŠŠµ могу Га ŠæŃ€ŠµŃƒŠ·Š¼ŠµŠ¼ Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ због:', 'unable to download layout': 'unable to download layout', 'unable to download plugin: %s': 'unable to download plugin: %s', -'Unable to download the list of plugins': 'Unable to download the list of plugins', -'unable to install plugin "%s"': 'unable to install plugin "%s"', -'unable to parse csv file': 'unable to parse csv file', -'unable to uninstall "%s"': 'unable to uninstall "%s"', -'unable to upgrade because "%s"': 'unable to upgrade because "%s"', +'Unable to download the list of plugins': 'ŠŠµ могу Га ŠæŃ€ŠµŃƒŠ·Š¼ŠµŠ¼ списак помоћних моГула', +'unable to install plugin "%s"': 'не могу Га инсталирам помоћни моГул "%s"', +'unable to parse csv file': 'не могу Га расчланим csv Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ', +'unable to uninstall "%s"': 'не могу Га Геинсталирам "%s"', +'unable to upgrade because "%s"': 'не могу Га Š°Š¶ŃƒŃ€ŠøŠ¼Š°Š¼ због "%s"', 'uncheck all': 'uncheck all', 'Uninstall': 'Š”ŠµŠøŠ½ŃŃ‚Š°Š»ŠøŃ€Š°Ń˜', 'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'Š°Š¶ŃƒŃ€ŠøŃ€Š°Ń˜', 'update all languages': 'Š°Š¶ŃƒŃ€ŠøŃ€Š°Ń˜ све језике', -'Update:': 'Update:', -'Upgrade': 'Upgrade', -'upgrade now to %s': 'upgrade now to %s', +'Update:': 'ŠŠ¶ŃƒŃ€ŠøŃ€Š°Ń˜:', +'Upgrade': 'ŠŠ°Š“Š¾Š³Ń€Š°Š“ŃšŠ°', +'upgrade now to %s': 'Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½ на %s', 'upload': 'ŠžŃ‚ŠæŃ€ŠµŠ¼Šø', -'Upload': 'Upload', +'Upload': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø', 'Upload a package:': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø пакет:', 'Upload and install packed application': 'ŠŸŃ€ŠµŃƒŠ·Š¼Šø Šø ŠøŠ½ŃŃ‚Š°Š»ŠøŃ€Š°Ń˜ запаковану Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Ńƒ', 'upload file:': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ:', -'upload plugin file:': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø плагин Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ:', +'upload plugin file:': 'ŠæŃ€ŠµŃƒŠ·Š¼Šø Š“Š°Ń‚Š¾Ń‚ŠµŠŗŃƒ помоћног моГула:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'User': 'User', -'Username': 'Username', -'Users': 'Users', +'User': 'ŠšŠ¾Ń€ŠøŃŠ½ŠøŠŗ', +'Username': 'ŠšŠ¾Ń€ŠøŃŠ½ŠøŃ‡ŠŗŠ¾ име', +'Users': 'ŠšŠ¾Ń€ŠøŃŠ½ŠøŃ†Šø', 'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', 'variables': 'variables', 'Version': 'Š’ŠµŃ€Š·ŠøŃ˜Š°', 'Version %s.%s.%s (%s) %s': 'Š’ŠµŃ€Š·ŠøŃ˜Š° %s.%s.%s (%s) %s', -'Versioning': 'Versioning', -'Views': 'Views', -'views': 'views', -'Warning!': 'Warning!', -'WARNING:': 'WARNING:', +'Versioning': 'ŠšŃ€ŠµŠøŃ€Š°ŃšŠµ Š²ŠµŃ€Š·ŠøŃ˜Š°', +'Views': 'ŠŸŃ€ŠøŠŗŠ°Š·Šø', +'views': 'прикази', +'Warning!': 'Š£ŠæŠ¾Š·Š¾Ń€ŠµŃšŠµ!', +'WARNING:': 'Š£ŠŸŠžŠ—ŠžŠ Š•ŠŠŠ•:', 'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', 'Web Framework': 'Web Framework', -'web2py Admin Password': 'web2py Admin Password', -'web2py apps to deploy': 'web2py apps to deploy', +'web2py Admin Password': 'web2py аГминистраторска лозинка', +'web2py apps to deploy': 'web2py Š°ŠæŠ»ŠøŠŗŠ°Ń†ŠøŃ˜Š° за ŠøŠ½ŃŃ‚Š°Š»Š°Ń†ŠøŃ˜Ńƒ', 'web2py Debugger': 'web2py Debugger', 'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py је Š°Š¶ŃƒŃ€Š°Š½', 'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'web2py Recent Tweets', -'web2py upgrade': 'web2py upgrade', -'web2py upgraded; please restart it': 'web2py upgraded; please restart it', -'Working...': 'Working...', +'web2py upgrade': 'web2py Š½Š°Š“Š¾Š³Ń€Š°Š“ŃšŠ°', +'web2py upgraded; please restart it': 'web2py је Š°Š¶ŃƒŃ€ŠøŃ€Š°Š½; молим Га Ń€ŠµŃŃ‚Š°Ń€Ń‚ŃƒŃ˜ŠµŃ‚Šµ', +'Working...': 'Š˜Š·Š²Ń€ŃˆŠ°Š²Š°Š¼...', 'Wrap with Abbreviation': 'Wrap with Abbreviation', 'WSGI reference name': 'WSGI reference name', -'YES': 'YES', -'Yes': 'Yes', +'YES': 'ДА', +'Yes': 'Да', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', 'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', diff --git a/applications/admin/languages/sr-lt.py b/applications/admin/languages/sr-lt.py index 659cb84e..4f6ad085 100755 --- a/applications/admin/languages/sr-lt.py +++ b/applications/admin/languages/sr-lt.py @@ -5,64 +5,65 @@ '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN', '"User Exception" debug mode. ': '"User Exception" debug mode. ', '%s': '%s', -'%s %%{row} deleted': '%s %%{row} deleted', -'%s %%{row} updated': '%s %%{row} updated', +'%s %%{row} deleted': '%s %%{row} je izbrisan', +'%s %%{row} updated': '%s %%{row} je ažuriran', '%s selected': '%s selected', '%s students registered': '%s students registered', '%Y-%m-%d': '%d-%m-%Y', '%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S', '(requires internet access)': '(zahtijeva pristup internetu)', -'(requires internet access, experimental)': '(requires internet access, experimental)', -'(something like "it-it")': '(neÅ”to kao "it-it")', +'(requires internet access, experimental)': '(zahtijeva pristup internetu, eksperimentalno)', +'(something like "it-it")': '(na primjer "it-it")', '(version %s)': '(version %s)', '?': '?', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(datoteka **gluon/contrib/plural_rules/%s.py** nije pronađena)', -'Abort': 'Abort', +'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', +'Abort': 'Odustani', 'About': 'Informacije', 'About application': 'O aplikaciji', -'Accept Terms': 'Accept Terms', +'Accept Terms': 'Prihvatanje uslova koriŔćenja', 'Add breakpoint': 'Add breakpoint', 'Additional code for your application': 'Dodatni kod za aplikaciju', 'Admin design page': 'Admin design page', -'admin disabled because no admin password': 'admin disabled because no admin password', -'admin disabled because not supported on google app engine': 'admin disabled because not supported on google app engine', -'admin disabled because too many invalid login attempts': 'admin disabled because too many invalid login attempts', +'admin disabled because no admin password': 'administracija onemogućena jer nema lozinke', +'admin disabled because not supported on google app engine': 'administracija onemogućena na google app engine', +'admin disabled because too many invalid login attempts': 'administracija onemogućena jer zbog većeg broja neuspjelih pokuÅ”aja', 'admin disabled because unable to access password file': 'administracija onemogućena jer ne mogu pristupiti datoteci sa lozinkom', -'Admin is disabled because insecure channel': 'Admin is disabled because insecure channel', +'Admin is disabled because insecure channel': 'Administracija onemogućena zbog nesigurne veze', 'Admin language': 'Jezik administratora', 'Admin versioning page': 'Admin versioning page', -'administrative interface': 'administrativni interfejs', +'administrative interface': 'administrativno okruženje', 'Administrator Password:': 'Lozinka administratora:', 'and rename it:': 'i preimenuj u:', -'App does not exist or you are not authorized': 'App does not exist or you are not authorized', +'App does not exist or you are not authorized': 'Aplikacija ne postoji ili nemate prava pristupa', 'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin is disabled because insecure channel', -'Application': 'Application', -'application "%s" uninstalled': 'application "%s" uninstalled', +'appadmin is disabled because insecure channel': 'administracija onemogućena zbog nesigurne veze', +'Application': 'Aplikacija', +'application "%s" uninstalled': 'aplikacija "%s" je deinstalirana', 'Application cannot be generated in demo mode': 'Application cannot be generated in demo mode', -'application compiled': 'application compiled', -'Application exists already': 'Application exists already', -'application is compiled and cannot be designed': 'application is compiled and cannot be designed', +'application compiled': 'aplikacija je kompajlirana', +'Application exists already': 'Aplikacija već postoji', +'application is compiled and cannot be designed': 'aplikacija je kompajlirana i ne može se dalje uređivati', 'Application name:': 'Naziv aplikacije:', -'Application updated via git pull': 'Application updated via git pull', +'Application updated via git pull': 'Aplikacija ažurirana preko git pull', 'are not used': 'nije koriÅ”teno', 'are not used yet': 'nije joÅ” koriÅ”teno', -'Are you sure you want to delete file "%s"?': 'Are you sure you want to delete file "%s"?', -'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?', +'Are you sure you want to delete file "%s"?': 'Da li ste sigurni da želite izbrisati datoteku "%s"?', +'Are you sure you want to delete plugin "%s"?': 'Da li ste sigurni da želite izbrisati pomoćni modul "%s"?', 'Are you sure you want to delete this object?': 'Da li ste sigurni da želite obrisati?', -'Are you sure you want to uninstall application "%s"?': 'Are you sure you want to uninstall application "%s"?', -'Are you sure?': 'Are you sure?', +'Are you sure you want to uninstall application "%s"?': 'Da li ste sigurni da želite deinstalirati aplikaciju "%s"?', +'Are you sure?': 'Da li ste sigurni?', 'arguments': 'arguments', 'at char %s': 'kod slova %s', 'at line %s': 'na liniji %s', -'ATTENTION:': 'ATTENTION:', +'ATTENTION:': 'PAŽNJA:', 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.', -'ATTENTION: you cannot edit the running application!': 'ATTENTION: you cannot edit the running application!', +'ATTENTION: you cannot edit the running application!': 'ATTENTION: ne možete uređivati pokrenutu aplikaciju!', 'Autocomplete Python Code': 'Autocomplete Python Code', -'Available Databases and Tables': 'Available Databases and Tables', +'Available Databases and Tables': 'Dostupne baze podataka i tabele', 'back': 'nazad', -'Back to the plugins list': 'Back to the plugins list', +'Back to the plugins list': 'Nazad na listu pomoćnih modula', 'Back to wizard': 'Back to wizard', 'Basics': 'Osnove', 'Begin': 'Početak', @@ -71,19 +72,19 @@ 'breakpoints': 'breakpoints', 'Bulk Register': 'Bulk Register', 'Bulk Student Registration': 'Bulk Student Registration', -'Cache': 'Cache', -'cache': 'cache', +'Cache': 'KeÅ”', +'cache': 'keÅ”', 'Cache Cleared': 'Cache Cleared', 'Cache Keys': 'Cache Keys', 'cache, errors and sessions cleaned': 'keÅ”, greÅ”ke i sesije su obrisani', -'can be a git repo': 'može biti git repo', -'Cancel': 'Cancel', -'Cannot be empty': 'Cannot be empty', -'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:', -'cannot create file': 'cannot create file', +'can be a git repo': 'može biti git repozitorijum', +'Cancel': 'Otkaži', +'Cannot be empty': 'Ne može biti prazno', +'Cannot compile: there are errors in your app:': 'Ne mogu kompajlirati: greÅ”ka u aplikaciji:', +'cannot create file': 'ne mogu kreirati datoteku', 'cannot upload file "%(filename)s"': 'ne mogu otpremiti datoteku "%(filename)s"', -'Change Admin Password': 'Change Admin Password', 'Change admin password': 'Promijeni lozinku administratora', +'Change Admin Password': 'Promijeni lozinku administratora', 'change editor settings': 'change editor settings', 'Changelog': 'Changelog', 'check all': 'check all', @@ -91,66 +92,66 @@ 'Check to delete': 'Check to delete', 'Checking for upgrades...': 'Provjeravam mogućnost nadogradnje...', 'Clean': 'Pročisti', -'Clear': 'Clear', -'Clear CACHE?': 'Clear CACHE?', -'Clear DISK': 'Clear DISK', -'Clear RAM': 'Clear RAM', +'Clear': 'ObriÅ”i', +'Clear CACHE?': 'ObriÅ”i CACHE?', +'Clear DISK': 'ObriÅ”i DISK', +'Clear RAM': 'ObriÅ”i RAM', 'Click row to expand traceback': 'Click row to expand traceback', 'Click row to view a ticket': 'Click row to view a ticket', 'code': 'kod', -'Code listing': 'Code listing', +'Code listing': 'Prikaz koda', 'collapse/expand all': 'sakriti/prikazati sve', -'Command': 'Command', -'Comment:': 'Comment:', +'Command': 'Naredba', +'Comment:': 'Komentar:', 'Commit': 'Commit', 'Commit form': 'Commit form', 'Committed files': 'Committed files', 'Compile': 'Kompajliraj', -'Compile (all or nothing)': 'Compile (all or nothing)', -'Compile (skip failed views)': 'Compile (skip failed views)', -'compiled application removed': 'compiled application removed', -'Condition': 'Condition', -'continue': 'continue', +'Compile (all or nothing)': 'Kompajliraj (sve ili niÅ”ta)', +'Compile (skip failed views)': 'Kompajliraj (ignoriÅ”i greÅ”ke)', +'compiled application removed': 'kompajlirana aplikacija je uklonjena', +'Condition': 'Stanje', +'continue': 'nastavi', 'Controllers': 'Kontroleri', 'controllers': 'kontroleri', -'Count': 'Count', +'Count': 'Redni broj', 'Create': 'Kreiraj', 'create file with filename:': 'Kreiraj datoteku pod nazivom:', 'Create rules': 'Kreiraj pravila', -'Create/Upload': 'Create/Upload', +'Create/Upload': 'Kreiraj/Preuzmi', 'created by': 'izradio', 'Created by:': 'Created by:', 'Created On': 'Created On', 'Created on:': 'Created on:', 'crontab': 'crontab', -'Current request': 'Current request', -'Current response': 'Current response', -'Current session': 'Current session', +'Current request': 'Trenutni zahtjev', +'Current response': 'Trenutni odgovor', +'Current session': 'Trenutna sesija', 'currently running': 'trenutno pokrenut', 'currently saved or': 'trenutno sačuvano ili', 'data uploaded': 'data uploaded', -'Database': 'Database', +'Database': 'Baza podataka', 'Database %s select': 'Database %s select', -'Database administration': 'Database administration', 'database administration': 'administracija baze podataka', -'Database Administration (appadmin)': 'Database Administration (appadmin)', -'Date and Time': 'Date and Time', +'Database administration': 'Administracija baze podataka', +'Database Administration (appadmin)': 'Administracija baze podataka (appadmin)', +'Date and Time': 'Datum i vrijeme', 'db': 'db', -'Debug': 'Debug', +'Debug': 'Otkloni greÅ”ku', 'defines tables': 'definiÅ”e tabele', -'Delete': 'ObriÅ”i', -'delete': 'obriÅ”i', -'delete all checked': 'delete all checked', -'delete plugin': 'delete plugin', -'Delete this file (you will be asked to confirm deletion)': 'ObriÅ”i ovu datoteku (bićete upitani za potvrdu brisanja)', -'Delete:': 'Delete:', +'delete': 'IzbriÅ”i', +'Delete': 'IzbriÅ”i', +'delete all checked': 'izbriÅ”i sve označeno', +'delete plugin': 'izbriÅ”i pomoćni modul', +'Delete this file (you will be asked to confirm deletion)': 'IzbriÅ”i ovu datoteku (bićete upitani za potvrdu brisanja)', +'Delete:': 'IzbriÅ”i:', 'deleted after first hit': 'deleted after first hit', 'Demo': 'Demo', 'Deploy': 'Postavi', 'Deploy on Google App Engine': 'Postavi na Google App Engine', 'Deploy to OpenShift': 'Postavi na OpenShift', -'Deploy to pythonanywhere': 'Deploy to pythonanywhere', -'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere', +'Deploy to pythonanywhere': 'Postavi na pythonanywhere', +'Deploy to PythonAnywhere': 'Postavi na PythonAnywhere', 'Deployment form': 'Deployment form', 'Deployment Interface': 'Deployment Interface', 'Description:': 'Description:', @@ -160,33 +161,33 @@ 'direction: ltr': 'direction: ltr', 'directory not found': 'directory not found', 'Disable': 'Isključi', -'Disabled': 'Disabled', -'disabled in demo mode': 'disabled in demo mode', -'disabled in GAE mode': 'disabled in GAE mode', -'disabled in multi user mode': 'disabled in multi user mode', +'Disabled': 'Isključeno', +'disabled in demo mode': 'onemogućeno u demo modu', +'disabled in GAE mode': 'onemogućeno u GAE modu', +'disabled in multi user mode': 'onemogućeno u viÅ”ekorisničkom modu', 'DISK': 'DISK', 'Disk Cache Keys': 'Disk Cache Keys', 'Disk Cleared': 'Disk Cleared', 'Display line numbers': 'Display line numbers', 'DO NOT use the "Pack compiled" feature.': 'DO NOT use the "Pack compiled" feature.', -'docs': 'dokumentacija', 'Docs': 'Docs', +'docs': 'dokumentacija', 'done!': 'done!', 'Downgrade': 'Downgrade', -'Download .w2p': 'Download .w2p', -'Download as .exe': 'Download as .exe', +'Download .w2p': 'Preuzmi kao .w2p', +'Download as .exe': 'Preuzmi kao .exe', 'download layouts': 'preuzmi layouts', 'Download layouts from repository': 'Download layouts from repository', -'download plugins': 'preuzmi plugins', -'Download plugins from repository': 'Download plugins from repository', -'Edit': 'Uređivanje', +'download plugins': 'preuzmi pomoćne module', +'Download plugins from repository': 'Preuzmi pomoćne module iz repozitorijum', +'Edit': 'Uredi', 'edit all': 'uredi sve', 'Edit application': 'Uredi aplikaciju', 'edit controller': 'uredi controller', 'edit controller:': 'edit controller:', 'Edit current record': 'Edit current record', -'edit views:': 'uredi views:', -'Editing %s': 'Editing %s', +'edit views:': 'uredi prikaz:', +'Editing %s': 'Uređivanje %s', 'Editing file "%s"': 'Uređivanje datoteke "%s"', 'Editing Language file': 'Uređivanje jezičke datoteke', 'Editing Plural Forms File': 'Editing Plural Forms File', @@ -206,21 +207,21 @@ 'Exit Fullscreen': 'Exit Fullscreen', 'Expand Abbreviation': 'Expand Abbreviation', 'Expand Abbreviation (html files only)': 'Expand Abbreviation (html files only)', -'export as csv file': 'export as csv file', -'Exports:': 'Exports:', -'exposes': 'exposes', -'exposes:': 'exposes:', +'export as csv file': 'izvezi kao csv datoteku', +'Exports:': 'Izvozi:', +'exposes': 'prikazuje', +'exposes:': 'prikazuje:', 'extends': 'proÅ”iruje', 'failed to compile file because:': 'nisam mogao da kompajliram zbog:', 'failed to reload module because:': 'failed to reload module because:', 'File': 'Datoteka', -'file "%(filename)s" created': 'file "%(filename)s" created', -'file "%(filename)s" deleted': 'file "%(filename)s" deleted', -'file "%(filename)s" uploaded': 'file "%(filename)s" uploaded', -'file "%s" of %s restored': 'file "%s" of %s restored', +'file "%(filename)s" created': 'datoteka "%(filename)s" je kreirana', +'file "%(filename)s" deleted': 'datoteka "%(filename)s" je izbrisana', +'file "%(filename)s" uploaded': 'datoteka "%(filename)s" je otpremljena', +'file "%s" of %s restored': 'datoteka "%s" od %s je vraćeno u prethodno stanje', 'file changed on disk': 'file changed on disk', 'file does not exist': 'datoteka ne postoji', -'file not found': 'file not found', +'file not found': 'datoteka nije pronađena', 'file saved on %(time)s': 'file saved on %(time)s', 'file saved on %s': 'datoteka sačuvana na %s', 'filename': 'filename', @@ -242,11 +243,11 @@ 'Globals##debug': 'Globals##debug', 'Go to Matching Pair': 'Go to Matching Pair', 'go!': 'kreni!', -'Google App Engine Deployment Interface': 'Google App Engine Deployment Interface', +'Google App Engine Deployment Interface': 'Google App Engine instalaciono okruženje', 'Google Application Id': 'Google Application Id', 'Goto': 'Goto', -'graph model': 'graph model', 'Graph Model': 'Graph Model', +'graph model': 'graph model', 'Help': 'Pomoć', 'here': 'here', 'Hide/Show Translated strings': 'Sakriti/Prikazati prevedene riječi', @@ -260,7 +261,7 @@ 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.', 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.': 'if your application uses a database other than sqlite you will then have to configure its DAL in pythonanywhere.', 'import': 'import', -'Import/Export': 'Import/Export', +'Import/Export': 'Uvoz/Izvoz', 'In development, use the default Rocket webserver that is currently supported by this debugger.': 'In development, use the default Rocket webserver that is currently supported by this debugger.', 'includes': 'uključuje', 'Indent with tabs': 'Indent with tabs', @@ -270,49 +271,49 @@ 'Installed applications': 'Instalirane aplikacije', 'Interaction at %s line %s': 'Interaction at %s line %s', 'Interactive console': 'Interactive console', -'internal error': 'internal error', -'internal error: %s': 'internal error: %s', +'internal error': 'unutraÅ”nja greÅ”ka', +'internal error: %s': 'unutraÅ”nja greÅ”ka: %s', 'Internal State': 'Internal State', 'Invalid action': 'Invalid action', 'Invalid application name': 'Invalid application name', 'invalid circular reference': 'invalid circular reference', 'Invalid git repository specified.': 'Invalid git repository specified.', -'invalid password': 'invalid password', -'invalid password.': 'pogreÅ”na lozinka.', -'Invalid Query': 'Invalid Query', +'invalid password': 'Nevažeća lozinka', +'invalid password.': 'nevažeća lozinka.', +'Invalid Query': 'PogreÅ”an upit', 'invalid request': 'invalid request', 'Invalid request': 'Invalid request', 'invalid table names (auth_* tables already defined)': 'invalid table names (auth_* tables already defined)', -'invalid ticket': 'invalid ticket', +'invalid ticket': 'pogreÅ”an tiket', 'Key': 'Key', 'Key bindings': 'Prečice', -'Key bindings for ZenCoding Plugin': 'Prečice za for ZenCoding Plugin', -'Keyboard shortcuts': 'Keyboard shortcuts', +'Key bindings for ZenCoding Plugin': 'Prečice za ZenCoding Plugin', +'Keyboard shortcuts': 'Prečice na tastaturi', 'kill process': 'kill process', -'language file "%(filename)s" created/updated': 'language file "%(filename)s" created/updated', +'language file "%(filename)s" created/updated': 'jezička datoteka "%(filename)s" je kreirana/ažurirana', 'Language files (static strings) updated': 'Jezičke datoteke su ažurirane', -'languages': 'jezici', 'Languages': 'Jezici', +'languages': 'jezici', 'Last Revision': 'Last Revision', 'Last saved on:': 'Posljednja izmjena:', 'License for': 'Licenca za', -'License:': 'License:', -'Line Nr': 'Line Nr', -'Line number': 'Line number', -'lists by exception': 'lists by exception', -'lists by ticket': 'lists by ticket', -'Loading...': 'Loading...', +'License:': 'Licenca:', +'Line Nr': 'Linija broj', +'Line number': 'Linija broj', +'lists by exception': 'prikaži greÅ”ke', +'lists by ticket': 'prikaži tikete', +'Loading...': 'Preuzimam...', 'loading...': 'preuzimam...', -'Local Apps': 'Local Apps', +'Local Apps': 'Lokalne aplikacije', 'locals': 'locals', 'Locals##debug': 'Locals##debug', 'Login': 'Prijava', 'Login successful': 'Login successful', -'Login to the Administrative Interface': 'Prijava za administrativni interfejs', -'Login/Register': 'Login/Register', -'Logout': 'Izlaz', -'lost password': 'lost password', -'Main Menu': 'Main Menu', +'Login to the Administrative Interface': 'Prijava za administrativno okruženje', +'Login/Register': 'Prijava/Registracija', +'Logout': 'Odjava', +'lost password': 'izgubljena lozinka', +'Main Menu': 'Glavni meni', 'Manage': 'Manage', 'Manage %(action)s': 'Manage %(action)s', 'Manage Access Control': 'Manage Access Control', @@ -321,68 +322,68 @@ 'Manage Students': 'Manage Students', 'Match Pair': 'Match Pair', 'Memberships': 'Memberships', -'merge': 'merge', +'merge': 'spoji', 'Merge Lines': 'Spoji linije', -'Models': 'Models', -'models': 'models', +'Models': 'Modeli', +'models': 'modeli', 'Modified On': 'Modified On', -'Modules': 'Modules', -'modules': 'modules', -'Multi User Mode': 'Multi User Mode', -'new application "%s" created': 'new application "%s" created', -'new application "%s" imported': 'new application "%s" imported', -'New Application Wizard': 'Čarobnjak za nove aplikacije', +'modules': 'moduli', +'Modules': 'Moduli', +'Multi User Mode': 'ViÅ”ekorisnički režim rada', +'new application "%s" created': 'nova aplikacija "%s" je kreirana', +'new application "%s" imported': 'nova aplikacija "%s" je uvežena', 'New application wizard': 'Čarobnjak za nove aplikacije', -'new plugin installed': 'new plugin installed', -'New plugin installed: %s': 'New plugin installed: %s', -'New Record': 'New Record', -'new record inserted': 'new record inserted', +'New Application Wizard': 'Čarobnjak za nove aplikacije', +'new plugin installed': 'novi pomoćni modul je instaliran', +'New plugin installed: %s': 'Instaliran novi pomoćni modul: %s', +'New Record': 'Novi zapis', +'new record inserted': 'uneÅ”en novi zapis', 'New simple application': 'Nova jednostavna aplikacija', -'next': 'next', +'next': 'sledeći', 'next %s rows': 'next %s rows', 'Next Edit Point': 'Next Edit Point', -'NO': 'NO', -'no changes': 'no changes', -'No databases in this application': 'No databases in this application', -'No Interaction yet': 'No Interaction yet', -'no match': 'no match', -'no package selected': 'no package selected', -'no permission to uninstall "%s"': 'no permission to uninstall "%s"', +'NO': 'NE', +'no changes': 'nema promjena', +'No databases in this application': 'Nema baze podataka u aplikaciji', +'No Interaction yet': 'Nema joÅ” interakcije', +'no match': 'nema podudaranja', +'no package selected': 'paket nije odabran', +'no permission to uninstall "%s"': 'nemate ovlaÅ”tenje da deinstalirate "%s"', 'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder', 'Node:': 'Node:', -'Not Authorized': 'Not Authorized', -'Not supported': 'Not supported', +'Not Authorized': 'Nemate ovlaŔćenje', +'Not supported': 'Nije podržano', 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.': 'Note: If you receive an error with github status code of 128, ensure the system and account you are deploying from has a cooresponding ssh key configured in the openshift account.', "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.": "On production, you'll have to configure your webserver to use one process and multiple threads to use this debugger.", 'online designer': 'onlajn dizajner', -'Open new app in new window': 'Open new app in new window', -'OpenShift Deployment Interface': 'OpenShift Deployment Interface', +'Open new app in new window': 'Otvori novu aplikaciju u novom prozoru', +'OpenShift Deployment Interface': 'OpenShift instalaciono okruženje', 'OpenShift Output': 'OpenShift Output', -'or alternatively': 'or alternatively', +'or alternatively': 'ili alternativno', 'Or Get from URL:': 'Or Get from URL:', -'or import from csv file': 'or import from csv file', +'or import from csv file': 'ili uvezi pomoću csv datoteke', 'Original/Translation': 'Original/Prevod', -'Overview': 'Overview', -'Overwrite installed app': 'PrebriÅ”i postojeću aplikaciju', +'Overview': 'Pregled', +'Overwrite installed app': 'Zamjeni već postojeću aplikaciju', 'Pack all': 'Zapakuj sve', -'Pack compiled': 'Pack compiled', -'Pack custom': 'Pack custom', -'pack plugin': 'pack plugin', -'password changed': 'password changed', -'Past revisions': 'Past revisions', +'Pack compiled': 'Zapakuj kompajlirano', +'Pack custom': 'Prilagođeno pakovanje', +'pack plugin': 'zapakuj pomoćni modul', +'password changed': 'lozinka je promijenjena', +'Past revisions': 'Prethodne korekcije', 'Path to appcfg.py': 'Path to appcfg.py', 'Path to local openshift repo root.': 'Path to local openshift repo root.', -'Peeking at file': 'Peeking at file', -'Permission': 'Permission', -'Permissions': 'Permissions', -'Please': 'Please', +'Peeking at file': 'Pregled datoteke', +'Permission': 'Dozvola', +'Permissions': 'Dozvole', +'Please': 'Molim', 'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...', -'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted', -'Plugin "%s" in application': 'Plugin "%s" in application', +'plugin "%(plugin)s" deleted': 'Pomoćni modul "%(plugin)s" je izbrisan', +'Plugin "%s" in application': 'Pomoćni modul "%s" u aplikaciji', 'plugin not specified': 'plugin not specified', -'Plugin page': 'Plugin page', -'plugins': 'plugins', -'Plugins': 'Plugins', +'Plugin page': 'Stranica pomoćnih modula', +'Plugins': 'Pomoćni moduli', +'plugins': 'pomoćni moduli', 'Plural Form #%s': 'Plural Form #%s', 'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Omogućio', @@ -390,18 +391,18 @@ 'Preferences saved on session only': 'Preferences saved on session only', 'previous %s rows': 'previous %s rows', 'Previous Edit Point': 'Previous Edit Point', -'Private files': 'Private files', -'private files': 'private files', +'private files': 'privatne datoteke', +'Private files': 'Privatne datoteke', 'Project Progress': 'Napredak projekta', 'Pull': 'Pull', 'Pull failed, certain files could not be checked out. Check logs for details.': 'Pull failed, certain files could not be checked out. Check logs for details.', 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.': 'Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.', 'Push': 'Push', 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.': 'Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.', -'pygraphviz library not found': 'pygraphviz library not found', +'pygraphviz library not found': 'pygraphviz biblioteka nije pronađena', 'PythonAnywhere Apps': 'PythonAnywhere Apps', 'PythonAnywhere Password': 'PythonAnywhere Password', -'Query:': 'Query:', +'Query:': 'Upit:', 'RAM': 'RAM', 'RAM Cache Keys': 'RAM Cache Keys', 'Ram Cleared': 'Ram Cleared', @@ -416,7 +417,7 @@ 'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s', 'Replace': 'Zamijeni', 'Replace All': 'Zamijeni sve', -'Repository (%s)': 'Repository (%s)', +'Repository (%s)': 'Repozitorijum (%s)', 'request': 'request', 'requires distutils, but not installed': 'requires distutils, but not installed', 'requires python-git, but not installed': 'requires python-git, but not installed', @@ -430,78 +431,78 @@ 'reverted to revision %s': 'reverted to revision %s', 'Revision %s': 'Revision %s', 'Revision:': 'Revision:', -'Role': 'Role', -'Roles': 'Roles', -'Rows in Table': 'Rows in Table', +'Role': 'Uloga', +'Roles': 'Uloge', +'Rows in Table': 'Zapisi u tabeli', 'Rows selected': 'Rows selected', 'rules are not defined': 'pravila nisu definisana', 'rules:': 'pravila:', -'Run tests': 'Run tests', -'Run tests in this file': 'Run tests in this file', +'Run tests': 'Pokreni testove', +'Run tests in this file': 'Pokreni testove u datoteci', "Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", 'Running on %s': 'Pokrenuto na %s', 'Save': 'Sačuvaj', -'Save file:': 'Save file:', -'Save file: %s': 'Save file: %s', -'Save model as...': 'Save model as...', -'Save via Ajax': 'Sačuvaj via Ajax', +'Save file:': 'Sačuvaj datoteku:', +'Save file: %s': 'Sačuvaj datoteku: %s', +'Save model as...': 'Sačuvaj model kao...', +'Save via Ajax': 'Sačuvaj preko Ajax', 'Saved file hash:': 'Sačuvano kao haÅ”:', -'Screenshot %s': 'Screenshot %s', -'Search': 'Search', -'Select Files to Package': 'Select Files to Package', +'Screenshot %s': 'Snimak ekrana %s', +'Search': 'Pretraga', +'Select Files to Package': 'Odaberi datoteke za pakovanje', 'session': 'sesija', -'session expired': 'sesija istekla', -'Session saved correctly': 'Session saved correctly', +'session expired': 'sesija je istekla', +'Session saved correctly': 'Sesija je uredno sačuvana', 'Session saved on session only': 'Session saved on session only', 'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s', 'shell': 'shell', -'Showing %s to %s of %s %s found': 'Showing %s to %s of %s %s found', +'Showing %s to %s of %s %s found': 'Prikazujem %s do %s od %s %s pronađenih', 'Singular Form': 'Singular Form', -'Site': 'Sajt', +'Site': 'Početna', 'Size of cache:': 'Size of cache:', -'skip to generate': 'skip to generate', -'some files could not be removed': 'some files could not be removed', +'skip to generate': 'preskoči generisanje', +'some files could not be removed': 'neke datoteke ne mogu biti uklonjene', 'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying', -'Sorry, could not find mercurial installed': 'Sorry, could not find mercurial installed', +'Sorry, could not find mercurial installed': 'Žalim, mercurial nije instaliran', 'source : db': 'source : db', 'source : filesystem': 'source : filesystem', 'Start a new app': 'Pokreni novu aplikaciju', 'Start searching': 'Pokreni pretragu', 'Start wizard': 'Pokreni čarobnjaka', 'state': 'state', -'Static': 'Static', 'static': 'static', +'Static': 'Static', 'Static files': 'Static files', 'Statistics': 'Statistics', 'Step': 'Korak', -'step': 'step', +'step': 'korak', 'stop': 'stop', -'submit': 'submit', 'Submit': 'Prihvati', +'submit': 'prihvati', 'successful': 'uspjeÅ”an', -'switch to : db': 'switch to : db', -'switch to : filesystem': 'switch to : filesystem', +'switch to : db': 'pređi na : db', +'switch to : filesystem': 'pređi na : filesystem', 'Tab width (# characters)': 'Tab width (# characters)', -'Table': 'Table', +'Table': 'Tabela', 'Temporary': 'Temporary', 'test': 'test', -'Testing application': 'Testing application', +'Testing application': 'Testiranje aplikacije', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.', 'The app exists, was created by wizard, continue to overwrite!': 'The app exists, was created by wizard, continue to overwrite!', 'The app exists, was NOT created by wizard, continue to overwrite!': 'The app exists, was NOT created by wizard, continue to overwrite!', 'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', -'Theme': 'Theme', -'There are no controllers': 'There are no controllers', -'There are no models': 'There are no models', -'There are no modules': 'There are no modules', -'There are no plugins': 'There are no plugins', -'There are no private files': 'There are no private files', -'There are no static files': 'There are no static files', -'There are no translators': 'There are no translators', +'Theme': 'Teme', +'There are no controllers': 'Nema kontolera', +'There are no models': 'Nema modela', +'There are no modules': 'Nema modula', +'There are no plugins': 'Nema pomoćnih modula', +'There are no private files': 'Nema privatnih datoteka', +'There are no static files': 'Nema statičnih datoteka', +'There are no translators': 'Nema prevodioca', 'There are no translators, only default language is supported': 'There are no translators, only default language is supported', -'There are no views': 'There are no views', +'There are no views': 'Nema stranica prikaza', 'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'These files are served without processing, your images go here', "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.": "This debugger may not work properly if you don't have a threaded webserver or you're using multiple daemon processes.", @@ -512,9 +513,9 @@ 'this page to see if a breakpoint was hit and debug interaction is required.': 'this page to see if a breakpoint was hit and debug interaction is required.', 'This will pull changes from the remote repo for application "%s"?': 'This will pull changes from the remote repo for application "%s"?', 'This will push changes to the remote repo for application "%s".': 'This will push changes to the remote repo for application "%s".', -'Ticket': 'Ticket', -'Ticket ID': 'Ticket ID', -'Ticket Missing': 'Ticket nedostaje', +'Ticket': 'Tiket', +'Ticket ID': 'Tiket ID', +'Ticket Missing': 'Nedostaje tiket', 'Time in Cache (h:m:s)': 'Time in Cache (h:m:s)', 'to previous version.': 'na prethodnu verziju.', 'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]', @@ -525,67 +526,67 @@ 'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'Translation strings for the application': 'Riječi u aplikaciji koje treba prevesti', -'try something like': 'try something like', -'Try the mobile interface': 'Probaj mobilni interfejs', -'try view': 'try view', +'try something like': 'na primjer', +'Try the mobile interface': 'Probaj mobilno okruženje', +'try view': 'probaj prikaz', 'Type PDB debugger command in here and hit Return (Enter) to execute it.': 'Type PDB debugger command in here and hit Return (Enter) to execute it.', 'Type some Python code in here and hit Return (Enter) to execute it.': 'Type some Python code in here and hit Return (Enter) to execute it.', -'Unable to check for upgrades': 'Unable to check for upgrades', +'Unable to check for upgrades': 'Ne mogu da provjerim mogućnost nadogradnje', 'unable to create application "%s"': 'unable to create application "%s"', -'unable to delete file "%(filename)s"': 'unable to delete file "%(filename)s"', -'unable to delete file plugin "%(plugin)s"': 'unable to delete file plugin "%(plugin)s"', -'Unable to determine the line number!': 'Unable to determine the line number!', -'Unable to download app because:': 'Unable to download app because:', +'unable to delete file "%(filename)s"': 'ne mogu izbrisati datoteku "%(filename)s"', +'unable to delete file plugin "%(plugin)s"': 'ne mogu izbrisati pomoćni modul "%(plugin)s"', +'Unable to determine the line number!': 'Ne mogu da utvrdim broj reda!', +'Unable to download app because:': 'Ne mogu da preuzmem aplikaciju zbog:', 'unable to download layout': 'unable to download layout', 'unable to download plugin: %s': 'unable to download plugin: %s', -'Unable to download the list of plugins': 'Unable to download the list of plugins', -'unable to install plugin "%s"': 'unable to install plugin "%s"', -'unable to parse csv file': 'unable to parse csv file', -'unable to uninstall "%s"': 'unable to uninstall "%s"', -'unable to upgrade because "%s"': 'unable to upgrade because "%s"', +'Unable to download the list of plugins': 'Ne mogu da preuzmem spisak pomoćnih modula', +'unable to install plugin "%s"': 'ne mogu da instaliram pomoćni modul "%s"', +'unable to parse csv file': 'ne mogu da rasčlanim csv datoteku', +'unable to uninstall "%s"': 'ne mogu da deinstaliram "%s"', +'unable to upgrade because "%s"': 'ne mogu da ažurimam zbog "%s"', 'uncheck all': 'uncheck all', 'Uninstall': 'Deinstaliraj', 'Unsupported webserver working mode: %s': 'Unsupported webserver working mode: %s', 'update': 'ažuriraj', 'update all languages': 'ažuriraj sve jezike', -'Update:': 'Update:', -'Upgrade': 'Upgrade', -'upgrade now to %s': 'upgrade now to %s', +'Update:': 'Ažuriraj:', +'Upgrade': 'Nadogradnja', +'upgrade now to %s': 'ažuriran na %s', 'upload': 'Otpremi', -'Upload': 'Upload', +'Upload': 'Preuzmi', 'Upload a package:': 'Preuzmi paket:', 'Upload and install packed application': 'Preuzmi i instaliraj zapakovanu aplikaciju', 'upload file:': 'preuzmi datoteku:', -'upload plugin file:': 'preuzmi plugin datoteku:', +'upload plugin file:': 'preuzmi datoteku pomoćnog modula:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.', -'User': 'User', -'Username': 'Username', -'Users': 'Users', +'User': 'Korisnik', +'Username': 'Korisničko ime', +'Users': 'Korisnici', 'Using the shell may lock the database to other users of this app.': 'Using the shell may lock the database to other users of this app.', -'variables': 'variables', +'variables': 'promenljive', 'Version': 'Verzija', 'Version %s.%s.%s (%s) %s': 'Verzija %s.%s.%s (%s) %s', -'Versioning': 'Versioning', -'Views': 'Views', -'views': 'views', -'Warning!': 'Warning!', -'WARNING:': 'WARNING:', -'WARNING: The following views could not be compiled:': 'WARNING: The following views could not be compiled:', +'Versioning': 'Kreiranje verzija', +'views': 'prikazi', +'Views': 'Prikazi', +'Warning!':A 'Upozorenje!', +'WARNING:': 'UPOZORENJE:', +'WARNING: The following views could not be compiled:': 'WARNING: Sledeći prikazi ne mogu biti kompajlirani:', 'Web Framework': 'Web Framework', -'web2py Admin Password': 'web2py Admin Password', -'web2py apps to deploy': 'web2py apps to deploy', +'web2py Admin Password': 'web2py administratorska lozinka', +'web2py apps to deploy': 'web2py aplikacija za instalaciju', 'web2py Debugger': 'web2py Debugger', 'web2py downgrade': 'web2py downgrade', 'web2py is up to date': 'web2py je ažuran', 'web2py online debugger': 'web2py online debugger', 'web2py Recent Tweets': 'web2py Recent Tweets', -'web2py upgrade': 'web2py upgrade', -'web2py upgraded; please restart it': 'web2py upgraded; please restart it', -'Working...': 'Working...', +'web2py upgrade': 'web2py nadogradnja', +'web2py upgraded; please restart it': 'web2py je ažuriran; molim da restartujete', +'Working...': 'IzvrÅ”avam...', 'Wrap with Abbreviation': 'Wrap with Abbreviation', -'WSGI reference name': 'WSGI reference name', -'YES': 'YES', -'Yes': 'Yes', +'WSGI reference name': 'WSGI referentni naziv', +'YES': 'DA', +'Yes': 'Da', 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button': 'You can also set and remove breakpoint in the edit window, using the Toggle Breakpoint button', 'You can inspect variables using the console below': 'You can inspect variables using the console below', 'You have one more login attempt before you are locked out': 'You have one more login attempt before you are locked out', From cee7c87859c298629e69cc19be273dd4c4d2f806 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 27 Sep 2016 16:41:17 -0500 Subject: [PATCH 120/436] fixed error in serbian tranlation file --- applications/admin/languages/sr-lt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/languages/sr-lt.py b/applications/admin/languages/sr-lt.py index 4f6ad085..a807b8cc 100755 --- a/applications/admin/languages/sr-lt.py +++ b/applications/admin/languages/sr-lt.py @@ -569,7 +569,7 @@ 'Versioning': 'Kreiranje verzija', 'views': 'prikazi', 'Views': 'Prikazi', -'Warning!':A 'Upozorenje!', +'Warning!': 'Upozorenje!', 'WARNING:': 'UPOZORENJE:', 'WARNING: The following views could not be compiled:': 'WARNING: Sledeći prikazi ne mogu biti kompajlirani:', 'Web Framework': 'Web Framework', From 40263576898994056c04b105854fa8afd02cee28 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 27 Sep 2016 16:49:20 -0500 Subject: [PATCH 121/436] fixed undefined var in web2py.js --- applications/admin/static/js/web2py.js | 4 ++-- applications/examples/static/js/web2py.js | 6 +++--- applications/welcome/static/js/web2py.js | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index f3d14b29..7853e8cb 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -617,8 +617,8 @@ } if (confirm_message) { if (confirm_message == 'default') { - confirm_message = w2p_ajax_confirm_message || - 'Are you sure you want to delete this object?'; + confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ? + w2p_ajax_confirm_message : 'Are you sure you want to delete this object?'; } if (!web2py.confirm(confirm_message)) { web2py.stopEverything(e); diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 15ff515c..7853e8cb 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -562,7 +562,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -617,8 +617,8 @@ } if (confirm_message) { if (confirm_message == 'default') { - confirm_message = w2p_ajax_confirm_message || - 'Are you sure you want to delete this object?'; + confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ? + w2p_ajax_confirm_message : 'Are you sure you want to delete this object?'; } if (!web2py.confirm(confirm_message)) { web2py.stopEverything(e); diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 2bedd1c5..7853e8cb 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -201,7 +201,7 @@ showsTime: true, timeFormat: '24' }); - $(this).prop('autocomplete', 'off'); + $(this).attr('autocomplete', 'off'); $(this).data('w2p_datetime', 1); $(this).trigger('click'); } @@ -218,7 +218,7 @@ showsTime: false }); $(this).data('w2p_date', 1); - $(this).prop('autocomplete', 'off'); + $(this).attr('autocomplete', 'off'); $(this).trigger('click'); } }); @@ -227,7 +227,7 @@ if (web2py.isUndefined(active)) { $(this).timeEntry({ spinnerImage: '' - }).prop('autocomplete', 'off'); + }).attr('autocomplete', 'off'); $(this).data('w2p_time', 1); } }); @@ -617,8 +617,8 @@ } if (confirm_message) { if (confirm_message == 'default') { - confirm_message = w2p_ajax_confirm_message || - 'Are you sure you want to delete this object?'; + confirm_message = !web2py.isUndefined(w2p_ajax_confirm_message) ? + w2p_ajax_confirm_message : 'Are you sure you want to delete this object?'; } if (!web2py.confirm(confirm_message)) { web2py.stopEverything(e); From 78cc6d69a29921d7dfd6fa207e8220f47eabeed8 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 27 Sep 2016 17:09:05 -0500 Subject: [PATCH 122/436] fixed web2py_ajax.html and escaping (not backward compatible fix) --- applications/admin/views/web2py_ajax.html | 10 +++++----- applications/examples/views/web2py_ajax.html | 9 +++++---- applications/welcome/views/web2py_ajax.html | 10 +++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/applications/admin/views/web2py_ajax.html b/applications/admin/views/web2py_ajax.html index 417859e9..357d8050 100644 --- a/applications/admin/views/web2py_ajax.html +++ b/applications/admin/views/web2py_ajax.html @@ -1,10 +1,10 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/examples/views/web2py_ajax.html b/applications/examples/views/web2py_ajax.html index a0ba6f43..357d8050 100644 --- a/applications/examples/views/web2py_ajax.html +++ b/applications/examples/views/web2py_ajax.html @@ -1,9 +1,10 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/welcome/views/web2py_ajax.html b/applications/welcome/views/web2py_ajax.html index 6fefd3a0..357d8050 100644 --- a/applications/welcome/views/web2py_ajax.html +++ b/applications/welcome/views/web2py_ajax.html @@ -1,10 +1,10 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) From 3ab30295ed037e8bcad086b5897a955cb487a4d6 Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Wed, 28 Sep 2016 13:10:42 +0200 Subject: [PATCH 123/436] Updated pyDAL to 16.9 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 012dbe3e..12bc6d97 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 012dbe3eed0404bc5d5516018f2bf9b3eb5b17ab +Subproject commit 12bc6d97402acce462c1193f57bbba4afde7c3c3 From a31cf09dd3d2dc682f58ee77529bd41ba644106e Mon Sep 17 00:00:00 2001 From: ilvalle Date: Wed, 28 Sep 2016 20:51:44 +0200 Subject: [PATCH 124/436] minor compileapp refactor --- gluon/compileapp.py | 53 +++++++++++++++++++++++---------------------- gluon/globals.py | 6 ++--- gluon/main.py | 4 ++-- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 23e66628..32c02177 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -230,8 +230,8 @@ def LOAD(c=None, f='index', args=None, vars=None, if isinstance(page, dict): other_response._vars = page other_response._view_environment.update(page) - run_view_in(other_response._view_environment) - page = other_response.body.getvalue() + page = run_view_in(other_response._view_environment) + current.request, current.response = original_request, original_response js = None if ajax_trap: @@ -309,8 +309,8 @@ class LoadFactory(object): if isinstance(page, dict): other_response._vars = page other_response._view_environment.update(page) - run_view_in(other_response._view_environment) - page = other_response.body.getvalue() + page = run_view_in(other_response._view_environment) + current.request, current.response = original_request, original_response js = None if ajax_trap: @@ -577,13 +577,11 @@ def run_models_in(environment): if not regex.search(fname) and c != 'appadmin': continue elif compiled: - code = getcfs(model, model, lambda: read_pyc(model)) + f = lambda: read_pyc(model) else: - code = getcfs(model, model, - lambda: compile2(read_file(model), model)) - - restricted(code, environment, layer=model) - + f = lambda: compile2(read_file(model), model) + ccode = getcfs(model, model, f) + restricted(ccode, environment, layer=model) def run_controller_in(controller, function, environment): """ @@ -594,17 +592,17 @@ def run_controller_in(controller, function, environment): # if compiled should run compiled! folder = current.request.folder - path = pjoin(folder, 'compiled') + cpath = pjoin(folder, 'compiled') badc = 'invalid controller (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function) - if os.path.exists(path): - filename = pjoin(path, 'controllers.%s.%s.pyc' + if os.path.exists(cpath): + filename = pjoin(cpath, 'controllers.%s.%s.pyc' % (controller, function)) if not os.path.exists(filename): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) - code = getcfs(filename, filename, lambda: read_pyc(filename)) + ccode = getcfs(filename, filename, lambda: read_pyc(filename)) elif function == '_TEST': # TESTING: adjust the path to include site packages from gluon.settings import global_settings @@ -623,6 +621,7 @@ def run_controller_in(controller, function, environment): environment['__symbols__'] = environment.keys() code = read_file(filename) code += TEST_CODE + ccode = compile2(code, filename) else: filename = pjoin(folder, 'controllers/%s.py' % controller) @@ -636,11 +635,11 @@ def run_controller_in(controller, function, environment): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) - code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function) - layer = filename + ':' + function - code = getcfs(layer, filename, lambda: compile2(code, layer)) + code = "%s\nresponse._vars=response._caller(%s)" % (code, function) + layer = "%s:%s" % (filename, function) + ccode = getcfs(layer, filename, lambda: compile2(code, layer)) - restricted(code, environment, layer=filename) + restricted(ccode, environment, layer=filename) response = current.response vars = response._vars if response.postprocessing: @@ -663,9 +662,10 @@ def run_view_in(environment): response = current.response view = environment['response'].view folder = request.folder - path = pjoin(folder, 'compiled') + cpath = pjoin(folder, 'compiled') badv = 'invalid view (%s)' % view patterns = response.get('generic_patterns') + layer = None if patterns: regex = re_compile('|'.join(map(fnmatch.translate, patterns))) short_action = '%(controller)s/%(function)s.%(extension)s' % request @@ -678,10 +678,10 @@ def run_view_in(environment): layer = 'file stream' else: filename = pjoin(folder, 'views', view) - if os.path.exists(path): # compiled views + if os.path.exists(cpath): # compiled views x = view.replace('/', '.') files = ['views.%s.pyc' % x] - is_compiled = os.path.exists(pjoin(path, files[0])) + is_compiled = os.path.exists(pjoin(cpath, files[0])) # Don't use a generic view if the non-compiled view exists. if is_compiled or (not is_compiled and not os.path.exists(filename)): if allow_generic: @@ -693,11 +693,11 @@ def run_view_in(environment): files.append('views.generic.pyc') # end backward compatibility code for f in files: - compiled = pjoin(path, f) + compiled = pjoin(cpath, f) if os.path.exists(compiled): - code = getcfs(compiled, compiled, lambda: read_pyc(compiled)) - restricted(code, environment, layer=compiled) - return + ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled)) + layer = compiled + break if not os.path.exists(filename) and allow_generic: view = 'generic.' + request.extension filename = pjoin(folder, 'views', view) @@ -713,7 +713,8 @@ def run_view_in(environment): context=environment), layer)) restricted(ccode, environment, layer=layer) - + # parse_template saves everything in response body + return environment['response'].body.getvalue() def remove_compiled_application(folder): """ diff --git a/gluon/globals.py b/gluon/globals.py index ae4e0bc9..29fb3f0d 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -439,13 +439,11 @@ class Response(Storage): from gluon._compat import StringIO (obody, oview) = (self.body, self.view) (self.body, self.view) = (StringIO(), view) - run_view_in(self._view_environment) - page = self.body.getvalue() + page = run_view_in(self._view_environment) self.body.close() (self.body, self.view) = (obody, oview) else: - run_view_in(self._view_environment) - page = self.body.getvalue() + page = run_view_in(self._view_environment) return page def include_meta(self): diff --git a/gluon/main.py b/gluon/main.py index 971c48a7..a830d7a8 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -180,8 +180,8 @@ def serve_controller(request, response, session): if isinstance(page, dict): response._vars = page response._view_environment.update(page) - run_view_in(response._view_environment) - page = response.body.getvalue() + page = run_view_in(response._view_environment) + # logic to garbage collect after exec, not always, once every 100 requests global requests requests = ('requests' in globals()) and (requests + 1) % 100 or 0 From 3a6df38d62fc975c1041c46643fc3f492c1b5501 Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 29 Sep 2016 00:03:45 +0200 Subject: [PATCH 125/436] fixes #1407 passing a properly escaped string --- applications/admin/views/web2py_ajax.html | 12 +++++++----- applications/examples/views/web2py_ajax.html | 11 ++++++----- applications/welcome/views/web2py_ajax.html | 12 +++++++----- gluon/tests/test_appadmin.py | 1 + 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/applications/admin/views/web2py_ajax.html b/applications/admin/views/web2py_ajax.html index 357d8050..28ec0e02 100644 --- a/applications/admin/views/web2py_ajax.html +++ b/applications/admin/views/web2py_ajax.html @@ -1,10 +1,12 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/examples/views/web2py_ajax.html b/applications/examples/views/web2py_ajax.html index 357d8050..0f064de4 100644 --- a/applications/examples/views/web2py_ajax.html +++ b/applications/examples/views/web2py_ajax.html @@ -1,10 +1,11 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/applications/welcome/views/web2py_ajax.html b/applications/welcome/views/web2py_ajax.html index 357d8050..28ec0e02 100644 --- a/applications/welcome/views/web2py_ajax.html +++ b/applications/welcome/views/web2py_ajax.html @@ -1,10 +1,12 @@ {{ response.files.insert(0,URL('static','js/jquery.js')) diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py index c5e7402e..4b502c12 100644 --- a/gluon/tests/test_appadmin.py +++ b/gluon/tests/test_appadmin.py @@ -31,6 +31,7 @@ class TestAppAdmin(unittest.TestCase): def setUp(self): from gluon.globals import Request, Response, Session, current from gluon.html import A, DIV, FORM, MENU, TABLE, TR, INPUT, URL, XML + from gluon.html import ASSIGNJS from gluon.validators import IS_NOT_EMPTY from gluon.compileapp import LOAD from gluon.http import HTTP, redirect From a1154a6f423347b580381043cd794945de081def Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Sep 2016 09:59:08 -0500 Subject: [PATCH 126/436] reverted 28db54d0e884fd454082ef05a3757cef32240d53 changes to compileapp.py --- gluon/compileapp.py | 69 +++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 23e66628..459c2848 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -430,10 +430,13 @@ def build_environment(request, response, session, store_current=True): current.T = t current.cache = c + global __builtins__ if is_jython: # jython hack - global __builtins__ __builtins__ = mybuiltin() - + elif is_pypy: # apply the same hack to pypy too + __builtins__ = mybuiltin() + elif PY2: + __builtins__['__import__'] = builtin.__import__ # WHY? environment['request'] = request environment['response'] = response environment['session'] = session @@ -441,6 +444,7 @@ def build_environment(request, response, session, store_current=True): lambda name, reload=False, app=request.application:\ local_import_aux(name, reload, app) BaseAdapter.set_folder(pjoin(request.folder, 'databases')) + response._view_environment = copy.copy(environment) custom_import_install() return environment @@ -483,7 +487,7 @@ def compile_views(folder, skip_failed_views=False): else: raise Exception("%s in %s" % (e, fname)) else: - filename = 'views.%s.py' % fname.replace(os.path.sep, '.') + filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_') filename = pjoin(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) @@ -499,7 +503,7 @@ def compile_models(folder): path = pjoin(folder, 'models') for fname in listdir(path, '.+\.py$'): data = read_file(pjoin(path, fname)) - modelfile = 'models.'+fname.replace(os.path.sep, '.') + modelfile = 'models.'+fname.replace(os.path.sep,'.') filename = pjoin(folder, 'compiled', modelfile) mktree(filename) write_file(filename, data) @@ -578,10 +582,11 @@ def run_models_in(environment): continue elif compiled: code = getcfs(model, model, lambda: read_pyc(model)) - else: + elif is_gae: code = getcfs(model, model, lambda: compile2(read_file(model), model)) - + else: + code = getcfs(model, model, None) restricted(code, environment, layer=model) @@ -605,6 +610,7 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = getcfs(filename, filename, lambda: read_pyc(filename)) + restricted(code, environment, layer=filename) elif function == '_TEST': # TESTING: adjust the path to include site packages from gluon.settings import global_settings @@ -623,6 +629,7 @@ def run_controller_in(controller, function, environment): environment['__symbols__'] = environment.keys() code = read_file(filename) code += TEST_CODE + restricted(code, environment, layer=filename) else: filename = pjoin(folder, 'controllers/%s.py' % controller) @@ -637,10 +644,10 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function) - layer = filename + ':' + function - code = getcfs(layer, filename, lambda: compile2(code, layer)) - - restricted(code, environment, layer=filename) + if is_gae: + layer = filename + ':' + function + code = getcfs(layer, filename, lambda: compile2(code, layer)) + restricted(code, environment, filename) response = current.response vars = response._vars if response.postprocessing: @@ -675,7 +682,7 @@ def run_view_in(environment): if not isinstance(view, str): ccode = parse_template(view, pjoin(folder, 'views'), context=environment) - layer = 'file stream' + restricted(ccode, environment, 'file stream') else: filename = pjoin(folder, 'views', view) if os.path.exists(path): # compiled views @@ -706,13 +713,16 @@ def run_view_in(environment): rewrite.THREAD_LOCAL.routes.error_message % badv, web2py_error=badv) layer = filename - # Cache the compiled template - ccode = getcfs(layer, filename, - lambda: compile2(parse_template(view, - pjoin(folder, 'views'), - context=environment), - layer)) - restricted(ccode, environment, layer=layer) + if is_gae: + ccode = getcfs(layer, filename, + lambda: compile2(parse_template(view, + pjoin(folder, 'views'), + context=environment), layer)) + else: + ccode = parse_template(view, + pjoin(folder, 'views'), + context=environment) + restricted(ccode, environment, layer) def remove_compiled_application(folder): @@ -738,3 +748,26 @@ def compile_application(folder, skip_failed_views=False): compile_controllers(folder) failed_views = compile_views(folder, skip_failed_views) return failed_views + + +def test(): + """ + Example:: + + >>> import traceback, types + >>> environment={'x':1} + >>> open('a.py', 'w').write('print 1/x') + >>> save_pyc('a.py') + >>> os.unlink('a.py') + >>> if type(read_pyc('a.pyc'))==types.CodeType: print 'code' + code + >>> exec read_pyc('a.pyc') in environment + 1 + """ + + return + + +if __name__ == '__main__': + import doctest + doctest.testmod() From d03337a737727de885db2ac4734352045f3b5378 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Sep 2016 11:24:55 -0500 Subject: [PATCH 127/436] compile views with . separator anot _ seperator --- applications/examples/static/css/stupid.css | 6 +++--- fabfile.py | 20 ++++++++++++++++++++ gluon/compileapp.py | 3 ++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/applications/examples/static/css/stupid.css b/applications/examples/static/css/stupid.css index 80bf9b09..3e68bedd 100644 --- a/applications/examples/static/css/stupid.css +++ b/applications/examples/static/css/stupid.css @@ -54,8 +54,8 @@ header, main, footer {display:block; with:100%} /* IE fix */ .right {right:0; text-align:right} .middle div {vertical-align:middle} .bottom div {vertical-align:bottom} -.xscroll {overflow-x:scroll} -.yscroll {overflow-y:scroll} +.xscroll {overflow-x:scroll !important} +.yscroll {overflow-y:scroll !important} .nowrap {white-space:nowrap; overflow-x:hidden} .fill {width:100%} .lifted {box-shadow:5px 5px 10px #666} @@ -75,7 +75,7 @@ input:invalid, input.error, textarea:invalid, textarea.error {background: #ffdfd /*** grid ***/ .container {margin-right:-20px} .container>.quarter, .container>.half, .container>.third, .container>.twothirds, .container>.threequarters {display:inline-block; padding: 0 20px 0 0; vertical-align:top} -.container>.fill{display: inline-block} +.container>.fill{display: inline-block; padding-right: 20px; margin-right:-10px} .container img, .container video {max-width:100%} .max900 {margin-left:auto; margin-right:auto} diff --git a/fabfile.py b/fabfile.py index 2e7db9e8..00a52c02 100644 --- a/fabfile.py +++ b/fabfile.py @@ -136,6 +136,26 @@ def deploy(appname=None, all=False): if backup: print 'TO RESTORE: fab restore:%s' % backup + +def deploynobackup(appname=None): + """fab -H username@host deploy:appname,all""" + appname = appname or os.path.split(os.getcwd())[-1] + appfolder = applications+'/'+appname + zipfile = os.path.join(appfolder, '_update.zip') + if os.path.exists(zipfile): + os.unlink(zipfile) + + local('zip -r _update.zip */*.py */*/*.py views/*.html views/*/*.html static/*') + + put('_update.zip','/tmp/_update.zip') + try: + with cd(appfolder): + sudo('unzip -o /tmp/_update.zip') + sudo('chown -R www-data:www-data *') + sudo('echo "%s" > DATE_DEPLOYMENT' % now) + + finally: + sudo('rm /tmp/_update.zip') def restore(backup): """fab -H username@host restore:backupfilename""" diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 459c2848..7ba67e8e 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -487,7 +487,8 @@ def compile_views(folder, skip_failed_views=False): else: raise Exception("%s in %s" % (e, fname)) else: - filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_') + filename = 'views.%s.py' % fname.replace(os.path.sep, '.') + # filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_') filename = pjoin(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) From b19c3419ec9313ce9cac62849e7d6f247ec40f65 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Sep 2016 13:54:37 -0500 Subject: [PATCH 128/436] do not cache normal views because of dependencies --- gluon/compileapp.py | 67 +++++++++++---------------------------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 7ba67e8e..7ca15433 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -430,13 +430,10 @@ def build_environment(request, response, session, store_current=True): current.T = t current.cache = c - global __builtins__ if is_jython: # jython hack + global __builtins__ __builtins__ = mybuiltin() - elif is_pypy: # apply the same hack to pypy too - __builtins__ = mybuiltin() - elif PY2: - __builtins__['__import__'] = builtin.__import__ # WHY? + environment['request'] = request environment['response'] = response environment['session'] = session @@ -444,7 +441,6 @@ def build_environment(request, response, session, store_current=True): lambda name, reload=False, app=request.application:\ local_import_aux(name, reload, app) BaseAdapter.set_folder(pjoin(request.folder, 'databases')) - response._view_environment = copy.copy(environment) custom_import_install() return environment @@ -488,7 +484,6 @@ def compile_views(folder, skip_failed_views=False): raise Exception("%s in %s" % (e, fname)) else: filename = 'views.%s.py' % fname.replace(os.path.sep, '.') - # filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_') filename = pjoin(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) @@ -504,7 +499,7 @@ def compile_models(folder): path = pjoin(folder, 'models') for fname in listdir(path, '.+\.py$'): data = read_file(pjoin(path, fname)) - modelfile = 'models.'+fname.replace(os.path.sep,'.') + modelfile = 'models.'+fname.replace(os.path.sep, '.') filename = pjoin(folder, 'compiled', modelfile) mktree(filename) write_file(filename, data) @@ -583,11 +578,10 @@ def run_models_in(environment): continue elif compiled: code = getcfs(model, model, lambda: read_pyc(model)) - elif is_gae: + else: code = getcfs(model, model, lambda: compile2(read_file(model), model)) - else: - code = getcfs(model, model, None) + restricted(code, environment, layer=model) @@ -611,7 +605,6 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = getcfs(filename, filename, lambda: read_pyc(filename)) - restricted(code, environment, layer=filename) elif function == '_TEST': # TESTING: adjust the path to include site packages from gluon.settings import global_settings @@ -630,7 +623,6 @@ def run_controller_in(controller, function, environment): environment['__symbols__'] = environment.keys() code = read_file(filename) code += TEST_CODE - restricted(code, environment, layer=filename) else: filename = pjoin(folder, 'controllers/%s.py' % controller) @@ -645,10 +637,10 @@ def run_controller_in(controller, function, environment): rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) code = "%s\nresponse._vars=response._caller(%s)\n" % (code, function) - if is_gae: - layer = filename + ':' + function - code = getcfs(layer, filename, lambda: compile2(code, layer)) - restricted(code, environment, filename) + layer = filename + ':' + function + code = getcfs(layer, filename, lambda: compile2(code, layer)) + + restricted(code, environment, layer=filename) response = current.response vars = response._vars if response.postprocessing: @@ -683,7 +675,7 @@ def run_view_in(environment): if not isinstance(view, str): ccode = parse_template(view, pjoin(folder, 'views'), context=environment) - restricted(ccode, environment, 'file stream') + layer = 'file stream' else: filename = pjoin(folder, 'views', view) if os.path.exists(path): # compiled views @@ -714,16 +706,12 @@ def run_view_in(environment): rewrite.THREAD_LOCAL.routes.error_message % badv, web2py_error=badv) layer = filename - if is_gae: - ccode = getcfs(layer, filename, - lambda: compile2(parse_template(view, - pjoin(folder, 'views'), - context=environment), layer)) - else: - ccode = parse_template(view, - pjoin(folder, 'views'), - context=environment) - restricted(ccode, environment, layer) + # Compile the template + ccode = parse_template(view, + pjoin(folder, 'views'), + context=environment) + + restricted(ccode, environment, layer=layer) def remove_compiled_application(folder): @@ -749,26 +737,3 @@ def compile_application(folder, skip_failed_views=False): compile_controllers(folder) failed_views = compile_views(folder, skip_failed_views) return failed_views - - -def test(): - """ - Example:: - - >>> import traceback, types - >>> environment={'x':1} - >>> open('a.py', 'w').write('print 1/x') - >>> save_pyc('a.py') - >>> os.unlink('a.py') - >>> if type(read_pyc('a.pyc'))==types.CodeType: print 'code' - code - >>> exec read_pyc('a.pyc') in environment - 1 - """ - - return - - -if __name__ == '__main__': - import doctest - doctest.testmod() From a867a65ebe0ad2e45d4ad2ea60e00eb0ed24ae71 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Sep 2016 13:55:48 -0500 Subject: [PATCH 129/436] syncing pydal 16.09 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 012dbe3e..12bc6d97 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 012dbe3eed0404bc5d5516018f2bf9b3eb5b17ab +Subproject commit 12bc6d97402acce462c1193f57bbba4afde7c3c3 From 590a505c540737117e0cf2ff8649936c12c82a8c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Sep 2016 16:02:40 -0500 Subject: [PATCH 130/436] upgraded PyMysql to 0.7.9, thanks niphlod --- gluon/contrib/pymysql/__init__.py | 75 +- gluon/contrib/pymysql/_compat.py | 21 + gluon/contrib/pymysql/_socketio.py | 134 + gluon/contrib/pymysql/charset.py | 50 +- gluon/contrib/pymysql/connections.py | 1724 +- gluon/contrib/pymysql/constants/CLIENT.py | 19 +- gluon/contrib/pymysql/constants/COMMAND.py | 10 + gluon/contrib/pymysql/constants/CR.py | 68 + gluon/contrib/pymysql/constants/FIELD_TYPE.py | 1 + .../pymysql/constants/SERVER_STATUS.py | 1 - gluon/contrib/pymysql/converters.py | 419 +- gluon/contrib/pymysql/cursors.py | 499 +- gluon/contrib/pymysql/err.py | 78 +- gluon/contrib/pymysql/optionfile.py | 20 + gluon/contrib/pymysql/tests/__init__.py | 23 +- gluon/contrib/pymysql/tests/base.py | 86 +- .../pymysql/tests/data/load_local_data.txt | 22749 ++++++++++++++++ .../tests/data/load_local_warn_data.txt | 50 + .../contrib/pymysql/tests/test_DictCursor.py | 149 +- gluon/contrib/pymysql/tests/test_SSCursor.py | 40 +- gluon/contrib/pymysql/tests/test_basic.py | 258 +- .../contrib/pymysql/tests/test_connection.py | 576 + .../contrib/pymysql/tests/test_converters.py | 67 + gluon/contrib/pymysql/tests/test_cursor.py | 104 + gluon/contrib/pymysql/tests/test_err.py | 21 + gluon/contrib/pymysql/tests/test_issues.py | 361 +- .../contrib/pymysql/tests/test_load_local.py | 93 + gluon/contrib/pymysql/tests/test_nextset.py | 68 + .../contrib/pymysql/tests/test_optionfile.py | 32 + .../pymysql/tests/thirdparty/__init__.py | 8 + .../tests/thirdparty/test_MySQLdb/__init__.py | 7 + .../thirdparty/test_MySQLdb/capabilities.py | 298 + .../tests/thirdparty/test_MySQLdb/dbapi20.py | 856 + .../test_MySQLdb/test_MySQLdb_capabilities.py | 109 + .../test_MySQLdb/test_MySQLdb_dbapi20.py | 210 + .../test_MySQLdb/test_MySQLdb_nonstandard.py | 101 + gluon/contrib/pymysql/times.py | 4 + gluon/contrib/pymysql/util.py | 3 + 38 files changed, 28064 insertions(+), 1328 deletions(-) create mode 100755 gluon/contrib/pymysql/_compat.py create mode 100755 gluon/contrib/pymysql/_socketio.py create mode 100755 gluon/contrib/pymysql/constants/CR.py create mode 100755 gluon/contrib/pymysql/optionfile.py create mode 100755 gluon/contrib/pymysql/tests/data/load_local_data.txt create mode 100755 gluon/contrib/pymysql/tests/data/load_local_warn_data.txt create mode 100755 gluon/contrib/pymysql/tests/test_connection.py create mode 100755 gluon/contrib/pymysql/tests/test_converters.py create mode 100755 gluon/contrib/pymysql/tests/test_cursor.py create mode 100755 gluon/contrib/pymysql/tests/test_err.py create mode 100755 gluon/contrib/pymysql/tests/test_load_local.py create mode 100755 gluon/contrib/pymysql/tests/test_nextset.py create mode 100755 gluon/contrib/pymysql/tests/test_optionfile.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/__init__.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py create mode 100755 gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py diff --git a/gluon/contrib/pymysql/__init__.py b/gluon/contrib/pymysql/__init__.py index c1e6d037..bf34c558 100644 --- a/gluon/contrib/pymysql/__init__.py +++ b/gluon/contrib/pymysql/__init__.py @@ -1,7 +1,7 @@ -''' -PyMySQL: A pure-Python drop-in replacement for MySQLdb. +""" +PyMySQL: A pure-Python MySQL client library. -Copyright (c) 2010 PyMySQL contributors +Copyright (c) 2010-2016 PyMySQL contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,40 +20,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -''' - -VERSION = (0, 5, None) - -from constants import FIELD_TYPE -from converters import escape_dict, escape_sequence, escape_string -from err import Warning, Error, InterfaceError, DataError, \ - DatabaseError, OperationalError, IntegrityError, InternalError, \ - NotSupportedError, ProgrammingError, MySQLError -from times import Date, Time, Timestamp, \ - DateFromTicks, TimeFromTicks, TimestampFromTicks - +""" import sys -try: - frozenset -except NameError: - from sets import ImmutableSet as frozenset - try: - from sets import BaseSet as set - except ImportError: - from sets import Set as set +from ._compat import PY2 +from .constants import FIELD_TYPE +from .converters import escape_dict, escape_sequence, escape_string +from .err import ( + Warning, Error, InterfaceError, DataError, + DatabaseError, OperationalError, IntegrityError, InternalError, + NotSupportedError, ProgrammingError, MySQLError) +from .times import ( + Date, Time, Timestamp, + DateFromTicks, TimeFromTicks, TimestampFromTicks) + +VERSION = (0, 7, 9, None) threadsafety = 1 apilevel = "2.0" -paramstyle = "format" +paramstyle = "pyformat" + class DBAPISet(frozenset): - def __ne__(self, other): if isinstance(other, set): - return super(DBAPISet, self).__ne__(self, other) + return frozenset.__ne__(self, other) else: return other not in self @@ -80,40 +72,52 @@ TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME]) DATETIME = TIMESTAMP ROWID = DBAPISet() + def Binary(x): """Return x as a binary type.""" - return str(x) + if PY2: + return bytearray(x) + else: + return bytes(x) + def Connect(*args, **kwargs): """ Connect to the database; see connections.Connection.__init__() for more information. """ - from connections import Connection + from .connections import Connection return Connection(*args, **kwargs) +from pymysql import connections as _orig_conn +if _orig_conn.Connection.__init__.__doc__ is not None: + Connect.__doc__ = _orig_conn.Connection.__init__.__doc__ +del _orig_conn + + def get_client_info(): # for MySQLdb compatibility - return '%s.%s.%s' % VERSION + return '.'.join(map(str, VERSION)) connect = Connection = Connect # we include a doctored version_info here for MySQLdb compatibility -version_info = (1,2,2,"final",0) +version_info = (1,2,6,"final",0) NULL = "NULL" __version__ = get_client_info() def thread_safe(): - return True # match MySQLdb.thread_safe() + return True # match MySQLdb.thread_safe() def install_as_MySQLdb(): """ After this function is called, any application that imports MySQLdb or - _mysql will unwittingly actually use + _mysql will unwittingly actually use """ sys.modules["MySQLdb"] = sys.modules["_mysql"] = sys.modules["pymysql"] + __all__ = [ 'BINARY', 'Binary', 'Connect', 'Connection', 'DATE', 'Date', 'Time', 'Timestamp', 'DateFromTicks', 'TimeFromTicks', 'TimestampFromTicks', @@ -126,6 +130,5 @@ __all__ = [ 'paramstyle', 'threadsafety', 'version_info', "install_as_MySQLdb", - - "NULL","__version__", - ] + "NULL", "__version__", +] diff --git a/gluon/contrib/pymysql/_compat.py b/gluon/contrib/pymysql/_compat.py new file mode 100755 index 00000000..252789ec --- /dev/null +++ b/gluon/contrib/pymysql/_compat.py @@ -0,0 +1,21 @@ +import sys + +PY2 = sys.version_info[0] == 2 +PYPY = hasattr(sys, 'pypy_translation_info') +JYTHON = sys.platform.startswith('java') +IRONPYTHON = sys.platform == 'cli' +CPYTHON = not PYPY and not JYTHON and not IRONPYTHON + +if PY2: + import __builtin__ + range_type = xrange + text_type = unicode + long_type = long + str_type = basestring + unichr = __builtin__.unichr +else: + range_type = range + text_type = str + long_type = int + str_type = str + unichr = chr diff --git a/gluon/contrib/pymysql/_socketio.py b/gluon/contrib/pymysql/_socketio.py new file mode 100755 index 00000000..6a11d42e --- /dev/null +++ b/gluon/contrib/pymysql/_socketio.py @@ -0,0 +1,134 @@ +""" +SocketIO imported from socket module in Python 3. + +Copyright (c) 2001-2013 Python Software Foundation; All Rights Reserved. +""" + +from socket import * +import io +import errno + +__all__ = ['SocketIO'] + +EINTR = errno.EINTR +_blocking_errnos = (errno.EAGAIN, errno.EWOULDBLOCK) + +class SocketIO(io.RawIOBase): + + """Raw I/O implementation for stream sockets. + + This class supports the makefile() method on sockets. It provides + the raw I/O interface on top of a socket object. + """ + + # One might wonder why not let FileIO do the job instead. There are two + # main reasons why FileIO is not adapted: + # - it wouldn't work under Windows (where you can't used read() and + # write() on a socket handle) + # - it wouldn't work with socket timeouts (FileIO would ignore the + # timeout and consider the socket non-blocking) + + # XXX More docs + + def __init__(self, sock, mode): + if mode not in ("r", "w", "rw", "rb", "wb", "rwb"): + raise ValueError("invalid mode: %r" % mode) + io.RawIOBase.__init__(self) + self._sock = sock + if "b" not in mode: + mode += "b" + self._mode = mode + self._reading = "r" in mode + self._writing = "w" in mode + self._timeout_occurred = False + + def readinto(self, b): + """Read up to len(b) bytes into the writable buffer *b* and return + the number of bytes read. If the socket is non-blocking and no bytes + are available, None is returned. + + If *b* is non-empty, a 0 return value indicates that the connection + was shutdown at the other end. + """ + self._checkClosed() + self._checkReadable() + if self._timeout_occurred: + raise IOError("cannot read from timed out object") + while True: + try: + return self._sock.recv_into(b) + except timeout: + self._timeout_occurred = True + raise + except error as e: + n = e.args[0] + if n == EINTR: + continue + if n in _blocking_errnos: + return None + raise + + def write(self, b): + """Write the given bytes or bytearray object *b* to the socket + and return the number of bytes written. This can be less than + len(b) if not all data could be written. If the socket is + non-blocking and no bytes could be written None is returned. + """ + self._checkClosed() + self._checkWritable() + try: + return self._sock.send(b) + except error as e: + # XXX what about EINTR? + if e.args[0] in _blocking_errnos: + return None + raise + + def readable(self): + """True if the SocketIO is open for reading. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return self._reading + + def writable(self): + """True if the SocketIO is open for writing. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return self._writing + + def seekable(self): + """True if the SocketIO is open for seeking. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return super().seekable() + + def fileno(self): + """Return the file descriptor of the underlying socket. + """ + self._checkClosed() + return self._sock.fileno() + + @property + def name(self): + if not self.closed: + return self.fileno() + else: + return -1 + + @property + def mode(self): + return self._mode + + def close(self): + """Close the SocketIO object. This doesn't close the underlying + socket, except if all references to it have disappeared. + """ + if self.closed: + return + io.RawIOBase.close(self) + self._sock._decref_socketios() + self._sock = None + diff --git a/gluon/contrib/pymysql/charset.py b/gluon/contrib/pymysql/charset.py index 507fd07d..968376cf 100644 --- a/gluon/contrib/pymysql/charset.py +++ b/gluon/contrib/pymysql/charset.py @@ -5,11 +5,28 @@ MBLENGTH = { 91:2 } -class Charset: + +class Charset(object): def __init__(self, id, name, collation, is_default): self.id, self.name, self.collation = id, name, collation self.is_default = is_default == 'Yes' + def __repr__(self): + return "Charset(id=%s, name=%r, collation=%r)" % ( + self.id, self.name, self.collation) + + @property + def encoding(self): + name = self.name + if name == 'utf8mb4': + return 'utf8' + return name + + @property + def is_binary(self): + return self.id == 63 + + class Charsets: def __init__(self): self._by_id = {} @@ -21,6 +38,7 @@ class Charsets: return self._by_id[id] def by_name(self, name): + name = name.lower() for c in self._by_id.values(): if c.name == name and c.is_default: return c @@ -92,13 +110,11 @@ _charsets.add(Charset(52, 'cp1251', 'cp1251_general_cs', '')) _charsets.add(Charset(53, 'macroman', 'macroman_bin', '')) _charsets.add(Charset(54, 'utf16', 'utf16_general_ci', 'Yes')) _charsets.add(Charset(55, 'utf16', 'utf16_bin', '')) -_charsets.add(Charset(56, 'utf16le', 'utf16le_general_ci', 'Yes')) _charsets.add(Charset(57, 'cp1256', 'cp1256_general_ci', 'Yes')) _charsets.add(Charset(58, 'cp1257', 'cp1257_bin', '')) _charsets.add(Charset(59, 'cp1257', 'cp1257_general_ci', 'Yes')) _charsets.add(Charset(60, 'utf32', 'utf32_general_ci', 'Yes')) _charsets.add(Charset(61, 'utf32', 'utf32_bin', '')) -_charsets.add(Charset(62, 'utf16le', 'utf16le_bin', '')) _charsets.add(Charset(63, 'binary', 'binary', 'Yes')) _charsets.add(Charset(64, 'armscii8', 'armscii8_bin', '')) _charsets.add(Charset(65, 'ascii', 'ascii_bin', '')) @@ -155,10 +171,6 @@ _charsets.add(Charset(117, 'utf16', 'utf16_persian_ci', '')) _charsets.add(Charset(118, 'utf16', 'utf16_esperanto_ci', '')) _charsets.add(Charset(119, 'utf16', 'utf16_hungarian_ci', '')) _charsets.add(Charset(120, 'utf16', 'utf16_sinhala_ci', '')) -_charsets.add(Charset(121, 'utf16', 'utf16_german2_ci', '')) -_charsets.add(Charset(122, 'utf16', 'utf16_croatian_ci', '')) -_charsets.add(Charset(123, 'utf16', 'utf16_unicode_520_ci', '')) -_charsets.add(Charset(124, 'utf16', 'utf16_vietnamese_ci', '')) _charsets.add(Charset(128, 'ucs2', 'ucs2_unicode_ci', '')) _charsets.add(Charset(129, 'ucs2', 'ucs2_icelandic_ci', '')) _charsets.add(Charset(130, 'ucs2', 'ucs2_latvian_ci', '')) @@ -179,10 +191,6 @@ _charsets.add(Charset(144, 'ucs2', 'ucs2_persian_ci', '')) _charsets.add(Charset(145, 'ucs2', 'ucs2_esperanto_ci', '')) _charsets.add(Charset(146, 'ucs2', 'ucs2_hungarian_ci', '')) _charsets.add(Charset(147, 'ucs2', 'ucs2_sinhala_ci', '')) -_charsets.add(Charset(148, 'ucs2', 'ucs2_german2_ci', '')) -_charsets.add(Charset(149, 'ucs2', 'ucs2_croatian_ci', '')) -_charsets.add(Charset(150, 'ucs2', 'ucs2_unicode_520_ci', '')) -_charsets.add(Charset(151, 'ucs2', 'ucs2_vietnamese_ci', '')) _charsets.add(Charset(159, 'ucs2', 'ucs2_general_mysql500_ci', '')) _charsets.add(Charset(160, 'utf32', 'utf32_unicode_ci', '')) _charsets.add(Charset(161, 'utf32', 'utf32_icelandic_ci', '')) @@ -204,10 +212,6 @@ _charsets.add(Charset(176, 'utf32', 'utf32_persian_ci', '')) _charsets.add(Charset(177, 'utf32', 'utf32_esperanto_ci', '')) _charsets.add(Charset(178, 'utf32', 'utf32_hungarian_ci', '')) _charsets.add(Charset(179, 'utf32', 'utf32_sinhala_ci', '')) -_charsets.add(Charset(180, 'utf32', 'utf32_german2_ci', '')) -_charsets.add(Charset(181, 'utf32', 'utf32_croatian_ci', '')) -_charsets.add(Charset(182, 'utf32', 'utf32_unicode_520_ci', '')) -_charsets.add(Charset(183, 'utf32', 'utf32_vietnamese_ci', '')) _charsets.add(Charset(192, 'utf8', 'utf8_unicode_ci', '')) _charsets.add(Charset(193, 'utf8', 'utf8_icelandic_ci', '')) _charsets.add(Charset(194, 'utf8', 'utf8_latvian_ci', '')) @@ -228,10 +232,6 @@ _charsets.add(Charset(208, 'utf8', 'utf8_persian_ci', '')) _charsets.add(Charset(209, 'utf8', 'utf8_esperanto_ci', '')) _charsets.add(Charset(210, 'utf8', 'utf8_hungarian_ci', '')) _charsets.add(Charset(211, 'utf8', 'utf8_sinhala_ci', '')) -_charsets.add(Charset(212, 'utf8', 'utf8_german2_ci', '')) -_charsets.add(Charset(213, 'utf8', 'utf8_croatian_ci', '')) -_charsets.add(Charset(214, 'utf8', 'utf8_unicode_520_ci', '')) -_charsets.add(Charset(215, 'utf8', 'utf8_vietnamese_ci', '')) _charsets.add(Charset(223, 'utf8', 'utf8_general_mysql500_ci', '')) _charsets.add(Charset(224, 'utf8mb4', 'utf8mb4_unicode_ci', '')) _charsets.add(Charset(225, 'utf8mb4', 'utf8mb4_icelandic_ci', '')) @@ -258,9 +258,13 @@ _charsets.add(Charset(245, 'utf8mb4', 'utf8mb4_croatian_ci', '')) _charsets.add(Charset(246, 'utf8mb4', 'utf8mb4_unicode_520_ci', '')) _charsets.add(Charset(247, 'utf8mb4', 'utf8mb4_vietnamese_ci', '')) -def charset_by_name(name): - return _charsets.by_name(name) -def charset_by_id(id): - return _charsets.by_id(id) +charset_by_name = _charsets.by_name +charset_by_id = _charsets.by_id + +def charset_to_encoding(name): + """Convert MySQL's charset name to Python's codec name""" + if name == 'utf8mb4': + return 'utf8' + return name diff --git a/gluon/contrib/pymysql/connections.py b/gluon/contrib/pymysql/connections.py index e4f91330..d5e39a1c 100644 --- a/gluon/contrib/pymysql/connections.py +++ b/gluon/contrib/pymysql/connections.py @@ -1,91 +1,141 @@ # Python implementation of the MySQL client-server protocol -# http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol - -try: - import hashlib - sha_new = lambda *args, **kwargs: hashlib.new("sha1", *args, **kwargs) -except ImportError: - import sha - sha_new = sha.new +# http://dev.mysql.com/doc/internals/en/client-server-protocol.html +# Error codes: +# http://dev.mysql.com/doc/refman/5.5/en/error-messages-client.html +from __future__ import print_function +from ._compat import PY2, range_type, text_type, str_type, JYTHON, IRONPYTHON +import errno +from functools import partial +import hashlib +import io +import os import socket +import struct +import sys +import traceback +import warnings + +from .charset import MBLENGTH, charset_by_name, charset_by_id +from .constants import CLIENT, COMMAND, FIELD_TYPE, SERVER_STATUS +from .converters import escape_item, escape_string, through, conversions as _conv +from .cursors import Cursor +from .optionfile import Parser +from .util import byte2int, int2byte +from . import err + try: import ssl SSL_ENABLED = True except ImportError: + ssl = None SSL_ENABLED = False -import struct -import sys -import os -import ConfigParser - -try: - import cStringIO as StringIO -except ImportError: - import StringIO - try: import getpass DEFAULT_USER = getpass.getuser() -except ImportError: + del getpass +except (ImportError, KeyError): + # KeyError occurs when there's no entry in OS database for a current user. DEFAULT_USER = None -from charset import MBLENGTH, charset_by_name, charset_by_id -from cursors import Cursor -from constants import FIELD_TYPE, FLAG -from constants import SERVER_STATUS -from constants.CLIENT import * -from constants.COMMAND import * -from util import join_bytes, byte2int, int2byte -from converters import escape_item, encoders, decoders -from err import raise_mysql_exception, Warning, Error, \ - InterfaceError, DataError, DatabaseError, OperationalError, \ - IntegrityError, InternalError, NotSupportedError, ProgrammingError DEBUG = False +_py_version = sys.version_info[:2] + + +# socket.makefile() in Python 2 is not usable because very inefficient and +# bad behavior about timeout. +# XXX: ._socketio doesn't work under IronPython. +if _py_version == (2, 7) and not IRONPYTHON: + # read method of file-like returned by sock.makefile() is very slow. + # So we copy io-based one from Python 3. + from ._socketio import SocketIO + + def _makefile(sock, mode): + return io.BufferedReader(SocketIO(sock, mode)) +elif _py_version == (2, 6): + # Python 2.6 doesn't have fast io module. + # So we make original one. + class SockFile(object): + def __init__(self, sock): + self._sock = sock + + def read(self, n): + read = self._sock.recv(n) + if len(read) == n: + return read + while True: + data = self._sock.recv(n-len(read)) + if not data: + return read + read += data + if len(read) == n: + return read + + def _makefile(sock, mode): + assert mode == 'rb' + return SockFile(sock) +else: + # socket.makefile in Python 3 is nice. + def _makefile(sock, mode): + return sock.makefile(mode) + + +TEXT_TYPES = set([ + FIELD_TYPE.BIT, + FIELD_TYPE.BLOB, + FIELD_TYPE.LONG_BLOB, + FIELD_TYPE.MEDIUM_BLOB, + FIELD_TYPE.STRING, + FIELD_TYPE.TINY_BLOB, + FIELD_TYPE.VAR_STRING, + FIELD_TYPE.VARCHAR, + FIELD_TYPE.GEOMETRY]) + +sha_new = partial(hashlib.new, 'sha1') + NULL_COLUMN = 251 UNSIGNED_CHAR_COLUMN = 251 UNSIGNED_SHORT_COLUMN = 252 UNSIGNED_INT24_COLUMN = 253 UNSIGNED_INT64_COLUMN = 254 -UNSIGNED_CHAR_LENGTH = 1 -UNSIGNED_SHORT_LENGTH = 2 -UNSIGNED_INT24_LENGTH = 3 -UNSIGNED_INT64_LENGTH = 8 DEFAULT_CHARSET = 'latin1' +MAX_PACKET_LEN = 2**24-1 -def dump_packet(data): - + +def dump_packet(data): # pragma: no cover def is_ascii(data): - if byte2int(data) >= 65 and byte2int(data) <= 122: #data.isalnum(): + if 65 <= byte2int(data) <= 122: + if isinstance(data, int): + return chr(data) return data return '.' - + try: - print "packet length %d" % len(data) - print "method call[1]: %s" % sys._getframe(1).f_code.co_name - print "method call[2]: %s" % sys._getframe(2).f_code.co_name - print "method call[3]: %s" % sys._getframe(3).f_code.co_name - print "method call[4]: %s" % sys._getframe(4).f_code.co_name - print "method call[5]: %s" % sys._getframe(5).f_code.co_name - print "-" * 88 - except ValueError: pass - dump_data = [data[i:i+16] for i in xrange(len(data)) if i%16 == 0] + print("packet length:", len(data)) + for i in range(1, 6): + f = sys._getframe(i) + print("call[%d]: %s (line %d)" % (i, f.f_code.co_name, f.f_lineno)) + print("-" * 66) + except ValueError: + pass + dump_data = [data[i:i+16] for i in range_type(0, min(len(data), 256), 16)] for d in dump_data: - print ' '.join(map(lambda x:"%02X" % byte2int(x), d)) + \ - ' ' * (16 - len(d)) + ' ' * 2 + \ - ' '.join(map(lambda x:"%s" % is_ascii(x), d)) - print "-" * 88 - print "" + print(' '.join(map(lambda x: "{:02X}".format(byte2int(x)), d)) + + ' ' * (16 - len(d)) + ' ' * 2 + + ''.join(map(lambda x: "{}".format(is_ascii(x)), d))) + print("-" * 66) + print() + def _scramble(password, message): - if password == None or len(password) == 0: - return int2byte(0) - if DEBUG: print 'password=' + password + if not password: + return b'' + if DEBUG: print('password=' + str(password)) stage1 = sha_new(password).digest() stage2 = sha_new(stage1).digest() s = sha_new() @@ -94,11 +144,12 @@ def _scramble(password, message): result = s.digest() return _my_crypt(result, stage1) + def _my_crypt(message1, message2): length = len(message1) - result = struct.pack('B', length) - for i in xrange(length): - x = (struct.unpack('B', message1[i:i+1])[0] ^ \ + result = b'' + for i in range_type(length): + x = (struct.unpack('B', message1[i:i+1])[0] ^ struct.unpack('B', message2[i:i+1])[0]) result += struct.pack('B', x) return result @@ -106,17 +157,19 @@ def _my_crypt(message1, message2): # old_passwords support ported from libmysql/password.c SCRAMBLE_LENGTH_323 = 8 + class RandStruct_323(object): def __init__(self, seed1, seed2): - self.max_value = 0x3FFFFFFFL + self.max_value = 0x3FFFFFFF self.seed1 = seed1 % self.max_value self.seed2 = seed2 % self.max_value def my_rnd(self): - self.seed1 = (self.seed1 * 3L + self.seed2) % self.max_value - self.seed2 = (self.seed1 + self.seed2 + 33L) % self.max_value + self.seed1 = (self.seed1 * 3 + self.seed2) % self.max_value + self.seed2 = (self.seed1 + self.seed2 + 33) % self.max_value return float(self.seed1) / float(self.max_value) + def _scramble_323(password, message): hash_pass = _hash_password_323(password) hash_message = _hash_password_323(message[:SCRAMBLE_LENGTH_323]) @@ -125,286 +178,273 @@ def _scramble_323(password, message): rand_st = RandStruct_323(hash_pass_n[0] ^ hash_message_n[0], hash_pass_n[1] ^ hash_message_n[1]) - outbuf = StringIO.StringIO() - for _ in xrange(min(SCRAMBLE_LENGTH_323, len(message))): + outbuf = io.BytesIO() + for _ in range_type(min(SCRAMBLE_LENGTH_323, len(message))): outbuf.write(int2byte(int(rand_st.my_rnd() * 31) + 64)) extra = int2byte(int(rand_st.my_rnd() * 31)) out = outbuf.getvalue() - outbuf = StringIO.StringIO() + outbuf = io.BytesIO() for c in out: outbuf.write(int2byte(byte2int(c) ^ byte2int(extra))) return outbuf.getvalue() + def _hash_password_323(password): - nr = 1345345333L - add = 7L - nr2 = 0x12345671L + nr = 1345345333 + add = 7 + nr2 = 0x12345671 - for c in [byte2int(x) for x in password if x not in (' ', '\t')]: - nr^= (((nr & 63)+add)*c)+ (nr << 8) & 0xFFFFFFFF - nr2= (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF - add= (add + c) & 0xFFFFFFFF + # x in py3 is numbers, p27 is chars + for c in [byte2int(x) for x in password if x not in (' ', '\t', 32, 9)]: + nr ^= (((nr & 63) + add) * c) + (nr << 8) & 0xFFFFFFFF + nr2 = (nr2 + ((nr2 << 8) ^ nr)) & 0xFFFFFFFF + add = (add + c) & 0xFFFFFFFF - r1 = nr & ((1L << 31) - 1L) # kill sign bits - r2 = nr2 & ((1L << 31) - 1L) - - # pack + r1 = nr & ((1 << 31) - 1) # kill sign bits + r2 = nr2 & ((1 << 31) - 1) return struct.pack(">LL", r1, r2) + def pack_int24(n): - return struct.pack('BBB', n&0xFF, (n>>8)&0xFF, (n>>16)&0xFF) + return struct.pack(' len(self.__data): - raise Exception('Invalid advance amount (%s) for cursor. ' - 'Position=%s' % (length, new_position)) - self.__position = new_position + def __init__(self, data, encoding): + self._position = 0 + self._data = data - def rewind(self, position=0): - """Set the position of the data buffer cursor to 'position'.""" - if position < 0 or position > len(self.__data): - raise Exception("Invalid position to rewind cursor to: %s." % position) - self.__position = position + def get_all_data(self): + return self._data - def peek(self, size): - """Look at the first 'size' bytes in packet without moving cursor.""" - result = self.__data[self.__position:(self.__position+size)] - if len(result) != size: - error = ('Result length not requested length:\n' - 'Expected=%s. Actual=%s. Position: %s. Data Length: %s' - % (size, len(result), self.__position, len(self.__data))) - if DEBUG: - print error - self.dump() - raise AssertionError(error) - return result + def read(self, size): + """Read the first 'size' bytes in packet and advance cursor past them.""" + result = self._data[self._position:(self._position+size)] + if len(result) != size: + error = ('Result length not requested length:\n' + 'Expected=%s. Actual=%s. Position: %s. Data Length: %s' + % (size, len(result), self._position, len(self._data))) + if DEBUG: + print(error) + self.dump() + raise AssertionError(error) + self._position += size + return result - def get_bytes(self, position, length=1): - """Get 'length' bytes starting at 'position'. + def read_all(self): + """Read all remaining data in the packet. - Position is start of payload (first four packet header bytes are not - included) starting at index '0'. + (Subsequent read() will return errors.) + """ + result = self._data[self._position:] + self._position = None # ensure no subsequent read() + return result - No error checking is done. If requesting outside end of buffer - an empty string (or string shorter than 'length') may be returned! - """ - return self.__data[position:(position+length)] + def advance(self, length): + """Advance the cursor in data buffer 'length' bytes.""" + new_position = self._position + length + if new_position < 0 or new_position > len(self._data): + raise Exception('Invalid advance amount (%s) for cursor. ' + 'Position=%s' % (length, new_position)) + self._position = new_position - def read_length_coded_binary(self): - """Read a 'Length Coded Binary' number from the data buffer. + def rewind(self, position=0): + """Set the position of the data buffer cursor to 'position'.""" + if position < 0 or position > len(self._data): + raise Exception("Invalid position to rewind cursor to: %s." % position) + self._position = position - Length coded numbers can be anywhere from 1 to 9 bytes depending - on the value of the first byte. - """ - c = byte2int(self.read(1)) - if c == NULL_COLUMN: - return None - if c < UNSIGNED_CHAR_COLUMN: - return c - elif c == UNSIGNED_SHORT_COLUMN: - return unpack_uint16(self.read(UNSIGNED_SHORT_LENGTH)) - elif c == UNSIGNED_INT24_COLUMN: - return unpack_int24(self.read(UNSIGNED_INT24_LENGTH)) - elif c == UNSIGNED_INT64_COLUMN: - # TODO: what was 'longlong'? confirm it wasn't used? - return unpack_int64(self.read(UNSIGNED_INT64_LENGTH)) + def get_bytes(self, position, length=1): + """Get 'length' bytes starting at 'position'. - def read_length_coded_string(self): - """Read a 'Length Coded String' from the data buffer. + Position is start of payload (first four packet header bytes are not + included) starting at index '0'. - A 'Length Coded String' consists first of a length coded - (unsigned, positive) integer represented in 1-9 bytes followed by - that many bytes of binary data. (For example "cat" would be "3cat".) - """ - length = self.read_length_coded_binary() - if length is None: - return None - return self.read(length) + No error checking is done. If requesting outside end of buffer + an empty string (or string shorter than 'length') may be returned! + """ + return self._data[position:(position+length)] - def is_ok_packet(self): - return byte2int(self.get_bytes(0)) == 0 + if PY2: + def read_uint8(self): + result = ord(self._data[self._position]) + self._position += 1 + return result + else: + def read_uint8(self): + result = self._data[self._position] + self._position += 1 + return result - def is_eof_packet(self): - return byte2int(self.get_bytes(0)) == 254 # 'fe' + def read_uint16(self): + result = struct.unpack_from('= 1 and field_count <= 250 + def read_uint24(self): + low, high = struct.unpack_from('= 7 + + def is_eof_packet(self): + # http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-EOF_Packet + # Caution: \xFE may be LengthEncodedInteger. + # If \xFE is LengthEncodedInteger header, 8bytes followed. + return self._data[0:1] == b'\xfe' and len(self._data) < 9 + + def is_auth_switch_request(self): + # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest + return self._data[0:1] == b'\xfe' + + def is_resultset_packet(self): + field_count = ord(self._data[0:1]) + return 1 <= field_count <= 250 + + def is_load_local_packet(self): + return self._data[0:1] == b'\xfb' + + def is_error_packet(self): + return self._data[0:1] == b'\xff' + + def check_error(self): + if self.is_error_packet(): + self.rewind() + self.advance(1) # field_count == error (we already know that) + errno = self.read_uint16() + if DEBUG: print("errno =", errno) + err.raise_mysql_exception(self._data) + + def dump(self): + dump_packet(self._data) class FieldDescriptorPacket(MysqlPacket): - """A MysqlPacket that represents a specific column's metadata in the result. + """A MysqlPacket that represents a specific column's metadata in the result. - Parsing is automatically done and the results are exported via public - attributes on the class such as: db, table_name, name, length, type_code. - """ - - def __init__(self, *args): - MysqlPacket.__init__(self, *args) - self.__parse_field_descriptor() - - def __parse_field_descriptor(self): - """Parse the 'Field Descriptor' (Metadata) packet. - - This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0). + Parsing is automatically done and the results are exported via public + attributes on the class such as: db, table_name, name, length, type_code. """ - self.catalog = self.read_length_coded_string() - self.db = self.read_length_coded_string() - self.table_name = self.read_length_coded_string() - self.org_table = self.read_length_coded_string() - self.name = self.read_length_coded_string().decode(self.connection.charset) - self.org_name = self.read_length_coded_string() - self.advance(1) # non-null filler - self.charsetnr = struct.unpack(' 2: use_unicode = True - if compress or named_pipe: - raise NotImplementedError, "compress and named_pipe arguments are not supported" + if db is not None and database is None: + database = db + if passwd is not None and not password: + password = passwd - if ssl and (ssl.has_key('capath') or ssl.has_key('cipher')): - raise NotImplementedError, 'ssl options capath and cipher are not supported' + if compress or named_pipe: + raise NotImplementedError("compress and named_pipe arguments are not supported") + + if local_infile: + client_flag |= CLIENT.LOCAL_FILES self.ssl = False if ssl: if not SSL_ENABLED: - raise NotImplementedError, "ssl module not found" + raise NotImplementedError("ssl module not found") self.ssl = True - client_flag |= SSL - for k in ('key', 'cert', 'ca'): - v = None - if ssl.has_key(k): - v = ssl[k] - setattr(self, k, v) + client_flag |= CLIENT.SSL + self.ctx = self._create_ssl_ctx(ssl) if read_default_group and not read_default_file: if sys.platform.startswith("win"): @@ -530,29 +615,37 @@ class Connection(object): if not read_default_group: read_default_group = "client" - cfg = ConfigParser.RawConfigParser() + cfg = Parser() cfg.read(os.path.expanduser(read_default_file)) - def _config(key, default): + def _config(key, arg): + if arg: + return arg try: - return cfg.get(read_default_group,key) - except: - return default + return cfg.get(read_default_group, key) + except Exception: + return arg - user = _config("user",user) - passwd = _config("password",passwd) + user = _config("user", user) + password = _config("password", password) host = _config("host", host) - db = _config("db",db) - unix_socket = _config("socket",unix_socket) + database = _config("database", database) + unix_socket = _config("socket", unix_socket) port = int(_config("port", port)) charset = _config("default-character-set", charset) - self.host = host - self.port = port + self.host = host or "localhost" + self.port = port or 3306 self.user = user or DEFAULT_USER - self.password = passwd - self.db = db + self.password = password or "" + self.db = database self.unix_socket = unix_socket + if read_timeout is not None and read_timeout <= 0: + raise ValueError("read_timeout should be >= 0") + self._read_timeout = read_timeout + if write_timeout is not None and write_timeout <= 0: + raise ValueError("write_timeout should be >= 0") + self._write_timeout = write_timeout if charset: self.charset = charset self.use_unicode = True @@ -563,102 +656,167 @@ class Connection(object): if use_unicode is not None: self.use_unicode = use_unicode - client_flag |= CAPABILITIES - client_flag |= MULTI_STATEMENTS + self.encoding = charset_by_name(self.charset).encoding + + client_flag |= CLIENT.CAPABILITIES if self.db: - client_flag |= CONNECT_WITH_DB + client_flag |= CLIENT.CONNECT_WITH_DB self.client_flag = client_flag self.cursorclass = cursorclass self.connect_timeout = connect_timeout - self._connect() - self._result = None self._affected_rows = 0 self.host_info = "Not connected" - self.messages = [] - self.set_charset(charset) - self.encoders = encoders - self.decoders = conv + #: specified autocommit mode. None means use server default. + self.autocommit_mode = autocommit - self.autocommit(False) - - if sql_mode is not None: - c = self.cursor() - c.execute("SET sql_mode=%s", (sql_mode,)) - - self.commit() - - if init_command is not None: - c = self.cursor() - c.execute(init_command) - - self.commit() + if conv is None: + conv = _conv + # Need for MySQLdb compatibility. + self.encoders = dict([(k, v) for (k, v) in conv.items() if type(k) is not int]) + self.decoders = dict([(k, v) for (k, v) in conv.items() if type(k) is int]) + self.sql_mode = sql_mode + self.init_command = init_command + self.max_allowed_packet = max_allowed_packet + self._auth_plugin_map = auth_plugin_map + if defer_connect: + self._sock = None + else: + self.connect() + def _create_ssl_ctx(self, sslp): + if isinstance(sslp, ssl.SSLContext): + return sslp + ca = sslp.get('ca') + capath = sslp.get('capath') + hasnoca = ca is None and capath is None + ctx = ssl.create_default_context(cafile=ca, capath=capath) + ctx.check_hostname = not hasnoca and sslp.get('check_hostname', True) + ctx.verify_mode = ssl.CERT_NONE if hasnoca else ssl.CERT_REQUIRED + if 'cert' in sslp: + ctx.load_cert_chain(sslp['cert'], keyfile=sslp.get('key')) + if 'cipher' in sslp: + ctx.set_ciphers(sslp['cipher']) + ctx.options |= ssl.OP_NO_SSLv2 + ctx.options |= ssl.OP_NO_SSLv3 + return ctx def close(self): - ''' Send the quit message and close the socket ''' - if self.socket is None: - raise Error("Already closed") - send_data = struct.pack('= 5: + self.client_flag |= CLIENT.MULTI_RESULTS if self.user is None: - raise ValueError, "Did not specify a username" + raise ValueError("Did not specify a username") charset_id = charset_by_name(self.charset).id - self.user = self.user.encode(self.charset) + if isinstance(self.user, text_type): + self.user = self.user.encode(self.encoding) - data_init = struct.pack('=5.0) + data += authresp + b'\0' - if self.db: - self.db = self.db.encode(self.charset) - data += self.db + int2byte(0) + if self.db and self.server_capabilities & CLIENT.CONNECT_WITH_DB: + if isinstance(self.db, text_type): + self.db = self.db.encode(self.encoding) + data += self.db + b'\0' - data = pack_int24(len(data)) + int2byte(next_packet) + data - next_packet += 2 + if self.server_capabilities & CLIENT.PLUGIN_AUTH: + name = self._auth_plugin_name + if isinstance(name, text_type): + name = name.encode('ascii') + data += name + b'\0' - if DEBUG: dump_packet(data) + self.write_packet(data) + auth_packet = self._read_packet() - self.wfile.write(data) - self.wfile.flush() + # if authentication method isn't accepted the first byte + # will have the octet 254 + if auth_packet.is_auth_switch_request(): + # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest + auth_packet.read_uint8() # 0xfe packet identifier + plugin_name = auth_packet.read_string() + if self.server_capabilities & CLIENT.PLUGIN_AUTH and plugin_name is not None: + auth_packet = self._process_auth(plugin_name, auth_packet) + else: + # send legacy handshake + data = _scramble_323(self.password.encode('latin1'), self.salt) + b'\0' + self.write_packet(data) + auth_packet = self._read_packet() - auth_packet = MysqlPacket(self) - auth_packet.check_error() - if DEBUG: auth_packet.dump() + def _process_auth(self, plugin_name, auth_packet): + plugin_class = self._auth_plugin_map.get(plugin_name) + if not plugin_class: + plugin_class = self._auth_plugin_map.get(plugin_name.decode('ascii')) + if plugin_class: + try: + handler = plugin_class(self) + return handler.authenticate(auth_packet) + except AttributeError: + if plugin_name != b'dialog': + raise err.OperationalError(2059, "Authentication plugin '%s'" \ + " not loaded: - %r missing authenticate method" % (plugin_name, plugin_class)) + except TypeError: + raise err.OperationalError(2059, "Authentication plugin '%s'" \ + " not loaded: - %r cannot be constructed with connection object" % (plugin_name, plugin_class)) + else: + handler = None + if plugin_name == b"mysql_native_password": + # https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41 + data = _scramble(self.password.encode('latin1'), auth_packet.read_all()) + b'\0' + elif plugin_name == b"mysql_old_password": + # https://dev.mysql.com/doc/internals/en/old-password-authentication.html + data = _scramble_323(self.password.encode('latin1'), auth_packet.read_all()) + b'\0' + elif plugin_name == b"mysql_clear_password": + # https://dev.mysql.com/doc/internals/en/clear-text-authentication.html + data = self.password.encode('latin1') + b'\0' + elif plugin_name == b"dialog": + pkt = auth_packet + while True: + flag = pkt.read_uint8() + echo = (flag & 0x06) == 0x02 + last = (flag & 0x01) == 0x01 + prompt = pkt.read_all() - # if old_passwords is enabled the packet will be 1 byte long and - # have the octet 254 - - if auth_packet.is_eof_packet(): - # send legacy handshake - #raise NotImplementedError, "old_passwords are not supported. Check to see if mysqld was started with --old-passwords, if old-passwords=1 in a my.cnf file, or if there are some short hashes in your mysql.user table." - # TODO: is this the correct charset? - data = _scramble_323(self.password.encode(self.charset), self.salt.encode(self.charset)) + int2byte(0) - data = pack_int24(len(data)) + int2byte(next_packet) + data - - self.wfile.write(data) - self.wfile.flush() - auth_packet = MysqlPacket(self) - auth_packet.check_error() - if DEBUG: auth_packet.dump() + if prompt == b"Password: ": + self.write_packet(self.password.encode('latin1') + b'\0') + elif handler: + resp = 'no response - TypeError within plugin.prompt method' + try: + resp = handler.prompt(echo, prompt) + self.write_packet(resp + b'\0') + except AttributeError: + raise err.OperationalError(2059, "Authentication plugin '%s'" \ + " not loaded: - %r missing prompt method" % (plugin_name, handler)) + except TypeError: + raise err.OperationalError(2061, "Authentication plugin '%s'" \ + " %r didn't respond with string. Returned '%r' to prompt %r" % (plugin_name, handler, resp, prompt)) + else: + raise err.OperationalError(2059, "Authentication plugin '%s' (%r) not configured" % (plugin_name, handler)) + pkt = self._read_packet() + pkt.check_error() + if pkt.is_ok_packet() or last: + break + return pkt + else: + raise err.OperationalError(2059, "Authentication plugin '%s' not configured" % plugin_name) + self.write_packet(data) + pkt = self._read_packet() + pkt.check_error() + return pkt # _mysql support def thread_id(self): @@ -877,64 +1204,87 @@ class Connection(object): def _get_server_information(self): i = 0 - packet = MysqlPacket(self) + packet = self._read_packet() data = packet.get_all_data() if DEBUG: dump_packet(data) - #packet_len = byte2int(data[i:i+1]) - #i += 4 self.protocol_version = byte2int(data[i:i+1]) - i += 1 - server_end = data.find(int2byte(0), i) - # TODO: is this the correct charset? should it be default_charset? - self.server_version = data[i:server_end].decode(self.charset) + server_end = data.find(b'\0', i) + self.server_version = data[i:server_end].decode('latin1') i = server_end + 1 - self.server_thread_id = struct.unpack('= i + 1: - i += 1 + self.server_capabilities = struct.unpack('= i + 6: + lang, stat, cap_h, salt_len = struct.unpack('= i+12-1: - rest_salt = data[i:i+12] - self.salt += rest_salt + self.server_capabilities |= cap_h << 16 + if DEBUG: print("salt_len:", salt_len) + salt_len = max(12, salt_len - 9) + + # reserved + i += 10 + + if len(data) >= i + salt_len: + # salt_len includes auth_plugin_data_part_1 and filler + self.salt += data[i:i+salt_len] + i += salt_len + + i+=1 + # AUTH PLUGIN NAME may appear here. + if self.server_capabilities & CLIENT.PLUGIN_AUTH and len(data) >= i: + # Due to Bug#59453 the auth-plugin-name is missing the terminating + # NUL-char in versions prior to 5.5.10 and 5.6.2. + # ref: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake + # didn't use version checks as mariadb is corrected and reports + # earlier than those two. + server_end = data.find(b'\0', i) + if server_end < 0: # pragma: no cover - very specific upstream bug + # not found \0 and last field so take it all + self._auth_plugin_name = data[i:].decode('latin1') + else: + self._auth_plugin_name = data[i:server_end].decode('latin1') def get_server_info(self): return self.server_version - Warning = Warning - Error = Error - InterfaceError = InterfaceError - DatabaseError = DatabaseError - DataError = DataError - OperationalError = OperationalError - IntegrityError = IntegrityError - InternalError = InternalError - ProgrammingError = ProgrammingError - NotSupportedError = NotSupportedError + Warning = err.Warning + Error = err.Error + InterfaceError = err.InterfaceError + DatabaseError = err.DatabaseError + DataError = err.DataError + OperationalError = err.OperationalError + IntegrityError = err.IntegrityError + InternalError = err.InternalError + ProgrammingError = err.ProgrammingError + NotSupportedError = err.NotSupportedError + -# TODO: move OK and EOF packet parsing/logic into a proper subclass -# of MysqlPacket like has been done with FieldDescriptorPacket. class MySQLResult(object): def __init__(self, connection): - from weakref import proxy - self.connection = proxy(connection) + """ + :type connection: Connection + """ + self.connection = connection self.affected_rows = None self.insert_id = None - self.server_status = 0 + self.server_status = None self.warning_count = 0 self.message = None self.field_count = 0 @@ -948,122 +1298,202 @@ class MySQLResult(object): self._finish_unbuffered_query() def read(self): - self.first_packet = self.connection.read_packet() + try: + first_packet = self.connection._read_packet() - # TODO: use classes for different packet types? - if self.first_packet.is_ok_packet(): - self._read_ok_packet() - else: - self._read_result_packet() + if first_packet.is_ok_packet(): + self._read_ok_packet(first_packet) + elif first_packet.is_load_local_packet(): + self._read_load_local_packet(first_packet) + else: + self._read_result_packet(first_packet) + finally: + self.connection = None def init_unbuffered_query(self): self.unbuffered_active = True - self.first_packet = self.connection.read_packet() + first_packet = self.connection._read_packet() - if self.first_packet.is_ok_packet(): - self._read_ok_packet() + if first_packet.is_ok_packet(): + self._read_ok_packet(first_packet) self.unbuffered_active = False + self.connection = None + elif first_packet.is_load_local_packet(): + self._read_load_local_packet(first_packet) + self.unbuffered_active = False + self.connection = None else: - self.field_count = byte2int(self.first_packet.read(1)) + self.field_count = first_packet.read_length_encoded_integer() self._get_descriptions() - + # Apparently, MySQLdb picks this number because it's the maximum # value of a 64bit unsigned integer. Since we're emulating MySQLdb, # we set it to this instead of None, which would be preferred. self.affected_rows = 18446744073709551615 - def _read_ok_packet(self): - ok_packet = OKPacketWrapper(self.first_packet) + def _read_ok_packet(self, first_packet): + ok_packet = OKPacketWrapper(first_packet) self.affected_rows = ok_packet.affected_rows self.insert_id = ok_packet.insert_id self.server_status = ok_packet.server_status self.warning_count = ok_packet.warning_count self.message = ok_packet.message + self.has_next = ok_packet.has_next + + def _read_load_local_packet(self, first_packet): + load_packet = LoadLocalPacketWrapper(first_packet) + sender = LoadLocalFile(load_packet.filename, self.connection) + try: + sender.send_data() + except: + self.connection._read_packet() # skip ok packet + raise + + ok_packet = self.connection._read_packet() + if not ok_packet.is_ok_packet(): # pragma: no cover - upstream induced protocol error + raise err.OperationalError(2014, "Commands Out of Sync") + self._read_ok_packet(ok_packet) def _check_packet_is_eof(self, packet): - if packet.is_eof_packet(): - eof_packet = EOFPacketWrapper(packet) - self.warning_count = eof_packet.warning_count - self.has_next = eof_packet.has_next - return True - return False + if not packet.is_eof_packet(): + return False + #TODO: Support CLIENT.DEPRECATE_EOF + # 1) Add DEPRECATE_EOF to CAPABILITIES + # 2) Mask CAPABILITIES with server_capabilities + # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: use OKPacketWrapper instead of EOFPacketWrapper + wp = EOFPacketWrapper(packet) + self.warning_count = wp.warning_count + self.has_next = wp.has_next + return True - def _read_result_packet(self): - self.field_count = byte2int(self.first_packet.read(1)) + def _read_result_packet(self, first_packet): + self.field_count = first_packet.read_length_encoded_integer() self._get_descriptions() self._read_rowdata_packet() def _read_rowdata_packet_unbuffered(self): # Check if in an active query - if self.unbuffered_active == False: return - + if not self.unbuffered_active: + return + # EOF - packet = self.connection.read_packet() + packet = self.connection._read_packet() if self._check_packet_is_eof(packet): self.unbuffered_active = False + self.connection = None self.rows = None return - row = [] - for field in self.fields: - data = packet.read_length_coded_string() - converted = None - if field.type_code in self.connection.decoders: - converter = self.connection.decoders[field.type_code] - if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter) - if data != None: - converted = converter(self.connection, field, data) - row.append(converted) - + row = self._read_row_from_packet(packet) self.affected_rows = 1 - self.rows = tuple((row)) - if DEBUG: self.rows + self.rows = (row,) # rows should tuple of row for MySQL-python compatibility. + return row def _finish_unbuffered_query(self): # After much reading on the MySQL protocol, it appears that there is, # in fact, no way to stop MySQL from sending all the data after # executing a query, so we just spin, and wait for an EOF packet. while self.unbuffered_active: - packet = self.connection.read_packet() + packet = self.connection._read_packet() if self._check_packet_is_eof(packet): self.unbuffered_active = False + self.connection = None # release reference to kill cyclic reference. - # TODO: implement this as an iteratable so that it is more - # memory efficient and lower-latency to client... def _read_rowdata_packet(self): - """Read a rowdata packet for each data row in the result set.""" - rows = [] - while True: - packet = self.connection.read_packet() - if self._check_packet_is_eof(packet): - break + """Read a rowdata packet for each data row in the result set.""" + rows = [] + while True: + packet = self.connection._read_packet() + if self._check_packet_is_eof(packet): + self.connection = None # release reference to kill cyclic reference. + break + rows.append(self._read_row_from_packet(packet)) + self.affected_rows = len(rows) + self.rows = tuple(rows) + + def _read_row_from_packet(self, packet): row = [] - for field in self.fields: - data = packet.read_length_coded_string() - converted = None - if field.type_code in self.connection.decoders: - converter = self.connection.decoders[field.type_code] - if DEBUG: print "DEBUG: field=%s, converter=%s" % (field, converter) - if data != None: - converted = converter(self.connection, field, data) - row.append(converted) - - rows.append(tuple(row)) - - self.affected_rows = len(rows) - self.rows = tuple(rows) - if DEBUG: self.rows + for encoding, converter in self.converters: + try: + data = packet.read_length_coded_string() + except IndexError: + # No more columns in this row + # See https://github.com/PyMySQL/PyMySQL/pull/434 + break + if data is not None: + if encoding is not None: + data = data.decode(encoding) + if DEBUG: print("DEBUG: DATA = ", data) + if converter is not None: + data = converter(data) + row.append(data) + return tuple(row) def _get_descriptions(self): """Read a column descriptor packet for each column in the result.""" self.fields = [] + self.converters = [] + use_unicode = self.connection.use_unicode + conn_encoding = self.connection.encoding description = [] - for i in xrange(self.field_count): - field = self.connection.read_packet(FieldDescriptorPacket) + + for i in range_type(self.field_count): + field = self.connection._read_packet(FieldDescriptorPacket) self.fields.append(field) description.append(field.description()) + field_type = field.type_code + if use_unicode: + if field_type == FIELD_TYPE.JSON: + # When SELECT from JSON column: charset = binary + # When SELECT CAST(... AS JSON): charset = connection encoding + # This behavior is different from TEXT / BLOB. + # We should decode result by connection encoding regardless charsetnr. + # See https://github.com/PyMySQL/PyMySQL/issues/488 + encoding = conn_encoding # SELECT CAST(... AS JSON) + elif field_type in TEXT_TYPES: + if field.charsetnr == 63: # binary + # TEXTs with charset=binary means BINARY types. + encoding = None + else: + encoding = conn_encoding + else: + # Integers, Dates and Times, and other basic data is encoded in ascii + encoding = 'ascii' + else: + encoding = None + converter = self.connection.decoders.get(field_type) + if converter is through: + converter = None + if DEBUG: print("DEBUG: field={}, converter={}".format(field, converter)) + self.converters.append((encoding, converter)) - eof_packet = self.connection.read_packet() + eof_packet = self.connection._read_packet() assert eof_packet.is_eof_packet(), 'Protocol error, expecting EOF' self.description = tuple(description) + + +class LoadLocalFile(object): + def __init__(self, filename, connection): + self.filename = filename + self.connection = connection + + def send_data(self): + """Send data packets from the local file to the server""" + if not self.connection._sock: + raise err.InterfaceError("(0, '')") + conn = self.connection + + try: + with open(self.filename, 'rb') as open_file: + packet_size = min(conn.max_allowed_packet, 16*1024) # 16KB is efficient enough + while True: + chunk = open_file.read(packet_size) + if not chunk: + break + conn.write_packet(chunk) + except IOError: + raise err.OperationalError(1017, "Can't find file '{0}'".format(self.filename)) + finally: + # send the empty packet to signify we are done sending data + conn.write_packet(b'') diff --git a/gluon/contrib/pymysql/constants/CLIENT.py b/gluon/contrib/pymysql/constants/CLIENT.py index 9d11ea10..6625b6a7 100644 --- a/gluon/contrib/pymysql/constants/CLIENT.py +++ b/gluon/contrib/pymysql/constants/CLIENT.py @@ -1,4 +1,4 @@ - +# https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags LONG_PASSWORD = 1 FOUND_ROWS = 1 << 1 LONG_FLAG = 1 << 2 @@ -12,9 +12,20 @@ PROTOCOL_41 = 1 << 9 INTERACTIVE = 1 << 10 SSL = 1 << 11 IGNORE_SIGPIPE = 1 << 12 -TRANSACTIONS = 1 << 13 +TRANSACTIONS = 1 << 13 SECURE_CONNECTION = 1 << 15 MULTI_STATEMENTS = 1 << 16 MULTI_RESULTS = 1 << 17 -CAPABILITIES = LONG_PASSWORD|LONG_FLAG|TRANSACTIONS| \ - PROTOCOL_41|SECURE_CONNECTION +PS_MULTI_RESULTS = 1 << 18 +PLUGIN_AUTH = 1 << 19 +PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21 +CAPABILITIES = ( + LONG_PASSWORD | LONG_FLAG | PROTOCOL_41 | TRANSACTIONS + | SECURE_CONNECTION | MULTI_STATEMENTS | MULTI_RESULTS + | PLUGIN_AUTH | PLUGIN_AUTH_LENENC_CLIENT_DATA) + +# Not done yet +CONNECT_ATTRS = 1 << 20 +HANDLE_EXPIRED_PASSWORDS = 1 << 22 +SESSION_TRACK = 1 << 23 +DEPRECATE_EOF = 1 << 24 diff --git a/gluon/contrib/pymysql/constants/COMMAND.py b/gluon/contrib/pymysql/constants/COMMAND.py index 4a757da2..1da27553 100644 --- a/gluon/contrib/pymysql/constants/COMMAND.py +++ b/gluon/contrib/pymysql/constants/COMMAND.py @@ -21,3 +21,13 @@ COM_BINLOG_DUMP = 0x12 COM_TABLE_DUMP = 0x13 COM_CONNECT_OUT = 0x14 COM_REGISTER_SLAVE = 0x15 +COM_STMT_PREPARE = 0x16 +COM_STMT_EXECUTE = 0x17 +COM_STMT_SEND_LONG_DATA = 0x18 +COM_STMT_CLOSE = 0x19 +COM_STMT_RESET = 0x1a +COM_SET_OPTION = 0x1b +COM_STMT_FETCH = 0x1c +COM_DAEMON = 0x1d +COM_BINLOG_DUMP_GTID = 0x1e +COM_END = 0x1f diff --git a/gluon/contrib/pymysql/constants/CR.py b/gluon/contrib/pymysql/constants/CR.py new file mode 100755 index 00000000..48ca956e --- /dev/null +++ b/gluon/contrib/pymysql/constants/CR.py @@ -0,0 +1,68 @@ +# flake8: noqa +# errmsg.h +CR_ERROR_FIRST = 2000 +CR_UNKNOWN_ERROR = 2000 +CR_SOCKET_CREATE_ERROR = 2001 +CR_CONNECTION_ERROR = 2002 +CR_CONN_HOST_ERROR = 2003 +CR_IPSOCK_ERROR = 2004 +CR_UNKNOWN_HOST = 2005 +CR_SERVER_GONE_ERROR = 2006 +CR_VERSION_ERROR = 2007 +CR_OUT_OF_MEMORY = 2008 +CR_WRONG_HOST_INFO = 2009 +CR_LOCALHOST_CONNECTION = 2010 +CR_TCP_CONNECTION = 2011 +CR_SERVER_HANDSHAKE_ERR = 2012 +CR_SERVER_LOST = 2013 +CR_COMMANDS_OUT_OF_SYNC = 2014 +CR_NAMEDPIPE_CONNECTION = 2015 +CR_NAMEDPIPEWAIT_ERROR = 2016 +CR_NAMEDPIPEOPEN_ERROR = 2017 +CR_NAMEDPIPESETSTATE_ERROR = 2018 +CR_CANT_READ_CHARSET = 2019 +CR_NET_PACKET_TOO_LARGE = 2020 +CR_EMBEDDED_CONNECTION = 2021 +CR_PROBE_SLAVE_STATUS = 2022 +CR_PROBE_SLAVE_HOSTS = 2023 +CR_PROBE_SLAVE_CONNECT = 2024 +CR_PROBE_MASTER_CONNECT = 2025 +CR_SSL_CONNECTION_ERROR = 2026 +CR_MALFORMED_PACKET = 2027 +CR_WRONG_LICENSE = 2028 + +CR_NULL_POINTER = 2029 +CR_NO_PREPARE_STMT = 2030 +CR_PARAMS_NOT_BOUND = 2031 +CR_DATA_TRUNCATED = 2032 +CR_NO_PARAMETERS_EXISTS = 2033 +CR_INVALID_PARAMETER_NO = 2034 +CR_INVALID_BUFFER_USE = 2035 +CR_UNSUPPORTED_PARAM_TYPE = 2036 + +CR_SHARED_MEMORY_CONNECTION = 2037 +CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038 +CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039 +CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040 +CR_SHARED_MEMORY_CONNECT_MAP_ERROR = 2041 +CR_SHARED_MEMORY_FILE_MAP_ERROR = 2042 +CR_SHARED_MEMORY_MAP_ERROR = 2043 +CR_SHARED_MEMORY_EVENT_ERROR = 2044 +CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045 +CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046 +CR_CONN_UNKNOW_PROTOCOL = 2047 +CR_INVALID_CONN_HANDLE = 2048 +CR_SECURE_AUTH = 2049 +CR_FETCH_CANCELED = 2050 +CR_NO_DATA = 2051 +CR_NO_STMT_METADATA = 2052 +CR_NO_RESULT_SET = 2053 +CR_NOT_IMPLEMENTED = 2054 +CR_SERVER_LOST_EXTENDED = 2055 +CR_STMT_CLOSED = 2056 +CR_NEW_STMT_METADATA = 2057 +CR_ALREADY_CONNECTED = 2058 +CR_AUTH_PLUGIN_CANNOT_LOAD = 2059 +CR_DUPLICATE_CONNECTION_ATTR = 2060 +CR_AUTH_PLUGIN_ERR = 2061 +CR_ERROR_LAST = 2061 diff --git a/gluon/contrib/pymysql/constants/FIELD_TYPE.py b/gluon/contrib/pymysql/constants/FIELD_TYPE.py index 3df7ff48..51bd5143 100644 --- a/gluon/contrib/pymysql/constants/FIELD_TYPE.py +++ b/gluon/contrib/pymysql/constants/FIELD_TYPE.py @@ -17,6 +17,7 @@ YEAR = 13 NEWDATE = 14 VARCHAR = 15 BIT = 16 +JSON = 245 NEWDECIMAL = 246 ENUM = 247 SET = 248 diff --git a/gluon/contrib/pymysql/constants/SERVER_STATUS.py b/gluon/contrib/pymysql/constants/SERVER_STATUS.py index 010f542d..6f5d5663 100644 --- a/gluon/contrib/pymysql/constants/SERVER_STATUS.py +++ b/gluon/contrib/pymysql/constants/SERVER_STATUS.py @@ -9,4 +9,3 @@ SERVER_STATUS_LAST_ROW_SENT = 128 SERVER_STATUS_DB_DROPPED = 256 SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512 SERVER_STATUS_METADATA_CHANGED = 1024 - diff --git a/gluon/contrib/pymysql/converters.py b/gluon/contrib/pymysql/converters.py index 56e93f9d..4c00bc45 100644 --- a/gluon/contrib/pymysql/converters.py +++ b/gluon/contrib/pymysql/converters.py @@ -1,106 +1,162 @@ -import re +from ._compat import PY2, text_type, long_type, JYTHON, IRONPYTHON, unichr + import datetime +from decimal import Decimal +import re import time -import sys -from constants import FIELD_TYPE, FLAG -from charset import charset_by_id +from .constants import FIELD_TYPE, FLAG +from .charset import charset_by_id, charset_to_encoding -PYTHON3 = sys.version_info[0] > 2 -try: - set -except NameError: - try: - from sets import BaseSet as set - except ImportError: - from sets import Set as set +def escape_item(val, charset, mapping=None): + if mapping is None: + mapping = encoders + encoder = mapping.get(type(val)) -ESCAPE_REGEX = re.compile(r"[\0\n\r\032\'\"\\]") -ESCAPE_MAP = {'\0': '\\0', '\n': '\\n', '\r': '\\r', '\032': '\\Z', - '\'': '\\\'', '"': '\\"', '\\': '\\\\'} + # Fallback to default when no encoder found + if not encoder: + try: + encoder = mapping[text_type] + except KeyError: + raise TypeError("no default type converter defined") -def escape_item(val, charset): - if type(val) in [tuple, list, set]: - return escape_sequence(val, charset) - if type(val) is dict: - return escape_dict(val, charset) - if PYTHON3 and hasattr(val, "decode") and not isinstance(val, unicode): - # deal with py3k bytes - val = val.decode(charset) - encoder = encoders[type(val)] - val = encoder(val) - if type(val) in [str, int]: - return val - val = val.encode(charset) + if encoder in (escape_dict, escape_sequence): + val = encoder(val, charset, mapping) + else: + val = encoder(val, mapping) return val -def escape_dict(val, charset): +def escape_dict(val, charset, mapping=None): n = {} for k, v in val.items(): - quoted = escape_item(v, charset) + quoted = escape_item(v, charset, mapping) n[k] = quoted return n -def escape_sequence(val, charset): +def escape_sequence(val, charset, mapping=None): n = [] for item in val: - quoted = escape_item(item, charset) + quoted = escape_item(item, charset, mapping) n.append(quoted) return "(" + ",".join(n) + ")" -def escape_set(val, charset): - val = map(lambda x: escape_item(x, charset), val) - return ','.join(val) +def escape_set(val, charset, mapping=None): + return ','.join([escape_item(x, charset, mapping) for x in val]) -def escape_bool(value): +def escape_bool(value, mapping=None): return str(int(value)) -def escape_object(value): +def escape_object(value, mapping=None): return str(value) -def escape_int(value): - return value +def escape_int(value, mapping=None): + return str(value) -escape_long = escape_object - -def escape_float(value): +def escape_float(value, mapping=None): return ('%.15g' % value) -def escape_string(value): - return ("'%s'" % ESCAPE_REGEX.sub( - lambda match: ESCAPE_MAP.get(match.group(0)), value)) +_escape_table = [unichr(x) for x in range(128)] +_escape_table[0] = u'\\0' +_escape_table[ord('\\')] = u'\\\\' +_escape_table[ord('\n')] = u'\\n' +_escape_table[ord('\r')] = u'\\r' +_escape_table[ord('\032')] = u'\\Z' +_escape_table[ord('"')] = u'\\"' +_escape_table[ord("'")] = u"\\'" -def escape_unicode(value): - return escape_string(value) +def _escape_unicode(value, mapping=None): + """escapes *value* without adding quote. -def escape_None(value): + Value should be unicode + """ + return value.translate(_escape_table) + +if PY2: + def escape_string(value, mapping=None): + """escape_string escapes *value* but not surround it with quotes. + + Value should be bytes or unicode. + """ + if isinstance(value, unicode): + return _escape_unicode(value) + assert isinstance(value, (bytes, bytearray)) + value = value.replace('\\', '\\\\') + value = value.replace('\0', '\\0') + value = value.replace('\n', '\\n') + value = value.replace('\r', '\\r') + value = value.replace('\032', '\\Z') + value = value.replace("'", "\\'") + value = value.replace('"', '\\"') + return value + + def escape_bytes(value, mapping=None): + assert isinstance(value, (bytes, bytearray)) + return b"_binary'%s'" % escape_string(value) +else: + escape_string = _escape_unicode + + # On Python ~3.5, str.decode('ascii', 'surrogateescape') is slow. + # (fixed in Python 3.6, http://bugs.python.org/issue24870) + # Workaround is str.decode('latin1') then translate 0x80-0xff into 0udc80-0udcff. + # We can escape special chars and surrogateescape at once. + _escape_bytes_table = _escape_table + [chr(i) for i in range(0xdc80, 0xdd00)] + + def escape_bytes(value, mapping=None): + return "_binary'%s'" % value.decode('latin1').translate(_escape_bytes_table) + + +def escape_unicode(value, mapping=None): + return u"'%s'" % _escape_unicode(value) + +def escape_str(value, mapping=None): + return "'%s'" % escape_string(str(value), mapping) + +def escape_None(value, mapping=None): return 'NULL' -def escape_timedelta(obj): +def escape_timedelta(obj, mapping=None): seconds = int(obj.seconds) % 60 minutes = int(obj.seconds // 60) % 60 hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24 - return escape_string('%02d:%02d:%02d' % (hours, minutes, seconds)) + if obj.microseconds: + fmt = "'{0:02d}:{1:02d}:{2:02d}.{3:06d}'" + else: + fmt = "'{0:02d}:{1:02d}:{2:02d}'" + return fmt.format(hours, minutes, seconds, obj.microseconds) -def escape_time(obj): - s = "%02d:%02d:%02d" % (int(obj.hour), int(obj.minute), - int(obj.second)) +def escape_time(obj, mapping=None): if obj.microsecond: - s += ".%f" % obj.microsecond + fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + else: + fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}'" + return fmt.format(obj) - return escape_string(s) +def escape_datetime(obj, mapping=None): + if obj.microsecond: + fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + else: + fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'" + return fmt.format(obj) -def escape_datetime(obj): - return escape_string(obj.strftime("%Y-%m-%d %H:%M:%S")) +def escape_date(obj, mapping=None): + fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'" + return fmt.format(obj) -def escape_date(obj): - return escape_string(obj.strftime("%Y-%m-%d")) - -def escape_struct_time(obj): +def escape_struct_time(obj, mapping=None): return escape_datetime(datetime.datetime(*obj[:6])) -def convert_datetime(connection, field, obj): +def _convert_second_fraction(s): + if not s: + return 0 + # Pad zeros to ensure the fraction length in microseconds + s = s.ljust(6, '0') + return int(s[:6]) + +DATETIME_RE = re.compile(r"(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_datetime(obj): """Returns a DATETIME or TIMESTAMP column value as a datetime object: >>> datetime_or_None('2007-02-25 23:06:20') @@ -116,22 +172,24 @@ def convert_datetime(connection, field, obj): True """ - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) - if ' ' in obj: - sep = ' ' - elif 'T' in obj: - sep = 'T' - else: - return convert_date(connection, field, obj) + if not PY2 and isinstance(obj, (bytes, bytearray)): + obj = obj.decode('ascii') + + m = DATETIME_RE.match(obj) + if not m: + return convert_date(obj) try: - ymd, hms = obj.split(sep, 1) - return datetime.datetime(*[ int(x) for x in ymd.split('-')+hms.split(':') ]) + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + return datetime.datetime(*[ int(x) for x in groups ]) except ValueError: - return convert_date(connection, field, obj) + return convert_date(obj) -def convert_timedelta(connection, field, obj): +TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_timedelta(obj): """Returns a TIME column as a timedelta object: >>> timedelta_or_None('25:06:17') @@ -148,25 +206,33 @@ def convert_timedelta(connection, field, obj): can accept values as (+|-)DD HH:MM:SS. The latter format will not be parsed correctly by this function. """ + if not PY2 and isinstance(obj, (bytes, bytearray)): + obj = obj.decode('ascii') + + m = TIMEDELTA_RE.match(obj) + if not m: + return None + try: - microseconds = 0 - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) - if "." in obj: - (obj, tail) = obj.split('.') - microseconds = int(tail) - hours, minutes, seconds = obj.split(':') + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + negate = -1 if groups[0] else 1 + hours, minutes, seconds, microseconds = groups[1:] + tdelta = datetime.timedelta( hours = int(hours), minutes = int(minutes), seconds = int(seconds), - microseconds = microseconds - ) + microseconds = int(microseconds) + ) * negate return tdelta except ValueError: return None -def convert_time(connection, field, obj): +TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_time(obj): """Returns a TIME column as a time object: >>> time_or_None('15:06:17') @@ -188,18 +254,24 @@ def convert_time(connection, field, obj): to be treated as time-of-day and not a time offset, then you can use set this function as the converter for FIELD_TYPE.TIME. """ + if not PY2 and isinstance(obj, (bytes, bytearray)): + obj = obj.decode('ascii') + + m = TIME_RE.match(obj) + if not m: + return None + try: - microseconds = 0 - if "." in obj: - (obj, tail) = obj.split('.') - microseconds = int(tail) - hours, minutes, seconds = obj.split(':') + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + hours, minutes, seconds, microseconds = groups return datetime.time(hour=int(hours), minute=int(minutes), - second=int(seconds), microsecond=microseconds) + second=int(seconds), microsecond=int(microseconds)) except ValueError: return None -def convert_date(connection, field, obj): + +def convert_date(obj): """Returns a DATE column as a date object: >>> date_or_None('2007-02-26') @@ -213,14 +285,15 @@ def convert_date(connection, field, obj): True """ + if not PY2 and isinstance(obj, (bytes, bytearray)): + obj = obj.decode('ascii') try: - if not isinstance(obj, unicode): - obj = obj.decode(connection.charset) return datetime.date(*[ int(x) for x in obj.split('-', 2) ]) except ValueError: return None -def convert_mysql_timestamp(connection, field, timestamp): + +def convert_mysql_timestamp(timestamp): """Convert a MySQL TIMESTAMP to a Timestamp object. MySQL >= 4.1 returns TIMESTAMP in the same format as DATETIME: @@ -241,11 +314,10 @@ def convert_mysql_timestamp(connection, field, timestamp): True """ - if not isinstance(timestamp, unicode): - timestamp = timestamp.decode(connection.charset) - + if not PY2 and isinstance(timestamp, (bytes, bytearray)): + timestamp = timestamp.decode('ascii') if timestamp[4] == '-': - return convert_datetime(connection, field, timestamp) + return convert_datetime(timestamp) timestamp += "0"*(14-len(timestamp)) # padding year, month, day, hour, minute, second = \ int(timestamp[:4]), int(timestamp[4:6]), int(timestamp[6:8]), \ @@ -256,101 +328,92 @@ def convert_mysql_timestamp(connection, field, timestamp): return None def convert_set(s): + if isinstance(s, (bytes, bytearray)): + return set(s.split(b",")) return set(s.split(",")) -def convert_bit(connection, field, b): - #b = "\x00" * (8 - len(b)) + b # pad w/ zeroes - #return struct.unpack(">Q", b)[0] - # - # the snippet above is right, but MySQLdb doesn't process bits, - # so we shouldn't either - return b + +def through(x): + return x + + +#def convert_bit(b): +# b = "\x00" * (8 - len(b)) + b # pad w/ zeroes +# return struct.unpack(">Q", b)[0] +# +# the snippet above is right, but MySQLdb doesn't process bits, +# so we shouldn't either +convert_bit = through + def convert_characters(connection, field, data): field_charset = charset_by_id(field.charsetnr).name + encoding = charset_to_encoding(field_charset) if field.flags & FLAG.SET: - return convert_set(data.decode(field_charset)) + return convert_set(data.decode(encoding)) if field.flags & FLAG.BINARY: return data if connection.use_unicode: - data = data.decode(field_charset) + data = data.decode(encoding) elif connection.charset != field_charset: - data = data.decode(field_charset) - data = data.encode(connection.charset) + data = data.decode(encoding) + data = data.encode(connection.encoding) return data -def convert_int(connection, field, data): - return int(data) - -def convert_long(connection, field, data): - return long(data) - -def convert_float(connection, field, data): - return float(data) - encoders = { - bool: escape_bool, - int: escape_int, - long: escape_long, - float: escape_float, - str: escape_string, - unicode: escape_unicode, - tuple: escape_sequence, - list:escape_sequence, - set:escape_sequence, - dict:escape_dict, - type(None):escape_None, - datetime.date: escape_date, - datetime.datetime : escape_datetime, - datetime.timedelta : escape_timedelta, - datetime.time : escape_time, - time.struct_time : escape_struct_time, - } + bool: escape_bool, + int: escape_int, + long_type: escape_int, + float: escape_float, + str: escape_str, + text_type: escape_unicode, + tuple: escape_sequence, + list: escape_sequence, + set: escape_sequence, + frozenset: escape_sequence, + dict: escape_dict, + bytearray: escape_bytes, + type(None): escape_None, + datetime.date: escape_date, + datetime.datetime: escape_datetime, + datetime.timedelta: escape_timedelta, + datetime.time: escape_time, + time.struct_time: escape_struct_time, + Decimal: escape_object, +} + +if not PY2 or JYTHON or IRONPYTHON: + encoders[bytes] = escape_bytes decoders = { - FIELD_TYPE.BIT: convert_bit, - FIELD_TYPE.TINY: convert_int, - FIELD_TYPE.SHORT: convert_int, - FIELD_TYPE.LONG: convert_long, - FIELD_TYPE.FLOAT: convert_float, - FIELD_TYPE.DOUBLE: convert_float, - FIELD_TYPE.DECIMAL: convert_float, - FIELD_TYPE.NEWDECIMAL: convert_float, - FIELD_TYPE.LONGLONG: convert_long, - FIELD_TYPE.INT24: convert_int, - FIELD_TYPE.YEAR: convert_int, - FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp, - FIELD_TYPE.DATETIME: convert_datetime, - FIELD_TYPE.TIME: convert_timedelta, - FIELD_TYPE.DATE: convert_date, - FIELD_TYPE.SET: convert_set, - FIELD_TYPE.BLOB: convert_characters, - FIELD_TYPE.TINY_BLOB: convert_characters, - FIELD_TYPE.MEDIUM_BLOB: convert_characters, - FIELD_TYPE.LONG_BLOB: convert_characters, - FIELD_TYPE.STRING: convert_characters, - FIELD_TYPE.VAR_STRING: convert_characters, - FIELD_TYPE.VARCHAR: convert_characters, - #FIELD_TYPE.BLOB: str, - #FIELD_TYPE.STRING: str, - #FIELD_TYPE.VAR_STRING: str, - #FIELD_TYPE.VARCHAR: str - } -conversions = decoders # for MySQLdb compatibility + FIELD_TYPE.BIT: convert_bit, + FIELD_TYPE.TINY: int, + FIELD_TYPE.SHORT: int, + FIELD_TYPE.LONG: int, + FIELD_TYPE.FLOAT: float, + FIELD_TYPE.DOUBLE: float, + FIELD_TYPE.LONGLONG: int, + FIELD_TYPE.INT24: int, + FIELD_TYPE.YEAR: int, + FIELD_TYPE.TIMESTAMP: convert_mysql_timestamp, + FIELD_TYPE.DATETIME: convert_datetime, + FIELD_TYPE.TIME: convert_timedelta, + FIELD_TYPE.DATE: convert_date, + FIELD_TYPE.SET: convert_set, + FIELD_TYPE.BLOB: through, + FIELD_TYPE.TINY_BLOB: through, + FIELD_TYPE.MEDIUM_BLOB: through, + FIELD_TYPE.LONG_BLOB: through, + FIELD_TYPE.STRING: through, + FIELD_TYPE.VAR_STRING: through, + FIELD_TYPE.VARCHAR: through, + FIELD_TYPE.DECIMAL: Decimal, + FIELD_TYPE.NEWDECIMAL: Decimal, +} -try: - # python version > 2.3 - from decimal import Decimal - def convert_decimal(connection, field, data): - data = data.decode(connection.charset) - return Decimal(data) - decoders[FIELD_TYPE.DECIMAL] = convert_decimal - decoders[FIELD_TYPE.NEWDECIMAL] = convert_decimal - def escape_decimal(obj): - return unicode(obj) - encoders[Decimal] = escape_decimal - -except ImportError: - pass +# for MySQLdb compatibility +conversions = encoders.copy() +conversions.update(decoders) +Thing2Literal = escape_str diff --git a/gluon/contrib/pymysql/cursors.py b/gluon/contrib/pymysql/cursors.py index f9775f56..c3e16ba1 100644 --- a/gluon/contrib/pymysql/cursors.py +++ b/gluon/contrib/pymysql/cursors.py @@ -1,67 +1,82 @@ # -*- coding: utf-8 -*- -import struct +from __future__ import print_function, absolute_import +from functools import partial import re +import warnings -try: - import cStringIO as StringIO -except ImportError: - import StringIO +from ._compat import range_type, text_type, PY2 +from . import err -from err import Warning, Error, InterfaceError, DataError, \ - DatabaseError, OperationalError, IntegrityError, InternalError, \ - NotSupportedError, ProgrammingError -insert_values = re.compile(r'\svalues\s*(\(.+\))', re.IGNORECASE) +#: Regular expression for :meth:`Cursor.executemany`. +#: executemany only suports simple bulk insert. +#: You can use it to load large dataset. +RE_INSERT_VALUES = re.compile( + r"\s*((?:INSERT|REPLACE)\s.+\sVALUES?\s+)" + + r"(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))" + + r"(\s*(?:ON DUPLICATE.*)?)\Z", + re.IGNORECASE | re.DOTALL) + class Cursor(object): - ''' + """ This is the object you use to interact with the database. - ''' + """ + + #: Max stetement size which :meth:`executemany` generates. + #: + #: Max size of allowed statement is max_allowed_packet - packet_header_size. + #: Default value of max_allowed_packet is 1048576. + max_stmt_length = 1024000 + + _defer_warnings = False + def __init__(self, connection): - ''' + """ Do not create an instance of a Cursor yourself. Call connections.Connection.cursor(). - ''' - from weakref import proxy - self.connection = proxy(connection) + """ + self.connection = connection self.description = None self.rownumber = 0 self.rowcount = -1 self.arraysize = 1 self._executed = None - self.messages = [] - self.errorhandler = connection.errorhandler - self._has_next = None - self._rows = () - - def __del__(self): - ''' - When this gets GC'd close it. - ''' - self.close() + self._result = None + self._rows = None + self._warnings_handled = False def close(self): - ''' + """ Closing a cursor just exhausts all remaining data. - ''' - if not self.connection: + """ + conn = self.connection + if conn is None: return try: while self.nextset(): pass - except: - pass + finally: + self.connection = None - self.connection = None + def __enter__(self): + return self + + def __exit__(self, *exc_info): + del exc_info + self.close() def _get_db(self): if not self.connection: - self.errorhandler(self, ProgrammingError, "cursor closed") + raise err.ProgrammingError("Cursor closed") return self.connection def _check_executed(self): if not self._executed: - self.errorhandler(self, ProgrammingError, "execute() first") + raise err.ProgrammingError("execute() first") + + def _conv_row(self, row): + return row def setinputsizes(self, *args): """Does nothing, required by DB API.""" @@ -69,69 +84,152 @@ class Cursor(object): def setoutputsizes(self, *args): """Does nothing, required by DB API.""" - def nextset(self): - ''' Get the next query set ''' - if self._executed: - self.fetchall() - del self.messages[:] - - if not self._has_next: + def _nextset(self, unbuffered=False): + """Get the next query set""" + conn = self._get_db() + current_result = self._result + # for unbuffered queries warnings are only available once whole result has been read + if unbuffered: + self._show_warnings() + if current_result is None or current_result is not conn._result: return None - connection = self._get_db() - connection.next_result() + if not current_result.has_next: + return None + conn.next_result(unbuffered=unbuffered) self._do_get_result() return True - def execute(self, query, args=None): - ''' Execute a query ''' - from sys import exc_info + def nextset(self): + return self._nextset(False) + def _ensure_bytes(self, x, encoding=None): + if isinstance(x, text_type): + x = x.encode(encoding) + elif isinstance(x, (tuple, list)): + x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x) + return x + + def _escape_args(self, args, conn): + ensure_bytes = partial(self._ensure_bytes, encoding=conn.encoding) + + if isinstance(args, (tuple, list)): + if PY2: + args = tuple(map(ensure_bytes, args)) + return tuple(conn.literal(arg) for arg in args) + elif isinstance(args, dict): + if PY2: + args = dict((ensure_bytes(key), ensure_bytes(val)) for + (key, val) in args.items()) + return dict((key, conn.literal(val)) for (key, val) in args.items()) + else: + # If it's not a dictionary let's try escaping it anyways. + # Worst case it will throw a Value error + if PY2: + args = ensure_bytes(args) + return conn.escape(args) + + def mogrify(self, query, args=None): + """ + Returns the exact string that is sent to the database by calling the + execute() method. + + This method follows the extension to the DB API 2.0 followed by Psycopg. + """ conn = self._get_db() - charset = conn.charset - del self.messages[:] - - # TODO: make sure that conn.escape is correct - - if isinstance(query, unicode): - query = query.encode(charset) + if PY2: # Use bytes on Python 2 always + query = self._ensure_bytes(query, encoding=conn.encoding) if args is not None: - if isinstance(args, tuple) or isinstance(args, list): - escaped_args = tuple(conn.escape(arg) for arg in args) - elif isinstance(args, dict): - escaped_args = dict((key, conn.escape(val)) for (key, val) in args.items()) - else: - #If it's not a dictionary let's try escaping it anyways. - #Worst case it will throw a Value error - escaped_args = conn.escape(args) + query = query % self._escape_args(args, conn) - query = query % escaped_args + return query - result = 0 - try: - result = self._query(query) - except: - exc, value, tb = exc_info() - del tb - self.messages.append((exc,value)) - self.errorhandler(self, exc, value) + def execute(self, query, args=None): + """Execute a query + :param str query: Query to execute. + + :param args: parameters used with query. (optional) + :type args: tuple, list or dict + + :return: Number of affected rows + :rtype: int + + If args is a list or tuple, %s can be used as a placeholder in the query. + If args is a dict, %(name)s can be used as a placeholder in the query. + """ + while self.nextset(): + pass + + query = self.mogrify(query, args) + + result = self._query(query) self._executed = query return result def executemany(self, query, args): - ''' Run several data against one query ''' - del self.messages[:] - #conn = self._get_db() + # type: (str, list) -> int + """Run several data against one query + + :param query: query to execute on server + :param args: Sequence of sequences or mappings. It is used as parameter. + :return: Number of rows affected, if any. + + This method improves performance on multiple-row INSERT and + REPLACE. Otherwise it is equivalent to looping over args with + execute(). + """ if not args: return - #charset = conn.charset - #if isinstance(query, unicode): - # query = query.encode(charset) - self.rowcount = sum([ self.execute(query, arg) for arg in args ]) + m = RE_INSERT_VALUES.match(query) + if m: + q_prefix = m.group(1) % () + q_values = m.group(2).rstrip() + q_postfix = m.group(3) or '' + assert q_values[0] == '(' and q_values[-1] == ')' + return self._do_execute_many(q_prefix, q_values, q_postfix, args, + self.max_stmt_length, + self._get_db().encoding) + + self.rowcount = sum(self.execute(query, arg) for arg in args) return self.rowcount + def _do_execute_many(self, prefix, values, postfix, args, max_stmt_length, encoding): + conn = self._get_db() + escape = self._escape_args + if isinstance(prefix, text_type): + prefix = prefix.encode(encoding) + if PY2 and isinstance(values, text_type): + values = values.encode(encoding) + if isinstance(postfix, text_type): + postfix = postfix.encode(encoding) + sql = bytearray(prefix) + args = iter(args) + v = values % escape(next(args), conn) + if isinstance(v, text_type): + if PY2: + v = v.encode(encoding) + else: + v = v.encode(encoding, 'surrogateescape') + sql += v + rows = 0 + for arg in args: + v = values % escape(arg, conn) + if isinstance(v, text_type): + if PY2: + v = v.encode(encoding) + else: + v = v.encode(encoding, 'surrogateescape') + if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: + rows += self.execute(sql + postfix) + sql = bytearray(prefix) + else: + sql += b',' + sql += v + rows += self.execute(sql + postfix) + self.rowcount = rows + return rows def callproc(self, procname, args=()): """Execute stored procedure procname with args @@ -164,23 +262,18 @@ class Cursor(object): conn = self._get_db() for index, arg in enumerate(args): q = "SET @_%s_%d=%s" % (procname, index, conn.escape(arg)) - if isinstance(q, unicode): - q = q.encode(conn.charset) self._query(q) self.nextset() q = "CALL %s(%s)" % (procname, ','.join(['@_%s_%d' % (procname, i) - for i in range(len(args))])) - if isinstance(q, unicode): - q = q.encode(conn.charset) + for i in range_type(len(args))])) self._query(q) self._executed = q - return args def fetchone(self): - ''' Fetch the next row ''' + """Fetch the next row""" self._check_executed() if self._rows is None or self.rownumber >= len(self._rows): return None @@ -189,20 +282,20 @@ class Cursor(object): return result def fetchmany(self, size=None): - ''' Fetch several rows ''' + """Fetch several rows""" self._check_executed() + if self._rows is None: + return () end = self.rownumber + (size or self.arraysize) result = self._rows[self.rownumber:end] - if self._rows is None: - return None self.rownumber = min(end, len(self._rows)) return result def fetchall(self): - ''' Fetch all the rows ''' + """Fetch all the rows""" self._check_executed() if self._rows is None: - return None + return () if self.rownumber: result = self._rows[self.rownumber:] else: @@ -217,11 +310,10 @@ class Cursor(object): elif mode == 'absolute': r = value else: - self.errorhandler(self, ProgrammingError, - "unknown scroll mode %s" % mode) + raise err.ProgrammingError("unknown scroll mode %s" % mode) - if r < 0 or r >= len(self._rows): - self.errorhandler(self, IndexError, "out of range") + if not (0 <= r < len(self._rows)): + raise IndexError("out of range") self.rownumber = r def _query(self, q): @@ -233,92 +325,112 @@ class Cursor(object): def _do_get_result(self): conn = self._get_db() - self.rowcount = conn._result.affected_rows self.rownumber = 0 - self.description = conn._result.description - self.lastrowid = conn._result.insert_id - self._rows = conn._result.rows - self._has_next = conn._result.has_next + self._result = result = conn._result + + self.rowcount = result.affected_rows + self.description = result.description + self.lastrowid = result.insert_id + self._rows = result.rows + self._warnings_handled = False + + if not self._defer_warnings: + self._show_warnings() + + def _show_warnings(self): + if self._warnings_handled: + return + self._warnings_handled = True + if self._result and (self._result.has_next or not self._result.warning_count): + return + ws = self._get_db().show_warnings() + if ws is None: + return + for w in ws: + msg = w[-1] + if PY2: + if isinstance(msg, unicode): + msg = msg.encode('utf-8', 'replace') + warnings.warn(err.Warning(*w[1:3]), stacklevel=4) def __iter__(self): return iter(self.fetchone, None) - Warning = Warning - Error = Error - InterfaceError = InterfaceError - DatabaseError = DatabaseError - DataError = DataError - OperationalError = OperationalError - IntegrityError = IntegrityError - InternalError = InternalError - ProgrammingError = ProgrammingError - NotSupportedError = NotSupportedError + Warning = err.Warning + Error = err.Error + InterfaceError = err.InterfaceError + DatabaseError = err.DatabaseError + DataError = err.DataError + OperationalError = err.OperationalError + IntegrityError = err.IntegrityError + InternalError = err.InternalError + ProgrammingError = err.ProgrammingError + NotSupportedError = err.NotSupportedError -class DictCursor(Cursor): + +class DictCursorMixin(object): + # You can override this to use OrderedDict or other dict-like types. + dict_type = dict + + def _do_get_result(self): + super(DictCursorMixin, self)._do_get_result() + fields = [] + if self.description: + for f in self._result.fields: + name = f.name + if name in fields: + name = f.table_name + '.' + name + fields.append(name) + self._fields = fields + + if fields and self._rows: + self._rows = [self._conv_row(r) for r in self._rows] + + def _conv_row(self, row): + if row is None: + return None + return self.dict_type(zip(self._fields, row)) + + +class DictCursor(DictCursorMixin, Cursor): """A cursor which returns results as a dictionary""" - def execute(self, query, args=None): - result = super(DictCursor, self).execute(query, args) - if self.description: - self._fields = [ field[0] for field in self.description ] - return result - - def fetchone(self): - ''' Fetch the next row ''' - self._check_executed() - if self._rows is None or self.rownumber >= len(self._rows): - return None - result = dict(zip(self._fields, self._rows[self.rownumber])) - self.rownumber += 1 - return result - - def fetchmany(self, size=None): - ''' Fetch several rows ''' - self._check_executed() - if self._rows is None: - return None - end = self.rownumber + (size or self.arraysize) - result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:end] ] - self.rownumber = min(end, len(self._rows)) - return tuple(result) - - def fetchall(self): - ''' Fetch all the rows ''' - self._check_executed() - if self._rows is None: - return None - if self.rownumber: - result = [ dict(zip(self._fields, r)) for r in self._rows[self.rownumber:] ] - else: - result = [ dict(zip(self._fields, r)) for r in self._rows ] - self.rownumber = len(self._rows) - return tuple(result) class SSCursor(Cursor): """ Unbuffered Cursor, mainly useful for queries that return a lot of data, or for connections to remote servers over a slow network. - + Instead of copying every row of data into a buffer, this will fetch rows as needed. The upside of this, is the client uses much less memory, and rows are returned much faster when traveling over a slow network, or if the result set is very big. - + There are limitations, though. The MySQL protocol doesn't support returning the total number of rows, so the only way to tell how many rows there are is to iterate over every row returned. Also, it currently isn't possible to scroll backwards, as only the current row is held in memory. """ - + + _defer_warnings = True + + def _conv_row(self, row): + return row + def close(self): - conn = self._get_db() - conn._result._finish_unbuffered_query() - + conn = self.connection + if conn is None: + return + + if self._result is not None and self._result is conn._result: + self._result._finish_unbuffered_query() + try: - if self._has_next: - while self.nextset(): pass - except: pass + while self.nextset(): + pass + finally: + self.connection = None def _query(self, q): conn = self._get_db() @@ -326,38 +438,31 @@ class SSCursor(Cursor): conn.query(q, unbuffered=True) self._do_get_result() return self.rowcount - + + def nextset(self): + return self._nextset(unbuffered=True) + def read_next(self): - """ Read next row """ - - conn = self._get_db() - conn._result._read_rowdata_packet_unbuffered() - return conn._result.rows - + """Read next row""" + return self._conv_row(self._result._read_rowdata_packet_unbuffered()) + def fetchone(self): - """ Fetch next row """ - + """Fetch next row""" self._check_executed() row = self.read_next() if row is None: + self._show_warnings() return None self.rownumber += 1 return row - + def fetchall(self): """ Fetch all, as per MySQLdb. Pretty useless for large queries, as it is buffered. See fetchall_unbuffered(), if you want an unbuffered generator version of this method. """ - - rows = [] - while True: - row = self.fetchone() - if row is None: - break - rows.append(row) - return tuple(rows) + return list(self.fetchall_unbuffered()) def fetchall_unbuffered(self): """ @@ -365,46 +470,50 @@ class SSCursor(Cursor): however, it doesn't make sense to return everything in a list, as that would use ridiculous memory for large result sets. """ - - row = self.fetchone() - while row is not None: - yield row - row = self.fetchone() - + return iter(self.fetchone, None) + + def __iter__(self): + return self.fetchall_unbuffered() + def fetchmany(self, size=None): - """ Fetch many """ - + """Fetch many""" self._check_executed() if size is None: size = self.arraysize - + rows = [] - for i in range(0, size): + for i in range_type(size): row = self.read_next() if row is None: + self._show_warnings() break rows.append(row) self.rownumber += 1 - return tuple(rows) - + return rows + def scroll(self, value, mode='relative'): self._check_executed() - if not mode == 'relative' and not mode == 'absolute': - self.errorhandler(self, ProgrammingError, - "unknown scroll mode %s" % mode) - + if mode == 'relative': if value < 0: - self.errorhandler(self, NotSupportedError, - "Backwards scrolling not supported by this cursor") - - for i in range(0, value): self.read_next() + raise err.NotSupportedError( + "Backwards scrolling not supported by this cursor") + + for _ in range_type(value): + self.read_next() self.rownumber += value - else: + elif mode == 'absolute': if value < self.rownumber: - self.errorhandler(self, NotSupportedError, + raise err.NotSupportedError( "Backwards scrolling not supported by this cursor") - + end = value - self.rownumber - for i in range(0, end): self.read_next() + for _ in range_type(end): + self.read_next() self.rownumber = value + else: + raise err.ProgrammingError("unknown scroll mode %s" % mode) + + +class SSDictCursor(DictCursorMixin, SSCursor): + """An unbuffered cursor, which returns results as a dictionary""" diff --git a/gluon/contrib/pymysql/err.py b/gluon/contrib/pymysql/err.py index b4322c63..24862632 100644 --- a/gluon/contrib/pymysql/err.py +++ b/gluon/contrib/pymysql/err.py @@ -1,57 +1,39 @@ import struct +from .constants import ER -try: - StandardError, Warning -except ImportError: - try: - from exceptions import StandardError, Warning - except ImportError: - import sys - e = sys.modules['exceptions'] - StandardError = e.StandardError - Warning = e.Warning - -from constants import ER -import sys -class MySQLError(StandardError): - +class MySQLError(Exception): """Exception related to operation with MySQL.""" class Warning(Warning, MySQLError): - """Exception raised for important warnings like data truncations while inserting, etc.""" -class Error(MySQLError): +class Error(MySQLError): """Exception that is the base class of all other error exceptions (not Warning).""" class InterfaceError(Error): - """Exception raised for errors that are related to the database interface rather than the database itself.""" class DatabaseError(Error): - """Exception raised for errors that are related to the database.""" class DataError(DatabaseError): - """Exception raised for errors that are due to problems with the processed data like division by zero, numeric value out of range, etc.""" class OperationalError(DatabaseError): - """Exception raised for errors that are related to the database's operation and not necessarily under the control of the programmer, e.g. an unexpected disconnect occurs, the data source name is not @@ -60,28 +42,24 @@ class OperationalError(DatabaseError): class IntegrityError(DatabaseError): - """Exception raised when the relational integrity of the database is affected, e.g. a foreign key check fails, duplicate key, etc.""" class InternalError(DatabaseError): - """Exception raised when the database encounters an internal error, e.g. the cursor is not valid anymore, the transaction is out of sync, etc.""" class ProgrammingError(DatabaseError): - """Exception raised for programming errors, e.g. table not found or already exists, syntax error in the SQL statement, wrong number of parameters specified, etc.""" class NotSupportedError(DatabaseError): - """Exception raised in case a method or database API was used which is not supported by the database, e.g. requesting a .rollback() on a connection that does not support transaction or @@ -90,10 +68,12 @@ class NotSupportedError(DatabaseError): error_map = {} + def _map_error(exc, *errors): for error in errors: error_map[error] = exc + _map_error(ProgrammingError, ER.DB_CREATE_EXISTS, ER.SYNTAX_ERROR, ER.PARSE_ERROR, ER.NO_SUCH_TABLE, ER.WRONG_DB_NAME, ER.WRONG_TABLE_NAME, ER.FIELD_SPECIFIED_TWICE, @@ -104,44 +84,24 @@ _map_error(DataError, ER.WARN_DATA_TRUNCATED, ER.WARN_NULL_TO_NOTNULL, ER.DATA_TOO_LONG, ER.DATETIME_FUNCTION_OVERFLOW) _map_error(IntegrityError, ER.DUP_ENTRY, ER.NO_REFERENCED_ROW, ER.NO_REFERENCED_ROW_2, ER.ROW_IS_REFERENCED, ER.ROW_IS_REFERENCED_2, - ER.CANNOT_ADD_FOREIGN) + ER.CANNOT_ADD_FOREIGN, ER.BAD_NULL_ERROR) _map_error(NotSupportedError, ER.WARNING_NOT_COMPLETE_ROLLBACK, ER.NOT_SUPPORTED_YET, ER.FEATURE_DISABLED, ER.UNKNOWN_STORAGE_ENGINE) -_map_error(OperationalError, ER.DBACCESS_DENIED_ERROR, ER.ACCESS_DENIED_ERROR, - ER.TABLEACCESS_DENIED_ERROR, ER.COLUMNACCESS_DENIED_ERROR) +_map_error(OperationalError, ER.DBACCESS_DENIED_ERROR, ER.ACCESS_DENIED_ERROR, + ER.CON_COUNT_ERROR, ER.TABLEACCESS_DENIED_ERROR, + ER.COLUMNACCESS_DENIED_ERROR) + del _map_error, ER - -def _get_error_info(data): - errno = struct.unpack('= 2 and value[0] == value[-1] == quote: + return value[1:-1] + return value + + def get(self, section, option): + value = configparser.RawConfigParser.get(self, section, option) + return self.__remove_quotes(value) diff --git a/gluon/contrib/pymysql/tests/__init__.py b/gluon/contrib/pymysql/tests/__init__.py index 8ad5f8aa..a9f5a4bf 100644 --- a/gluon/contrib/pymysql/tests/__init__.py +++ b/gluon/contrib/pymysql/tests/__init__.py @@ -1,13 +1,18 @@ -from pymysql.tests.test_issues import * -from pymysql.tests.test_example import * -from pymysql.tests.test_basic import * +# Sorted by alphabetical order from pymysql.tests.test_DictCursor import * +from pymysql.tests.test_SSCursor import * +from pymysql.tests.test_basic import * +from pymysql.tests.test_connection import * +from pymysql.tests.test_converters import * +from pymysql.tests.test_cursor import * +from pymysql.tests.test_err import * +from pymysql.tests.test_issues import * +from pymysql.tests.test_load_local import * +from pymysql.tests.test_nextset import * +from pymysql.tests.test_optionfile import * -import sys -if sys.version_info[0] == 2: - # MySQLdb tests were designed for Python 3 - from pymysql.tests.thirdparty import * +from pymysql.tests.thirdparty import * if __name__ == "__main__": - import unittest - unittest.main() + import unittest2 + unittest2.main() diff --git a/gluon/contrib/pymysql/tests/base.py b/gluon/contrib/pymysql/tests/base.py index ce11b6b2..740157b1 100644 --- a/gluon/contrib/pymysql/tests/base.py +++ b/gluon/contrib/pymysql/tests/base.py @@ -1,20 +1,86 @@ -import pymysql -import unittest +import gc +import json +import os +import re +import warnings -class PyMySQLTestCase(unittest.TestCase): - # Edit this to suit your test environment. - databases = [ - {"host":"localhost","user":"root", - "passwd":"","db":"test_pymysql", "use_unicode": True}, - {"host":"localhost","user":"root","passwd":"","db":"test_pymysql2"}] +import unittest2 + +import pymysql +from .._compat import CPYTHON + + +class PyMySQLTestCase(unittest2.TestCase): + # You can specify your test environment creating a file named + # "databases.json" or editing the `databases` variable below. + fname = os.path.join(os.path.dirname(__file__), "databases.json") + if os.path.exists(fname): + with open(fname) as f: + databases = json.load(f) + else: + databases = [ + {"host":"localhost","user":"root", + "passwd":"","db":"test_pymysql", "use_unicode": True, 'local_infile': True}, + {"host":"localhost","user":"root","passwd":"","db":"test_pymysql2"}] + + def mysql_server_is(self, conn, version_tuple): + """Return True if the given connection is on the version given or + greater. + + e.g.:: + + if self.mysql_server_is(conn, (5, 6, 4)): + # do something for MySQL 5.6.4 and above + """ + server_version = conn.get_server_info() + server_version_tuple = tuple( + (int(dig) if dig is not None else 0) + for dig in + re.match(r'(\d+)\.(\d+)\.(\d+)', server_version).group(1, 2, 3) + ) + return server_version_tuple >= version_tuple def setUp(self): self.connections = [] - for params in self.databases: self.connections.append(pymysql.connect(**params)) + self.addCleanup(self._teardown_connections) - def tearDown(self): + def _teardown_connections(self): for connection in self.connections: connection.close() + def safe_create_table(self, connection, tablename, ddl, cleanup=True): + """create a table. + + Ensures any existing version of that table is first dropped. + + Also adds a cleanup rule to drop the table after the test + completes. + """ + cursor = connection.cursor() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cursor.execute("drop table if exists `%s`" % (tablename,)) + cursor.execute(ddl) + cursor.close() + if cleanup: + self.addCleanup(self.drop_table, connection, tablename) + + def drop_table(self, connection, tablename): + cursor = connection.cursor() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cursor.execute("drop table if exists `%s`" % (tablename,)) + cursor.close() + + def safe_gc_collect(self): + """Ensure cycles are collected via gc. + + Runs additional times on non-CPython platforms. + + """ + gc.collect() + if not CPYTHON: + gc.collect() diff --git a/gluon/contrib/pymysql/tests/data/load_local_data.txt b/gluon/contrib/pymysql/tests/data/load_local_data.txt new file mode 100755 index 00000000..f9f99c8f --- /dev/null +++ b/gluon/contrib/pymysql/tests/data/load_local_data.txt @@ -0,0 +1,22749 @@ +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +71,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, diff --git a/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt b/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt new file mode 100755 index 00000000..2bda5a96 --- /dev/null +++ b/gluon/contrib/pymysql/tests/data/load_local_warn_data.txt @@ -0,0 +1,50 @@ +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, +5,6, +7,8, +1,2, +3,4, diff --git a/gluon/contrib/pymysql/tests/test_DictCursor.py b/gluon/contrib/pymysql/tests/test_DictCursor.py index 0166214a..9a0d638b 100755 --- a/gluon/contrib/pymysql/tests/test_DictCursor.py +++ b/gluon/contrib/pymysql/tests/test_DictCursor.py @@ -2,54 +2,117 @@ from pymysql.tests import base import pymysql.cursors import datetime +import warnings + class TestDictCursor(base.PyMySQLTestCase): + bob = {'name': 'bob', 'age': 21, 'DOB': datetime.datetime(1990, 2, 6, 23, 4, 56)} + jim = {'name': 'jim', 'age': 56, 'DOB': datetime.datetime(1955, 5, 9, 13, 12, 45)} + fred = {'name': 'fred', 'age': 100, 'DOB': datetime.datetime(1911, 9, 12, 1, 1, 1)} + + cursor_type = pymysql.cursors.DictCursor + + def setUp(self): + super(TestDictCursor, self).setUp() + self.conn = conn = self.connections[0] + c = conn.cursor(self.cursor_type) + + # create a table ane some data to query + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists dictcursor") + # include in filterwarnings since for unbuffered dict cursor warning for lack of table + # will only be propagated at start of next execute() call + c.execute("""CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""") + data = [("bob", 21, "1990-02-06 23:04:56"), + ("jim", 56, "1955-05-09 13:12:45"), + ("fred", 100, "1911-09-12 01:01:01")] + c.executemany("insert into dictcursor values (%s,%s,%s)", data) + + def tearDown(self): + c = self.conn.cursor() + c.execute("drop table dictcursor") + super(TestDictCursor, self).tearDown() + + def _ensure_cursor_expired(self, cursor): + pass def test_DictCursor(self): - #all assert test compare to the structure as would come out from MySQLdb - conn = self.connections[0] - c = conn.cursor(pymysql.cursors.DictCursor) - # create a table ane some data to query - c.execute("""CREATE TABLE dictcursor (name char(20), age int , DOB datetime)""") - data = (("bob",21,"1990-02-06 23:04:56"), - ("jim",56,"1955-05-09 13:12:45"), - ("fred",100,"1911-09-12 01:01:01")) - bob = {'name':'bob','age':21,'DOB':datetime.datetime(1990, 02, 6, 23, 04, 56)} - jim = {'name':'jim','age':56,'DOB':datetime.datetime(1955, 05, 9, 13, 12, 45)} - fred = {'name':'fred','age':100,'DOB':datetime.datetime(1911, 9, 12, 1, 1, 1)} - try: - c.executemany("insert into dictcursor values (%s,%s,%s)", data) - # try an update which should return no rows - c.execute("update dictcursor set age=20 where name='bob'") - bob['age'] = 20 - # pull back the single row dict for bob and check - c.execute("SELECT * from dictcursor where name='bob'") - r = c.fetchone() - self.assertEqual(bob,r,"fetchone via DictCursor failed") - # same again, but via fetchall => tuple) - c.execute("SELECT * from dictcursor where name='bob'") - r = c.fetchall() - self.assertEqual((bob,),r,"fetch a 1 row result via fetchall failed via DictCursor") - # same test again but iterate over the - c.execute("SELECT * from dictcursor where name='bob'") - for r in c: - self.assertEqual(bob, r,"fetch a 1 row result via iteration failed via DictCursor") - # get all 3 row via fetchall - c.execute("SELECT * from dictcursor") - r = c.fetchall() - self.assertEqual((bob,jim,fred), r, "fetchall failed via DictCursor") - #same test again but do a list comprehension - c.execute("SELECT * from dictcursor") - r = [x for x in c] - self.assertEqual([bob,jim,fred], r, "list comprehension failed via DictCursor") - # get all 2 row via fetchmany - c.execute("SELECT * from dictcursor") - r = c.fetchmany(2) - self.assertEqual((bob,jim), r, "fetchmany failed via DictCursor") - finally: - c.execute("drop table dictcursor") + bob, jim, fred = self.bob.copy(), self.jim.copy(), self.fred.copy() + #all assert test compare to the structure as would come out from MySQLdb + conn = self.conn + c = conn.cursor(self.cursor_type) -__all__ = ["TestDictCursor"] + # try an update which should return no rows + c.execute("update dictcursor set age=20 where name='bob'") + bob['age'] = 20 + # pull back the single row dict for bob and check + c.execute("SELECT * from dictcursor where name='bob'") + r = c.fetchone() + self.assertEqual(bob, r, "fetchone via DictCursor failed") + self._ensure_cursor_expired(c) + + # same again, but via fetchall => tuple) + c.execute("SELECT * from dictcursor where name='bob'") + r = c.fetchall() + self.assertEqual([bob], r, "fetch a 1 row result via fetchall failed via DictCursor") + # same test again but iterate over the + c.execute("SELECT * from dictcursor where name='bob'") + for r in c: + self.assertEqual(bob, r, "fetch a 1 row result via iteration failed via DictCursor") + # get all 3 row via fetchall + c.execute("SELECT * from dictcursor") + r = c.fetchall() + self.assertEqual([bob,jim,fred], r, "fetchall failed via DictCursor") + #same test again but do a list comprehension + c.execute("SELECT * from dictcursor") + r = list(c) + self.assertEqual([bob,jim,fred], r, "DictCursor should be iterable") + # get all 2 row via fetchmany + c.execute("SELECT * from dictcursor") + r = c.fetchmany(2) + self.assertEqual([bob, jim], r, "fetchmany failed via DictCursor") + self._ensure_cursor_expired(c) + + def test_custom_dict(self): + class MyDict(dict): pass + + class MyDictCursor(self.cursor_type): + dict_type = MyDict + + keys = ['name', 'age', 'DOB'] + bob = MyDict([(k, self.bob[k]) for k in keys]) + jim = MyDict([(k, self.jim[k]) for k in keys]) + fred = MyDict([(k, self.fred[k]) for k in keys]) + + cur = self.conn.cursor(MyDictCursor) + cur.execute("SELECT * FROM dictcursor WHERE name='bob'") + r = cur.fetchone() + self.assertEqual(bob, r, "fetchone() returns MyDictCursor") + self._ensure_cursor_expired(cur) + + cur.execute("SELECT * FROM dictcursor") + r = cur.fetchall() + self.assertEqual([bob, jim, fred], r, + "fetchall failed via MyDictCursor") + + cur.execute("SELECT * FROM dictcursor") + r = list(cur) + self.assertEqual([bob, jim, fred], r, + "list failed via MyDictCursor") + + cur.execute("SELECT * FROM dictcursor") + r = cur.fetchmany(2) + self.assertEqual([bob, jim], r, + "list failed via MyDictCursor") + self._ensure_cursor_expired(cur) + + +class TestSSDictCursor(TestDictCursor): + cursor_type = pymysql.cursors.SSDictCursor + + def _ensure_cursor_expired(self, cursor): + list(cursor.fetchall_unbuffered()) if __name__ == "__main__": import unittest diff --git a/gluon/contrib/pymysql/tests/test_SSCursor.py b/gluon/contrib/pymysql/tests/test_SSCursor.py index a0e3caf3..e6d6cf53 100755 --- a/gluon/contrib/pymysql/tests/test_SSCursor.py +++ b/gluon/contrib/pymysql/tests/test_SSCursor.py @@ -3,7 +3,7 @@ import sys try: from pymysql.tests import base import pymysql.cursors -except: +except Exception: # For local testing from top-level directory, without installing sys.path.append('../pymysql') from pymysql.tests import base @@ -12,7 +12,7 @@ except: class TestSSCursor(base.PyMySQLTestCase): def test_SSCursor(self): affected_rows = 18446744073709551615 - + conn = self.connections[0] data = [ ('America', '', 'America/Jamaica'), @@ -25,22 +25,23 @@ class TestSSCursor(base.PyMySQLTestCase): ('America', '', 'America/Costa_Rica'), ('America', '', 'America/Denver'), ('America', '', 'America/Detroit'),] - + try: cursor = conn.cursor(pymysql.cursors.SSCursor) - + # Create table cursor.execute(('CREATE TABLE tz_data (' 'region VARCHAR(64),' 'zone VARCHAR(64),' 'name VARCHAR(64))')) - + + conn.begin() # Test INSERT for i in data: cursor.execute('INSERT INTO tz_data VALUES (%s, %s, %s)', i) self.assertEqual(conn.affected_rows(), 1, 'affected_rows does not match') conn.commit() - + # Test fetchone() iter = 0 cursor.execute('SELECT * FROM tz_data') @@ -49,46 +50,55 @@ class TestSSCursor(base.PyMySQLTestCase): if row is None: break iter += 1 - + # Test cursor.rowcount self.assertEqual(cursor.rowcount, affected_rows, 'cursor.rowcount != %s' % (str(affected_rows))) - + # Test cursor.rownumber self.assertEqual(cursor.rownumber, iter, 'cursor.rowcount != %s' % (str(iter))) - + # Test row came out the same as it went in self.assertEqual((row in data), True, 'Row not found in source data') - + # Test fetchall cursor.execute('SELECT * FROM tz_data') self.assertEqual(len(cursor.fetchall()), len(data), 'fetchall failed. Number of rows does not match') - + # Test fetchmany cursor.execute('SELECT * FROM tz_data') self.assertEqual(len(cursor.fetchmany(2)), 2, 'fetchmany failed. Number of rows does not match') - + # So MySQLdb won't throw "Commands out of sync" while True: res = cursor.fetchone() if res is None: break - + # Test update, affected_rows() cursor.execute('UPDATE tz_data SET zone = %s', ['Foo']) conn.commit() self.assertEqual(cursor.rowcount, len(data), 'Update failed. affected_rows != %s' % (str(len(data)))) - + # Test executemany cursor.executemany('INSERT INTO tz_data VALUES (%s, %s, %s)', data) self.assertEqual(cursor.rowcount, len(data), 'executemany failed. cursor.rowcount != %s' % (str(len(data)))) - + + # Test multiple datasets + cursor.execute('SELECT 1; SELECT 2; SELECT 3') + self.assertListEqual(list(cursor), [(1, )]) + self.assertTrue(cursor.nextset()) + self.assertListEqual(list(cursor), [(2, )]) + self.assertTrue(cursor.nextset()) + self.assertListEqual(list(cursor), [(3, )]) + self.assertFalse(cursor.nextset()) + finally: cursor.execute('DROP TABLE tz_data') cursor.close() diff --git a/gluon/contrib/pymysql/tests/test_basic.py b/gluon/contrib/pymysql/tests/test_basic.py index fb7a30f8..ed9c4f61 100644 --- a/gluon/contrib/pymysql/tests/test_basic.py +++ b/gluon/contrib/pymysql/tests/test_basic.py @@ -1,8 +1,19 @@ -from pymysql.tests import base -from pymysql import util - -import time +# coding: utf-8 import datetime +import json +import time +import warnings + +from unittest2 import SkipTest + +from pymysql import util +import pymysql.cursors +from pymysql.tests import base +from pymysql.err import ProgrammingError + + +__all__ = ["TestConversion", "TestCursor", "TestBulkInserts"] + class TestConversion(base.PyMySQLTestCase): def test_datatypes(self): @@ -12,16 +23,13 @@ class TestConversion(base.PyMySQLTestCase): c.execute("create table test_datatypes (b bit, i int, l bigint, f real, s varchar(32), u varchar(32), bb blob, d date, dt datetime, ts timestamp, td time, t time, st datetime)") try: # insert values - v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime.now(), datetime.timedelta(5,6), datetime.time(16,32), time.localtime()) + + v = (True, -3, 123456789012, 5.7, "hello'\" world", u"Espa\xc3\xb1ol", "binary\x00data".encode(conn.charset), datetime.date(1988,2,2), datetime.datetime(2014, 5, 15, 7, 45, 57), datetime.timedelta(5,6), datetime.time(16,32), time.localtime()) c.execute("insert into test_datatypes (b,i,l,f,s,u,bb,d,dt,td,t,st) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", v) c.execute("select b,i,l,f,s,u,bb,d,dt,td,t,st from test_datatypes") r = c.fetchone() self.assertEqual(util.int2byte(1), r[0]) - self.assertEqual(v[1:8], r[1:8]) - # mysql throws away microseconds so we need to check datetimes - # specially. additionally times are turned into timedeltas. - self.assertEqual(datetime.datetime(*v[8].timetuple()[:6]), r[8]) - self.assertEqual(v[9], r[9]) # just timedeltas + self.assertEqual(v[1:10], r[1:10]) self.assertEqual(datetime.timedelta(0, 60 * (v[10].hour * 60 + v[10].minute)), r[10]) self.assertEqual(datetime.datetime(*v[-1][:6]), r[-1]) @@ -35,11 +43,15 @@ class TestConversion(base.PyMySQLTestCase): c.execute("delete from test_datatypes") - # check sequence type - c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)") - c.execute("select l from test_datatypes where i in %s order by i", ((2,6),)) - r = c.fetchall() - self.assertEqual(((4,),(8,)), r) + # check sequences type + for seq_type in (tuple, list, set, frozenset): + c.execute("insert into test_datatypes (i, l) values (2,4), (6,8), (10,12)") + seq = seq_type([2,6]) + c.execute("select l from test_datatypes where i in %s order by i", (seq,)) + r = c.fetchall() + self.assertEqual(((4,),(8,)), r) + c.execute("delete from test_datatypes") + finally: c.execute("drop table test_datatypes") @@ -79,20 +91,18 @@ class TestConversion(base.PyMySQLTestCase): finally: c.execute("drop table test_dict") - - def test_big_blob(self): - """ test tons of data """ + def test_blob(self): + """test binary data""" + data = bytes(bytearray(range(256)) * 4) conn = self.connections[0] - c = conn.cursor() - c.execute("create table test_big_blob (b blob)") - try: - data = "pymysql" * 1024 - c.execute("insert into test_big_blob (b) values (%s)", (data,)) - c.execute("select b from test_big_blob") - self.assertEqual(data.encode(conn.charset), c.fetchone()[0]) - finally: - c.execute("drop table test_big_blob") - + self.safe_create_table( + conn, "test_blob", "create table test_blob (b blob)") + + with conn.cursor() as c: + c.execute("insert into test_blob (b) values (%s)", (data,)) + c.execute("select b from test_blob") + self.assertEqual(data, c.fetchone()[0]) + def test_untyped(self): """ test conversion of null, empty string """ conn = self.connections[0] @@ -101,17 +111,40 @@ class TestConversion(base.PyMySQLTestCase): self.assertEqual((None,u''), c.fetchone()) c.execute("select '',null") self.assertEqual((u'',None), c.fetchone()) - - def test_datetime(self): - """ test conversion of null, empty string """ + + def test_timedelta(self): + """ test timedelta conversion """ conn = self.connections[0] c = conn.cursor() - c.execute("select time('12:30'), time('23:12:59'), time('23:12:59.05100')") + c.execute("select time('12:30'), time('23:12:59'), time('23:12:59.05100'), time('-12:30'), time('-23:12:59'), time('-23:12:59.05100'), time('-00:30')") self.assertEqual((datetime.timedelta(0, 45000), datetime.timedelta(0, 83579), - datetime.timedelta(0, 83579, 51000)), + datetime.timedelta(0, 83579, 51000), + -datetime.timedelta(0, 45000), + -datetime.timedelta(0, 83579), + -datetime.timedelta(0, 83579, 51000), + -datetime.timedelta(0, 1800)), c.fetchone()) + def test_datetime_microseconds(self): + """ test datetime conversion w microseconds""" + + conn = self.connections[0] + if not self.mysql_server_is(conn, (5, 6, 4)): + raise SkipTest("target backend does not support microseconds") + c = conn.cursor() + dt = datetime.datetime(2013, 11, 12, 9, 9, 9, 123450) + c.execute("create table test_datetime (id int, ts datetime(6))") + try: + c.execute( + "insert into test_datetime values (%s, %s)", + (1, dt) + ) + c.execute("select ts from test_datetime") + self.assertEqual((dt,), c.fetchone()) + finally: + c.execute("drop table test_datetime") + class TestCursor(base.PyMySQLTestCase): # this test case does not work quite right yet, however, @@ -185,7 +218,7 @@ class TestCursor(base.PyMySQLTestCase): c = conn.cursor() try: c.execute('create table test_aggregates (i integer)') - for i in xrange(0, 10): + for i in range(0, 10): c.execute('insert into test_aggregates (i) values (%s)', (i,)) c.execute('select sum(i) from test_aggregates') r, = c.fetchone() @@ -197,17 +230,150 @@ class TestCursor(base.PyMySQLTestCase): """ test a single tuple """ conn = self.connections[0] c = conn.cursor() - try: - c.execute("create table mystuff (id integer primary key)") - c.execute("insert into mystuff (id) values (1)") - c.execute("insert into mystuff (id) values (2)") - c.execute("select id from mystuff where id in %s", ((1,),)) - self.assertEqual([(1,)], list(c.fetchall())) - finally: - c.execute("drop table mystuff") + self.safe_create_table( + conn, 'mystuff', + "create table mystuff (id integer primary key)") + c.execute("insert into mystuff (id) values (1)") + c.execute("insert into mystuff (id) values (2)") + c.execute("select id from mystuff where id in %s", ((1,),)) + self.assertEqual([(1,)], list(c.fetchall())) + c.close() -__all__ = ["TestConversion","TestCursor"] + def test_json(self): + args = self.databases[0].copy() + args["charset"] = "utf8mb4" + conn = pymysql.connect(**args) + if not self.mysql_server_is(conn, (5, 7, 0)): + raise SkipTest("JSON type is not supported on MySQL <= 5.6") -if __name__ == "__main__": - import unittest - unittest.main() + self.safe_create_table(conn, "test_json", """\ +create table test_json ( + id int not null, + json JSON not null, + primary key (id) +);""") + cur = conn.cursor() + + json_str = u'{"hello": "こんにごは"}' + cur.execute("INSERT INTO test_json (id, `json`) values (42, %s)", (json_str,)) + cur.execute("SELECT `json` from `test_json` WHERE `id`=42") + res = cur.fetchone()[0] + self.assertEqual(json.loads(res), json.loads(json_str)) + + cur.execute("SELECT CAST(%s AS JSON) AS x", (json_str,)) + res = cur.fetchone()[0] + self.assertEqual(json.loads(res), json.loads(json_str)) + + +class TestBulkInserts(base.PyMySQLTestCase): + + cursor_type = pymysql.cursors.DictCursor + + def setUp(self): + super(TestBulkInserts, self).setUp() + self.conn = conn = self.connections[0] + c = conn.cursor(self.cursor_type) + + # create a table ane some data to query + self.safe_create_table(conn, 'bulkinsert', """\ +CREATE TABLE bulkinsert +( +id int(11), +name char(20), +age int, +height int, +PRIMARY KEY (id) +) +""") + + def _verify_records(self, data): + conn = self.connections[0] + cursor = conn.cursor() + cursor.execute("SELECT id, name, age, height from bulkinsert") + result = cursor.fetchall() + self.assertEqual(sorted(data), sorted(result)) + + def test_bulk_insert(self): + conn = self.connections[0] + cursor = conn.cursor() + + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany("insert into bulkinsert (id, name, age, height) " + "values (%s,%s,%s,%s)", data) + self.assertEqual( + cursor._last_executed, bytearray( + b"insert into bulkinsert (id, name, age, height) values " + b"(0,'bob',21,123),(1,'jim',56,45),(2,'fred',100,180)")) + cursor.execute('commit') + self._verify_records(data) + + def test_bulk_insert_multiline_statement(self): + conn = self.connections[0] + cursor = conn.cursor() + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany("""insert +into bulkinsert (id, name, +age, height) +values (%s, +%s , %s, +%s ) + """, data) + self.assertEqual(cursor._last_executed.strip(), bytearray(b"""insert +into bulkinsert (id, name, +age, height) +values (0, +'bob' , 21, +123 ),(1, +'jim' , 56, +45 ),(2, +'fred' , 100, +180 )""")) + cursor.execute('commit') + self._verify_records(data) + + def test_bulk_insert_single_record(self): + conn = self.connections[0] + cursor = conn.cursor() + data = [(0, "bob", 21, 123)] + cursor.executemany("insert into bulkinsert (id, name, age, height) " + "values (%s,%s,%s,%s)", data) + cursor.execute('commit') + self._verify_records(data) + + def test_issue_288(self): + """executemany should work with "insert ... on update" """ + conn = self.connections[0] + cursor = conn.cursor() + data = [(0, "bob", 21, 123), (1, "jim", 56, 45), (2, "fred", 100, 180)] + cursor.executemany("""insert +into bulkinsert (id, name, +age, height) +values (%s, +%s , %s, +%s ) on duplicate key update +age = values(age) + """, data) + self.assertEqual(cursor._last_executed.strip(), bytearray(b"""insert +into bulkinsert (id, name, +age, height) +values (0, +'bob' , 21, +123 ),(1, +'jim' , 56, +45 ),(2, +'fred' , 100, +180 ) on duplicate key update +age = values(age)""")) + cursor.execute('commit') + self._verify_records(data) + + def test_warnings(self): + con = self.connections[0] + cur = con.cursor() + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + cur.execute("drop table if exists no_exists_table") + self.assertEqual(len(ws), 1) + self.assertEqual(ws[0].category, pymysql.Warning) + if u"no_exists_table" not in str(ws[0].message): + self.fail("'no_exists_table' not in %s" % (str(ws[0].message),)) diff --git a/gluon/contrib/pymysql/tests/test_connection.py b/gluon/contrib/pymysql/tests/test_connection.py new file mode 100755 index 00000000..518b6fe7 --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_connection.py @@ -0,0 +1,576 @@ +import datetime +import sys +import time +import unittest2 +import pymysql +from pymysql.tests import base +from pymysql._compat import text_type + + +class TempUser: + def __init__(self, c, user, db, auth=None, authdata=None, password=None): + self._c = c + self._user = user + self._db = db + create = "CREATE USER " + user + if password is not None: + create += " IDENTIFIED BY '%s'" % password + elif auth is not None: + create += " IDENTIFIED WITH %s" % auth + if authdata is not None: + create += " AS '%s'" % authdata + try: + c.execute(create) + self._created = True + except pymysql.err.InternalError: + # already exists - TODO need to check the same plugin applies + self._created = False + try: + c.execute("GRANT SELECT ON %s.* TO %s" % (db, user)) + self._grant = True + except pymysql.err.InternalError: + self._grant = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self._grant: + self._c.execute("REVOKE SELECT ON %s.* FROM %s" % (self._db, self._user)) + if self._created: + self._c.execute("DROP USER %s" % self._user) + + +class TestAuthentication(base.PyMySQLTestCase): + + socket_auth = False + socket_found = False + two_questions_found = False + three_attempts_found = False + pam_found = False + mysql_old_password_found = False + sha256_password_found = False + + import os + osuser = os.environ.get('USER') + + # socket auth requires the current user and for the connection to be a socket + # rest do grants @localhost due to incomplete logic - TODO change to @% then + db = base.PyMySQLTestCase.databases[0].copy() + + socket_auth = db.get('unix_socket') is not None \ + and db.get('host') in ('localhost', '127.0.0.1') + + cur = pymysql.connect(**db).cursor() + del db['user'] + cur.execute("SHOW PLUGINS") + for r in cur: + if (r[1], r[2]) != (u'ACTIVE', u'AUTHENTICATION'): + continue + if r[3] == u'auth_socket.so': + socket_plugin_name = r[0] + socket_found = True + elif r[3] == u'dialog_examples.so': + if r[0] == 'two_questions': + two_questions_found = True + elif r[0] == 'three_attempts': + three_attempts_found = True + elif r[0] == u'pam': + pam_found = True + pam_plugin_name = r[3].split('.')[0] + if pam_plugin_name == 'auth_pam': + pam_plugin_name = 'pam' + # MySQL: authentication_pam + # https://dev.mysql.com/doc/refman/5.5/en/pam-authentication-plugin.html + + # MariaDB: pam + # https://mariadb.com/kb/en/mariadb/pam-authentication-plugin/ + + # Names differ but functionality is close + elif r[0] == u'mysql_old_password': + mysql_old_password_found = True + elif r[0] == u'sha256_password': + sha256_password_found = True + #else: + # print("plugin: %r" % r[0]) + + def test_plugin(self): + # Bit of an assumption that the current user is a native password + self.assertEqual('mysql_native_password', self.connections[0]._auth_plugin_name) + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipIf(socket_found, "socket plugin already installed") + def testSocketAuthInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connections[0].cursor() + try: + cur.execute("install plugin auth_socket soname 'auth_socket.so'") + TestAuthentication.socket_found = True + self.socket_plugin_name = 'auth_socket' + self.realtestSocketAuth() + except pymysql.err.InternalError: + try: + cur.execute("install soname 'auth_socket'") + TestAuthentication.socket_found = True + self.socket_plugin_name = 'unix_socket' + self.realtestSocketAuth() + except pymysql.err.InternalError: + TestAuthentication.socket_found = False + raise unittest2.SkipTest('we couldn\'t install the socket plugin') + finally: + if TestAuthentication.socket_found: + cur.execute("uninstall plugin %s" % self.socket_plugin_name) + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(socket_found, "no socket plugin") + def testSocketAuth(self): + self.realtestSocketAuth() + + def realtestSocketAuth(self): + with TempUser(self.connections[0].cursor(), TestAuthentication.osuser + '@localhost', + self.databases[0]['db'], self.socket_plugin_name) as u: + c = pymysql.connect(user=TestAuthentication.osuser, **self.db) + + class Dialog(object): + fail=False + + def __init__(self, con): + self.fail=TestAuthentication.Dialog.fail + pass + + def prompt(self, echo, prompt): + if self.fail: + self.fail=False + return b'bad guess at a password' + return self.m.get(prompt) + + class DialogHandler(object): + + def __init__(self, con): + self.con=con + + def authenticate(self, pkt): + while True: + flag = pkt.read_uint8() + echo = (flag & 0x06) == 0x02 + last = (flag & 0x01) == 0x01 + prompt = pkt.read_all() + + if prompt == b'Password, please:': + self.con.write_packet(b'stillnotverysecret\0') + else: + self.con.write_packet(b'no idea what to do with this prompt\0') + pkt = self.con._read_packet() + pkt.check_error() + if pkt.is_ok_packet() or last: + break + return pkt + + class DefectiveHandler(object): + def __init__(self, con): + self.con=con + + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipIf(two_questions_found, "two_questions plugin already installed") + def testDialogAuthTwoQuestionsInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connections[0].cursor() + try: + cur.execute("install plugin two_questions soname 'dialog_examples.so'") + TestAuthentication.two_questions_found = True + self.realTestDialogAuthTwoQuestions() + except pymysql.err.InternalError: + raise unittest2.SkipTest('we couldn\'t install the two_questions plugin') + finally: + if TestAuthentication.two_questions_found: + cur.execute("uninstall plugin two_questions") + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(two_questions_found, "no two questions auth plugin") + def testDialogAuthTwoQuestions(self): + self.realTestDialogAuthTwoQuestions() + + def realTestDialogAuthTwoQuestions(self): + TestAuthentication.Dialog.fail=False + TestAuthentication.Dialog.m = {b'Password, please:': b'notverysecret', + b'Are you sure ?': b'yes, of course'} + with TempUser(self.connections[0].cursor(), 'pymysql_2q@localhost', + self.databases[0]['db'], 'two_questions', 'notverysecret') as u: + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_2q', **self.db) + pymysql.connect(user='pymysql_2q', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db) + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipIf(three_attempts_found, "three_attempts plugin already installed") + def testDialogAuthThreeAttemptsQuestionsInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connections[0].cursor() + try: + cur.execute("install plugin three_attempts soname 'dialog_examples.so'") + TestAuthentication.three_attempts_found = True + self.realTestDialogAuthThreeAttempts() + except pymysql.err.InternalError: + raise unittest2.SkipTest('we couldn\'t install the three_attempts plugin') + finally: + if TestAuthentication.three_attempts_found: + cur.execute("uninstall plugin three_attempts") + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(three_attempts_found, "no three attempts plugin") + def testDialogAuthThreeAttempts(self): + self.realTestDialogAuthThreeAttempts() + + def realTestDialogAuthThreeAttempts(self): + TestAuthentication.Dialog.m = {b'Password, please:': b'stillnotverysecret'} + TestAuthentication.Dialog.fail=True # fail just once. We've got three attempts after all + with TempUser(self.connections[0].cursor(), 'pymysql_3a@localhost', + self.databases[0]['db'], 'three_attempts', 'stillnotverysecret') as u: + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db) + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DialogHandler}, **self.db) + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': object}, **self.db) + + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.DefectiveHandler}, **self.db) + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'notdialogplugin': TestAuthentication.Dialog}, **self.db) + TestAuthentication.Dialog.m = {b'Password, please:': b'I do not know'} + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db) + TestAuthentication.Dialog.m = {b'Password, please:': None} + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_3a', auth_plugin_map={b'dialog': TestAuthentication.Dialog}, **self.db) + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipIf(pam_found, "pam plugin already installed") + @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required") + @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required") + def testPamAuthInstallPlugin(self): + # needs plugin. lets install it. + cur = self.connections[0].cursor() + try: + cur.execute("install plugin pam soname 'auth_pam.so'") + TestAuthentication.pam_found = True + self.realTestPamAuth() + except pymysql.err.InternalError: + raise unittest2.SkipTest('we couldn\'t install the auth_pam plugin') + finally: + if TestAuthentication.pam_found: + cur.execute("uninstall plugin pam") + + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(pam_found, "no pam plugin") + @unittest2.skipIf(os.environ.get('PASSWORD') is None, "PASSWORD env var required") + @unittest2.skipIf(os.environ.get('PAMSERVICE') is None, "PAMSERVICE env var required") + def testPamAuth(self): + self.realTestPamAuth() + + def realTestPamAuth(self): + db = self.db.copy() + import os + db['password'] = os.environ.get('PASSWORD') + cur = self.connections[0].cursor() + try: + cur.execute('show grants for ' + TestAuthentication.osuser + '@localhost') + grants = cur.fetchone()[0] + cur.execute('drop user ' + TestAuthentication.osuser + '@localhost') + except pymysql.OperationalError as e: + # assuming the user doesn't exist which is ok too + self.assertEqual(1045, e.args[0]) + grants = None + with TempUser(cur, TestAuthentication.osuser + '@localhost', + self.databases[0]['db'], 'pam', os.environ.get('PAMSERVICE')) as u: + try: + c = pymysql.connect(user=TestAuthentication.osuser, **db) + db['password'] = 'very bad guess at password' + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user=TestAuthentication.osuser, + auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler}, + **self.db) + except pymysql.OperationalError as e: + self.assertEqual(1045, e.args[0]) + # we had 'bad guess at password' work with pam. Well at least we get a permission denied here + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user=TestAuthentication.osuser, + auth_plugin_map={b'mysql_cleartext_password': TestAuthentication.DefectiveHandler}, + **self.db) + if grants: + # recreate the user + cur.execute(grants) + + # select old_password("crummy p\tassword"); + #| old_password("crummy p\tassword") | + #| 2a01785203b08770 | + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(mysql_old_password_found, "no mysql_old_password plugin") + def testMySQLOldPasswordAuth(self): + if self.mysql_server_is(self.connections[0], (5, 7, 0)): + raise unittest2.SkipTest('Old passwords aren\'t supported in 5.7') + # pymysql.err.OperationalError: (1045, "Access denied for user 'old_pass_user'@'localhost' (using password: YES)") + # from login in MySQL-5.6 + if self.mysql_server_is(self.connections[0], (5, 6, 0)): + raise unittest2.SkipTest('Old passwords don\'t authenticate in 5.6') + db = self.db.copy() + db['password'] = "crummy p\tassword" + with self.connections[0] as c: + # deprecated in 5.6 + if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)): + with self.assertWarns(pymysql.err.Warning) as cm: + c.execute("SELECT OLD_PASSWORD('%s')" % db['password']) + else: + c.execute("SELECT OLD_PASSWORD('%s')" % db['password']) + v = c.fetchone()[0] + self.assertEqual(v, '2a01785203b08770') + # only works in MariaDB and MySQL-5.6 - can't separate out by version + #if self.mysql_server_is(self.connections[0], (5, 5, 0)): + # with TempUser(c, 'old_pass_user@localhost', + # self.databases[0]['db'], 'mysql_old_password', '2a01785203b08770') as u: + # cur = pymysql.connect(user='old_pass_user', **db).cursor() + # cur.execute("SELECT VERSION()") + c.execute("SELECT @@secure_auth") + secure_auth_setting = c.fetchone()[0] + c.execute('set old_passwords=1') + # pymysql.err.Warning: 'pre-4.1 password hash' is deprecated and will be removed in a future release. Please use post-4.1 password hash instead + if sys.version_info[0:2] >= (3,2) and self.mysql_server_is(self.connections[0], (5, 6, 0)): + with self.assertWarns(pymysql.err.Warning) as cm: + c.execute('set global secure_auth=0') + else: + c.execute('set global secure_auth=0') + with TempUser(c, 'old_pass_user@localhost', + self.databases[0]['db'], password=db['password']) as u: + cur = pymysql.connect(user='old_pass_user', **db).cursor() + cur.execute("SELECT VERSION()") + c.execute('set global secure_auth=%r' % secure_auth_setting) + + @unittest2.skipUnless(socket_auth, "connection to unix_socket required") + @unittest2.skipUnless(sha256_password_found, "no sha256 password authentication plugin found") + def testAuthSHA256(self): + c = self.connections[0].cursor() + with TempUser(c, 'pymysql_sha256@localhost', + self.databases[0]['db'], 'sha256_password') as u: + if self.mysql_server_is(self.connections[0], (5, 7, 0)): + c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' ='Sh@256Pa33'") + else: + c.execute('SET old_passwords = 2') + c.execute("SET PASSWORD FOR 'pymysql_sha256'@'localhost' = PASSWORD('Sh@256Pa33')") + db = self.db.copy() + db['password'] = "Sh@256Pa33" + # not implemented yet so thows error + with self.assertRaises(pymysql.err.OperationalError): + pymysql.connect(user='pymysql_256', **db) + +class TestConnection(base.PyMySQLTestCase): + + def test_utf8mb4(self): + """This test requires MySQL >= 5.5""" + arg = self.databases[0].copy() + arg['charset'] = 'utf8mb4' + conn = pymysql.connect(**arg) + + def test_largedata(self): + """Large query and response (>=16MB)""" + cur = self.connections[0].cursor() + cur.execute("SELECT @@max_allowed_packet") + if cur.fetchone()[0] < 16*1024*1024 + 10: + print("Set max_allowed_packet to bigger than 17MB") + return + t = 'a' * (16*1024*1024) + cur.execute("SELECT '" + t + "'") + assert cur.fetchone()[0] == t + + def test_autocommit(self): + con = self.connections[0] + self.assertFalse(con.get_autocommit()) + + cur = con.cursor() + cur.execute("SET AUTOCOMMIT=1") + self.assertTrue(con.get_autocommit()) + + con.autocommit(False) + self.assertFalse(con.get_autocommit()) + cur.execute("SELECT @@AUTOCOMMIT") + self.assertEqual(cur.fetchone()[0], 0) + + def test_select_db(self): + con = self.connections[0] + current_db = self.databases[0]['db'] + other_db = self.databases[1]['db'] + + cur = con.cursor() + cur.execute('SELECT database()') + self.assertEqual(cur.fetchone()[0], current_db) + + con.select_db(other_db) + cur.execute('SELECT database()') + self.assertEqual(cur.fetchone()[0], other_db) + + def test_connection_gone_away(self): + """ + http://dev.mysql.com/doc/refman/5.0/en/gone-away.html + http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html#error_cr_server_gone_error + """ + con = self.connections[0] + cur = con.cursor() + cur.execute("SET wait_timeout=1") + time.sleep(2) + with self.assertRaises(pymysql.OperationalError) as cm: + cur.execute("SELECT 1+1") + # error occures while reading, not writing because of socket buffer. + #self.assertEqual(cm.exception.args[0], 2006) + self.assertIn(cm.exception.args[0], (2006, 2013)) + + def test_init_command(self): + conn = pymysql.connect( + init_command='SELECT "bar"; SELECT "baz"', + **self.databases[0] + ) + c = conn.cursor() + c.execute('select "foobar";') + self.assertEqual(('foobar',), c.fetchone()) + conn.close() + with self.assertRaises(pymysql.err.Error): + conn.ping(reconnect=False) + + def test_read_default_group(self): + conn = pymysql.connect( + read_default_group='client', + **self.databases[0] + ) + self.assertTrue(conn.open) + + def test_context(self): + with self.assertRaises(ValueError): + c = pymysql.connect(**self.databases[0]) + with c as cur: + cur.execute('create table test ( a int )') + c.begin() + cur.execute('insert into test values ((1))') + raise ValueError('pseudo abort') + c.commit() + c = pymysql.connect(**self.databases[0]) + with c as cur: + cur.execute('select count(*) from test') + self.assertEqual(0, cur.fetchone()[0]) + cur.execute('insert into test values ((1))') + with c as cur: + cur.execute('select count(*) from test') + self.assertEqual(1,cur.fetchone()[0]) + cur.execute('drop table test') + + def test_set_charset(self): + c = pymysql.connect(**self.databases[0]) + c.set_charset('utf8') + # TODO validate setting here + + def test_defer_connect(self): + import socket + for db in self.databases: + d = db.copy() + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(d['unix_socket']) + except KeyError: + sock = socket.create_connection( + (d.get('host', 'localhost'), d.get('port', 3306))) + for k in ['unix_socket', 'host', 'port']: + try: + del d[k] + except KeyError: + pass + + c = pymysql.connect(defer_connect=True, **d) + self.assertFalse(c.open) + c.connect(sock) + c.close() + sock.close() + + @unittest2.skipUnless(sys.version_info[0:2] >= (3,2), "required py-3.2") + def test_no_delay_warning(self): + current_db = self.databases[0].copy() + current_db['no_delay'] = True + with self.assertWarns(DeprecationWarning) as cm: + conn = pymysql.connect(**current_db) + + +# A custom type and function to escape it +class Foo(object): + value = "bar" + + +def escape_foo(x, d): + return x.value + + +class TestEscape(base.PyMySQLTestCase): + def test_escape_string(self): + con = self.connections[0] + cur = con.cursor() + + self.assertEqual(con.escape("foo'bar"), "'foo\\'bar'") + # added NO_AUTO_CREATE_USER as not including it in 5.7 generates warnings + cur.execute("SET sql_mode='NO_BACKSLASH_ESCAPES,NO_AUTO_CREATE_USER'") + self.assertEqual(con.escape("foo'bar"), "'foo''bar'") + + def test_escape_builtin_encoders(self): + con = self.connections[0] + cur = con.cursor() + + val = datetime.datetime(2012, 3, 4, 5, 6) + self.assertEqual(con.escape(val, con.encoders), "'2012-03-04 05:06:00'") + + def test_escape_custom_object(self): + con = self.connections[0] + cur = con.cursor() + + mapping = {Foo: escape_foo} + self.assertEqual(con.escape(Foo(), mapping), "bar") + + def test_escape_fallback_encoder(self): + con = self.connections[0] + cur = con.cursor() + + class Custom(str): + pass + + mapping = {text_type: pymysql.escape_string} + self.assertEqual(con.escape(Custom('foobar'), mapping), "'foobar'") + + def test_escape_no_default(self): + con = self.connections[0] + cur = con.cursor() + + self.assertRaises(TypeError, con.escape, 42, {}) + + def test_escape_dict_value(self): + con = self.connections[0] + cur = con.cursor() + + mapping = con.encoders.copy() + mapping[Foo] = escape_foo + self.assertEqual(con.escape({'foo': Foo()}, mapping), {'foo': "bar"}) + + def test_escape_list_item(self): + con = self.connections[0] + cur = con.cursor() + + mapping = con.encoders.copy() + mapping[Foo] = escape_foo + self.assertEqual(con.escape([Foo()], mapping), "(bar)") + + def test_previous_cursor_not_closed(self): + con = self.connections[0] + cur1 = con.cursor() + cur1.execute("SELECT 1; SELECT 2") + cur2 = con.cursor() + cur2.execute("SELECT 3") + self.assertEqual(cur2.fetchone()[0], 3) + + def test_commit_during_multi_result(self): + con = self.connections[0] + cur = con.cursor() + cur.execute("SELECT 1; SELECT 2") + con.commit() + cur.execute("SELECT 3") + self.assertEqual(cur.fetchone()[0], 3) diff --git a/gluon/contrib/pymysql/tests/test_converters.py b/gluon/contrib/pymysql/tests/test_converters.py new file mode 100755 index 00000000..b7b5a984 --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_converters.py @@ -0,0 +1,67 @@ +import datetime +from unittest import TestCase + +from pymysql._compat import PY2 +from pymysql import converters + + +__all__ = ["TestConverter"] + + +class TestConverter(TestCase): + + def test_escape_string(self): + self.assertEqual( + converters.escape_string(u"foo\nbar"), + u"foo\\nbar" + ) + + if PY2: + def test_escape_string_bytes(self): + self.assertEqual( + converters.escape_string(b"foo\nbar"), + b"foo\\nbar" + ) + + def test_convert_datetime(self): + expected = datetime.datetime(2007, 2, 24, 23, 6, 20) + dt = converters.convert_datetime('2007-02-24 23:06:20') + self.assertEqual(dt, expected) + + def test_convert_datetime_with_fsp(self): + expected = datetime.datetime(2007, 2, 24, 23, 6, 20, 511581) + dt = converters.convert_datetime('2007-02-24 23:06:20.511581') + self.assertEqual(dt, expected) + + def _test_convert_timedelta(self, with_negate=False, with_fsp=False): + d = {'hours': 789, 'minutes': 12, 'seconds': 34} + s = '%(hours)s:%(minutes)s:%(seconds)s' % d + if with_fsp: + d['microseconds'] = 511581 + s += '.%(microseconds)s' % d + + expected = datetime.timedelta(**d) + if with_negate: + expected = -expected + s = '-' + s + + tdelta = converters.convert_timedelta(s) + self.assertEqual(tdelta, expected) + + def test_convert_timedelta(self): + self._test_convert_timedelta(with_negate=False, with_fsp=False) + self._test_convert_timedelta(with_negate=True, with_fsp=False) + + def test_convert_timedelta_with_fsp(self): + self._test_convert_timedelta(with_negate=False, with_fsp=True) + self._test_convert_timedelta(with_negate=False, with_fsp=True) + + def test_convert_time(self): + expected = datetime.time(23, 6, 20) + time_obj = converters.convert_time('23:06:20') + self.assertEqual(time_obj, expected) + + def test_convert_time_with_fsp(self): + expected = datetime.time(23, 6, 20, 511581) + time_obj = converters.convert_time('23:06:20.511581') + self.assertEqual(time_obj, expected) diff --git a/gluon/contrib/pymysql/tests/test_cursor.py b/gluon/contrib/pymysql/tests/test_cursor.py new file mode 100755 index 00000000..431ef4dd --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_cursor.py @@ -0,0 +1,104 @@ +import warnings + +from pymysql.tests import base +import pymysql.cursors + +class CursorTest(base.PyMySQLTestCase): + def setUp(self): + super(CursorTest, self).setUp() + + conn = self.connections[0] + self.safe_create_table( + conn, + "test", "create table test (data varchar(10))", + ) + cursor = conn.cursor() + cursor.execute( + "insert into test (data) values " + "('row1'), ('row2'), ('row3'), ('row4'), ('row5')") + cursor.close() + self.test_connection = pymysql.connect(**self.databases[0]) + self.addCleanup(self.test_connection.close) + + def test_cleanup_rows_unbuffered(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.SSCursor) + + cursor.execute("select * from test as t1, test as t2") + for counter, row in enumerate(cursor): + if counter > 10: + break + + del cursor + self.safe_gc_collect() + + c2 = conn.cursor() + + c2.execute("select 1") + self.assertEqual(c2.fetchone(), (1,)) + self.assertIsNone(c2.fetchone()) + + def test_cleanup_rows_buffered(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.Cursor) + + cursor.execute("select * from test as t1, test as t2") + for counter, row in enumerate(cursor): + if counter > 10: + break + + del cursor + self.safe_gc_collect() + + c2 = conn.cursor() + + c2.execute("select 1") + + self.assertEqual( + c2.fetchone(), (1,) + ) + self.assertIsNone(c2.fetchone()) + + def test_executemany(self): + conn = self.test_connection + cursor = conn.cursor(pymysql.cursors.Cursor) + + m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%s, %s)") + self.assertIsNotNone(m, 'error parse %s') + self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?') + + m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id)s, %(name)s)") + self.assertIsNotNone(m, 'error parse %(name)s') + self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?') + + m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s)") + self.assertIsNotNone(m, 'error parse %(id_name)s') + self.assertEqual(m.group(3), '', 'group 3 not blank, bug in RE_INSERT_VALUES?') + + m = pymysql.cursors.RE_INSERT_VALUES.match("INSERT INTO TEST (ID, NAME) VALUES (%(id_name)s, %(name)s) ON duplicate update") + self.assertIsNotNone(m, 'error parse %(id_name)s') + self.assertEqual(m.group(3), ' ON duplicate update', 'group 3 not ON duplicate update, bug in RE_INSERT_VALUES?') + + # cursor._executed must bee "insert into test (data) values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)" + # list args + data = range(10) + cursor.executemany("insert into test (data) values (%s)", data) + self.assertTrue(cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %s not in one query') + + # dict args + data_dict = [{'data': i} for i in range(10)] + cursor.executemany("insert into test (data) values (%(data)s)", data_dict) + self.assertTrue(cursor._executed.endswith(b",(7),(8),(9)"), 'execute many with %(data)s not in one query') + + # %% in column set + cursor.execute("""\ + CREATE TABLE percent_test ( + `A%` INTEGER, + `B%` INTEGER)""") + try: + q = "INSERT INTO percent_test (`A%%`, `B%%`) VALUES (%s, %s)" + self.assertIsNotNone(pymysql.cursors.RE_INSERT_VALUES.match(q)) + cursor.executemany(q, [(3, 4), (5, 6)]) + self.assertTrue(cursor._executed.endswith(b"(3, 4),(5, 6)"), "executemany with %% not in one query") + finally: + cursor.execute("DROP TABLE IF EXISTS percent_test") diff --git a/gluon/contrib/pymysql/tests/test_err.py b/gluon/contrib/pymysql/tests/test_err.py new file mode 100755 index 00000000..3468d1b1 --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_err.py @@ -0,0 +1,21 @@ +import unittest2 + +from pymysql import err + + +__all__ = ["TestRaiseException"] + + +class TestRaiseException(unittest2.TestCase): + + def test_raise_mysql_exception(self): + data = b"\xff\x15\x04Access denied" + with self.assertRaises(err.OperationalError) as cm: + err.raise_mysql_exception(data) + self.assertEqual(cm.exception.args, (1045, 'Access denied')) + + def test_raise_mysql_exception_client_protocol_41(self): + data = b"\xff\x15\x04#28000Access denied" + with self.assertRaises(err.OperationalError) as cm: + err.raise_mysql_exception(data) + self.assertEqual(cm.exception.args, (1045, 'Access denied')) diff --git a/gluon/contrib/pymysql/tests/test_issues.py b/gluon/contrib/pymysql/tests/test_issues.py index 6f7fc3d1..fe3e2984 100644 --- a/gluon/contrib/pymysql/tests/test_issues.py +++ b/gluon/contrib/pymysql/tests/test_issues.py @@ -1,26 +1,31 @@ -import pymysql -from pymysql.tests import base -import unittest - +import datetime +import time +import warnings import sys +import pymysql +from pymysql import cursors +from pymysql._compat import text_type +from pymysql.tests import base +import unittest2 + try: import imp reload = imp.reload except AttributeError: pass -import datetime -# backwards compatibility: -if not hasattr(unittest, "skip"): - unittest.skip = lambda message: lambda f: f +__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"] class TestOldIssues(base.PyMySQLTestCase): def test_issue_3(self): """ undefined methods datetime_or_None, date_or_None """ conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue3") c.execute("create table issue3 (d date, t time, dt datetime, ts timestamp)") try: c.execute("insert into issue3 (d, t, dt, ts) values (%s,%s,%s,%s)", (None, None, None, None)) @@ -39,6 +44,9 @@ class TestOldIssues(base.PyMySQLTestCase): """ can't retrieve TIMESTAMP fields """ conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue4") c.execute("create table issue4 (ts timestamp)") try: c.execute("insert into issue4 (ts) values (now())") @@ -55,7 +63,10 @@ class TestOldIssues(base.PyMySQLTestCase): def test_issue_6(self): """ exception: TypeError: ord() expected a character, but string of length 0 found """ - conn = pymysql.connect(host="localhost",user="root",passwd="",db="mysql") + # ToDo: this test requires access to db 'mysql'. + kwargs = self.databases[0].copy() + kwargs['db'] = "mysql" + conn = pymysql.connect(**kwargs) c = conn.cursor() c.execute("select * from user") conn.close() @@ -64,8 +75,11 @@ class TestOldIssues(base.PyMySQLTestCase): """ Primary Key and Index error when selecting data """ conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists test") c.execute("""CREATE TABLE `test` (`station` int(10) NOT NULL DEFAULT '0', `dh` -datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `echeance` int(1) NOT NULL +datetime NOT NULL DEFAULT '2015-01-01 00:00:00', `echeance` int(1) NOT NULL DEFAULT '0', `me` double DEFAULT NULL, `mo` double DEFAULT NULL, PRIMARY KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") try: @@ -82,18 +96,13 @@ KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") except DeprecationWarning: self.fail() - def test_issue_10(self): - """ Allocate a variable to return when the exception handler is permissive """ - conn = self.connections[0] - conn.errorhandler = lambda cursor, errorclass, errorvalue: None - cur = conn.cursor() - cur.execute( "create table t( n int )" ) - cur.execute( "create table t( n int )" ) - def test_issue_13(self): """ can't handle large result fields """ conn = self.connections[0] cur = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("drop table if exists issue13") try: cur.execute("create table issue13 (t text)") # ticket says 18k @@ -106,18 +115,13 @@ KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") finally: cur.execute("drop table issue13") - def test_issue_14(self): - """ typo in converters.py """ - self.assertEqual('1', pymysql.converters.escape_item(1, "utf8")) - self.assertEqual('1', pymysql.converters.escape_item(1L, "utf8")) - - self.assertEqual('1', pymysql.converters.escape_object(1)) - self.assertEqual('1', pymysql.converters.escape_object(1L)) - def test_issue_15(self): """ query should be expanded before perform character encoding """ conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue15") c.execute("create table issue15 (t varchar(32))") try: c.execute("insert into issue15 (t) values (%s)", (u'\xe4\xf6\xfc',)) @@ -130,6 +134,9 @@ KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") """ Patch for string and tuple escaping """ conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue16") c.execute("create table issue16 (name varchar(32) primary key, email varchar(32))") try: c.execute("insert into issue16 (name, email) values ('pete', 'floydophone')") @@ -138,20 +145,24 @@ KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") finally: c.execute("drop table issue16") - @unittest.skip("test_issue_17() requires a custom, legacy MySQL configuration and will not be run.") + @unittest2.skip("test_issue_17() requires a custom, legacy MySQL configuration and will not be run.") def test_issue_17(self): - """ could not connect mysql use passwod """ + """could not connect mysql use passwod""" conn = self.connections[0] host = self.databases[0]["host"] db = self.databases[0]["db"] c = conn.cursor() + # grant access to a table to a user with a password try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue17") c.execute("create table issue17 (x varchar(32) primary key)") c.execute("insert into issue17 (x) values ('hello, world!')") c.execute("grant all privileges on %s.issue17 to 'issue17user'@'%%' identified by '1234'" % db) conn.commit() - + conn2 = pymysql.connect(host=host, user="issue17user", passwd="1234", db=db) c2 = conn2.cursor() c2.execute("select x from issue17") @@ -159,71 +170,71 @@ KEY (`station`,`dh`,`echeance`)) ENGINE=MyISAM DEFAULT CHARSET=latin1;""") finally: c.execute("drop table issue17") -def _uni(s, e): - # hack for py3 - if sys.version_info[0] > 2: - return unicode(bytes(s, sys.getdefaultencoding()), e) - else: - return unicode(s, e) - class TestNewIssues(base.PyMySQLTestCase): def test_issue_34(self): try: pymysql.connect(host="localhost", port=1237, user="root") self.fail() - except pymysql.OperationalError, e: + except pymysql.OperationalError as e: self.assertEqual(2003, e.args[0]) - except: + except Exception: self.fail() def test_issue_33(self): - conn = pymysql.connect(host="localhost", user="root", db=self.databases[0]["db"], charset="utf8") + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table(conn, u'hei\xdfe', + u'create table hei\xdfe (name varchar(32))') c = conn.cursor() - try: - c.execute(_uni("create table hei\xc3\x9fe (name varchar(32))", "utf8")) - c.execute(_uni("insert into hei\xc3\x9fe (name) values ('Pi\xc3\xb1ata')", "utf8")) - c.execute(_uni("select name from hei\xc3\x9fe", "utf8")) - self.assertEqual(_uni("Pi\xc3\xb1ata","utf8"), c.fetchone()[0]) - finally: - c.execute(_uni("drop table hei\xc3\x9fe", "utf8")) + c.execute(u"insert into hei\xdfe (name) values ('Pi\xdfata')") + c.execute(u"select name from hei\xdfe") + self.assertEqual(u"Pi\xdfata", c.fetchone()[0]) - @unittest.skip("This test requires manual intervention") + @unittest2.skip("This test requires manual intervention") def test_issue_35(self): conn = self.connections[0] c = conn.cursor() - print "sudo killall -9 mysqld within the next 10 seconds" + print("sudo killall -9 mysqld within the next 10 seconds") try: c.execute("select sleep(10)") self.fail() - except pymysql.OperationalError, e: + except pymysql.OperationalError as e: self.assertEqual(2013, e.args[0]) def test_issue_36(self): - conn = self.connections[0] + # connection 0 is super user, connection 1 isn't + conn = self.connections[1] c = conn.cursor() - # kill connections[0] c.execute("show processlist") kill_id = None - for id,user,host,db,command,time,state,info in c.fetchall(): + for row in c.fetchall(): + id = row[0] + info = row[7] if info == "show processlist": kill_id = id break + self.assertEqual(kill_id, conn.thread_id()) # now nuke the connection - conn.kill(kill_id) + self.connections[0].kill(kill_id) # make sure this connection has broken try: c.execute("show tables") self.fail() - except: + except Exception: pass + c.close() + conn.close() + # check the process list from the other connection try: - c = self.connections[1].cursor() + # Wait since Travis-CI sometimes fail this test. + time.sleep(0.1) + + c = self.connections[0].cursor() c.execute("show processlist") ids = [row[0] for row in c.fetchall()] self.assertFalse(kill_id in ids) finally: - del self.connections[0] + del self.connections[1] def test_issue_37(self): conn = self.connections[0] @@ -237,8 +248,11 @@ class TestNewIssues(base.PyMySQLTestCase): conn = self.connections[0] c = conn.cursor() datum = "a" * 1024 * 1023 # reduced size for most default mysql installs - + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue38") c.execute("create table issue38 (id integer, data mediumblob)") c.execute("insert into issue38 values (1, %s)", (datum,)) finally: @@ -247,8 +261,11 @@ class TestNewIssues(base.PyMySQLTestCase): def disabled_test_issue_54(self): conn = self.connections[0] c = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue54") big_sql = "select * from issue54 where " - big_sql += " and ".join("%d=%d" % (i,i) for i in xrange(0, 100000)) + big_sql += " and ".join("%d=%d" % (i,i) for i in range(0, 100000)) try: c.execute("create table issue54 (id integer primary key)") @@ -260,10 +277,14 @@ class TestNewIssues(base.PyMySQLTestCase): class TestGitHubIssues(base.PyMySQLTestCase): def test_issue_66(self): + """ 'Connection' object has no attribute 'insert_id' """ conn = self.connections[0] c = conn.cursor() self.assertEqual(0, conn.insert_id()) try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists issue66") c.execute("create table issue66 (id integer primary key auto_increment, x integer)") c.execute("insert into issue66 (x) values (1)") c.execute("insert into issue66 (x) values (1)") @@ -271,8 +292,224 @@ class TestGitHubIssues(base.PyMySQLTestCase): finally: c.execute("drop table issue66") -__all__ = ["TestOldIssues", "TestNewIssues", "TestGitHubIssues"] + def test_issue_79(self): + """ Duplicate field overwrites the previous one in the result of DictCursor """ + conn = self.connections[0] + c = conn.cursor(pymysql.cursors.DictCursor) -if __name__ == "__main__": - import unittest - unittest.main() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + c.execute("drop table if exists a") + c.execute("drop table if exists b") + c.execute("""CREATE TABLE a (id int, value int)""") + c.execute("""CREATE TABLE b (id int, value int)""") + + a=(1,11) + b=(1,22) + try: + c.execute("insert into a values (%s, %s)", a) + c.execute("insert into b values (%s, %s)", b) + + c.execute("SELECT * FROM a inner join b on a.id = b.id") + r = c.fetchall()[0] + self.assertEqual(r['id'], 1) + self.assertEqual(r['value'], 11) + self.assertEqual(r['b.value'], 22) + finally: + c.execute("drop table a") + c.execute("drop table b") + + def test_issue_95(self): + """ Leftover trailing OK packet for "CALL my_sp" queries """ + conn = self.connections[0] + cur = conn.cursor() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("DROP PROCEDURE IF EXISTS `foo`") + cur.execute("""CREATE PROCEDURE `foo` () + BEGIN + SELECT 1; + END""") + try: + cur.execute("""CALL foo()""") + cur.execute("""SELECT 1""") + self.assertEqual(cur.fetchone()[0], 1) + finally: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute("DROP PROCEDURE IF EXISTS `foo`") + + def test_issue_114(self): + """ autocommit is not set after reconnecting with ping() """ + conn = pymysql.connect(charset="utf8", **self.databases[0]) + conn.autocommit(False) + c = conn.cursor() + c.execute("""select @@autocommit;""") + self.assertFalse(c.fetchone()[0]) + conn.close() + conn.ping() + c.execute("""select @@autocommit;""") + self.assertFalse(c.fetchone()[0]) + conn.close() + + # Ensure autocommit() is still working + conn = pymysql.connect(charset="utf8", **self.databases[0]) + c = conn.cursor() + c.execute("""select @@autocommit;""") + self.assertFalse(c.fetchone()[0]) + conn.close() + conn.ping() + conn.autocommit(True) + c.execute("""select @@autocommit;""") + self.assertTrue(c.fetchone()[0]) + conn.close() + + def test_issue_175(self): + """ The number of fields returned by server is read in wrong way """ + conn = self.connections[0] + cur = conn.cursor() + for length in (200, 300): + columns = ', '.join('c{0} integer'.format(i) for i in range(length)) + sql = 'create table test_field_count ({0})'.format(columns) + try: + cur.execute(sql) + cur.execute('select * from test_field_count') + assert len(cur.description) == length + finally: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + cur.execute('drop table if exists test_field_count') + + def test_issue_321(self): + """ Test iterable as query argument. """ + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, "issue321", + "create table issue321 (value_1 varchar(1), value_2 varchar(1))") + + sql_insert = "insert into issue321 (value_1, value_2) values (%s, %s)" + sql_dict_insert = ("insert into issue321 (value_1, value_2) " + "values (%(value_1)s, %(value_2)s)") + sql_select = ("select * from issue321 where " + "value_1 in %s and value_2=%s") + data = [ + [(u"a", ), u"\u0430"], + [[u"b"], u"\u0430"], + {"value_1": [[u"c"]], "value_2": u"\u0430"} + ] + cur = conn.cursor() + self.assertEqual(cur.execute(sql_insert, data[0]), 1) + self.assertEqual(cur.execute(sql_insert, data[1]), 1) + self.assertEqual(cur.execute(sql_dict_insert, data[2]), 1) + self.assertEqual( + cur.execute(sql_select, [(u"a", u"b", u"c"), u"\u0430"]), 3) + self.assertEqual(cur.fetchone(), (u"a", u"\u0430")) + self.assertEqual(cur.fetchone(), (u"b", u"\u0430")) + self.assertEqual(cur.fetchone(), (u"c", u"\u0430")) + + def test_issue_364(self): + """ Test mixed unicode/binary arguments in executemany. """ + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, "issue364", + "create table issue364 (value_1 binary(3), value_2 varchar(3)) " + "engine=InnoDB default charset=utf8") + + sql = "insert into issue364 (value_1, value_2) values (%s, %s)" + usql = u"insert into issue364 (value_1, value_2) values (%s, %s)" + values = [pymysql.Binary(b"\x00\xff\x00"), u"\xe4\xf6\xfc"] + + # test single insert and select + cur = conn.cursor() + cur.execute(sql, args=values) + cur.execute("select * from issue364") + self.assertEqual(cur.fetchone(), tuple(values)) + + # test single insert unicode query + cur.execute(usql, args=values) + + # test multi insert and select + cur.executemany(sql, args=(values, values, values)) + cur.execute("select * from issue364") + for row in cur.fetchall(): + self.assertEqual(row, tuple(values)) + + # test multi insert with unicode query + cur.executemany(usql, args=(values, values, values)) + + def test_issue_363(self): + """ Test binary / geometry types. """ + conn = pymysql.connect(charset="utf8", **self.databases[0]) + self.safe_create_table( + conn, "issue363", + "CREATE TABLE issue363 ( " + "id INTEGER PRIMARY KEY, geom LINESTRING NOT NULL, " + "SPATIAL KEY geom (geom)) " + "ENGINE=MyISAM default charset=utf8") + + cur = conn.cursor() + query = ("INSERT INTO issue363 (id, geom) VALUES" + "(1998, GeomFromText('LINESTRING(1.1 1.1,2.2 2.2)'))") + # From MySQL 5.7, ST_GeomFromText is added and GeomFromText is deprecated. + if self.mysql_server_is(conn, (5, 7, 0)): + with self.assertWarns(pymysql.err.Warning) as cm: + cur.execute(query) + else: + cur.execute(query) + + # select WKT + query = "SELECT AsText(geom) FROM issue363" + if self.mysql_server_is(conn, (5, 7, 0)): + with self.assertWarns(pymysql.err.Warning) as cm: + cur.execute(query) + else: + cur.execute(query) + row = cur.fetchone() + self.assertEqual(row, ("LINESTRING(1.1 1.1,2.2 2.2)", )) + + # select WKB + query = "SELECT AsBinary(geom) FROM issue363" + if self.mysql_server_is(conn, (5, 7, 0)): + with self.assertWarns(pymysql.err.Warning) as cm: + cur.execute(query) + else: + cur.execute(query) + row = cur.fetchone() + self.assertEqual(row, + (b"\x01\x02\x00\x00\x00\x02\x00\x00\x00" + b"\x9a\x99\x99\x99\x99\x99\xf1?" + b"\x9a\x99\x99\x99\x99\x99\xf1?" + b"\x9a\x99\x99\x99\x99\x99\x01@" + b"\x9a\x99\x99\x99\x99\x99\x01@", )) + + # select internal binary + cur.execute("SELECT geom FROM issue363") + row = cur.fetchone() + # don't assert the exact internal binary value, as it could + # vary across implementations + self.assertTrue(isinstance(row[0], bytes)) + + def test_issue_491(self): + """ Test warning propagation """ + conn = pymysql.connect(charset="utf8", **self.databases[0]) + + with warnings.catch_warnings(): + # Ignore all warnings other than pymysql generated ones + warnings.simplefilter("ignore") + warnings.simplefilter("error", category=pymysql.Warning) + + # verify for both buffered and unbuffered cursor types + for cursor_class in (cursors.Cursor, cursors.SSCursor): + c = conn.cursor(cursor_class) + try: + c.execute("SELECT CAST('124b' AS SIGNED)") + c.fetchall() + except pymysql.Warning as e: + # Warnings should have errorcode and string message, just like exceptions + self.assertEqual(len(e.args), 2) + self.assertEqual(e.args[0], 1292) + self.assertTrue(isinstance(e.args[1], text_type)) + else: + self.fail("Should raise Warning") + finally: + c.close() diff --git a/gluon/contrib/pymysql/tests/test_load_local.py b/gluon/contrib/pymysql/tests/test_load_local.py new file mode 100755 index 00000000..85fd94ea --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_load_local.py @@ -0,0 +1,93 @@ +from pymysql import cursors, OperationalError, Warning +from pymysql.tests import base + +import os +import warnings + +__all__ = ["TestLoadLocal"] + + +class TestLoadLocal(base.PyMySQLTestCase): + def test_no_file(self): + """Test load local infile when the file does not exist""" + conn = self.connections[0] + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + try: + self.assertRaises( + OperationalError, + c.execute, + ("LOAD DATA LOCAL INFILE 'no_data.txt' INTO TABLE " + "test_load_local fields terminated by ','") + ) + finally: + c.execute("DROP TABLE test_load_local") + c.close() + + def test_load_file(self): + """Test load local infile with a valid file""" + conn = self.connections[0] + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data', + 'load_local_data.txt') + try: + c.execute( + ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " + + "test_load_local FIELDS TERMINATED BY ','").format(filename) + ) + c.execute("SELECT COUNT(*) FROM test_load_local") + self.assertEqual(22749, c.fetchone()[0]) + finally: + c.execute("DROP TABLE test_load_local") + + def test_unbuffered_load_file(self): + """Test unbuffered load local infile with a valid file""" + conn = self.connections[0] + c = conn.cursor(cursors.SSCursor) + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data', + 'load_local_data.txt') + try: + c.execute( + ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " + + "test_load_local FIELDS TERMINATED BY ','").format(filename) + ) + c.execute("SELECT COUNT(*) FROM test_load_local") + self.assertEqual(22749, c.fetchone()[0]) + finally: + c.close() + conn.close() + conn.connect() + c = conn.cursor() + c.execute("DROP TABLE test_load_local") + + def test_load_warnings(self): + """Test load local infile produces the appropriate warnings""" + conn = self.connections[0] + c = conn.cursor() + c.execute("CREATE TABLE test_load_local (a INTEGER, b INTEGER)") + filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'data', + 'load_local_warn_data.txt') + try: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + c.execute( + ("LOAD DATA LOCAL INFILE '{0}' INTO TABLE " + + "test_load_local FIELDS TERMINATED BY ','").format(filename) + ) + self.assertEqual(w[0].category, Warning) + expected_message = "Incorrect integer value" + if expected_message not in str(w[-1].message): + self.fail("%r not in %r" % (expected_message, w[-1].message)) + finally: + c.execute("DROP TABLE test_load_local") + c.close() + + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/gluon/contrib/pymysql/tests/test_nextset.py b/gluon/contrib/pymysql/tests/test_nextset.py new file mode 100755 index 00000000..cdb6754f --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_nextset.py @@ -0,0 +1,68 @@ +import unittest2 + +from pymysql.tests import base +from pymysql import util + + +class TestNextset(base.PyMySQLTestCase): + + def setUp(self): + super(TestNextset, self).setUp() + self.con = self.connections[0] + + def test_nextset(self): + cur = self.con.cursor() + cur.execute("SELECT 1; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + + r = cur.nextset() + self.assertTrue(r) + + self.assertEqual([(2,)], list(cur)) + self.assertIsNone(cur.nextset()) + + def test_skip_nextset(self): + cur = self.con.cursor() + cur.execute("SELECT 1; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + + cur.execute("SELECT 42") + self.assertEqual([(42,)], list(cur)) + + def test_ok_and_next(self): + cur = self.con.cursor() + cur.execute("SELECT 1; commit; SELECT 2;") + self.assertEqual([(1,)], list(cur)) + self.assertTrue(cur.nextset()) + self.assertTrue(cur.nextset()) + self.assertEqual([(2,)], list(cur)) + self.assertFalse(bool(cur.nextset())) + + @unittest2.expectedFailure + def test_multi_cursor(self): + cur1 = self.con.cursor() + cur2 = self.con.cursor() + + cur1.execute("SELECT 1; SELECT 2;") + cur2.execute("SELECT 42") + + self.assertEqual([(1,)], list(cur1)) + self.assertEqual([(42,)], list(cur2)) + + r = cur1.nextset() + self.assertTrue(r) + + self.assertEqual([(2,)], list(cur1)) + self.assertIsNone(cur1.nextset()) + + def test_multi_statement_warnings(self): + cursor = self.con.cursor() + + try: + cursor.execute('DROP TABLE IF EXISTS a; ' + 'DROP TABLE IF EXISTS b;') + except TypeError: + self.fail() + + #TODO: How about SSCursor and nextset? + # It's very hard to implement correctly... diff --git a/gluon/contrib/pymysql/tests/test_optionfile.py b/gluon/contrib/pymysql/tests/test_optionfile.py new file mode 100755 index 00000000..abc63ea1 --- /dev/null +++ b/gluon/contrib/pymysql/tests/test_optionfile.py @@ -0,0 +1,32 @@ +from pymysql.optionfile import Parser +from unittest import TestCase +from pymysql._compat import PY2 + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + + +__all__ = ['TestParser'] + + +_cfg_file = (r""" +[default] +string = foo +quoted = "bar" +single_quoted = 'foobar' +""") + + +class TestParser(TestCase): + + def test_string(self): + parser = Parser() + if PY2: + parser.readfp(StringIO(_cfg_file)) + else: + parser.read_file(StringIO(_cfg_file)) + self.assertEqual(parser.get("default", "string"), "foo") + self.assertEqual(parser.get("default", "quoted"), "bar") + self.assertEqual(parser.get("default", "single_quoted"), "foobar") diff --git a/gluon/contrib/pymysql/tests/thirdparty/__init__.py b/gluon/contrib/pymysql/tests/thirdparty/__init__.py new file mode 100755 index 00000000..6d59e112 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/__init__.py @@ -0,0 +1,8 @@ +from .test_MySQLdb import * + +if __name__ == "__main__": + try: + import unittest2 as unittest + except ImportError: + import unittest + unittest.main() diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py new file mode 100755 index 00000000..e4237c69 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/__init__.py @@ -0,0 +1,7 @@ +from .test_MySQLdb_capabilities import test_MySQLdb as test_capabilities +from .test_MySQLdb_nonstandard import * +from .test_MySQLdb_dbapi20 import test_MySQLdb as test_dbapi2 + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py new file mode 100755 index 00000000..e4aae206 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/capabilities.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python -O +""" Script to test database capabilities and the DB-API interface + for functionality and memory leaks. + + Adapted from a script by M-A Lemburg. + +""" +import sys +from time import time +try: + import unittest2 as unittest +except ImportError: + import unittest + +PY2 = sys.version_info[0] == 2 + +class DatabaseTest(unittest.TestCase): + + db_module = None + connect_args = () + connect_kwargs = dict(use_unicode=True, charset="utf8") + create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" + rows = 10 + debug = False + + def setUp(self): + db = self.db_module.connect(*self.connect_args, **self.connect_kwargs) + self.connection = db + self.cursor = db.cursor() + self.BLOBText = ''.join([chr(i) for i in range(256)] * 100); + if PY2: + self.BLOBUText = unicode().join(unichr(i) for i in range(16834)) + else: + self.BLOBUText = "".join(chr(i) for i in range(16834)) + data = bytearray(range(256)) * 16 + self.BLOBBinary = self.db_module.Binary(data) + + leak_test = True + + def tearDown(self): + if self.leak_test: + import gc + del self.cursor + orphans = gc.collect() + self.assertFalse(orphans, "%d orphaned objects found after deleting cursor" % orphans) + + del self.connection + orphans = gc.collect() + self.assertFalse(orphans, "%d orphaned objects found after deleting connection" % orphans) + + def table_exists(self, name): + try: + self.cursor.execute('select * from %s where 1=0' % name) + except Exception: + return False + else: + return True + + def quote_identifier(self, ident): + return '"%s"' % ident + + def new_table_name(self): + i = id(self.cursor) + while True: + name = self.quote_identifier('tb%08x' % i) + if not self.table_exists(name): + return name + i = i + 1 + + def create_table(self, columndefs): + + """ Create a table using a list of column definitions given in + columndefs. + + generator must be a function taking arguments (row_number, + col_number) returning a suitable data object for insertion + into the table. + + """ + self.table = self.new_table_name() + self.cursor.execute('CREATE TABLE %s (%s) %s' % + (self.table, + ',\n'.join(columndefs), + self.create_table_extra)) + + def check_data_integrity(self, columndefs, generator): + # insert + self.create_table(columndefs) + insert_statement = ('INSERT INTO %s VALUES (%s)' % + (self.table, + ','.join(['%s'] * len(columndefs)))) + data = [ [ generator(i,j) for j in range(len(columndefs)) ] + for i in range(self.rows) ] + if self.debug: + print(data) + self.cursor.executemany(insert_statement, data) + self.connection.commit() + # verify + self.cursor.execute('select * from %s' % self.table) + l = self.cursor.fetchall() + if self.debug: + print(l) + self.assertEqual(len(l), self.rows) + try: + for i in range(self.rows): + for j in range(len(columndefs)): + self.assertEqual(l[i][j], generator(i,j)) + finally: + if not self.debug: + self.cursor.execute('drop table %s' % (self.table)) + + def test_transactions(self): + columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') + def generator(row, col): + if col == 0: return row + else: return ('%i' % (row%10))*255 + self.create_table(columndefs) + insert_statement = ('INSERT INTO %s VALUES (%s)' % + (self.table, + ','.join(['%s'] * len(columndefs)))) + data = [ [ generator(i,j) for j in range(len(columndefs)) ] + for i in range(self.rows) ] + self.cursor.executemany(insert_statement, data) + # verify + self.connection.commit() + self.cursor.execute('select * from %s' % self.table) + l = self.cursor.fetchall() + self.assertEqual(len(l), self.rows) + for i in range(self.rows): + for j in range(len(columndefs)): + self.assertEqual(l[i][j], generator(i,j)) + delete_statement = 'delete from %s where col1=%%s' % self.table + self.cursor.execute(delete_statement, (0,)) + self.cursor.execute('select col1 from %s where col1=%s' % \ + (self.table, 0)) + l = self.cursor.fetchall() + self.assertFalse(l, "DELETE didn't work") + self.connection.rollback() + self.cursor.execute('select col1 from %s where col1=%s' % \ + (self.table, 0)) + l = self.cursor.fetchall() + self.assertTrue(len(l) == 1, "ROLLBACK didn't work") + self.cursor.execute('drop table %s' % (self.table)) + + def test_truncation(self): + columndefs = ( 'col1 INT', 'col2 VARCHAR(255)') + def generator(row, col): + if col == 0: return row + else: return ('%i' % (row%10))*((255-self.rows//2)+row) + self.create_table(columndefs) + insert_statement = ('INSERT INTO %s VALUES (%s)' % + (self.table, + ','.join(['%s'] * len(columndefs)))) + + try: + self.cursor.execute(insert_statement, (0, '0'*256)) + except Warning: + if self.debug: print(self.cursor.messages) + except self.connection.DataError: + pass + else: + self.fail("Over-long column did not generate warnings/exception with single insert") + + self.connection.rollback() + + try: + for i in range(self.rows): + data = [] + for j in range(len(columndefs)): + data.append(generator(i,j)) + self.cursor.execute(insert_statement,tuple(data)) + except Warning: + if self.debug: print(self.cursor.messages) + except self.connection.DataError: + pass + else: + self.fail("Over-long columns did not generate warnings/exception with execute()") + + self.connection.rollback() + + try: + data = [ [ generator(i,j) for j in range(len(columndefs)) ] + for i in range(self.rows) ] + self.cursor.executemany(insert_statement, data) + except Warning: + if self.debug: print(self.cursor.messages) + except self.connection.DataError: + pass + else: + self.fail("Over-long columns did not generate warnings/exception with executemany()") + + self.connection.rollback() + self.cursor.execute('drop table %s' % (self.table)) + + def test_CHAR(self): + # Character data + def generator(row,col): + return ('%i' % ((row+col) % 10)) * 255 + self.check_data_integrity( + ('col1 char(255)','col2 char(255)'), + generator) + + def test_INT(self): + # Number data + def generator(row,col): + return row*row + self.check_data_integrity( + ('col1 INT',), + generator) + + def test_DECIMAL(self): + # DECIMAL + def generator(row,col): + from decimal import Decimal + return Decimal("%d.%02d" % (row, col)) + self.check_data_integrity( + ('col1 DECIMAL(5,2)',), + generator) + + def test_DATE(self): + ticks = time() + def generator(row,col): + return self.db_module.DateFromTicks(ticks+row*86400-col*1313) + self.check_data_integrity( + ('col1 DATE',), + generator) + + def test_TIME(self): + ticks = time() + def generator(row,col): + return self.db_module.TimeFromTicks(ticks+row*86400-col*1313) + self.check_data_integrity( + ('col1 TIME',), + generator) + + def test_DATETIME(self): + ticks = time() + def generator(row,col): + return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) + self.check_data_integrity( + ('col1 DATETIME',), + generator) + + def test_TIMESTAMP(self): + ticks = time() + def generator(row,col): + return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313) + self.check_data_integrity( + ('col1 TIMESTAMP',), + generator) + + def test_fractional_TIMESTAMP(self): + ticks = time() + def generator(row,col): + return self.db_module.TimestampFromTicks(ticks+row*86400-col*1313+row*0.7*col/3.0) + self.check_data_integrity( + ('col1 TIMESTAMP',), + generator) + + def test_LONG(self): + def generator(row,col): + if col == 0: + return row + else: + return self.BLOBUText # 'BLOB Text ' * 1024 + self.check_data_integrity( + ('col1 INT', 'col2 LONG'), + generator) + + def test_TEXT(self): + def generator(row,col): + if col == 0: + return row + else: + return self.BLOBUText[:5192] # 'BLOB Text ' * 1024 + self.check_data_integrity( + ('col1 INT', 'col2 TEXT'), + generator) + + def test_LONG_BYTE(self): + def generator(row,col): + if col == 0: + return row + else: + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + self.check_data_integrity( + ('col1 INT','col2 LONG BYTE'), + generator) + + def test_BLOB(self): + def generator(row,col): + if col == 0: + return row + else: + return self.BLOBBinary # 'BLOB\000Binary ' * 1024 + self.check_data_integrity( + ('col1 INT','col2 BLOB'), + generator) diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py new file mode 100755 index 00000000..e8665248 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/dbapi20.py @@ -0,0 +1,856 @@ +#!/usr/bin/env python +''' Python DB API 2.0 driver compliance unit test suite. + + This software is Public Domain and may be used without restrictions. + + "Now we have booze and barflies entering the discussion, plus rumours of + DBAs on drugs... and I won't tell you what flashes through my mind each + time I read the subject line with 'Anal Compliance' in it. All around + this is turning out to be a thoroughly unwholesome unit test." + + -- Ian Bicking +''' + +__rcs_id__ = '$Id$' +__version__ = '$Revision$'[11:-2] +__author__ = 'Stuart Bishop ' + +try: + import unittest2 as unittest +except ImportError: + import unittest + +import time + +# $Log$ +# Revision 1.1.2.1 2006/02/25 03:44:32 adustman +# Generic DB-API unit test module +# +# Revision 1.10 2003/10/09 03:14:14 zenzen +# Add test for DB API 2.0 optional extension, where database exceptions +# are exposed as attributes on the Connection object. +# +# Revision 1.9 2003/08/13 01:16:36 zenzen +# Minor tweak from Stefan Fleiter +# +# Revision 1.8 2003/04/10 00:13:25 zenzen +# Changes, as per suggestions by M.-A. Lemburg +# - Add a table prefix, to ensure namespace collisions can always be avoided +# +# Revision 1.7 2003/02/26 23:33:37 zenzen +# Break out DDL into helper functions, as per request by David Rushby +# +# Revision 1.6 2003/02/21 03:04:33 zenzen +# Stuff from Henrik Ekelund: +# added test_None +# added test_nextset & hooks +# +# Revision 1.5 2003/02/17 22:08:43 zenzen +# Implement suggestions and code from Henrik Eklund - test that cursor.arraysize +# defaults to 1 & generic cursor.callproc test added +# +# Revision 1.4 2003/02/15 00:16:33 zenzen +# Changes, as per suggestions and bug reports by M.-A. Lemburg, +# Matthew T. Kromer, Federico Di Gregorio and Daniel Dittmar +# - Class renamed +# - Now a subclass of TestCase, to avoid requiring the driver stub +# to use multiple inheritance +# - Reversed the polarity of buggy test in test_description +# - Test exception heirarchy correctly +# - self.populate is now self._populate(), so if a driver stub +# overrides self.ddl1 this change propogates +# - VARCHAR columns now have a width, which will hopefully make the +# DDL even more portible (this will be reversed if it causes more problems) +# - cursor.rowcount being checked after various execute and fetchXXX methods +# - Check for fetchall and fetchmany returning empty lists after results +# are exhausted (already checking for empty lists if select retrieved +# nothing +# - Fix bugs in test_setoutputsize_basic and test_setinputsizes +# + +class DatabaseAPI20Test(unittest.TestCase): + ''' Test a database self.driver for DB API 2.0 compatibility. + This implementation tests Gadfly, but the TestCase + is structured so that other self.drivers can subclass this + test case to ensure compiliance with the DB-API. It is + expected that this TestCase may be expanded in the future + if ambiguities or edge conditions are discovered. + + The 'Optional Extensions' are not yet being tested. + + self.drivers should subclass this test, overriding setUp, tearDown, + self.driver, connect_args and connect_kw_args. Class specification + should be as follows: + + import dbapi20 + class mytest(dbapi20.DatabaseAPI20Test): + [...] + + Don't 'import DatabaseAPI20Test from dbapi20', or you will + confuse the unit tester - just 'import dbapi20'. + ''' + + # The self.driver module. This should be the module where the 'connect' + # method is to be found + driver = None + connect_args = () # List of arguments to pass to connect + connect_kw_args = {} # Keyword arguments for connect + table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables + + ddl1 = 'create table %sbooze (name varchar(20))' % table_prefix + ddl2 = 'create table %sbarflys (name varchar(20))' % table_prefix + xddl1 = 'drop table %sbooze' % table_prefix + xddl2 = 'drop table %sbarflys' % table_prefix + + lowerfunc = 'lower' # Name of stored procedure to convert string->lowercase + + # Some drivers may need to override these helpers, for example adding + # a 'commit' after the execute. + def executeDDL1(self,cursor): + cursor.execute(self.ddl1) + + def executeDDL2(self,cursor): + cursor.execute(self.ddl2) + + def setUp(self): + ''' self.drivers should override this method to perform required setup + if any is necessary, such as creating the database. + ''' + pass + + def tearDown(self): + ''' self.drivers should override this method to perform required cleanup + if any is necessary, such as deleting the test database. + The default drops the tables that may be created. + ''' + con = self._connect() + try: + cur = con.cursor() + for ddl in (self.xddl1,self.xddl2): + try: + cur.execute(ddl) + con.commit() + except self.driver.Error: + # Assume table didn't exist. Other tests will check if + # execute is busted. + pass + finally: + con.close() + + def _connect(self): + try: + return self.driver.connect( + *self.connect_args,**self.connect_kw_args + ) + except AttributeError: + self.fail("No connect method found in self.driver module") + + def test_connect(self): + con = self._connect() + con.close() + + def test_apilevel(self): + try: + # Must exist + apilevel = self.driver.apilevel + # Must equal 2.0 + self.assertEqual(apilevel,'2.0') + except AttributeError: + self.fail("Driver doesn't define apilevel") + + def test_threadsafety(self): + try: + # Must exist + threadsafety = self.driver.threadsafety + # Must be a valid value + self.assertTrue(threadsafety in (0,1,2,3)) + except AttributeError: + self.fail("Driver doesn't define threadsafety") + + def test_paramstyle(self): + try: + # Must exist + paramstyle = self.driver.paramstyle + # Must be a valid value + self.assertTrue(paramstyle in ( + 'qmark','numeric','named','format','pyformat' + )) + except AttributeError: + self.fail("Driver doesn't define paramstyle") + + def test_Exceptions(self): + # Make sure required exceptions exist, and are in the + # defined heirarchy. + self.assertTrue(issubclass(self.driver.Warning,Exception)) + self.assertTrue(issubclass(self.driver.Error,Exception)) + self.assertTrue( + issubclass(self.driver.InterfaceError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.DatabaseError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.OperationalError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.IntegrityError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.InternalError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.ProgrammingError,self.driver.Error) + ) + self.assertTrue( + issubclass(self.driver.NotSupportedError,self.driver.Error) + ) + + def test_ExceptionsAsConnectionAttributes(self): + # OPTIONAL EXTENSION + # Test for the optional DB API 2.0 extension, where the exceptions + # are exposed as attributes on the Connection object + # I figure this optional extension will be implemented by any + # driver author who is using this test suite, so it is enabled + # by default. + con = self._connect() + drv = self.driver + self.assertTrue(con.Warning is drv.Warning) + self.assertTrue(con.Error is drv.Error) + self.assertTrue(con.InterfaceError is drv.InterfaceError) + self.assertTrue(con.DatabaseError is drv.DatabaseError) + self.assertTrue(con.OperationalError is drv.OperationalError) + self.assertTrue(con.IntegrityError is drv.IntegrityError) + self.assertTrue(con.InternalError is drv.InternalError) + self.assertTrue(con.ProgrammingError is drv.ProgrammingError) + self.assertTrue(con.NotSupportedError is drv.NotSupportedError) + + + def test_commit(self): + con = self._connect() + try: + # Commit must work, even if it doesn't do anything + con.commit() + finally: + con.close() + + def test_rollback(self): + con = self._connect() + # If rollback is defined, it should either work or throw + # the documented exception + if hasattr(con,'rollback'): + try: + con.rollback() + except self.driver.NotSupportedError: + pass + + def test_cursor(self): + con = self._connect() + try: + cur = con.cursor() + finally: + con.close() + + def test_cursor_isolation(self): + con = self._connect() + try: + # Make sure cursors created from the same connection have + # the documented transaction isolation level + cur1 = con.cursor() + cur2 = con.cursor() + self.executeDDL1(cur1) + cur1.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) + cur2.execute("select name from %sbooze" % self.table_prefix) + booze = cur2.fetchall() + self.assertEqual(len(booze),1) + self.assertEqual(len(booze[0]),1) + self.assertEqual(booze[0][0],'Victoria Bitter') + finally: + con.close() + + def test_description(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) + self.assertEqual(cur.description,None, + 'cursor.description should be none after executing a ' + 'statement that can return no rows (such as DDL)' + ) + cur.execute('select name from %sbooze' % self.table_prefix) + self.assertEqual(len(cur.description),1, + 'cursor.description describes too many columns' + ) + self.assertEqual(len(cur.description[0]),7, + 'cursor.description[x] tuples must have 7 elements' + ) + self.assertEqual(cur.description[0][0].lower(),'name', + 'cursor.description[x][0] must return column name' + ) + self.assertEqual(cur.description[0][1],self.driver.STRING, + 'cursor.description[x][1] must return column type. Got %r' + % cur.description[0][1] + ) + + # Make sure self.description gets reset + self.executeDDL2(cur) + self.assertEqual(cur.description,None, + 'cursor.description not being set to None when executing ' + 'no-result statements (eg. DDL)' + ) + finally: + con.close() + + def test_rowcount(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) + self.assertEqual(cur.rowcount,-1, + 'cursor.rowcount should be -1 after executing no-result ' + 'statements' + ) + cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) + self.assertTrue(cur.rowcount in (-1,1), + 'cursor.rowcount should == number or rows inserted, or ' + 'set to -1 after executing an insert statement' + ) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertTrue(cur.rowcount in (-1,1), + 'cursor.rowcount should == number of rows returned, or ' + 'set to -1 after executing a select statement' + ) + self.executeDDL2(cur) + self.assertEqual(cur.rowcount,-1, + 'cursor.rowcount not being reset to -1 after executing ' + 'no-result statements' + ) + finally: + con.close() + + lower_func = 'lower' + def test_callproc(self): + con = self._connect() + try: + cur = con.cursor() + if self.lower_func and hasattr(cur,'callproc'): + r = cur.callproc(self.lower_func,('FOO',)) + self.assertEqual(len(r),1) + self.assertEqual(r[0],'FOO') + r = cur.fetchall() + self.assertEqual(len(r),1,'callproc produced no result set') + self.assertEqual(len(r[0]),1, + 'callproc produced invalid result set' + ) + self.assertEqual(r[0][0],'foo', + 'callproc produced invalid results' + ) + finally: + con.close() + + def test_close(self): + con = self._connect() + try: + cur = con.cursor() + finally: + con.close() + + # cursor.execute should raise an Error if called after connection + # closed + self.assertRaises(self.driver.Error,self.executeDDL1,cur) + + # connection.commit should raise an Error if called after connection' + # closed.' + self.assertRaises(self.driver.Error,con.commit) + + # connection.close should raise an Error if called more than once + self.assertRaises(self.driver.Error,con.close) + + def test_execute(self): + con = self._connect() + try: + cur = con.cursor() + self._paraminsert(cur) + finally: + con.close() + + def _paraminsert(self,cur): + self.executeDDL1(cur) + cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) + self.assertTrue(cur.rowcount in (-1,1)) + + if self.driver.paramstyle == 'qmark': + cur.execute( + 'insert into %sbooze values (?)' % self.table_prefix, + ("Cooper's",) + ) + elif self.driver.paramstyle == 'numeric': + cur.execute( + 'insert into %sbooze values (:1)' % self.table_prefix, + ("Cooper's",) + ) + elif self.driver.paramstyle == 'named': + cur.execute( + 'insert into %sbooze values (:beer)' % self.table_prefix, + {'beer':"Cooper's"} + ) + elif self.driver.paramstyle == 'format': + cur.execute( + 'insert into %sbooze values (%%s)' % self.table_prefix, + ("Cooper's",) + ) + elif self.driver.paramstyle == 'pyformat': + cur.execute( + 'insert into %sbooze values (%%(beer)s)' % self.table_prefix, + {'beer':"Cooper's"} + ) + else: + self.fail('Invalid paramstyle') + self.assertTrue(cur.rowcount in (-1,1)) + + cur.execute('select name from %sbooze' % self.table_prefix) + res = cur.fetchall() + self.assertEqual(len(res),2,'cursor.fetchall returned too few rows') + beers = [res[0][0],res[1][0]] + beers.sort() + self.assertEqual(beers[0],"Cooper's", + 'cursor.fetchall retrieved incorrect data, or data inserted ' + 'incorrectly' + ) + self.assertEqual(beers[1],"Victoria Bitter", + 'cursor.fetchall retrieved incorrect data, or data inserted ' + 'incorrectly' + ) + + def test_executemany(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) + largs = [ ("Cooper's",) , ("Boag's",) ] + margs = [ {'beer': "Cooper's"}, {'beer': "Boag's"} ] + if self.driver.paramstyle == 'qmark': + cur.executemany( + 'insert into %sbooze values (?)' % self.table_prefix, + largs + ) + elif self.driver.paramstyle == 'numeric': + cur.executemany( + 'insert into %sbooze values (:1)' % self.table_prefix, + largs + ) + elif self.driver.paramstyle == 'named': + cur.executemany( + 'insert into %sbooze values (:beer)' % self.table_prefix, + margs + ) + elif self.driver.paramstyle == 'format': + cur.executemany( + 'insert into %sbooze values (%%s)' % self.table_prefix, + largs + ) + elif self.driver.paramstyle == 'pyformat': + cur.executemany( + 'insert into %sbooze values (%%(beer)s)' % ( + self.table_prefix + ), + margs + ) + else: + self.fail('Unknown paramstyle') + self.assertTrue(cur.rowcount in (-1,2), + 'insert using cursor.executemany set cursor.rowcount to ' + 'incorrect value %r' % cur.rowcount + ) + cur.execute('select name from %sbooze' % self.table_prefix) + res = cur.fetchall() + self.assertEqual(len(res),2, + 'cursor.fetchall retrieved incorrect number of rows' + ) + beers = [res[0][0],res[1][0]] + beers.sort() + self.assertEqual(beers[0],"Boag's",'incorrect data retrieved') + self.assertEqual(beers[1],"Cooper's",'incorrect data retrieved') + finally: + con.close() + + def test_fetchone(self): + con = self._connect() + try: + cur = con.cursor() + + # cursor.fetchone should raise an Error if called before + # executing a select-type query + self.assertRaises(self.driver.Error,cur.fetchone) + + # cursor.fetchone should raise an Error if called after + # executing a query that cannnot return rows + self.executeDDL1(cur) + self.assertRaises(self.driver.Error,cur.fetchone) + + cur.execute('select name from %sbooze' % self.table_prefix) + self.assertEqual(cur.fetchone(),None, + 'cursor.fetchone should return None if a query retrieves ' + 'no rows' + ) + self.assertTrue(cur.rowcount in (-1,0)) + + # cursor.fetchone should raise an Error if called after + # executing a query that cannnot return rows + cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) + self.assertRaises(self.driver.Error,cur.fetchone) + + cur.execute('select name from %sbooze' % self.table_prefix) + r = cur.fetchone() + self.assertEqual(len(r),1, + 'cursor.fetchone should have retrieved a single row' + ) + self.assertEqual(r[0],'Victoria Bitter', + 'cursor.fetchone retrieved incorrect data' + ) + self.assertEqual(cur.fetchone(),None, + 'cursor.fetchone should return None if no more rows available' + ) + self.assertTrue(cur.rowcount in (-1,1)) + finally: + con.close() + + samples = [ + 'Carlton Cold', + 'Carlton Draft', + 'Mountain Goat', + 'Redback', + 'Victoria Bitter', + 'XXXX' + ] + + def _populate(self): + ''' Return a list of sql commands to setup the DB for the fetch + tests. + ''' + populate = [ + "insert into %sbooze values ('%s')" % (self.table_prefix,s) + for s in self.samples + ] + return populate + + def test_fetchmany(self): + con = self._connect() + try: + cur = con.cursor() + + # cursor.fetchmany should raise an Error if called without + #issuing a query + self.assertRaises(self.driver.Error,cur.fetchmany,4) + + self.executeDDL1(cur) + for sql in self._populate(): + cur.execute(sql) + + cur.execute('select name from %sbooze' % self.table_prefix) + r = cur.fetchmany() + self.assertEqual(len(r),1, + 'cursor.fetchmany retrieved incorrect number of rows, ' + 'default of arraysize is one.' + ) + cur.arraysize=10 + r = cur.fetchmany(3) # Should get 3 rows + self.assertEqual(len(r),3, + 'cursor.fetchmany retrieved incorrect number of rows' + ) + r = cur.fetchmany(4) # Should get 2 more + self.assertEqual(len(r),2, + 'cursor.fetchmany retrieved incorrect number of rows' + ) + r = cur.fetchmany(4) # Should be an empty sequence + self.assertEqual(len(r),0, + 'cursor.fetchmany should return an empty sequence after ' + 'results are exhausted' + ) + self.assertTrue(cur.rowcount in (-1,6)) + + # Same as above, using cursor.arraysize + cur.arraysize=4 + cur.execute('select name from %sbooze' % self.table_prefix) + r = cur.fetchmany() # Should get 4 rows + self.assertEqual(len(r),4, + 'cursor.arraysize not being honoured by fetchmany' + ) + r = cur.fetchmany() # Should get 2 more + self.assertEqual(len(r),2) + r = cur.fetchmany() # Should be an empty sequence + self.assertEqual(len(r),0) + self.assertTrue(cur.rowcount in (-1,6)) + + cur.arraysize=6 + cur.execute('select name from %sbooze' % self.table_prefix) + rows = cur.fetchmany() # Should get all rows + self.assertTrue(cur.rowcount in (-1,6)) + self.assertEqual(len(rows),6) + self.assertEqual(len(rows),6) + rows = [r[0] for r in rows] + rows.sort() + + # Make sure we get the right data back out + for i in range(0,6): + self.assertEqual(rows[i],self.samples[i], + 'incorrect data retrieved by cursor.fetchmany' + ) + + rows = cur.fetchmany() # Should return an empty list + self.assertEqual(len(rows),0, + 'cursor.fetchmany should return an empty sequence if ' + 'called after the whole result set has been fetched' + ) + self.assertTrue(cur.rowcount in (-1,6)) + + self.executeDDL2(cur) + cur.execute('select name from %sbarflys' % self.table_prefix) + r = cur.fetchmany() # Should get empty sequence + self.assertEqual(len(r),0, + 'cursor.fetchmany should return an empty sequence if ' + 'query retrieved no rows' + ) + self.assertTrue(cur.rowcount in (-1,0)) + + finally: + con.close() + + def test_fetchall(self): + con = self._connect() + try: + cur = con.cursor() + # cursor.fetchall should raise an Error if called + # without executing a query that may return rows (such + # as a select) + self.assertRaises(self.driver.Error, cur.fetchall) + + self.executeDDL1(cur) + for sql in self._populate(): + cur.execute(sql) + + # cursor.fetchall should raise an Error if called + # after executing a a statement that cannot return rows + self.assertRaises(self.driver.Error,cur.fetchall) + + cur.execute('select name from %sbooze' % self.table_prefix) + rows = cur.fetchall() + self.assertTrue(cur.rowcount in (-1,len(self.samples))) + self.assertEqual(len(rows),len(self.samples), + 'cursor.fetchall did not retrieve all rows' + ) + rows = [r[0] for r in rows] + rows.sort() + for i in range(0,len(self.samples)): + self.assertEqual(rows[i],self.samples[i], + 'cursor.fetchall retrieved incorrect rows' + ) + rows = cur.fetchall() + self.assertEqual( + len(rows),0, + 'cursor.fetchall should return an empty list if called ' + 'after the whole result set has been fetched' + ) + self.assertTrue(cur.rowcount in (-1,len(self.samples))) + + self.executeDDL2(cur) + cur.execute('select name from %sbarflys' % self.table_prefix) + rows = cur.fetchall() + self.assertTrue(cur.rowcount in (-1,0)) + self.assertEqual(len(rows),0, + 'cursor.fetchall should return an empty list if ' + 'a select query returns no rows' + ) + + finally: + con.close() + + def test_mixedfetch(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) + for sql in self._populate(): + cur.execute(sql) + + cur.execute('select name from %sbooze' % self.table_prefix) + rows1 = cur.fetchone() + rows23 = cur.fetchmany(2) + rows4 = cur.fetchone() + rows56 = cur.fetchall() + self.assertTrue(cur.rowcount in (-1,6)) + self.assertEqual(len(rows23),2, + 'fetchmany returned incorrect number of rows' + ) + self.assertEqual(len(rows56),2, + 'fetchall returned incorrect number of rows' + ) + + rows = [rows1[0]] + rows.extend([rows23[0][0],rows23[1][0]]) + rows.append(rows4[0]) + rows.extend([rows56[0][0],rows56[1][0]]) + rows.sort() + for i in range(0,len(self.samples)): + self.assertEqual(rows[i],self.samples[i], + 'incorrect data retrieved or inserted' + ) + finally: + con.close() + + def help_nextset_setUp(self,cur): + ''' Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + ''' + raise NotImplementedError('Helper not implemented') + #sql=""" + # create procedure deleteme as + # begin + # select count(*) from booze + # select name from booze + # end + #""" + #cur.execute(sql) + + def help_nextset_tearDown(self,cur): + 'If cleaning up is needed after nextSetTest' + raise NotImplementedError('Helper not implemented') + #cur.execute("drop procedure deleteme") + + def test_nextset(self): + con = self._connect() + try: + cur = con.cursor() + if not hasattr(cur,'nextset'): + return + + try: + self.executeDDL1(cur) + sql=self._populate() + for sql in self._populate(): + cur.execute(sql) + + self.help_nextset_setUp(cur) + + cur.callproc('deleteme') + numberofrows=cur.fetchone() + assert numberofrows[0]== len(self.samples) + assert cur.nextset() + names=cur.fetchall() + assert len(names) == len(self.samples) + s=cur.nextset() + assert s == None,'No more return sets, should return None' + finally: + self.help_nextset_tearDown(cur) + + finally: + con.close() + + def test_nextset(self): + raise NotImplementedError('Drivers need to override this test') + + def test_arraysize(self): + # Not much here - rest of the tests for this are in test_fetchmany + con = self._connect() + try: + cur = con.cursor() + self.assertTrue(hasattr(cur,'arraysize'), + 'cursor.arraysize must be defined' + ) + finally: + con.close() + + def test_setinputsizes(self): + con = self._connect() + try: + cur = con.cursor() + cur.setinputsizes( (25,) ) + self._paraminsert(cur) # Make sure cursor still works + finally: + con.close() + + def test_setoutputsize_basic(self): + # Basic test is to make sure setoutputsize doesn't blow up + con = self._connect() + try: + cur = con.cursor() + cur.setoutputsize(1000) + cur.setoutputsize(2000,0) + self._paraminsert(cur) # Make sure the cursor still works + finally: + con.close() + + def test_setoutputsize(self): + # Real test for setoutputsize is driver dependant + raise NotImplementedError('Driver need to override this test') + + def test_None(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) + cur.execute('insert into %sbooze values (NULL)' % self.table_prefix) + cur.execute('select name from %sbooze' % self.table_prefix) + r = cur.fetchall() + self.assertEqual(len(r),1) + self.assertEqual(len(r[0]),1) + self.assertEqual(r[0][0],None,'NULL value not returned as None') + finally: + con.close() + + def test_Date(self): + d1 = self.driver.Date(2002,12,25) + d2 = self.driver.DateFromTicks(time.mktime((2002,12,25,0,0,0,0,0,0))) + # Can we assume this? API doesn't specify, but it seems implied + # self.assertEqual(str(d1),str(d2)) + + def test_Time(self): + t1 = self.driver.Time(13,45,30) + t2 = self.driver.TimeFromTicks(time.mktime((2001,1,1,13,45,30,0,0,0))) + # Can we assume this? API doesn't specify, but it seems implied + # self.assertEqual(str(t1),str(t2)) + + def test_Timestamp(self): + t1 = self.driver.Timestamp(2002,12,25,13,45,30) + t2 = self.driver.TimestampFromTicks( + time.mktime((2002,12,25,13,45,30,0,0,0)) + ) + # Can we assume this? API doesn't specify, but it seems implied + # self.assertEqual(str(t1),str(t2)) + + def test_Binary(self): + b = self.driver.Binary(b'Something') + b = self.driver.Binary(b'') + + def test_STRING(self): + self.assertTrue(hasattr(self.driver,'STRING'), + 'module.STRING must be defined' + ) + + def test_BINARY(self): + self.assertTrue(hasattr(self.driver,'BINARY'), + 'module.BINARY must be defined.' + ) + + def test_NUMBER(self): + self.assertTrue(hasattr(self.driver,'NUMBER'), + 'module.NUMBER must be defined.' + ) + + def test_DATETIME(self): + self.assertTrue(hasattr(self.driver,'DATETIME'), + 'module.DATETIME must be defined.' + ) + + def test_ROWID(self): + self.assertTrue(hasattr(self.driver,'ROWID'), + 'module.ROWID must be defined.' + ) diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py new file mode 100755 index 00000000..9cacbbd3 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +from . import capabilities +try: + import unittest2 as unittest +except ImportError: + import unittest +import pymysql +from pymysql.tests import base +import warnings + +warnings.filterwarnings('error') + +class test_MySQLdb(capabilities.DatabaseTest): + + db_module = pymysql + connect_args = () + connect_kwargs = base.PyMySQLTestCase.databases[0].copy() + connect_kwargs.update(dict(read_default_file='~/.my.cnf', + use_unicode=True, + charset='utf8', sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")) + + create_table_extra = "ENGINE=INNODB CHARACTER SET UTF8" + leak_test = False + + def quote_identifier(self, ident): + return "`%s`" % ident + + def test_TIME(self): + from datetime import timedelta + def generator(row,col): + return timedelta(0, row*8000) + self.check_data_integrity( + ('col1 TIME',), + generator) + + def test_TINYINT(self): + # Number data + def generator(row,col): + v = (row*row) % 256 + if v > 127: + v = v-256 + return v + self.check_data_integrity( + ('col1 TINYINT',), + generator) + + def test_stored_procedures(self): + db = self.connection + c = self.cursor + try: + self.create_table(('pos INT', 'tree CHAR(20)')) + c.executemany("INSERT INTO %s (pos,tree) VALUES (%%s,%%s)" % self.table, + list(enumerate('ash birch cedar larch pine'.split()))) + db.commit() + + c.execute(""" + CREATE PROCEDURE test_sp(IN t VARCHAR(255)) + BEGIN + SELECT pos FROM %s WHERE tree = t; + END + """ % self.table) + db.commit() + + c.callproc('test_sp', ('larch',)) + rows = c.fetchall() + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], 3) + c.nextset() + finally: + c.execute("DROP PROCEDURE IF EXISTS test_sp") + c.execute('drop table %s' % (self.table)) + + def test_small_CHAR(self): + # Character data + def generator(row,col): + i = ((row+1)*(col+1)+62)%256 + if i == 62: return '' + if i == 63: return None + return chr(i) + self.check_data_integrity( + ('col1 char(1)','col2 char(1)'), + generator) + + def test_bug_2671682(self): + from pymysql.constants import ER + try: + self.cursor.execute("describe some_non_existent_table"); + except self.connection.ProgrammingError as msg: + self.assertEqual(msg.args[0], ER.NO_SUCH_TABLE) + + def test_ping(self): + self.connection.ping() + + def test_literal_int(self): + self.assertTrue("2" == self.connection.literal(2)) + + def test_literal_float(self): + self.assertTrue("3.1415" == self.connection.literal(3.1415)) + + def test_literal_string(self): + self.assertTrue("'foo'" == self.connection.literal("foo")) + + +if __name__ == '__main__': + if test_MySQLdb.leak_test: + import gc + gc.enable() + gc.set_debug(gc.DEBUG_LEAK) + unittest.main() diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py new file mode 100755 index 00000000..ca430fd0 --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +from . import dbapi20 +import pymysql +from pymysql.tests import base + +try: + import unittest2 as unittest +except ImportError: + import unittest + + +class test_MySQLdb(dbapi20.DatabaseAPI20Test): + driver = pymysql + connect_args = () + connect_kw_args = base.PyMySQLTestCase.databases[0].copy() + connect_kw_args.update(dict(read_default_file='~/.my.cnf', + charset='utf8', + sql_mode="ANSI,STRICT_TRANS_TABLES,TRADITIONAL")) + + def test_setoutputsize(self): pass + def test_setoutputsize_basic(self): pass + def test_nextset(self): pass + + """The tests on fetchone and fetchall and rowcount bogusly + test for an exception if the statement cannot return a + result set. MySQL always returns a result set; it's just that + some things return empty result sets.""" + + def test_fetchall(self): + con = self._connect() + try: + cur = con.cursor() + # cursor.fetchall should raise an Error if called + # without executing a query that may return rows (such + # as a select) + self.assertRaises(self.driver.Error, cur.fetchall) + + self.executeDDL1(cur) + for sql in self._populate(): + cur.execute(sql) + + # cursor.fetchall should raise an Error if called + # after executing a a statement that cannot return rows +## self.assertRaises(self.driver.Error,cur.fetchall) + + cur.execute('select name from %sbooze' % self.table_prefix) + rows = cur.fetchall() + self.assertTrue(cur.rowcount in (-1,len(self.samples))) + self.assertEqual(len(rows),len(self.samples), + 'cursor.fetchall did not retrieve all rows' + ) + rows = [r[0] for r in rows] + rows.sort() + for i in range(0,len(self.samples)): + self.assertEqual(rows[i],self.samples[i], + 'cursor.fetchall retrieved incorrect rows' + ) + rows = cur.fetchall() + self.assertEqual( + len(rows),0, + 'cursor.fetchall should return an empty list if called ' + 'after the whole result set has been fetched' + ) + self.assertTrue(cur.rowcount in (-1,len(self.samples))) + + self.executeDDL2(cur) + cur.execute('select name from %sbarflys' % self.table_prefix) + rows = cur.fetchall() + self.assertTrue(cur.rowcount in (-1,0)) + self.assertEqual(len(rows),0, + 'cursor.fetchall should return an empty list if ' + 'a select query returns no rows' + ) + + finally: + con.close() + + def test_fetchone(self): + con = self._connect() + try: + cur = con.cursor() + + # cursor.fetchone should raise an Error if called before + # executing a select-type query + self.assertRaises(self.driver.Error,cur.fetchone) + + # cursor.fetchone should raise an Error if called after + # executing a query that cannnot return rows + self.executeDDL1(cur) +## self.assertRaises(self.driver.Error,cur.fetchone) + + cur.execute('select name from %sbooze' % self.table_prefix) + self.assertEqual(cur.fetchone(),None, + 'cursor.fetchone should return None if a query retrieves ' + 'no rows' + ) + self.assertTrue(cur.rowcount in (-1,0)) + + # cursor.fetchone should raise an Error if called after + # executing a query that cannnot return rows + cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) +## self.assertRaises(self.driver.Error,cur.fetchone) + + cur.execute('select name from %sbooze' % self.table_prefix) + r = cur.fetchone() + self.assertEqual(len(r),1, + 'cursor.fetchone should have retrieved a single row' + ) + self.assertEqual(r[0],'Victoria Bitter', + 'cursor.fetchone retrieved incorrect data' + ) +## self.assertEqual(cur.fetchone(),None, +## 'cursor.fetchone should return None if no more rows available' +## ) + self.assertTrue(cur.rowcount in (-1,1)) + finally: + con.close() + + # Same complaint as for fetchall and fetchone + def test_rowcount(self): + con = self._connect() + try: + cur = con.cursor() + self.executeDDL1(cur) +## self.assertEqual(cur.rowcount,-1, +## 'cursor.rowcount should be -1 after executing no-result ' +## 'statements' +## ) + cur.execute("insert into %sbooze values ('Victoria Bitter')" % ( + self.table_prefix + )) +## self.assertTrue(cur.rowcount in (-1,1), +## 'cursor.rowcount should == number or rows inserted, or ' +## 'set to -1 after executing an insert statement' +## ) + cur.execute("select name from %sbooze" % self.table_prefix) + self.assertTrue(cur.rowcount in (-1,1), + 'cursor.rowcount should == number of rows returned, or ' + 'set to -1 after executing a select statement' + ) + self.executeDDL2(cur) +## self.assertEqual(cur.rowcount,-1, +## 'cursor.rowcount not being reset to -1 after executing ' +## 'no-result statements' +## ) + finally: + con.close() + + def test_callproc(self): + pass # performed in test_MySQL_capabilities + + def help_nextset_setUp(self,cur): + ''' Should create a procedure called deleteme + that returns two result sets, first the + number of rows in booze then "name from booze" + ''' + sql=""" + create procedure deleteme() + begin + select count(*) from %(tp)sbooze; + select name from %(tp)sbooze; + end + """ % dict(tp=self.table_prefix) + cur.execute(sql) + + def help_nextset_tearDown(self,cur): + 'If cleaning up is needed after nextSetTest' + cur.execute("drop procedure deleteme") + + def test_nextset(self): + from warnings import warn + con = self._connect() + try: + cur = con.cursor() + if not hasattr(cur,'nextset'): + return + + try: + self.executeDDL1(cur) + sql=self._populate() + for sql in self._populate(): + cur.execute(sql) + + self.help_nextset_setUp(cur) + + cur.callproc('deleteme') + numberofrows=cur.fetchone() + assert numberofrows[0]== len(self.samples) + assert cur.nextset() + names=cur.fetchall() + assert len(names) == len(self.samples) + s=cur.nextset() + if s: + empty = cur.fetchall() + self.assertEqual(len(empty), 0, + "non-empty result set after other result sets") + #warn("Incompatibility: MySQL returns an empty result set for the CALL itself", + # Warning) + #assert s == None,'No more return sets, should return None' + finally: + self.help_nextset_tearDown(cur) + + finally: + con.close() + + +if __name__ == '__main__': + unittest.main() diff --git a/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py new file mode 100755 index 00000000..17fc2cde --- /dev/null +++ b/gluon/contrib/pymysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py @@ -0,0 +1,101 @@ +import sys +try: + import unittest2 as unittest +except ImportError: + import unittest + +import pymysql +_mysql = pymysql +from pymysql.constants import FIELD_TYPE +from pymysql.tests import base +from pymysql._compat import PY2, long_type + +if not PY2: + basestring = str + + +class TestDBAPISet(unittest.TestCase): + def test_set_equality(self): + self.assertTrue(pymysql.STRING == pymysql.STRING) + + def test_set_inequality(self): + self.assertTrue(pymysql.STRING != pymysql.NUMBER) + + def test_set_equality_membership(self): + self.assertTrue(FIELD_TYPE.VAR_STRING == pymysql.STRING) + + def test_set_inequality_membership(self): + self.assertTrue(FIELD_TYPE.DATE != pymysql.STRING) + + +class CoreModule(unittest.TestCase): + """Core _mysql module features.""" + + def test_NULL(self): + """Should have a NULL constant.""" + self.assertEqual(_mysql.NULL, 'NULL') + + def test_version(self): + """Version information sanity.""" + self.assertTrue(isinstance(_mysql.__version__, basestring)) + + self.assertTrue(isinstance(_mysql.version_info, tuple)) + self.assertEqual(len(_mysql.version_info), 5) + + def test_client_info(self): + self.assertTrue(isinstance(_mysql.get_client_info(), basestring)) + + def test_thread_safe(self): + self.assertTrue(isinstance(_mysql.thread_safe(), int)) + + +class CoreAPI(unittest.TestCase): + """Test _mysql interaction internals.""" + + def setUp(self): + kwargs = base.PyMySQLTestCase.databases[0].copy() + kwargs["read_default_file"] = "~/.my.cnf" + self.conn = _mysql.connect(**kwargs) + + def tearDown(self): + self.conn.close() + + def test_thread_id(self): + tid = self.conn.thread_id() + self.assertTrue(isinstance(tid, (int, long_type)), + "thread_id didn't return an integral value.") + + self.assertRaises(TypeError, self.conn.thread_id, ('evil',), + "thread_id shouldn't accept arguments.") + + def test_affected_rows(self): + self.assertEqual(self.conn.affected_rows(), 0, + "Should return 0 before we do anything.") + + + #def test_debug(self): + ## FIXME Only actually tests if you lack SUPER + #self.assertRaises(pymysql.OperationalError, + #self.conn.dump_debug_info) + + def test_charset_name(self): + self.assertTrue(isinstance(self.conn.character_set_name(), basestring), + "Should return a string.") + + def test_host_info(self): + assert isinstance(self.conn.get_host_info(), basestring), "should return a string" + + def test_proto_info(self): + self.assertTrue(isinstance(self.conn.get_proto_info(), int), + "Should return an int.") + + def test_server_info(self): + if sys.version_info[0] == 2: + self.assertTrue(isinstance(self.conn.get_server_info(), basestring), + "Should return an str.") + else: + self.assertTrue(isinstance(self.conn.get_server_info(), basestring), + "Should return an str.") + +if __name__ == "__main__": + unittest.main() diff --git a/gluon/contrib/pymysql/times.py b/gluon/contrib/pymysql/times.py index c47db09e..4497dacf 100644 --- a/gluon/contrib/pymysql/times.py +++ b/gluon/contrib/pymysql/times.py @@ -1,16 +1,20 @@ from time import localtime from datetime import date, datetime, time, timedelta + Date = date Time = time TimeDelta = timedelta Timestamp = datetime + def DateFromTicks(ticks): return date(*localtime(ticks)[:3]) + def TimeFromTicks(ticks): return time(*localtime(ticks)[3:6]) + def TimestampFromTicks(ticks): return datetime(*localtime(ticks)[:6]) diff --git a/gluon/contrib/pymysql/util.py b/gluon/contrib/pymysql/util.py index cc622e57..3e82ac7b 100644 --- a/gluon/contrib/pymysql/util.py +++ b/gluon/contrib/pymysql/util.py @@ -1,14 +1,17 @@ import struct + def byte2int(b): if isinstance(b, int): return b else: return struct.unpack("!B", b)[0] + def int2byte(i): return struct.pack("!B", i) + def join_bytes(bs): if len(bs) == 0: return "" From 9539cc7542b2e1f09dc84feec5d840be9b0c21ce Mon Sep 17 00:00:00 2001 From: niphlod Date: Sat, 1 Oct 2016 00:15:47 +0200 Subject: [PATCH 131/436] make pymysql usable inside web2py --- CHANGELOG | 1 + gluon/contrib/pymysql/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e4701ea0..22406bdd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ "from gluon.storage import Storage" - tests can only be run with the usual web2py.py --run_system_tests OR with python -m unittest -v gluon.tests on the root dir +- updated pymysql driver ## 2.14.6 diff --git a/gluon/contrib/pymysql/__init__.py b/gluon/contrib/pymysql/__init__.py index bf34c558..7bf34c1c 100644 --- a/gluon/contrib/pymysql/__init__.py +++ b/gluon/contrib/pymysql/__init__.py @@ -89,7 +89,7 @@ def Connect(*args, **kwargs): from .connections import Connection return Connection(*args, **kwargs) -from pymysql import connections as _orig_conn +from . import connections as _orig_conn if _orig_conn.Connection.__init__.__doc__ is not None: Connect.__doc__ = _orig_conn.Connection.__init__.__doc__ del _orig_conn From 9256d67ec7e11fc8ba9ad74a0933273b95b7ab3a Mon Sep 17 00:00:00 2001 From: niphlod Date: Sat, 1 Oct 2016 11:25:52 +0200 Subject: [PATCH 132/436] remove pymysql dependency --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9f276d6f..2907e30e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ install: virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000 pymysql; fi; + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000; fi; - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; before_script: From b7219ba2fd524a67e2b5a43d18d2b6da49ee6a7f Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 3 Oct 2016 21:09:27 +0200 Subject: [PATCH 133/436] fixes #1484, thanks @abastardi --- gluon/scheduler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/scheduler.py b/gluon/scheduler.py index a68152b5..e758c379 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -477,7 +477,8 @@ def executor(queue, task, out): # Get controller-specific subdirectory if task.app is of # form 'app/controller' (a, c, f) = parse_path_info(task.app) - _env = env(a=a, c=c, import_models=True) + _env = env(a=a, c=c, import_models=True, + extra_request={'is_scheduler': True}) logging.getLogger().setLevel(level) f = task.function functions = current._scheduler.tasks From 15769857cb576f59dc68174f98db780a87790d71 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 3 Oct 2016 22:10:20 +0200 Subject: [PATCH 134/436] 1st trial --- .travis.yml | 19 +- gluon/contrib/pg8000/__init__.py | 374 ++---------------- gluon/contrib/pg8000/_version.py | 391 ++++++++++++++++--- gluon/contrib/pg8000/core.py | 643 +++++++++++++++++++++++-------- gluon/contrib/pg8000/six.py | 504 ++++++++++++++++++------ 5 files changed, 1217 insertions(+), 714 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2907e30e..18bb51e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,7 @@ language: python sudo: false -cache: - directories: - - $HOME/.pip-cache/ +cache: pip python: - '2.7' @@ -12,20 +10,7 @@ python: - '3.5' install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - pushd "$PYENV_ROOT" && git pull && popd - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="5.0.1" - "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION" - virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" - source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" - fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000; fi; + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto; fi; - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; before_script: diff --git a/gluon/contrib/pg8000/__init__.py b/gluon/contrib/pg8000/__init__.py index c30a8cb6..4a997174 100644 --- a/gluon/contrib/pg8000/__init__.py +++ b/gluon/contrib/pg8000/__init__.py @@ -1,3 +1,14 @@ +from .core import ( + Warning, Bytea, DataError, DatabaseError, InterfaceError, ProgrammingError, + Error, OperationalError, IntegrityError, InternalError, NotSupportedError, + ArrayContentNotHomogenousError, ArrayContentEmptyError, + ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, utc, + Connection, Cursor, Binary, Date, DateFromTicks, Time, TimeFromTicks, + Timestamp, TimestampFromTicks, BINARY, Interval) +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + # Copyright (c) 2007-2009, Mathieu Fenniak # All rights reserved. # @@ -28,258 +39,9 @@ __author__ = "Mathieu Fenniak" -exec("from struct import Struct") -for fmt in ( - "i", "h", "q", "d", "f", "iii", "ii", "qii", "dii", "ihihih", "ci", - "bh", "cccc"): - exec(fmt + "_struct = Struct('!" + fmt + "')") - exec(fmt + "_unpack = " + fmt + "_struct.unpack_from") - exec(fmt + "_pack = " + fmt + "_struct.pack") - -import datetime -import time -from .six import binary_type, integer_types, PY2 - -min_int2, max_int2 = -2 ** 15, 2 ** 15 -min_int4, max_int4 = -2 ** 31, 2 ** 31 -min_int8, max_int8 = -2 ** 63, 2 ** 63 - - -class Warning(Exception): - """Generic exception raised for important database warnings like data - truncations. This exception is not currently used by pg8000. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class Error(Exception): - """Generic exception that is the base exception of all other error - exceptions. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class InterfaceError(Error): - """Generic exception raised for errors that are related to the database - interface rather than the database itself. For example, if the interface - attempts to use an SSL connection but the server refuses, an InterfaceError - will be raised. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class DatabaseError(Error): - """Generic exception raised for errors that are related to the database. - This exception is currently never raised by pg8000. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class DataError(DatabaseError): - """Generic exception raised for errors that are due to problems with the - processed data. This exception is not currently raised by pg8000. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class OperationalError(DatabaseError): - """ - Generic exception raised for errors that are related to the database's - operation and not necessarily under the control of the programmer. This - exception is currently never raised by pg8000. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class IntegrityError(DatabaseError): - """ - Generic exception raised when the relational integrity of the database is - affected. This exception is not currently raised by pg8000. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class InternalError(DatabaseError): - """Generic exception raised when the database encounters an internal error. - This is currently only raised when unexpected state occurs in the pg8000 - interface itself, and is typically the result of a interface bug. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class ProgrammingError(DatabaseError): - """Generic exception raised for programming errors. For example, this - exception is raised if more parameter fields are in a query string than - there are available parameters. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class NotSupportedError(DatabaseError): - """Generic exception raised in case a method or database API was used which - is not supported by the database. - - This exception is part of the `DBAPI 2.0 specification - `_. - """ - pass - - -class ArrayContentNotSupportedError(NotSupportedError): - """ - Raised when attempting to transmit an array where the base type is not - supported for binary data transfer by the interface. - """ - pass - - -class ArrayContentNotHomogenousError(ProgrammingError): - """ - Raised when attempting to transmit an array that doesn't contain only a - single type of object. - """ - pass - - -class ArrayContentEmptyError(ProgrammingError): - """Raised when attempting to transmit an empty array. The type oid of an - empty array cannot be determined, and so sending them is not permitted. - """ - pass - - -class ArrayDimensionsNotConsistentError(ProgrammingError): - """ - Raised when attempting to transmit an array that has inconsistent - multi-dimension sizes. - """ - pass - - -class Bytea(binary_type): - """Bytea is a str-derived class that is mapped to a PostgreSQL byte array. - This class is only used in Python 2, the built-in ``bytes`` type is used in - Python 3. - """ - pass - - -class Interval(object): - """An Interval represents a measurement of time. In PostgreSQL, an interval - is defined in the measure of months, days, and microseconds; as such, the - pg8000 interval type represents the same information. - - Note that values of the :attr:`microseconds`, :attr:`days` and - :attr:`months` properties are independently measured and cannot be - converted to each other. A month may be 28, 29, 30, or 31 days, and a day - may occasionally be lengthened slightly by a leap second. - - .. attribute:: microseconds - - Measure of microseconds in the interval. - - The microseconds value is constrained to fit into a signed 64-bit - integer. Any attempt to set a value too large or too small will result - in an OverflowError being raised. - - .. attribute:: days - - Measure of days in the interval. - - The days value is constrained to fit into a signed 32-bit integer. - Any attempt to set a value too large or too small will result in an - OverflowError being raised. - - .. attribute:: months - - Measure of months in the interval. - - The months value is constrained to fit into a signed 32-bit integer. - Any attempt to set a value too large or too small will result in an - OverflowError being raised. - """ - - def __init__(self, microseconds=0, days=0, months=0): - self.microseconds = microseconds - self.days = days - self.months = months - - def _setMicroseconds(self, value): - if not isinstance(value, integer_types): - raise TypeError("microseconds must be an integer type") - elif not (min_int8 < value < max_int8): - raise OverflowError( - "microseconds must be representable as a 64-bit integer") - else: - self._microseconds = value - - def _setDays(self, value): - if not isinstance(value, integer_types): - raise TypeError("days must be an integer type") - elif not (min_int4 < value < max_int4): - raise OverflowError( - "days must be representable as a 32-bit integer") - else: - self._days = value - - def _setMonths(self, value): - if not isinstance(value, integer_types): - raise TypeError("months must be an integer type") - elif not (min_int4 < value < max_int4): - raise OverflowError( - "months must be representable as a 32-bit integer") - else: - self._months = value - - microseconds = property(lambda self: self._microseconds, _setMicroseconds) - days = property(lambda self: self._days, _setDays) - months = property(lambda self: self._months, _setMonths) - - def __repr__(self): - return "" % ( - self.months, self.days, self.microseconds) - - def __eq__(self, other): - return other is not None and isinstance(other, Interval) and \ - self.months == other.months and self.days == other.days and \ - self.microseconds == other.microseconds - - def __neq__(self, other): - return not self.__eq__(other) - -from .core import Connection - - def connect( user=None, host='localhost', unix_sock=None, port=5432, database=None, - password=None, ssl=False, **kwargs): + password=None, ssl=False, timeout=None, **kwargs): """Creates a connection to a PostgreSQL database. This function is part of the `DBAPI 2.0 specification @@ -287,9 +49,7 @@ def connect( function are not defined by the specification. :param user: - The username to connect to the PostgreSQL server with. If this is not - provided, pg8000 looks first for the PGUSER then the USER environment - variables. + The username to connect to the PostgreSQL server with. If your server character encoding is not ``ascii`` or ``utf8``, then you need to provide ``user`` as bytes, eg. @@ -325,15 +85,24 @@ def connect( authentication, the connection will fail to open. If this parameter is provided but not requested by the server, no error will occur. + If your server character encoding is not ``ascii`` or ``utf8``, then + you need to provide ``user`` as bytes, eg. + ``"my_password".encode('EUC-JP')``. + :keyword ssl: Use SSL encryption for TCP/IP sockets if ``True``. Defaults to ``False``. + :keyword timeout: + Only used with Python 3, this is the time in seconds before the + connection to the database will time out. The default is ``None`` which + means no timeout. + :rtype: A :class:`Connection` object. """ return Connection( - user, host, unix_sock, port, database, password, ssl) + user, host, unix_sock, port, database, password, ssl, timeout) apilevel = "2.0" """The DBAPI level supported, currently "2.0". @@ -382,10 +151,6 @@ can be changed to any of the following values: STRING = 1043 """String type oid.""" -if PY2: - BINARY = Bytea -else: - BINARY = bytes NUMBER = 1700 """Numeric type oid""" @@ -396,104 +161,15 @@ DATETIME = 1114 ROWID = 26 """ROWID type oid""" - -def Date(year, month, day): - """Constuct an object holding a date value. - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.date` - """ - return datetime.date(year, month, day) - - -def Time(hour, minute, second): - """Construct an object holding a time value. - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.time` - """ - return datetime.time(hour, minute, second) - - -def Timestamp(year, month, day, hour, minute, second): - """Construct an object holding a timestamp value. - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.datetime` - """ - return datetime.datetime(year, month, day, hour, minute, second) - - -def DateFromTicks(ticks): - """Construct an object holding a date value from the given ticks value - (number of seconds since the epoch). - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.date` - """ - return Date(*time.localtime(ticks)[:3]) - - -def TimeFromTicks(ticks): - """Construct an objet holding a time value from the given ticks value - (number of seconds since the epoch). - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.time` - """ - return Time(*time.localtime(ticks)[3:6]) - - -def TimestampFromTicks(ticks): - """Construct an object holding a timestamp value from the given ticks value - (number of seconds since the epoch). - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`datetime.datetime` - """ - return Timestamp(*time.localtime(ticks)[:6]) - - -def Binary(value): - """Construct an object holding binary data. - - This function is part of the `DBAPI 2.0 specification - `_. - - :rtype: :class:`pg8000.types.Bytea` for Python 2, otherwise :class:`bytes` - """ - if PY2: - return Bytea(value) - else: - return value - - -from .core import utc, Cursor - __all__ = [ Warning, Bytea, DataError, DatabaseError, connect, InterfaceError, ProgrammingError, Error, OperationalError, IntegrityError, InternalError, NotSupportedError, ArrayContentNotHomogenousError, ArrayContentEmptyError, ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, utc, - Connection, Cursor] + Connection, Cursor, Binary, Date, DateFromTicks, Time, TimeFromTicks, + Timestamp, TimestampFromTicks, BINARY, Interval] """Version string for pg8000. .. versionadded:: 1.9.11 """ - -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions diff --git a/gluon/contrib/pg8000/_version.py b/gluon/contrib/pg8000/_version.py index 982cf246..5677d1e0 100644 --- a/gluon/contrib/pg8000/_version.py +++ b/gluon/contrib/pg8000/_version.py @@ -6,22 +6,58 @@ # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.12 (https://github.com/warner/python-versioneer) - -# these strings will be replaced by git during git-archive -git_refnames = "$Format:%d$" -git_full = "$Format:%H$" - -# these strings are filled in when 'setup.py versioneer' creates _version.py -tag_prefix = "" -parentdir_prefix = "pg8000-" -versionfile_source = "pg8000/_version.py" +# versioneer-0.15 (https://github.com/warner/python-versioneer) +import errno import os -import sys import re import subprocess -import errno +import sys + + +def get_keywords(): + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = " (tag: 1.10.6)" + git_full = "4098abf6be90683ab10b7b080983ed6f08476485" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + pass + + +def get_config(): + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "pg8000-" + cfg.versionfile_source = "pg8000/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + pass + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + def decorate(f): + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): @@ -29,6 +65,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): p = None for c in commands: try: + dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr @@ -39,7 +76,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): if e.errno == errno.ENOENT: continue if verbose: - print("unable to run %s" % args[0]) + print("unable to run %s" % dispcmd) print(e) return None else: @@ -47,28 +84,30 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): print("unable to find command, tried %s" % (commands,)) return None stdout = p.communicate()[0].strip() - if sys.version >= '3': + if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: - print("unable to run %s (error)" % args[0]) + print("unable to run %s (error)" % dispcmd) return None return stdout -def versions_from_parentdir(parentdir_prefix, root, verbose=False): +def versions_from_parentdir(parentdir_prefix, root, verbose): # Source tarballs conventionally unpack into a directory that includes # both the project name and a version string. dirname = os.path.basename(root) if not dirname.startswith(parentdir_prefix): if verbose: - print( - "guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} +@register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, @@ -92,14 +131,15 @@ def git_get_keywords(versionfile_abs): return keywords -def git_versions_from_keywords(keywords, tag_prefix, verbose=False): +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): if not keywords: - return {} # keyword-finding function failed to find keywords + raise NotThisMethod("no keywords at all, weird") refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") - return {} # unexpanded, so not in an unpacked git-archive tarball + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. @@ -124,18 +164,20 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose=False): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) - return { - "version": r, - "full": keywords["full"].strip()} - # no suitable tags, so we use the full revision id + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: - print("no suitable tags, using full revision id") - return { - "version": keywords["full"].strip(), - "full": keywords["full"].strip()} + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} -def git_versions_from_vcs(tag_prefix, root, verbose=False): +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # this runs 'git' from the root of the source tree. This only gets called # if the git-archive 'subst' keywords were *not* expanded, and # _version.py hasn't already been rewritten with a short version string, @@ -144,52 +186,275 @@ def git_versions_from_vcs(tag_prefix, root, verbose=False): if not os.path.exists(os.path.join(root, ".git")): if verbose: print("no .git in %s" % root) - return {} + raise NotThisMethod("no .git directory") GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"], - cwd=root) - if stdout is None: - return {} - if not stdout.startswith(tag_prefix): - if verbose: - print( - "tag '%s' doesn't start with prefix '%s'" % - (stdout, tag_prefix)) - return {} - tag = stdout[len(tag_prefix):] - stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if stdout is None: - return {} - full = stdout.strip() - if tag.endswith("-dirty"): - full += "-dirty" - return {"version": tag, "full": full} + # if there is a tag, this yields TAG-NUM-gHEX[-dirty] + # if there are no tags, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long"], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces -def get_versions(default={"version": "unknown", "full": ""}, verbose=False): +def plus_or_dot(pieces): + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + # now build up version string, with post-release "local version + # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + # exceptions: + # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + # TAG[.post.devDISTANCE] . No -dirty + + # exceptions: + # 1: no tags. 0.post.devDISTANCE + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that + # .dev0 sorts backwards (a dirty tree will appear "older" than the + # corresponding clean one), but you shouldn't be releasing software with + # -dirty anyways. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. + + # exceptions: + # 1: no tags. 0.postDISTANCE[.dev0] + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty + # --always' + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty + # --always -long'. The distance/hash is unconditional. + + # exceptions: + # 1: no tags. HEX[-dirty] (note: no 'g' prefix) + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. - keywords = {"refnames": git_refnames, "full": git_full} - ver = git_versions_from_keywords(keywords, tag_prefix, verbose) - if ver: - return ver + cfg = get_config() + verbose = cfg.verbose try: - root = os.path.abspath(__file__) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in range(len(versionfile_source.split(os.sep))): + for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: - return default + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} - return (git_versions_from_vcs(tag_prefix, root, verbose) - or versions_from_parentdir(parentdir_prefix, root, verbose) - or default) + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/gluon/contrib/pg8000/core.py b/gluon/contrib/pg8000/core.py index f19481bd..29f88d9a 100644 --- a/gluon/contrib/pg8000/core.py +++ b/gluon/contrib/pg8000/core.py @@ -1,3 +1,23 @@ +import datetime +from datetime import timedelta +from warnings import warn +import socket +import threading +from struct import pack +from hashlib import md5 +from decimal import Decimal +from collections import deque, defaultdict +from itertools import count, islice +from .six.moves import map +from .six import b, PY2, integer_types, next, text_type, u, binary_type +from uuid import UUID +from copy import deepcopy +from calendar import timegm +from distutils.version import LooseVersion +from struct import Struct +import time + + # Copyright (c) 2007-2009, Mathieu Fenniak # All rights reserved. # @@ -27,34 +47,6 @@ __author__ = "Mathieu Fenniak" -import datetime -from datetime import timedelta -from . import ( - Interval, min_int2, max_int2, min_int4, max_int4, min_int8, max_int8, - Bytea, NotSupportedError, ProgrammingError, InternalError, IntegrityError, - OperationalError, DatabaseError, InterfaceError, Error, - ArrayContentNotHomogenousError, ArrayContentEmptyError, - ArrayDimensionsNotConsistentError, ArrayContentNotSupportedError, Warning, - i_unpack, ii_unpack, iii_unpack, h_pack, d_unpack, q_unpack, d_pack, - f_unpack, q_pack, i_pack, h_unpack, dii_unpack, qii_unpack, ci_unpack, - bh_unpack, ihihih_unpack, cccc_unpack, ii_pack, iii_pack, dii_pack, - qii_pack) -from warnings import warn -import socket -import threading -from struct import pack -from hashlib import md5 -from decimal import Decimal -from collections import deque, defaultdict -from itertools import count, islice -from .six.moves import map -from .six import b, PY2, integer_types, next, PRE_26, text_type, u -from sys import exc_info -from uuid import UUID -from copy import deepcopy -from calendar import timegm -import os -from distutils.version import LooseVersion try: from json import loads @@ -78,9 +70,350 @@ class UTC(datetime.tzinfo): utc = UTC() -if PRE_26: - bytearray = list +class Interval(object): + """An Interval represents a measurement of time. In PostgreSQL, an + interval is defined in the measure of months, days, and microseconds; as + such, the pg8000 interval type represents the same information. + + Note that values of the :attr:`microseconds`, :attr:`days` and + :attr:`months` properties are independently measured and cannot be + converted to each other. A month may be 28, 29, 30, or 31 days, and a day + may occasionally be lengthened slightly by a leap second. + + .. attribute:: microseconds + + Measure of microseconds in the interval. + + The microseconds value is constrained to fit into a signed 64-bit + integer. Any attempt to set a value too large or too small will result + in an OverflowError being raised. + + .. attribute:: days + + Measure of days in the interval. + + The days value is constrained to fit into a signed 32-bit integer. + Any attempt to set a value too large or too small will result in an + OverflowError being raised. + + .. attribute:: months + + Measure of months in the interval. + + The months value is constrained to fit into a signed 32-bit integer. + Any attempt to set a value too large or too small will result in an + OverflowError being raised. + """ + + def __init__(self, microseconds=0, days=0, months=0): + self.microseconds = microseconds + self.days = days + self.months = months + + def _setMicroseconds(self, value): + if not isinstance(value, integer_types): + raise TypeError("microseconds must be an integer type") + elif not (min_int8 < value < max_int8): + raise OverflowError( + "microseconds must be representable as a 64-bit integer") + else: + self._microseconds = value + + def _setDays(self, value): + if not isinstance(value, integer_types): + raise TypeError("days must be an integer type") + elif not (min_int4 < value < max_int4): + raise OverflowError( + "days must be representable as a 32-bit integer") + else: + self._days = value + + def _setMonths(self, value): + if not isinstance(value, integer_types): + raise TypeError("months must be an integer type") + elif not (min_int4 < value < max_int4): + raise OverflowError( + "months must be representable as a 32-bit integer") + else: + self._months = value + + microseconds = property(lambda self: self._microseconds, _setMicroseconds) + days = property(lambda self: self._days, _setDays) + months = property(lambda self: self._months, _setMonths) + + def __repr__(self): + return "" % ( + self.months, self.days, self.microseconds) + + def __eq__(self, other): + return other is not None and isinstance(other, Interval) and \ + self.months == other.months and self.days == other.days and \ + self.microseconds == other.microseconds + + def __neq__(self, other): + return not self.__eq__(other) + + +def pack_funcs(fmt): + struc = Struct('!' + fmt) + return struc.pack, struc.unpack_from + +i_pack, i_unpack = pack_funcs('i') +h_pack, h_unpack = pack_funcs('h') +q_pack, q_unpack = pack_funcs('q') +d_pack, d_unpack = pack_funcs('d') +f_pack, f_unpack = pack_funcs('f') +iii_pack, iii_unpack = pack_funcs('iii') +ii_pack, ii_unpack = pack_funcs('ii') +qii_pack, qii_unpack = pack_funcs('qii') +dii_pack, dii_unpack = pack_funcs('dii') +ihihih_pack, ihihih_unpack = pack_funcs('ihihih') +ci_pack, ci_unpack = pack_funcs('ci') +bh_pack, bh_unpack = pack_funcs('bh') +cccc_pack, cccc_unpack = pack_funcs('cccc') + + +Struct('!i') + + +min_int2, max_int2 = -2 ** 15, 2 ** 15 +min_int4, max_int4 = -2 ** 31, 2 ** 31 +min_int8, max_int8 = -2 ** 63, 2 ** 63 + + +class Warning(Exception): + """Generic exception raised for important database warnings like data + truncations. This exception is not currently used by pg8000. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class Error(Exception): + """Generic exception that is the base exception of all other error + exceptions. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class InterfaceError(Error): + """Generic exception raised for errors that are related to the database + interface rather than the database itself. For example, if the interface + attempts to use an SSL connection but the server refuses, an InterfaceError + will be raised. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class DatabaseError(Error): + """Generic exception raised for errors that are related to the database. + This exception is currently never raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class DataError(DatabaseError): + """Generic exception raised for errors that are due to problems with the + processed data. This exception is not currently raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class OperationalError(DatabaseError): + """ + Generic exception raised for errors that are related to the database's + operation and not necessarily under the control of the programmer. This + exception is currently never raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class IntegrityError(DatabaseError): + """ + Generic exception raised when the relational integrity of the database is + affected. This exception is not currently raised by pg8000. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class InternalError(DatabaseError): + """Generic exception raised when the database encounters an internal error. + This is currently only raised when unexpected state occurs in the pg8000 + interface itself, and is typically the result of a interface bug. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class ProgrammingError(DatabaseError): + """Generic exception raised for programming errors. For example, this + exception is raised if more parameter fields are in a query string than + there are available parameters. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class NotSupportedError(DatabaseError): + """Generic exception raised in case a method or database API was used which + is not supported by the database. + + This exception is part of the `DBAPI 2.0 specification + `_. + """ + pass + + +class ArrayContentNotSupportedError(NotSupportedError): + """ + Raised when attempting to transmit an array where the base type is not + supported for binary data transfer by the interface. + """ + pass + + +class ArrayContentNotHomogenousError(ProgrammingError): + """ + Raised when attempting to transmit an array that doesn't contain only a + single type of object. + """ + pass + + +class ArrayContentEmptyError(ProgrammingError): + """Raised when attempting to transmit an empty array. The type oid of an + empty array cannot be determined, and so sending them is not permitted. + """ + pass + + +class ArrayDimensionsNotConsistentError(ProgrammingError): + """ + Raised when attempting to transmit an array that has inconsistent + multi-dimension sizes. + """ + pass + + +class Bytea(binary_type): + """Bytea is a str-derived class that is mapped to a PostgreSQL byte array. + This class is only used in Python 2, the built-in ``bytes`` type is used in + Python 3. + """ + pass + + +def Date(year, month, day): + """Constuct an object holding a date value. + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.date` + """ + return datetime.date(year, month, day) + + +def Time(hour, minute, second): + """Construct an object holding a time value. + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.time` + """ + return datetime.time(hour, minute, second) + + +def Timestamp(year, month, day, hour, minute, second): + """Construct an object holding a timestamp value. + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.datetime` + """ + return datetime.datetime(year, month, day, hour, minute, second) + + +def DateFromTicks(ticks): + """Construct an object holding a date value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.date` + """ + return Date(*time.localtime(ticks)[:3]) + + +def TimeFromTicks(ticks): + """Construct an objet holding a time value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.time` + """ + return Time(*time.localtime(ticks)[3:6]) + + +def TimestampFromTicks(ticks): + """Construct an object holding a timestamp value from the given ticks value + (number of seconds since the epoch). + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`datetime.datetime` + """ + return Timestamp(*time.localtime(ticks)[:6]) + + +def Binary(value): + """Construct an object holding binary data. + + This function is part of the `DBAPI 2.0 specification + `_. + + :rtype: :class:`pg8000.types.Bytea` for Python 2, otherwise :class:`bytes` + """ + if PY2: + return Bytea(value) + else: + return value + +if PY2: + BINARY = Bytea +else: + BINARY = bytes FC_TEXT = 0 FC_BINARY = 1 @@ -271,13 +604,13 @@ def timestamp_recv_integer(data, offset, length): micros = q_unpack(data, offset)[0] try: return EPOCH + timedelta(microseconds=micros) - except OverflowError: + except OverflowError as e: if micros == INFINITY_MICROSECONDS: return datetime.datetime.max elif micros == MINUS_INFINITY_MICROSECONDS: return datetime.datetime.min else: - raise exc_info()[1] + raise e # data is double-precision float representing seconds since 2000-01-01 @@ -299,7 +632,7 @@ def timestamp_send_integer(v): # data is double-precision float representing seconds since 2000-01-01 def timestamp_send_float(v): - return d_pack(timegm(v.timetuple) + v.microsecond / 1e6 - EPOCH_SECONDS) + return d_pack(timegm(v.timetuple()) + v.microsecond / 1e6 - EPOCH_SECONDS) def timestamptz_send_integer(v): @@ -325,13 +658,13 @@ def timestamptz_recv_integer(data, offset, length): micros = q_unpack(data, offset)[0] try: return EPOCH_TZ + timedelta(microseconds=micros) - except OverflowError: + except OverflowError as e: if micros == INFINITY_MICROSECONDS: return DATETIME_MAX_TZ elif micros == MINUS_INFINITY_MICROSECONDS: return DATETIME_MIN_TZ else: - raise exc_info()[1] + raise e def timestamptz_recv_float(data, offset, length): @@ -565,21 +898,19 @@ class Cursor(): .. versionadded:: 1.9.11 """ try: - self._c._lock.acquire() - self.stream = stream + with self._c._lock: + self.stream = stream - if not self._c.in_transaction and not self._c.autocommit: - self._c.execute(self, "begin transaction", None) - self._c.execute(self, operation, args) - except AttributeError: + if not self._c.in_transaction and not self._c.autocommit: + self._c.execute(self, "begin transaction", None) + self._c.execute(self, operation, args) + except AttributeError as e: if self._c is None: raise InterfaceError("Cursor closed") elif self._c._sock is None: raise InterfaceError("connection is closed") else: - raise exc_info()[1] - finally: - self._c._lock.release() + raise e def executemany(self, operation, param_sets): """Prepare a database operation, and then execute it against all @@ -690,28 +1021,26 @@ class Cursor(): pass def __next__(self): - try: - self._c._lock.acquire() - return self._cached_rows.popleft() - except IndexError: - if self.portal_suspended: - self._c.send_EXECUTE(self) - self._c._write(SYNC_MSG) - self._c._flush() - self._c.handle_messages(self) - if not self.portal_suspended: - self._c.close_portal(self) + with self._c._lock: try: return self._cached_rows.popleft() except IndexError: - if self.ps is None: - raise ProgrammingError("A query hasn't been issued.") - elif len(self.ps['row_desc']) == 0: - raise ProgrammingError("no result set") - else: - raise StopIteration() - finally: - self._c._lock.release() + if self.portal_suspended: + self._c.send_EXECUTE(self) + self._c._write(SYNC_MSG) + self._c._flush() + self._c.handle_messages(self) + if not self.portal_suspended: + self._c.close_portal(self) + try: + return self._cached_rows.popleft() + except IndexError: + if self.ps is None: + raise ProgrammingError("A query hasn't been issued.") + elif len(self.ps['row_desc']) == 0: + raise ProgrammingError("no result set") + else: + raise StopIteration() if PY2: Cursor.next = Cursor.__next__ @@ -737,6 +1066,7 @@ COPY_DONE = b("c") COPY_DATA = b("d") COPY_IN_RESPONSE = b("G") COPY_OUT_RESPONSE = b("H") +EMPTY_QUERY_RESPONSE = b("I") BIND = b("B") PARSE = b("P") @@ -776,14 +1106,6 @@ IDLE_IN_TRANSACTION = b("T") IDLE_IN_FAILED_TRANSACTION = b("E") -# Byte1('N') - Identifier -# Int32 - Message length -# Any number of these, followed by a zero byte: -# Byte1 - code identifying the field type (see responseKeys) -# String - field value -def data_into_dict(data): - return dict((s[0:1], s[1:]) for s in data.split(NULL_BYTE)) - arr_trans = dict(zip(map(ord, u("[] 'u")), list(u('{}')) + [None] * 3)) @@ -895,7 +1217,9 @@ class Connection(object): error.__name__, stacklevel=3) return error - def __init__(self, user, host, unix_sock, port, database, password, ssl): + def __init__( + self, user, host, unix_sock, port, database, password, ssl, + timeout): self._client_encoding = "utf8" self._commands_with_count = ( b("INSERT"), b("DELETE"), b("UPDATE"), b("MOVE"), @@ -903,23 +1227,19 @@ class Connection(object): self._lock = threading.Lock() if user is None: - try: - self.user = os.environ['PGUSER'] - except KeyError: - try: - self.user = os.environ['USER'] - except KeyError: - raise InterfaceError( - "The 'user' connection parameter was omitted, and " - "neither the PGUSER or USER environment variables " - "were set.") + raise InterfaceError( + "The 'user' connection parameter cannot be None") + + if isinstance(user, text_type): + self.user = user.encode('utf8') else: self.user = user - if isinstance(self.user, text_type): - self.user = self.user.encode('utf8') + if isinstance(password, text_type): + self.password = password.encode('utf8') + else: + self.password = password - self.password = password self.autocommit = False self._xid = None @@ -939,41 +1259,38 @@ class Connection(object): else: raise ProgrammingError( "one of host or unix_sock must be provided") + if not PY2 and timeout is not None: + self._usock.settimeout(timeout) + if unix_sock is None and host is not None: self._usock.connect((host, port)) elif unix_sock is not None: self._usock.connect(unix_sock) if ssl: - try: - self._lock.acquire() - import ssl as sslmodule - # Int32(8) - Message length, including self. - # Int32(80877103) - The SSL request code. - self._usock.sendall(ii_pack(8, 80877103)) - resp = self._usock.recv(1) - if resp == b('S'): - self._usock = sslmodule.wrap_socket(self._usock) - else: - raise InterfaceError("Server refuses SSL") - except ImportError: - raise InterfaceError( - "SSL required but ssl module not available in " - "this python installation") - finally: - self._lock.release() + with self._lock: + try: + import ssl as sslmodule + # Int32(8) - Message length, including self. + # Int32(80877103) - The SSL request code. + self._usock.sendall(ii_pack(8, 80877103)) + resp = self._usock.recv(1) + if resp == b('S'): + self._usock = sslmodule.wrap_socket(self._usock) + else: + raise InterfaceError("Server refuses SSL") + except ImportError: + raise InterfaceError( + "SSL required but ssl module not available in " + "this python installation") self._sock = self._usock.makefile(mode="rwb") - except socket.error: + except socket.error as e: self._usock.close() - raise InterfaceError("communication error", exc_info()[1]) + raise InterfaceError("communication error", e) self._flush = self._sock.flush self._read = self._sock.read - - if PRE_26: - self._write = self._sock.writelines - else: - self._write = self._sock.write + self._write = self._sock.write self._backend_key_data = None ## @@ -1182,7 +1499,6 @@ class Connection(object): bool: (16, FC_BINARY, bool_send), int: (705, FC_TEXT, unknown_out), float: (701, FC_BINARY, d_pack), # float8 - str: (705, FC_TEXT, text_out), # unknown datetime.date: (1082, FC_TEXT, date_out), # date datetime.time: (1083, FC_TEXT, time_out), # time 1114: (1114, FC_BINARY, timestamp_send_integer), # timestamp @@ -1203,10 +1519,12 @@ class Connection(object): if PY2: self.py_types[Bytea] = (17, FC_BINARY, bytea_send) # bytea self.py_types[text_type] = (705, FC_TEXT, text_out) # unknown + self.py_types[str] = (705, FC_TEXT, bytea_send) # unknown self.py_types[long] = (705, FC_TEXT, unknown_out) # noqa else: self.py_types[bytes] = (17, FC_BINARY, bytea_send) # bytea + self.py_types[str] = (705, FC_TEXT, text_out) # unknown try: from ipaddress import ( @@ -1240,6 +1558,7 @@ class Connection(object): READY_FOR_QUERY: self.handle_READY_FOR_QUERY, ROW_DESCRIPTION: self.handle_ROW_DESCRIPTION, ERROR_RESPONSE: self.handle_ERROR_RESPONSE, + EMPTY_QUERY_RESPONSE: self.handle_EMPTY_QUERY_RESPONSE, DATA_ROW: self.handle_DATA_ROW, COMMAND_COMPLETE: self.handle_COMMAND_COMPLETE, PARSE_COMPLETE: self.handle_PARSE_COMPLETE, @@ -1272,32 +1591,37 @@ class Connection(object): self._flush() self._cursor = self.cursor() - try: - self._lock.acquire() - code = self.error = None - while code not in (READY_FOR_QUERY, ERROR_RESPONSE): - code, data_len = ci_unpack(self._read(5)) - self.message_types[code](self._read(data_len - 4), None) - if self.error is not None: - raise self.error - except: - self._close() - raise - finally: - self._lock.release() + with self._lock: + try: + code = self.error = None + while code not in (READY_FOR_QUERY, ERROR_RESPONSE): + code, data_len = ci_unpack(self._read(5)) + self.message_types[code](self._read(data_len - 4), None) + if self.error is not None: + raise self.error + except Exception as e: + try: + self._close() + except Exception: + pass + raise e self.in_transaction = False self.notifies = [] self.notifies_lock = threading.Lock() def handle_ERROR_RESPONSE(self, data, ps): - msg_dict = data_into_dict(data) + responses = tuple( + (s[0:1], s[1:].decode(self._client_encoding)) for s in + data.split(NULL_BYTE)) + msg_dict = dict(responses) if msg_dict[RESPONSE_CODE] == "28000": self.error = InterfaceError("md5 password authentication failed") else: - self.error = ProgrammingError( - msg_dict[RESPONSE_SEVERITY], msg_dict[RESPONSE_CODE], - msg_dict[RESPONSE_MSG]) + self.error = ProgrammingError(*tuple(v for k, v in responses)) + + def handle_EMPTY_QUERY_RESPONSE(self, data, ps): + self.error = ProgrammingError("query was empty") def handle_CLOSE_COMPLETE(self, data, ps): pass @@ -1391,11 +1715,8 @@ class Connection(object): # additional_info = data[idx:idx + null] # psycopg2 compatible notification interface - try: - self.notifies_lock.acquire() + with self.notifies_lock: self.notifies.append((backend_pid, condition)) - finally: - self.notifies_lock.release() def cursor(self): """Creates a :class:`Cursor` object bound to this @@ -1412,11 +1733,8 @@ class Connection(object): This function is part of the `DBAPI 2.0 specification `_. """ - try: - self._lock.acquire() + with self._lock: self.execute(self._cursor, "commit", None) - finally: - self._lock.release() def rollback(self): """Rolls back the current database transaction. @@ -1424,11 +1742,8 @@ class Connection(object): This function is part of the `DBAPI 2.0 specification `_. """ - try: - self._lock.acquire() + with self._lock: self.execute(self._cursor, "rollback", None) - finally: - self._lock.release() def _close(self): try: @@ -1437,12 +1752,15 @@ class Connection(object): self._write(TERMINATE_MSG) self._flush() self._sock.close() - self._usock.close() - self._sock = None except AttributeError: raise InterfaceError("connection is closed") except ValueError: raise InterfaceError("connection is closed") + except socket.error as e: + raise OperationalError(str(e)) + finally: + self._usock.close() + self._sock = None def close(self): """Closes the database connection. @@ -1450,11 +1768,8 @@ class Connection(object): This function is part of the `DBAPI 2.0 specification `_. """ - try: - self._lock.acquire() + with self._lock: self._close() - finally: - self._lock.release() def handle_AUTHENTICATION_REQUEST(self, data, cursor): assert self._lock.locked() @@ -1463,7 +1778,7 @@ class Connection(object): # 0 = AuthenticationOk # 5 = MD5 pwd # 2 = Kerberos v5 (not supported by pg8000) - # 3 = Cleartext pwd (not supported by pg8000) + # 3 = Cleartext pwd # 4 = crypt() pwd (not supported by pg8000) # 6 = SCM credential (not supported by pg8000) # 7 = GSSAPI (not supported by pg8000) @@ -1480,8 +1795,7 @@ class Connection(object): raise InterfaceError( "server requesting password authentication, but no " "password was provided") - self._send_message( - PASSWORD, self.password.encode("ascii") + NULL_BYTE) + self._send_message(PASSWORD, self.password + NULL_BYTE) self._flush() elif auth_code == 5: ## @@ -1497,8 +1811,8 @@ class Connection(object): "server requesting MD5 password authentication, but no " "password was provided") pwd = b("md5") + md5( - md5(self.password.encode("ascii") + self.user). - hexdigest().encode("ascii") + salt).hexdigest().encode("ascii") + md5(self.password + self.user).hexdigest().encode("ascii") + + salt).hexdigest().encode("ascii") # Byte1('p') - Identifies the message as a password message. # Int32 - Message length including self. # String - The password. Password may be encrypted. @@ -1536,11 +1850,10 @@ class Connection(object): except KeyError: try: params.append(self.inspect_funcs[typ](value)) - except KeyError: + except KeyError as e: raise NotSupportedError( - "type " + str(exc_info()[1]) + - "not mapped to pg type") - return params + "type " + str(e) + "not mapped to pg type") + return tuple(params) def handle_ROW_DESCRIPTION(self, data, cursor): count = h_unpack(data)[0] @@ -1572,8 +1885,7 @@ class Connection(object): args = make_args(vals) params = self.make_params(args) - - key = tuple(oid for oid, x, y in params), operation + key = operation, params try: ps = cache['ps'][key] @@ -1617,11 +1929,13 @@ class Connection(object): try: self._flush() - except AttributeError: + except AttributeError as e: if self._sock is None: raise InterfaceError("connection is closed") else: - raise exc_info()[1] + raise e + except socket.error as e: + raise OperationalError(str(e)) self.handle_messages(cursor) @@ -1711,11 +2025,11 @@ class Connection(object): self._write(i_pack(len(data) + 4)) self._write(data) self._write(FLUSH_MSG) - except ValueError: - if str(exc_info()[1]) == "write to closed file": + except ValueError as e: + if str(e) == "write to closed file": raise InterfaceError("connection is closed") else: - raise exc_info()[1] + raise e except AttributeError: raise InterfaceError("connection is closed") @@ -1783,8 +2097,13 @@ class Connection(object): self._flush() self.handle_messages(cursor) + # Byte1('N') - Identifier + # Int32 - Message length + # Any number of these, followed by a zero byte: + # Byte1 - code identifying the field type (see responseKeys) + # String - field value def handle_NOTICE_RESPONSE(self, data, ps): - resp = data_into_dict(data) + resp = dict((s[0:1], s[1:]) for s in data.split(NULL_BYTE)) self.NoticeReceived(resp) def handle_PARAMETER_STATUS(self, data, ps): diff --git a/gluon/contrib/pg8000/six.py b/gluon/contrib/pg8000/six.py index 6ec96ed8..190c0239 100644 --- a/gluon/contrib/pg8000/six.py +++ b/gluon/contrib/pg8000/six.py @@ -1,6 +1,6 @@ """Utilities for writing code that runs on Python 2 and 3""" -# Copyright (c) 2010-2013 Benjamin Peterson +# Copyright (c) 2010-2015 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,8 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -21,22 +21,21 @@ # SOFTWARE. from __future__ import absolute_import + +import functools +import itertools import operator import sys import types __author__ = "Benjamin Peterson " -__version__ = "1.4.1" +__version__ = "1.10.0" # Useful for very coarse version differentiation. PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 - -PRE_26 = PY2 and sys.version_info[1] < 6 - -IS_JYTHON = sys.platform.lower().count('java') > 0 - +PY34 = sys.version_info[0:2] >= (3, 4) if PY3: string_types = str, @@ -47,10 +46,10 @@ if PY3: MAXSIZE = sys.maxsize else: - string_types = basestring, # noqa - integer_types = (int, long) # noqa + string_types = basestring, + integer_types = (int, long) class_types = (type, types.ClassType) - text_type = unicode # noqa + text_type = unicode binary_type = str if sys.platform.startswith("java"): @@ -59,6 +58,7 @@ else: else: # It's possible to have sizeof(long) != sizeof(Py_ssize_t). class X(object): + def __len__(self): return 1 << 31 try: @@ -90,9 +90,13 @@ class _LazyDescr(object): def __get__(self, obj, tp): result = self._resolve() - setattr(obj, self.name, result) - # This is a bit ugly, but it avoids running this again. - delattr(tp, self.name) + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass return result @@ -110,6 +114,27 @@ class MovedModule(_LazyDescr): def _resolve(self): return _import_module(self.mod) + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + class MovedAttribute(_LazyDescr): @@ -136,38 +161,109 @@ class MovedAttribute(_LazyDescr): return getattr(module, self.attr) -class _MovedItems(types.ModuleType): +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + """Lazy loading of moved objects""" + __path__ = [] # mark as package _moved_attributes = [ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute( - "filterfalse", "itertools", "itertools", "ifilterfalse", - "filterfalse"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), MovedAttribute("UserString", "UserString", "collections"), MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute( - "zip_longest", "itertools", "itertools", "izip_longest", - "zip_longest"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), MovedModule("builtins", "__builtin__"), MovedModule("configparser", "ConfigParser"), MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), MovedModule("http_cookies", "Cookie", "http.cookies"), MovedModule("html_entities", "htmlentitydefs", "html.entities"), MovedModule("html_parser", "HTMLParser", "html.parser"), MovedModule("http_client", "httplib", "http.client"), - MovedModule( - "email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), @@ -177,14 +273,14 @@ _moved_attributes = [ MovedModule("queue", "Queue"), MovedModule("reprlib", "repr"), MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), MovedModule("tkinter", "Tkinter"), MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule( - "tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule( - "tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), MovedModule("tkinter_colorchooser", "tkColorChooser", @@ -194,30 +290,41 @@ _moved_attributes = [ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), MovedModule("tkinter_font", "tkFont", "tkinter.font"), MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule( - "tkinter_tksimpledialog", "tkSimpleDialog", "tkinter.simpledialog"), - MovedModule( - "urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule( - "urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule( - "urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("winreg", "_winreg"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), ] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + for attr in _moved_attributes: setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) del attr -moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves") +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") -class Module_six_moves_urllib_parse(types.ModuleType): +class Module_six_moves_urllib_parse(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_parse""" _urllib_parse_moved_attributes = [ MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), MovedAttribute("parse_qs", "urlparse", "urllib.parse"), MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), MovedAttribute("urldefrag", "urlparse", "urllib.parse"), @@ -231,18 +338,27 @@ _urllib_parse_moved_attributes = [ MovedAttribute("unquote", "urllib", "urllib.parse"), MovedAttribute("unquote_plus", "urllib", "urllib.parse"), MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), ] for attr in _urllib_parse_moved_attributes: setattr(Module_six_moves_urllib_parse, attr.name, attr) del attr -sys.modules[__name__ + ".moves.urllib_parse"] = Module_six_moves_urllib_parse( - __name__ + ".moves.urllib_parse") -sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse( - __name__ + ".moves.urllib.parse") +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") -class Module_six_moves_urllib_error(types.ModuleType): +class Module_six_moves_urllib_error(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_error""" @@ -255,13 +371,14 @@ for attr in _urllib_error_moved_attributes: setattr(Module_six_moves_urllib_error, attr.name, attr) del attr -sys.modules[__name__ + ".moves.urllib_error"] = Module_six_moves_urllib_error( - __name__ + ".moves.urllib_error") -sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error( - __name__ + ".moves.urllib.error") +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") -class Module_six_moves_urllib_request(types.ModuleType): +class Module_six_moves_urllib_request(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_request""" @@ -280,8 +397,7 @@ _urllib_request_moved_attributes = [ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), MovedAttribute("BaseHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute( - "HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), @@ -299,18 +415,20 @@ _urllib_request_moved_attributes = [ MovedAttribute("urlcleanup", "urllib", "urllib.request"), MovedAttribute("URLopener", "urllib", "urllib.request"), MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), ] for attr in _urllib_request_moved_attributes: setattr(Module_six_moves_urllib_request, attr.name, attr) del attr -sys.modules[__name__ + ".moves.urllib_request"] = \ - Module_six_moves_urllib_request(__name__ + ".moves.urllib_request") -sys.modules[__name__ + ".moves.urllib.request"] = \ - Module_six_moves_urllib_request(__name__ + ".moves.urllib.request") +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") -class Module_six_moves_urllib_response(types.ModuleType): +class Module_six_moves_urllib_response(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_response""" @@ -324,13 +442,14 @@ for attr in _urllib_response_moved_attributes: setattr(Module_six_moves_urllib_response, attr.name, attr) del attr -sys.modules[__name__ + ".moves.urllib_response"] = \ - Module_six_moves_urllib_response(__name__ + ".moves.urllib_response") -sys.modules[__name__ + ".moves.urllib.response"] = \ - Module_six_moves_urllib_response(__name__ + ".moves.urllib.response") +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") -class Module_six_moves_urllib_robotparser(types.ModuleType): +class Module_six_moves_urllib_robotparser(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_robotparser""" @@ -341,25 +460,27 @@ for attr in _urllib_robotparser_moved_attributes: setattr(Module_six_moves_urllib_robotparser, attr.name, attr) del attr -sys.modules[__name__ + ".moves.urllib_robotparser"] = \ - Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib_robotparser") -sys.modules[__name__ + ".moves.urllib.robotparser"] = \ - Module_six_moves_urllib_robotparser( - __name__ + ".moves.urllib.robotparser") +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") class Module_six_moves_urllib(types.ModuleType): - """Create a six.moves.urllib namespace that resembles the Python 3 - namespace""" - parse = sys.modules[__name__ + ".moves.urllib_parse"] - error = sys.modules[__name__ + ".moves.urllib_error"] - request = sys.modules[__name__ + ".moves.urllib_request"] - response = sys.modules[__name__ + ".moves.urllib_response"] - robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"] + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") -sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib( - __name__ + ".moves.urllib") + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") def add_move(move): @@ -386,11 +507,6 @@ if PY3: _func_code = "__code__" _func_defaults = "__defaults__" _func_globals = "__globals__" - - _iterkeys = "keys" - _itervalues = "values" - _iteritems = "items" - _iterlists = "lists" else: _meth_func = "im_func" _meth_self = "im_self" @@ -400,11 +516,6 @@ else: _func_defaults = "func_defaults" _func_globals = "func_globals" - _iterkeys = "iterkeys" - _itervalues = "itervalues" - _iteritems = "iteritems" - _iterlists = "iterlists" - try: advance_iterator = next @@ -427,6 +538,9 @@ if PY3: create_bound_method = types.MethodType + def create_unbound_method(func, cls): + return func + Iterator = object else: def get_unbound_function(unbound): @@ -435,6 +549,9 @@ else: def create_bound_method(func, obj): return types.MethodType(func, obj, obj.__class__) + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + class Iterator(object): def next(self): @@ -453,24 +570,49 @@ get_function_defaults = operator.attrgetter(_func_defaults) get_function_globals = operator.attrgetter(_func_globals) -def iterkeys(d, **kw): - """Return an iterator over the keys of a dictionary.""" - return iter(getattr(d, _iterkeys)(**kw)) +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + def itervalues(d, **kw): + return iter(d.values(**kw)) -def itervalues(d, **kw): - """Return an iterator over the values of a dictionary.""" - return iter(getattr(d, _itervalues)(**kw)) + def iteritems(d, **kw): + return iter(d.items(**kw)) + def iterlists(d, **kw): + return iter(d.lists(**kw)) -def iteritems(d, **kw): - """Return an iterator over the (key, value) pairs of a dictionary.""" - return iter(getattr(d, _iteritems)(**kw)) + viewkeys = operator.methodcaller("keys") + viewvalues = operator.methodcaller("values") -def iterlists(d, **kw): - """Return an iterator over the (key, [values]) pairs of a dictionary.""" - return iter(getattr(d, _iterlists)(**kw)) + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") if PY3: @@ -480,24 +622,29 @@ if PY3: def u(s): return s unichr = chr - if sys.version_info[1] <= 1: - def int2byte(i): - return bytes((i,)) - else: - # This is about 2x faster than the implementation above on 3.2+ - int2byte = operator.methodcaller("to_bytes", 1, "big") + import struct + int2byte = struct.Struct(">B").pack + del struct byte2int = operator.itemgetter(0) indexbytes = operator.getitem iterbytes = iter import io StringIO = io.StringIO BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" else: def b(s): return s + # Workaround for standalone backslash def u(s): - return unicode(s, "unicode_escape") # noqa + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") unichr = unichr int2byte = chr @@ -506,27 +653,38 @@ else: def indexbytes(buf, i): return ord(buf[i]) - - def iterbytes(buf): - return (ord(byte) for byte in buf) + iterbytes = functools.partial(itertools.imap, ord) import StringIO StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" _add_doc(b, """Byte literal""") _add_doc(u, """Text literal""") +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + if PY3: - import builtins - exec_ = getattr(builtins, "exec") + exec_ = getattr(moves.builtins, "exec") def reraise(tp, value, tb=None): + if value is None: + value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value - print_ = getattr(builtins, "print") - del builtins - else: def exec_(_code_, _globs_=None, _locs_=None): """Execute code in a namespace.""" @@ -544,26 +702,52 @@ else: raise tp, value, tb """) + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: def print_(*args, **kwargs): - """The new-style print function.""" + """The new-style print function for Python 2.4 and 2.5.""" fp = kwargs.pop("file", sys.stdout) if fp is None: return def write(data): - if not isinstance(data, basestring): # noqa + if not isinstance(data, basestring): data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) fp.write(data) want_unicode = False sep = kwargs.pop("sep", None) if sep is not None: - if isinstance(sep, unicode): # noqa + if isinstance(sep, unicode): want_unicode = True elif not isinstance(sep, str): raise TypeError("sep must be None or a string") end = kwargs.pop("end", None) if end is not None: - if isinstance(end, unicode): # noqa + if isinstance(end, unicode): want_unicode = True elif not isinstance(end, str): raise TypeError("end must be None or a string") @@ -571,12 +755,12 @@ else: raise TypeError("invalid keyword arguments to print()") if not want_unicode: for arg in args: - if isinstance(arg, unicode): # noqa + if isinstance(arg, unicode): want_unicode = True break if want_unicode: - newline = unicode("\n") # noqa - space = unicode(" ") # noqa + newline = unicode("\n") + space = unicode(" ") else: newline = "\n" space = " " @@ -589,22 +773,96 @@ else: write(sep) write(arg) write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() _add_doc(reraise, """Reraise an exception.""") +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + def with_metaclass(meta, *bases): """Create a base class with a metaclass.""" - return meta("NewBase", bases, {}) + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" def wrapper(cls): orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) orig_vars.pop('__dict__', None) orig_vars.pop('__weakref__', None) - for slots_var in orig_vars.get('__slots__', ()): - orig_vars.pop(slots_var) return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) From bcc237ead871df46f614e358bfb35c44fe2cee38 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 3 Oct 2016 22:55:21 +0200 Subject: [PATCH 135/436] removing pycrypto hard dep --- .travis.yml | 20 +- README.markdown | 4 +- gluon/contrib/aes.py | 502 ------------------------ gluon/contrib/pbkdf2.py | 48 +-- gluon/contrib/pbkdf2_ctypes.py | 3 +- gluon/contrib/pyaes/LICENSE.txt | 22 ++ gluon/contrib/pyaes/README.md | 363 ++++++++++++++++++ gluon/contrib/pyaes/__init__.py | 53 +++ gluon/contrib/pyaes/aes.py | 589 +++++++++++++++++++++++++++++ gluon/contrib/pyaes/blockfeeder.py | 227 +++++++++++ gluon/contrib/pyaes/util.py | 60 +++ gluon/utils.py | 86 +++-- requirements.txt | 1 - 13 files changed, 1390 insertions(+), 588 deletions(-) delete mode 100644 gluon/contrib/aes.py create mode 100644 gluon/contrib/pyaes/LICENSE.txt create mode 100644 gluon/contrib/pyaes/README.md create mode 100644 gluon/contrib/pyaes/__init__.py create mode 100644 gluon/contrib/pyaes/aes.py create mode 100644 gluon/contrib/pyaes/blockfeeder.py create mode 100644 gluon/contrib/pyaes/util.py delete mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index 2907e30e..472896fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,7 @@ language: python sudo: false -cache: - directories: - - $HOME/.pip-cache/ +cache: pip python: - '2.7' @@ -12,21 +10,7 @@ python: - '3.5' install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - pushd "$PYENV_ROOT" && git pull && popd - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="5.0.1" - "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION" - virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" - source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" - fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install pycrypto pg8000; fi; - - if [[ $TRAVIS_PYTHON_VERSION != '3.5' ]]; then pip install -e .; fi; + - pip install -e . before_script: - pip install coverage diff --git a/README.markdown b/README.markdown index 1c906755..27fa2369 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,6 @@ ## Readme -web2py is a free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications. +web2py is a free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications. It is written and programmable in Python. LGPLv3 License @@ -10,7 +10,7 @@ Learn more at http://web2py.com cp examples/app.yaml ./ cp handlers/gaehandler.py ./ - + Then edit ./app.yaml and replace "yourappname" with yourappname. ## Important reminder about this GIT repo diff --git a/gluon/contrib/aes.py b/gluon/contrib/aes.py deleted file mode 100644 index cecf2d90..00000000 --- a/gluon/contrib/aes.py +++ /dev/null @@ -1,502 +0,0 @@ -"""Simple AES cipher implementation in pure Python following PEP-272 API - -Homepage: https://bitbucket.org/intgr/pyaes/ - -The goal of this module is to be as fast as reasonable in Python while still -being Pythonic and readable/understandable. It is licensed under the permissive -MIT license. - -Hopefully the code is readable and commented enough that it can serve as an -introduction to the AES cipher for Python coders. In fact, it should go along -well with the Stick Figure Guide to AES: -http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html - -Contrary to intuition, this implementation numbers the 4x4 matrices from top to -bottom for efficiency reasons:: - - 0 4 8 12 - 1 5 9 13 - 2 6 10 14 - 3 7 11 15 - -Effectively it's the transposition of what you'd expect. This actually makes -the code simpler -- except the ShiftRows step, but hopefully the explanation -there clears it up. - -""" - -#### -# Copyright (c) 2010 Marti Raudsepp -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -#### - - -from array import array - -# Globals mandated by PEP 272: -# http://www.python.org/dev/peps/pep-0272/ -MODE_ECB = 1 -MODE_CBC = 2 -#MODE_CTR = 6 - -block_size = 16 -key_size = None - -def new(key, mode=MODE_CBC, IV=None): - if mode == MODE_ECB: - return ECBMode(AES(key)) - elif mode == MODE_CBC: - if IV is None: - raise ValueError("CBC mode needs an IV value!") - - return CBCMode(AES(key), IV) - else: - raise NotImplementedError - -#### AES cipher implementation - -class AES(object): - block_size = 16 - - def __init__(self, key): - self.setkey(key) - - def setkey(self, key): - """Sets the key and performs key expansion.""" - - self.key = key - self.key_size = len(key) - - if self.key_size == 16: - self.rounds = 10 - elif self.key_size == 24: - self.rounds = 12 - elif self.key_size == 32: - self.rounds = 14 - else: - raise ValueError("Key length must be 16, 24 or 32 bytes") - - self.expand_key() - - def expand_key(self): - """Performs AES key expansion on self.key and stores in self.exkey""" - - # The key schedule specifies how parts of the key are fed into the - # cipher's round functions. "Key expansion" means performing this - # schedule in advance. Almost all implementations do this. - # - # Here's a description of AES key schedule: - # http://en.wikipedia.org/wiki/Rijndael_key_schedule - - # The expanded key starts with the actual key itself - exkey = array('B', self.key) - - # extra key expansion steps - if self.key_size == 16: - extra_cnt = 0 - elif self.key_size == 24: - extra_cnt = 2 - else: - extra_cnt = 3 - - # 4-byte temporary variable for key expansion - word = exkey[-4:] - # Each expansion cycle uses 'i' once for Rcon table lookup - for i in xrange(1, 11): - - #### key schedule core: - # left-rotate by 1 byte - word = word[1:4] + word[0:1] - - # apply S-box to all bytes - for j in xrange(4): - word[j] = aes_sbox[word[j]] - - # apply the Rcon table to the leftmost byte - word[0] = word[0] ^ aes_Rcon[i] - #### end key schedule core - - for z in xrange(4): - for j in xrange(4): - # mix in bytes from the last subkey - word[j] ^= exkey[-self.key_size + j] - exkey.extend(word) - - # Last key expansion cycle always finishes here - if len(exkey) >= (self.rounds+1) * self.block_size: - break - - # Special substitution step for 256-bit key - if self.key_size == 32: - for j in xrange(4): - # mix in bytes from the last subkey XORed with S-box of - # current word bytes - word[j] = aes_sbox[word[j]] ^ exkey[-self.key_size + j] - exkey.extend(word) - - # Twice for 192-bit key, thrice for 256-bit key - for z in xrange(extra_cnt): - for j in xrange(4): - # mix in bytes from the last subkey - word[j] ^= exkey[-self.key_size + j] - exkey.extend(word) - - self.exkey = exkey - - def add_round_key(self, block, round): - """AddRoundKey step in AES. This is where the key is mixed into plaintext""" - - offset = round * 16 - exkey = self.exkey - - for i in xrange(16): - block[i] ^= exkey[offset + i] - - #print 'AddRoundKey:', block - - def sub_bytes(self, block, sbox): - """SubBytes step, apply S-box to all bytes - - Depending on whether encrypting or decrypting, a different sbox array - is passed in. - """ - - for i in xrange(16): - block[i] = sbox[block[i]] - - #print 'SubBytes :', block - - def shift_rows(self, b): - """ShiftRows step. Shifts 2nd row to left by 1, 3rd row by 2, 4th row by 3 - - Since we're performing this on a transposed matrix, cells are numbered - from top to bottom:: - - 0 4 8 12 -> 0 4 8 12 -- 1st row doesn't change - 1 5 9 13 -> 5 9 13 1 -- row shifted to left by 1 (wraps around) - 2 6 10 14 -> 10 14 2 6 -- shifted by 2 - 3 7 11 15 -> 15 3 7 11 -- shifted by 3 - """ - - b[1], b[5], b[ 9], b[13] = b[ 5], b[ 9], b[13], b[ 1] - b[2], b[6], b[10], b[14] = b[10], b[14], b[ 2], b[ 6] - b[3], b[7], b[11], b[15] = b[15], b[ 3], b[ 7], b[11] - - #print 'ShiftRows :', b - - def shift_rows_inv(self, b): - """Similar to shift_rows above, but performed in inverse for decryption.""" - - b[ 5], b[ 9], b[13], b[ 1] = b[1], b[5], b[ 9], b[13] - b[10], b[14], b[ 2], b[ 6] = b[2], b[6], b[10], b[14] - b[15], b[ 3], b[ 7], b[11] = b[3], b[7], b[11], b[15] - - #print 'ShiftRows :', b - - def mix_columns(self, block): - """MixColumns step. Mixes the values in each column""" - - # Cache global multiplication tables (see below) - mul_by_2 = gf_mul_by_2 - mul_by_3 = gf_mul_by_3 - - # Since we're dealing with a transposed matrix, columns are already - # sequential - for i in xrange(4): - col = i * 4 - - #v0, v1, v2, v3 = block[col : col+4] - v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2], - block[col + 3]) - - block[col ] = mul_by_2[v0] ^ v3 ^ v2 ^ mul_by_3[v1] - block[col+1] = mul_by_2[v1] ^ v0 ^ v3 ^ mul_by_3[v2] - block[col+2] = mul_by_2[v2] ^ v1 ^ v0 ^ mul_by_3[v3] - block[col+3] = mul_by_2[v3] ^ v2 ^ v1 ^ mul_by_3[v0] - - #print 'MixColumns :', block - - def mix_columns_inv(self, block): - """Similar to mix_columns above, but performed in inverse for decryption.""" - - # Cache global multiplication tables (see below) - mul_9 = gf_mul_by_9 - mul_11 = gf_mul_by_11 - mul_13 = gf_mul_by_13 - mul_14 = gf_mul_by_14 - - # Since we're dealing with a transposed matrix, columns are already - # sequential - for i in xrange(4): - col = i * 4 - - v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2], - block[col + 3]) - #v0, v1, v2, v3 = block[col:col+4] - - block[col ] = mul_14[v0] ^ mul_9[v3] ^ mul_13[v2] ^ mul_11[v1] - block[col+1] = mul_14[v1] ^ mul_9[v0] ^ mul_13[v3] ^ mul_11[v2] - block[col+2] = mul_14[v2] ^ mul_9[v1] ^ mul_13[v0] ^ mul_11[v3] - block[col+3] = mul_14[v3] ^ mul_9[v2] ^ mul_13[v1] ^ mul_11[v0] - - #print 'MixColumns :', block - - def encrypt_block(self, block): - """Encrypts a single block. This is the main AES function""" - - # For efficiency reasons, the state between steps is transmitted via a - # mutable array, not returned. - self.add_round_key(block, 0) - - for round in xrange(1, self.rounds): - self.sub_bytes(block, aes_sbox) - self.shift_rows(block) - self.mix_columns(block) - self.add_round_key(block, round) - - self.sub_bytes(block, aes_sbox) - self.shift_rows(block) - # no mix_columns step in the last round - self.add_round_key(block, self.rounds) - - def decrypt_block(self, block): - """Decrypts a single block. This is the main AES decryption function""" - - # For efficiency reasons, the state between steps is transmitted via a - # mutable array, not returned. - self.add_round_key(block, self.rounds) - - # count rounds down from 15 ... 1 - for round in xrange(self.rounds-1, 0, -1): - self.shift_rows_inv(block) - self.sub_bytes(block, aes_inv_sbox) - self.add_round_key(block, round) - self.mix_columns_inv(block) - - self.shift_rows_inv(block) - self.sub_bytes(block, aes_inv_sbox) - self.add_round_key(block, 0) - # no mix_columns step in the last round - - -#### ECB mode implementation - -class ECBMode(object): - """Electronic CodeBook (ECB) mode encryption. - - Basically this mode applies the cipher function to each block individually; - no feedback is done. NB! This is insecure for almost all purposes - """ - - def __init__(self, cipher): - self.cipher = cipher - self.block_size = cipher.block_size - - def ecb(self, data, block_func): - """Perform ECB mode with the given function""" - - if len(data) % self.block_size != 0: - raise ValueError("Plaintext length must be multiple of 16") - - block_size = self.block_size - data = array('B', data) - - for offset in xrange(0, len(data), block_size): - block = data[offset : offset+block_size] - block_func(block) - data[offset : offset+block_size] = block - - return data.tostring() - - def encrypt(self, data): - """Encrypt data in ECB mode""" - - return self.ecb(data, self.cipher.encrypt_block) - - def decrypt(self, data): - """Decrypt data in ECB mode""" - - return self.ecb(data, self.cipher.decrypt_block) - -#### CBC mode - -class CBCMode(object): - """Cipher Block Chaining (CBC) mode encryption. This mode avoids content leaks. - - In CBC encryption, each plaintext block is XORed with the ciphertext block - preceding it; decryption is simply the inverse. - """ - - # A better explanation of CBC can be found here: - # http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29 - - def __init__(self, cipher, IV): - self.cipher = cipher - self.block_size = cipher.block_size - self.IV = array('B', IV) - - def encrypt(self, data): - """Encrypt data in CBC mode""" - - block_size = self.block_size - if len(data) % block_size != 0: - raise ValueError("Plaintext length must be multiple of 16") - - data = array('B', data) - IV = self.IV - - for offset in xrange(0, len(data), block_size): - block = data[offset : offset+block_size] - - # Perform CBC chaining - for i in xrange(block_size): - block[i] ^= IV[i] - - self.cipher.encrypt_block(block) - data[offset : offset+block_size] = block - IV = block - - self.IV = IV - return data.tostring() - - def decrypt(self, data): - """Decrypt data in CBC mode""" - - block_size = self.block_size - if len(data) % block_size != 0: - raise ValueError("Ciphertext length must be multiple of 16") - - data = array('B', data) - IV = self.IV - - for offset in xrange(0, len(data), block_size): - ctext = data[offset : offset+block_size] - block = ctext[:] - self.cipher.decrypt_block(block) - - # Perform CBC chaining - #for i in xrange(block_size): - # data[offset + i] ^= IV[i] - for i in xrange(block_size): - block[i] ^= IV[i] - data[offset : offset+block_size] = block - - IV = ctext - #data[offset : offset+block_size] = block - - self.IV = IV - return data.tostring() - -#### - -def galois_multiply(a, b): - """Galois Field multiplicaiton for AES""" - p = 0 - while b: - if b & 1: - p ^= a - a <<= 1 - if a & 0x100: - a ^= 0x1b - b >>= 1 - - return p & 0xff - -# Precompute the multiplication tables for encryption -gf_mul_by_2 = array('B', [galois_multiply(x, 2) for x in range(256)]) -gf_mul_by_3 = array('B', [galois_multiply(x, 3) for x in range(256)]) -# ... for decryption -gf_mul_by_9 = array('B', [galois_multiply(x, 9) for x in range(256)]) -gf_mul_by_11 = array('B', [galois_multiply(x, 11) for x in range(256)]) -gf_mul_by_13 = array('B', [galois_multiply(x, 13) for x in range(256)]) -gf_mul_by_14 = array('B', [galois_multiply(x, 14) for x in range(256)]) - -#### - -# The S-box is a 256-element array, that maps a single byte value to another -# byte value. Since it's designed to be reversible, each value occurs only once -# in the S-box -# -# More information: http://en.wikipedia.org/wiki/Rijndael_S-box - -aes_sbox = array('B', - '637c777bf26b6fc53001672bfed7ab76' - 'ca82c97dfa5947f0add4a2af9ca472c0' - 'b7fd9326363ff7cc34a5e5f171d83115' - '04c723c31896059a071280e2eb27b275' - '09832c1a1b6e5aa0523bd6b329e32f84' - '53d100ed20fcb15b6acbbe394a4c58cf' - 'd0efaafb434d338545f9027f503c9fa8' - '51a3408f929d38f5bcb6da2110fff3d2' - 'cd0c13ec5f974417c4a77e3d645d1973' - '60814fdc222a908846eeb814de5e0bdb' - 'e0323a0a4906245cc2d3ac629195e479' - 'e7c8376d8dd54ea96c56f4ea657aae08' - 'ba78252e1ca6b4c6e8dd741f4bbd8b8a' - '703eb5664803f60e613557b986c11d9e' - 'e1f8981169d98e949b1e87e9ce5528df' - '8ca1890dbfe6426841992d0fb054bb16'.decode('hex') -) - -# This is the inverse of the above. In other words: -# aes_inv_sbox[aes_sbox[val]] == val - -aes_inv_sbox = array('B', - '52096ad53036a538bf40a39e81f3d7fb' - '7ce339829b2fff87348e4344c4dee9cb' - '547b9432a6c2233dee4c950b42fac34e' - '082ea16628d924b2765ba2496d8bd125' - '72f8f66486689816d4a45ccc5d65b692' - '6c704850fdedb9da5e154657a78d9d84' - '90d8ab008cbcd30af7e45805b8b34506' - 'd02c1e8fca3f0f02c1afbd0301138a6b' - '3a9111414f67dcea97f2cfcef0b4e673' - '96ac7422e7ad3585e2f937e81c75df6e' - '47f11a711d29c5896fb7620eaa18be1b' - 'fc563e4bc6d279209adbc0fe78cd5af4' - '1fdda8338807c731b11210592780ec5f' - '60517fa919b54a0d2de57a9f93c99cef' - 'a0e03b4dae2af5b0c8ebbb3c83539961' - '172b047eba77d626e169146355210c7d'.decode('hex') -) - -# The Rcon table is used in AES's key schedule (key expansion) -# It's a pre-computed table of exponentation of 2 in AES's finite field -# -# More information: http://en.wikipedia.org/wiki/Rijndael_key_schedule - -aes_Rcon = array('B', - '8d01020408102040801b366cd8ab4d9a' - '2f5ebc63c697356ad4b37dfaefc59139' - '72e4d3bd61c29f254a943366cc831d3a' - '74e8cb8d01020408102040801b366cd8' - 'ab4d9a2f5ebc63c697356ad4b37dfaef' - 'c5913972e4d3bd61c29f254a943366cc' - '831d3a74e8cb8d01020408102040801b' - '366cd8ab4d9a2f5ebc63c697356ad4b3' - '7dfaefc5913972e4d3bd61c29f254a94' - '3366cc831d3a74e8cb8d010204081020' - '40801b366cd8ab4d9a2f5ebc63c69735' - '6ad4b37dfaefc5913972e4d3bd61c29f' - '254a943366cc831d3a74e8cb8d010204' - '08102040801b366cd8ab4d9a2f5ebc63' - 'c697356ad4b37dfaefc5913972e4d3bd' - '61c29f254a943366cc831d3a74e8cb'.decode('hex') -) diff --git a/gluon/contrib/pbkdf2.py b/gluon/contrib/pbkdf2.py index 66b8b287..b7a7dd42 100644 --- a/gluon/contrib/pbkdf2.py +++ b/gluon/contrib/pbkdf2.py @@ -40,26 +40,15 @@ :copyright: (c) Copyright 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import print_function import hmac -try: - from hashlib import sha1 -except ImportError: - # hashlib not available. Use the old sha module. - import sha as sha1 - +import hashlib from struct import Struct from operator import xor from itertools import izip, starmap -from collections import deque + _pack_int = Struct('>I').pack -try: - from Crypto.Util.strxor import strxor -except ImportError: - def strxor(a, b): - return ''.join(chr(xor(ord(x), ord(y))) for x,y in izip(a, b)) def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" @@ -72,20 +61,20 @@ def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): key of `keylen` bytes. By default SHA-1 is used as hash function, a different hashlib `hashfunc` can be provided. """ - hashfunc = hashfunc or sha1 + hashfunc = hashfunc or hashlib.sha1 mac = hmac.new(data, None, hashfunc) def _pseudorandom(x, mac=mac): h = mac.copy() h.update(x) - return h.digest() - buf = deque() + return map(ord, h.digest()) + buf = [] for block in xrange(1, -(-keylen // mac.digest_size) + 1): rv = u = _pseudorandom(salt + _pack_int(block)) for i in xrange(iterations - 1): - u = _pseudorandom(u) - rv = strxor(rv, u) + u = _pseudorandom(''.join(map(chr, u))) + rv = starmap(xor, izip(rv, u)) buf.extend(rv) - return ''.join(buf)[:keylen] + return ''.join(map(chr, buf))[:keylen] def test(): @@ -93,14 +82,14 @@ def test(): def check(data, salt, iterations, keylen, expected): rv = pbkdf2_hex(data, salt, iterations, keylen) if rv != expected: - print('Test failed:') - print(' Expected: %s' % expected) - print(' Got: %s' % rv) - print(' Parameters:') - print(' data=%s' % data) - print(' salt=%s' % salt) - print(' iterations=%d' % iterations) - print() + print 'Test failed:' + print ' Expected: %s' % expected + print ' Got: %s' % rv + print ' Parameters:' + print ' data=%s' % data + print ' salt=%s' % salt + print ' iterations=%d' % iterations + print failed.append(1) # From RFC 6070 @@ -115,8 +104,9 @@ def test(): check('pass\x00word', 'sa\x00lt', 4096, 16, '56fa6aa75548099dcc37d7f03425e0c3') # This one is from the RFC but it just takes for ages - check('password', 'salt', 16777216, 20, - 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984') + ##check('password', 'salt', 16777216, 20, + ## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984') + # From Crypt-PBKDF2 check('password', 'ATHENA.MIT.EDUraeburn', 1, 16, 'cdedb5281bb2f801565a1122b2563515') diff --git a/gluon/contrib/pbkdf2_ctypes.py b/gluon/contrib/pbkdf2_ctypes.py index 4d64a12f..b2a36de3 100644 --- a/gluon/contrib/pbkdf2_ctypes.py +++ b/gluon/contrib/pbkdf2_ctypes.py @@ -18,7 +18,6 @@ :license: LGPLv3 """ -from __future__ import print_function import ctypes import ctypes.util @@ -29,7 +28,7 @@ import binascii import sys __all__ = ['pkcs5_pbkdf2_hmac', 'pbkdf2_bin', 'pbkdf2_hex'] -__version__ = '0.99.3' +__version__ = '0.99.4' def _commoncrypto_hashlib_to_crypto_map_get(hashfunc): hashlib_to_crypto_map = {hashlib.sha1: 1, diff --git a/gluon/contrib/pyaes/LICENSE.txt b/gluon/contrib/pyaes/LICENSE.txt new file mode 100644 index 00000000..0417a6c2 --- /dev/null +++ b/gluon/contrib/pyaes/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Richard Moore + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/gluon/contrib/pyaes/README.md b/gluon/contrib/pyaes/README.md new file mode 100644 index 00000000..05b39f3f --- /dev/null +++ b/gluon/contrib/pyaes/README.md @@ -0,0 +1,363 @@ +pyaes +===== + +A pure-Python implmentation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB). + + +Features +-------- + +* Supports all AES key sizes +* Supports all AES common modes +* Pure-Python (no external dependancies) +* BlockFeeder API allows streams to easily be encrypted and decrypted +* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3) + + +API +--- + +All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long. + +To generate a random key use: +```python +import os + +# 128 bit, 192 bit and 256 bit keys +key_128 = os.urandom(16) +key_192 = os.urandom(24) +key_256 = os.urandom(32) +``` + +To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt). + + +### Common Modes of Operation + +There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarilty for completeness. + +Each of the following examples assumes the following key: +```python +import pyaes + +# A 256 bit (32 byte) key +key = "This_key_for_demo_purposes_only!" + +# For some modes of operation we need a random initialization vector +# of 16 bytes +iv = "InitializationVe" +``` + + +#### Counter Mode of Operation (recommended) + +```python +aes = pyaes.AESModeOfOperationCTR(key) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee +# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6 +# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationCTR(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext + +# To use a custom initial value +counter = pyaes.Counter(initial_value = 100) +aes = pyaes.AESModeOfOperationCTR(key, counter = counter) +ciphertext = aes.encrypt(plaintext) + +# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d +# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5 +# \xb2\x0e\\\x0f\x00\x13,\x07''' +print repr(ciphertext) +``` + + +#### Cipher-Block Chaining (recommended) + +```python +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Cipher Feedback + +```python +# Each block into the mode of operation must be a multiple of the segment +# size. For this example we choose 8 bytes. +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +plaintext = "TextMustBeAMultipleOfSegmentSize" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp +# \x85\xfe\xafM\x0c)\xd5\xeb\xaf''' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Output Feedback Mode of Operation + +```python +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s +# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac +# \xa1\xb8\xea\x0f\x8ev\xb5''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Electronic Codebook (NOT recommended) + +```python +aes = pyaes.AESModeOfOperationECB(key) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3' +print repr(ciphertext) + +# Since there is no state stored in this mode of operation, it +# is not necessary to create a new aes object for decryption. +#aes = pyaes.AESModeOfOperationECB(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +### BlockFeeder + +Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data. + +The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary. + +```python +import pyaes + +# Any mode of operation can be used; for this example CBC +key = "This_key_for_demo_purposes_only!" +iv = "InitializationVe" + +ciphertext = '' + +# We can encrypt one line at a time, regardles of length +encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) +for line in file('/etc/passwd'): + ciphertext += encrypter.feed(line) + +# Make a final call to flush any remaining bytes and add paddin +ciphertext += encrypter.feed() + +# We can decrypt the cipher text in chunks (here we split it in half) +decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) +decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2]) +decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:]) + +# Again, make a final call to flush any remaining bytes and strip padding +decrypted += decrypter.feed() + +print file('/etc/passwd').read() == decrypted +``` + +### Stream Feeder + +This is meant to make it even easier to encrypt and decrypt streams and large files. + +```python +import pyaes + +# Any mode of operation can be used; for this example CTR +key = "This_key_for_demo_purposes_only!" + +# Create the mode of operation to encrypt with +mode = pyaes.AESModeOfOperationCTR(key) + +# The input and output files +file_in = file('/etc/passwd') +file_out = file('/tmp/encrypted.bin', 'wb') + +# Encrypt the data as a stream, the file is read in 8kb chunks, be default +pyaes.encrypt_stream(mode, file_in, file_out) + +# Close the files +file_in.close() +file_out.close() +``` + +Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`. + +### AES block cipher + +Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks. + +The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte. + +```python +import pyaes + +# 16 byte block of plain text +plaintext = "Hello World!!!!!" +plaintext_bytes = [ ord(c) for c in plaintext ] + +# 32 byte key (256 bit) +key = "This_key_for_demo_purposes_only!" + +# Our AES instance +aes = pyaes.AES(key) + +# Encrypt! +ciphertext = aes.encrypt(plaintext_bytes) + +# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115] +print repr(ciphertext) + +# Decrypt! +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext_bytes +``` + +What is a key? +-------------- + +This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length. + +With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length. + +Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose. + +Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF: + +``` +# See: https://www.dlitz.net/software/python-pbkdf2/ +import pbkdf2 + +password = "HelloWorld" + +# The crypt PBKDF returns a 48-byte string +key = pbkdf2.crypt(password) + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = key[:16] +key_24 = key[:24] +key_32 = key[:32] +``` + +The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password: + +``` +# See: https://github.com/ricmoo/pyscrypt +import pyscrypt + +password = "HelloWorld" + +# Salt is required, and prevents Rainbow Table attacks +salt = "SeaSalt" + +# N, r, and p are parameters to specify how difficult it should be to +# generate a key; bigger numbers take longer and more memory +N = 1024 +r = 1 +p = 1 + +# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes +# a 6-th parameter, indicating key length +key_16 = pyscrypt.hash(password, salt, N, r, p, 16) +key_24 = pyscrypt.hash(password, salt, N, r, p, 24) +key_32 = pyscrypt.hash(password, salt, N, r, p, 32) +``` + +Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)). + +```python +import hashlib + +password = "HelloWorld" + +# The SHA256 hash algorithm returns a 32-byte string +hashed = hashlib.sha256(password).digest() + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = hashed[:16] +key_24 = hashed[:24] +key_32 = hashed +``` + + + + +Performance +----------- + +There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test). + +Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR. + +Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR. + +The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future. + + +FAQ +--- + +#### Why do this? + +The short answer, *why not?* + +The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?* + +#### How do I get a question I have added? + +E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera. + + +#### Can I give you my money? + +Umm... Ok? :-) + +_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9` diff --git a/gluon/contrib/pyaes/__init__.py b/gluon/contrib/pyaes/__init__.py new file mode 100644 index 00000000..5712f794 --- /dev/null +++ b/gluon/contrib/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/gluon/contrib/pyaes/aes.py b/gluon/contrib/pyaes/aes.py new file mode 100644 index 00000000..c6e8bc02 --- /dev/null +++ b/gluon/contrib/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except Exception: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/gluon/contrib/pyaes/blockfeeder.py b/gluon/contrib/pyaes/blockfeeder.py new file mode 100644 index 00000000..f4113c37 --- /dev/null +++ b/gluon/contrib/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if not data: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/gluon/contrib/pyaes/util.py b/gluon/contrib/pyaes/util.py new file mode 100644 index 00000000..081a3759 --- /dev/null +++ b/gluon/contrib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/gluon/utils.py b/gluon/utils.py index a9e9f842..a7ffdeb6 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -18,24 +18,24 @@ import inspect import time import os import re -import sys import logging import socket import base64 import zlib +import hashlib +import binascii +import hmac +from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from gluon._compat import basestring, pickle, PY2, xrange, to_bytes, to_native _struct_2_long_long = struct.Struct('=QQ') -import hashlib, binascii -from hashlib import md5, sha1, sha224, sha256, sha384, sha512 - try: from Crypto.Cipher import AES + HAVE_AES = True except ImportError: - import gluon.contrib.aes as AES - -import hmac + import gluon.contrib.pyaes as PYAES + HAVE_AES = False if hasattr(hashlib, "pbkdf2_hmac"): def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): @@ -66,11 +66,35 @@ logger = logging.getLogger("web2py") def AES_new(key, IV=None): - """ Returns an AES cipher object and random IV if None specified """ + """Return an AES cipher object and random IV if None specified.""" if IV is None: IV = fast_urandom16() + if HAVE_AES: + return AES.new(key, AES.MODE_CBC, IV), IV + else: + return PYAES.AESModeOfOperationCBC(key, iv=IV), IV - return AES.new(key, AES.MODE_CBC, IV), IV + +def AES_enc(cipher, data): + """Encrypt data with the cipher.""" + if HAVE_AES: + return cipher.encrypt(data) + else: + encrypter = PYAES.Encrypter(cipher) + enc = encrypter.feed(data) + enc += encrypter.feed() + return enc + + +def AES_dec(cipher, data): + """Decrypt data with the cipher.""" + if HAVE_AES: + return cipher.decrypt(data) + else: + decrypter = PYAES.Decrypter(cipher) + dec = decrypter.feed(data) + dec += decrypter.feed() + return dec def compare(a, b): @@ -79,20 +103,17 @@ def compare(a, b): return hmac.compare_digest(a, b) result = len(a) ^ len(b) for i in xrange(len(b)): - result |= ord(a[i%len(a)]) ^ ord(b[i]) + result |= ord(a[i % len(a)]) ^ ord(b[i]) return result == 0 def md5_hash(text): - """ Generates a md5 hash with the given text """ + """Generate an md5 hash with the given text.""" return md5(to_bytes(text)).hexdigest() def simple_hash(text, key='', salt='', digest_alg='md5'): - """ - Generates hash with the given text using the specified - digest hashing algorithm - """ + """Generate hash with the given text using the specified digest algorithm.""" text = to_bytes(text) key = to_bytes(key) salt = to_bytes(salt) @@ -114,9 +135,7 @@ def simple_hash(text, key='', salt='', digest_alg='md5'): def get_digest(value): - """ - Returns a hashlib digest algorithm from a string - """ + """Return a hashlib digest algorithm from a string.""" if not isinstance(value, str): return value value = value.lower() @@ -165,11 +184,11 @@ def pad(s, n=32): def unpad(s, n=32): padlen = s[-1] - if isinstance(padlen,str): - padlen = ord(padlen) # python2 - if (padlen < 1) | (padlen > n): # avoid short-circuit + if isinstance(padlen, str): + padlen = ord(padlen) # python2 + if (padlen < 1) | (padlen > n): # avoid short-circuit # return garbage to minimize side channels - return bytes(bytearray(len(s)*[0])) + return bytes(bytearray(len(s) * [0])) return s[:-padlen] @@ -181,7 +200,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): if not hash_key: hash_key = hashlib.sha256(encryption_key).digest() cipher, IV = AES_new(pad(encryption_key)[:32]) - encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) + encrypted_data = base64.urlsafe_b64encode(IV + AES_enc(cipher, pad(dump))) signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.sha256).hexdigest()) return b'hmac256:' + signature + b':' + encrypted_data @@ -192,7 +211,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None): return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) if components != 2: return None - version,signature,encrypted_data = data.split(b':', 2) + version, signature, encrypted_data = data.split(b':', 2) if version != b'hmac256': return None encryption_key = to_bytes(encryption_key) @@ -205,7 +224,7 @@ def secure_loads(data, encryption_key, hash_key=None, compression_level=None): IV, encrypted_data = encrypted_data[:16], encrypted_data[16:] cipher, _ = AES_new(pad(encryption_key)[:32], IV=IV) try: - data = unpad(cipher.decrypt(encrypted_data)) + data = unpad(AES_dec(cipher, encrypted_data)) if compression_level: data = zlib.decompress(data) return pickle.loads(data) @@ -226,7 +245,7 @@ def secure_dumps_deprecated(data, encryption_key, hash_key=None, compression_lev dump = zlib.compress(dump, compression_level) key = __pad_deprecated(encryption_key)[:32] cipher, IV = AES_new(key) - encrypted_data = base64.urlsafe_b64encode(IV + cipher.encrypt(pad(dump))) + encrypted_data = base64.urlsafe_b64encode(IV + AES_enc(cipher, pad(dump))) signature = to_bytes(hmac.new(to_bytes(hash_key), encrypted_data, hashlib.md5).hexdigest()) return signature + b':' + encrypted_data @@ -248,7 +267,7 @@ def secure_loads_deprecated(data, encryption_key, hash_key=None, compression_lev IV, encrypted_data = encrypted_data[:16], encrypted_data[16:] cipher, _ = AES_new(key, IV=IV) try: - data = cipher.decrypt(encrypted_data) + data = AES_dec(cipher, encrypted_data) data = data.rstrip(b' ') if compression_level: data = zlib.decompress(data) @@ -285,9 +304,9 @@ def initialize_urandom(): frandom = open('/dev/urandom', 'wb') try: if PY2: - frandom.write(''.join(chr(t) for t in ctokens)) # python 2 + frandom.write(''.join(chr(t) for t in ctokens)) else: - frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) # python 3 + frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) finally: frandom.close() except IOError: @@ -300,9 +319,9 @@ def initialize_urandom(): your system does not provide a cryptographically secure entropy source. This is not specific to web2py; consider deploying on a different operating system.""") if PY2: - packed = ''.join(chr(x) for x in ctokens) # python 2 + packed = ''.join(chr(x) for x in ctokens) else: - packed = bytes([]).join(bytes([x]) for x in ctokens) # python 3 + packed = bytes([]).join(bytes([x]) for x in ctokens) unpacked_ctokens = _struct_2_long_long.unpack(packed) return unpacked_ctokens, have_urandom UNPACKED_CTOKENS, HAVE_URANDOM = initialize_urandom() @@ -392,7 +411,7 @@ def is_loopback_ip_address(ip=None, addrinfo=None): Determines whether the address appears to be a loopback address. This assumes that the IP is valid. """ - if addrinfo: # see socket.getaddrinfo() for layout of addrinfo tuple + if addrinfo: # see socket.getaddrinfo() for layout of addrinfo tuple if addrinfo[0] == socket.AF_INET or addrinfo[0] == socket.AF_INET6: ip = addrinfo[4] if not isinstance(ip, basestring): @@ -433,11 +452,10 @@ def local_html_escape(data, quote=False): import html if isinstance(data, str): return html.escape(data, quote=quote) - data = data.replace(b"&", b"&") # Must be done first! + data = data.replace(b"&", b"&") # Must be done first! data = data.replace(b"<", b"<") data = data.replace(b">", b">") if quote: data = data.replace(b'"', b""") data = data.replace(b'\'', b"'") return data - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 904545b4..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pycrypto From a1c1d1357f029cc82048b22f2a73fd7607b5e501 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 4 Oct 2016 00:30:11 +0200 Subject: [PATCH 136/436] remove __pycache__ from the list of apps in case someone wonders how many core devs are actively using the admin app...here's the evidence ^_^' --- applications/admin/controllers/default.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index b7831e58..e9560edf 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -310,12 +310,13 @@ def site(): regex = re.compile('^\w+$') if is_manager(): - apps = [f for f in os.listdir(apath(r=request)) if regex.match(f)] + apps = [a for a in os.listdir(apath(r=request)) if regex.match(f) and + a != '__pycache__'] else: - apps = [f.name for f in db(db.app.owner == auth.user_id).select()] + apps = [a.name for a in db(db.app.owner == auth.user_id).select()] if FILTER_APPS: - apps = [f for f in apps if f in FILTER_APPS] + apps = [a for a in apps if a in FILTER_APPS] apps = sorted(apps, key=lambda a: a.upper()) myplatform = platform.python_version() From b9c1b4d62bdb5b8ece74bb10599a076cfb3380dc Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 4 Oct 2016 00:40:29 +0200 Subject: [PATCH 137/436] fixes #1485, thanks @abastardi once again --- gluon/shell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gluon/shell.py b/gluon/shell.py index 95359c3a..9d93f1a6 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -15,6 +15,7 @@ from __future__ import print_function import os import sys import code +import copy import logging import types import re @@ -167,6 +168,8 @@ def env( sys.stderr.write(e.traceback + '\n') sys.exit(1) + response._view_environment = copy.copy(environment) + environment['__name__'] = '__main__' return environment From 77c0384f79bc646f3333e1c1a163324cdc89e497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 5 Oct 2016 12:41:47 -0400 Subject: [PATCH 138/436] Make pass test_login_bare() and little other improvements --- gluon/tests/test_tools.py | 87 ++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index 1fdf80f1..083d83af 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -525,28 +525,47 @@ class TestAuth(unittest.TestCase): self.auth.define_tables(username=True, signature=False) self.db.define_table('t0', Field('tt'), self.auth.signature) self.auth.enable_record_versioning(self.db) + self.auth.settings.registration_requires_verification = False + self.auth.settings.registration_requires_approval = False # Create a user - self.auth.get_or_create_user(dict(first_name='Bart', - last_name='Simpson', - username='bart', - email='bart@simpson.com', - password='bart_password', - registration_key='bart', - registration_id='' - ), - login=False) - self.db.commit() - self.assertFalse(self.auth.is_logged_in()) - # self.auth.settings.registration_requires_verification = False - # self.auth.settings.registration_requires_approval = False + # Note: get_or_create_user() doesn't seems to create user properly it better to use register_bare() and + # prevent login_bare() test from succeed. db insert the user manually not properly work either. + # Not working + # self.auth.get_or_create_user(dict(first_name='Bart', + # last_name='Simpson', + # username='bart', + # email='bart@simpson.com', + # password='bart_password', + # # registration_key=None, + # #registration_id='bart@simpson.com' + # ), + # login=False) + # Not working + # self.db.auth_user.insert(first_name='Bart', + # last_name='Simpson', + # username='bart', + # email='bart@simpson.com', + # password='bart_password') + # self.db.commit() + self.auth.register_bare(first_name='Bart', + last_name='Simpson', + username='bart', + email='bart@simpson.com', + password='bart_password') def test_assert_setup(self): - self.assertEqual(self.db(self.db.auth_user.username == 'bart').select().first()['username'], 'bart') self.assertTrue('auth_user' in self.db) self.assertTrue('auth_group' in self.db) self.assertTrue('auth_membership' in self.db) self.assertTrue('auth_permission' in self.db) self.assertTrue('auth_event' in self.db) + bart_record = self.db(self.db.auth_user.username == 'bart').select().first() + self.assertEqual(bart_record['username'], 'bart') + self.assertEqual(bart_record['registration_key'], '') + bart_id = self.db(self.db.auth_user.username == 'bart').select().first().id + bart_group_id = self.db(self.db.auth_group.role == 'user_{0}'.format(bart_id)).select().first().id + self.assertTrue(self.db((self.db.auth_membership.group_id == bart_group_id) & + (self.db.auth_membership.user_id == bart_id)).select().first()) # Just calling many form functions def test_basic_blank_forms(self): @@ -652,16 +671,31 @@ class TestAuth(unittest.TestCase): # TODO: def test_login_user(self): # TODO: def test__get_login_settings(self): - # login_bare() seems broken see my post on web2py-developpers - # commented for now - # def test_login_bare(self): - # # The following test case should succeed but failed as I never received the user record but False - # self.auth.login_bare(username='bart', password='bart_password') - # self.assertTrue(self.auth.is_logged_in()) - # # Failing login because bad_password - # self.assertEqual(self.auth.login_bare(username='bart', password='wrong_password'), False) - # self.auth.logout_bare() - # self.db.auth_user.truncate() + def test_login_bare(self): + self.auth.login_bare(username='bart', password='bart_password') + self.assertTrue(self.auth.is_logged_in()) + self.auth.logout_bare() + # Failing login because wrong_password + self.assertFalse(self.auth.login_bare(username='bart', password='wrong_password')) + # NOTE : The following failed for some reason, but I can't find out why + # self.auth = Auth(self.db) + # self.auth.define_tables(username=False, signature=False) + # self.auth.settings.registration_requires_verification = False + # self.auth.settings.registration_requires_approval = False + # self.auth.register_bare(first_name='Omer', + # last_name='Simpson', + # # no username field passed, failed with : + # # ValueError('register_bare: userfield not provided or invalid') + # # Or + # # username='omer', + # # Or + # # username='omer@simpson.com', + # # In either previous cases, it failed with : + # # self.assertTrue(self.auth.is_logged_in()) AssertionError: False is not true + # email='omer@simpson.com', + # password='omer_password') + # self.auth.login_bare(username='omer@sympson.com', password='omer_password') + # self.assertTrue(self.auth.is_logged_in()) def test_register_bare(self): # corner case empty register call register_bare without args @@ -817,7 +851,10 @@ class TestAuth(unittest.TestCase): self.myassertRaisesRegex(HTTP, "400*", self.auth.allows_jwt) # TODO: def test_requires(self): - # TODO: def test_requires_login(self): + + # def test_login(self): + # Basic testing above in "test_basic_blank_forms()" could be refined here + # TODO: def test_requires_login_or_token(self): # TODO: def test_requires_membership(self): # TODO: def test_requires_permission(self): From 27ce91474d24389dc72062214244e7100f154f9d Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 7 Oct 2016 13:10:44 +0200 Subject: [PATCH 139/436] fix issue with regex --- applications/admin/controllers/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index e9560edf..6ad8b332 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -310,7 +310,7 @@ def site(): regex = re.compile('^\w+$') if is_manager(): - apps = [a for a in os.listdir(apath(r=request)) if regex.match(f) and + apps = [a for a in os.listdir(apath(r=request)) if regex.match(a) and a != '__pycache__'] else: apps = [a.name for a in db(db.app.owner == auth.user_id).select()] From 39e8cfb11a72dd7eda316e74ccb49590214beb50 Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 7 Oct 2016 21:14:56 +0200 Subject: [PATCH 140/436] fix ccache page in appadmin, thanks MDSIII fixes the ccache page to now raise an exception if a dict is stored in cache --- applications/admin/controllers/appadmin.py | 9 ++++----- applications/examples/controllers/appadmin.py | 9 ++++----- applications/welcome/controllers/appadmin.py | 3 +-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 3060945c..c5ba354e 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -213,7 +213,7 @@ def select(): if is_imap: step = 3 - + stop = start + step table = None @@ -409,7 +409,7 @@ def ccache(): import copy import time import math - from gluon import portalocker + from pydal.contrib import portalocker ram = { 'entries': 0, @@ -421,7 +421,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -465,8 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: - value = cache.disk.storage[key] - if isinstance(value[1], dict): + if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] try: diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 3060945c..c5ba354e 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -213,7 +213,7 @@ def select(): if is_imap: step = 3 - + stop = start + step table = None @@ -409,7 +409,7 @@ def ccache(): import copy import time import math - from gluon import portalocker + from pydal.contrib import portalocker ram = { 'entries': 0, @@ -421,7 +421,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -465,8 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: - value = cache.disk.storage[key] - if isinstance(value[1], dict): + if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] try: diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 4d4e965f..c5ba354e 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -465,8 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: - value = cache.disk.storage[key] - if isinstance(value[1], dict): + if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] try: From 555f2ad0405abf54356941039aac3fcffdf7f5b5 Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 7 Oct 2016 21:20:10 +0200 Subject: [PATCH 141/436] fixes #1493 (at @abastardi's bidding) --- gluon/dal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index d73720cf..95476a9f 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -75,10 +75,10 @@ def _default_validators(db, field): if field.unique: requires.insert(0, validators.IS_NOT_IN_DB(db, field)) excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] - if (field.notnull or field.unique) and not field_type in excluded_fields: + if (field.notnull or field.unique) and field_type not in excluded_fields: requires.insert(0, validators.IS_NOT_EMPTY()) elif not field.notnull and not field.unique and requires: - requires[0] = validators.IS_EMPTY_OR(requires[0], null='' if field in ('string', 'text', 'password') else None) + requires[0] = validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) return requires from gluon.serializers import custom_json, xml From 8efd0cb01fe334497f0a2efaf21c624a7477b5de Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 7 Oct 2016 21:23:00 +0200 Subject: [PATCH 142/436] fixes #1489 --- gluon/rocket.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gluon/rocket.py b/gluon/rocket.py index c41ac32d..ba13faac 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -11,7 +11,8 @@ import errno import socket import logging import platform -from gluon._compat import iteritems, to_bytes, StringIO, urllib_unquote +from gluon._compat import iteritems, to_bytes, StringIO +from gluon._compat import urllib_unquote, to_native # Define Constants VERSION = '1.2.6' @@ -1661,7 +1662,7 @@ class WSGIWorker(Worker): try: peercert = conn.socket.getpeercert(binary_form=True) environ['SSL_CLIENT_RAW_CERT'] = \ - peercert and ssl.DER_cert_to_PEM_cert(peercert) + peercert and to_native(ssl.DER_cert_to_PEM_cert(peercert)) except Exception: print(sys.exc_info()[1]) else: From 87489f33630042842fb5379ab90e8e568c0603be Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 7 Oct 2016 22:43:06 +0200 Subject: [PATCH 143/436] fixes #1491 --- gluon/shell.py | 2 +- gluon/tests/test_scheduler.py | 42 ++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/gluon/shell.py b/gluon/shell.py index 9d93f1a6..b6c8f27b 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -254,7 +254,7 @@ def run( die(errmsg) if f: - exec('print %s()' % f, _env) + exec('print( %s())' % f, _env) return _env.update(exec_pythonrc()) diff --git a/gluon/tests/test_scheduler.py b/gluon/tests/test_scheduler.py index b473fe7b..ee1270fb 100644 --- a/gluon/tests/test_scheduler.py +++ b/gluon/tests/test_scheduler.py @@ -601,7 +601,8 @@ class testForSchedulerRunnerBase(BaseTestScheduler): fdest = os.path.join(current.request.folder, 'models', 'scheduler.py') os.unlink(fdest) additional_files = [ - os.path.join(current.request.folder, 'private', 'demo8.pholder') + os.path.join(current.request.folder, 'private', 'demo8.pholder'), + os.path.join(current.request.folder, 'views', 'issue_1485_2.html'), ] for f in additional_files: try: @@ -609,6 +610,12 @@ class testForSchedulerRunnerBase(BaseTestScheduler): except: pass + def writeview(self, content, dest=None): + from gluon import current + fdest = os.path.join(current.request.folder, 'views', dest) + with open(fdest, 'w') as q: + q.write(content) + def writefunction(self, content, initlines=None): from gluon import current fdest = os.path.join(current.request.folder, 'models', 'scheduler.py') @@ -620,6 +627,9 @@ from gluon.scheduler import Scheduler db_dal = os.path.abspath(os.path.join(request.folder, '..', '..', 'dummy2.db')) sched_dal = DAL('sqlite://%s' % db_dal, folder=os.path.dirname(db_dal)) sched = Scheduler(sched_dal, max_empty_runs=15, migrate=False, heartbeat=1) +def termination(): + sched.terminate() + sched_dal.commit() """ with open(fdest, 'w') as q: q.write(initlines) @@ -699,10 +709,11 @@ def demo4(): timeout1 = s.queue_task('demo4', timeout=5) timeout2 = s.queue_task('demo4') progress = s.queue_task('demo6', sync_output=2) + termination = s.queue_task('termination') self.db.commit() self.writefunction(r""" def demo3(): - time.sleep(15) + time.sleep(3) print(1/0) return None @@ -712,7 +723,7 @@ def demo4(): return dict(a=1, b=2) def demo5(): - time.sleep(15) + time.sleep(3) print("I'm printing something") rtn = dict(a=1, b=2) @@ -758,6 +769,7 @@ def demo6(): immediate = s.queue_task('demo1', ['a', 'b'], dict(c=1, d=2), immediate=True) env = s.queue_task('demo7') drift = s.queue_task('demo1', ['a', 'b'], dict(c=1, d=2), period=93, prevent_drift=True) + termination = s.queue_task('termination') self.db.commit() self.writefunction(r""" def demo1(*args,**vars): @@ -844,27 +856,41 @@ def demo8(): ] self.exec_asserts(res, 'FAILED_CONSECUTIVE') - def testHugeResult(self): + def testRegressions(self): s = Scheduler(self.db) huge_result = s.queue_task('demo10', retry_failed=1, period=1) + issue_1485 = s.queue_task('issue_1485') + termination = s.queue_task('termination') self.db.commit() self.writefunction(r""" def demo10(): res = 'a' * 99999 return dict(res=res) + +def issue_1485(): + return response.render('issue_1485.html', dict(variable='abc')) """) + self.writeview(r"""{{=variable}}""", 'issue_1485.html') ret = self.exec_sched() # process finished just fine self.assertEqual(ret, 0) # huge_result - checks - task = s.task_status(huge_result.id, output=True) + task_huge = s.task_status(huge_result.id, output=True) res = [ - ("task status completed", task.scheduler_task.status == 'COMPLETED'), - ("task times_run is 1", task.scheduler_task.times_run == 1), - ("result is the correct one", task.result == dict(res='a' * 99999)) + ("task status completed", task_huge.scheduler_task.status == 'COMPLETED'), + ("task times_run is 1", task_huge.scheduler_task.times_run == 1), + ("result is the correct one", task_huge.result == dict(res='a' * 99999)) ] self.exec_asserts(res, 'HUGE_RESULT') + task_issue_1485 = s.task_status(issue_1485.id, output=True) + res = [ + ("task status completed", task_issue_1485.scheduler_task.status == 'COMPLETED'), + ("task times_run is 1", task_issue_1485.scheduler_task.times_run == 1), + ("result is the correct one", task_issue_1485.result == 'abc') + ] + self.exec_asserts(res, 'issue_1485') + if __name__ == '__main__': unittest.main() From c1d81a8f601716b20558c7ec83780bbc293abd1a Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 8 Oct 2016 12:22:47 +0200 Subject: [PATCH 144/436] fix few app admin py3 issues, updated qdb (close #1471) --- applications/admin/controllers/appadmin.py | 2 +- applications/admin/controllers/debug.py | 18 +- applications/admin/controllers/webservices.py | 36 +- applications/admin/controllers/wizard.py | 22 +- applications/examples/controllers/appadmin.py | 2 +- applications/welcome/controllers/appadmin.py | 2 +- gluon/contrib/{qdb.py => dbg.py} | 514 +++++++++++------- gluon/contrib/populate.py | 11 +- gluon/debug.py | 32 +- 9 files changed, 395 insertions(+), 244 deletions(-) rename gluon/contrib/{qdb.py => dbg.py} (70%) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 3060945c..df6c7f94 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -482,7 +482,7 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys = list(ram) # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] ram_keys.remove('ratio') ram_keys.remove('oldest') for key in ram_keys: diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 4f04ab09..447948d6 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -4,7 +4,7 @@ import gluon.dal import gluon.html import gluon.validators import code -from gluon.debug import communicate, web_debugger, qdb_debugger +from gluon.debug import communicate, web_debugger, dbg_debugger from gluon._compat import thread import pydoc @@ -39,7 +39,7 @@ def reset(): return 'done' -# new implementation using qdb +# new implementation using dbg def interact(): app = request.args(0) or 'admin' @@ -149,7 +149,7 @@ def breakpoints(): if form.accepts(request.vars, session): filename = os.path.join(request.env['applications_parent'], 'applications', form.vars.filename) - err = qdb_debugger.do_set_breakpoint(filename, + err = dbg_debugger.do_set_breakpoint(filename, form.vars.lineno, form.vars.temporary, form.vars.condition) @@ -158,13 +158,13 @@ def breakpoints(): for item in request.vars: if item[:7] == 'delete_': - qdb_debugger.do_clear(item[7:]) + dbg_debugger.do_clear(item[7:]) breakpoints = [{'number': bp[0], 'filename': os.path.basename(bp[1]), 'path': bp[1], 'lineno': bp[2], 'temporary': bp[3], 'enabled': bp[4], 'hits': bp[5], 'condition': bp[6]} - for bp in qdb_debugger.do_list_breakpoint()] + for bp in dbg_debugger.do_list_breakpoint()] return dict(breakpoints=breakpoints, form=form) @@ -193,18 +193,18 @@ def toggle_breakpoint(): else: lineno = None if lineno is not None: - for bp in qdb_debugger.do_list_breakpoint(): + for bp in dbg_debugger.do_list_breakpoint(): no, bp_filename, bp_lineno, temporary, enabled, hits, cond = bp # normalize path name: replace slashes, references, etc... bp_filename = os.path.normpath(os.path.normcase(bp_filename)) if filename == bp_filename and lineno == bp_lineno: - err = qdb_debugger.do_clear_breakpoint(filename, lineno) + err = dbg_debugger.do_clear_breakpoint(filename, lineno) response.flash = T("Removed Breakpoint on %s at line %s", ( filename, lineno)) ok = False break else: - err = qdb_debugger.do_set_breakpoint(filename, lineno) + err = dbg_debugger.do_set_breakpoint(filename, lineno) response.flash = T("Set Breakpoint on %s at line %s: %s") % ( filename, lineno, err or T('successful')) ok = True @@ -224,7 +224,7 @@ def list_breakpoints(): 'applications', request.vars.filename) # normalize path name: replace slashes, references, etc... filename = os.path.normpath(os.path.normcase(filename)) - for bp in qdb_debugger.do_list_breakpoint(): + for bp in dbg_debugger.do_list_breakpoint(): no, bp_filename, bp_lineno, temporary, enabled, hits, cond = bp # normalize path name: replace slashes, references, etc... bp_filename = os.path.normpath(os.path.normcase(bp_filename)) diff --git a/applications/admin/controllers/webservices.py b/applications/admin/controllers/webservices.py index 4f5a352c..9ae704db 100644 --- a/applications/admin/controllers/webservices.py +++ b/applications/admin/controllers/webservices.py @@ -81,44 +81,44 @@ def install(app_name, filename, data, overwrite=True): @service.jsonrpc def attach_debugger(host='localhost', port=6000, authkey='secret password'): - import gluon.contrib.qdb as qdb + import gluon.contrib.dbg as dbg import gluon.debug from multiprocessing.connection import Listener if isinstance(authkey, unicode): authkey = authkey.encode('utf8') - if not hasattr(gluon.debug, 'qdb_listener'): + if not hasattr(gluon.debug, 'dbg_listener'): # create a remote debugger server and wait for connection address = (host, port) # family is deduced to be 'AF_INET' - gluon.debug.qdb_listener = Listener(address, authkey=authkey) - gluon.debug.qdb_connection = gluon.debug.qdb_listener.accept() + gluon.debug.dbg_listener = Listener(address, authkey=authkey) + gluon.debug.dbg_connection = gluon.debug.dbg_listener.accept() # create the backend - gluon.debug.qdb_debugger = qdb.Qdb(gluon.debug.qdb_connection) - gluon.debug.dbg = gluon.debug.qdb_debugger + gluon.debug.dbg_debugger = dbg.Qdb(gluon.debug.dbg_connection) + gluon.debug.dbg = gluon.debug.dbg_debugger # welcome message (this should be displayed on the frontend) - print('debugger connected to', gluon.debug.qdb_listener.last_accepted) + print('debugger connected to', gluon.debug.dbg_listener.last_accepted) return True # connection successful! @service.jsonrpc def detach_debugger(): - import gluon.contrib.qdb as qdb + import gluon.contrib.dbg as dbg import gluon.debug # stop current debugger - if gluon.debug.qdb_debugger: + if gluon.debug.dbg_debugger: try: - gluon.debug.qdb_debugger.do_quit() + gluon.debug.dbg_debugger.do_quit() except: pass - if hasattr(gluon.debug, 'qdb_listener'): - if gluon.debug.qdb_connection: - gluon.debug.qdb_connection.close() - del gluon.debug.qdb_connection - if gluon.debug.qdb_listener: - gluon.debug.qdb_listener.close() - del gluon.debug.qdb_listener - gluon.debug.qdb_debugger = None + if hasattr(gluon.debug, 'dbg_listener'): + if gluon.debug.dbg_connection: + gluon.debug.dbg_connection.close() + del gluon.debug.dbg_connection + if gluon.debug.dbg_listener: + gluon.debug.dbg_listener.close() + del gluon.debug.dbg_listener + gluon.debug.dbg_debugger = None return True diff --git a/applications/admin/controllers/wizard.py b/applications/admin/controllers/wizard.py index f4511b96..c3d53548 100644 --- a/applications/admin/controllers/wizard.py +++ b/applications/admin/controllers/wizard.py @@ -7,7 +7,7 @@ import pickle import urllib import glob from gluon.admin import app_create, plugin_install -from gluon.fileutils import abspath, read_file, write_file +from gluon.fileutils import abspath, read_file, write_file, open_file def reset(session): @@ -67,7 +67,7 @@ def index(): '..', app, 'wizard.metadata')) if os.path.exists(meta): try: - metafile = open(meta, 'rb') + metafile = open_file(meta, 'rb') try: session.app = pickle.load(metafile) finally: @@ -402,7 +402,7 @@ db.auth_user.email.requires = ( def fix_db(filename): params = dict(session.app['params']) - content = read_file(filename, 'rb') + content = read_file(filename, 'r') if 'auth_user' in session.app['tables']: auth_user = make_table('auth_user', session.app['table_auth_user']) content = content.replace('sqlite://storage.sqlite', @@ -424,7 +424,7 @@ auth.settings.login_form = RPXAccount(request, domain = settings.login_config.split(':')[0], url = "http://%s/%s/default/user/login" % (request.env.http_host,request.application)) """ - write_file(filename, content, 'wb') + write_file(filename, content, 'w') def make_menu(pages): @@ -493,7 +493,7 @@ def create(options): ### save metadata in newapp/wizard.metadata try: meta = os.path.join(request.folder, '..', app, 'wizard.metadata') - file = open(meta, 'wb') + file = open_file(meta, 'wb') pickle.dump(session.app, file) file.close() except IOError: @@ -521,7 +521,7 @@ def create(options): ### write configuration file into newapp/models/0.py model = os.path.join(request.folder, '..', app, 'models', '0.py') - file = open(model, 'wb') + file = open_file(model, 'w') try: file.write("from gluon.storage import Storage\n") file.write("settings = Storage()\n\n") @@ -534,7 +534,7 @@ def create(options): ### write configuration file into newapp/models/menu.py if options.generate_menu: model = os.path.join(request.folder, '..', app, 'models', 'menu.py') - file = open(model, 'wb') + file = open_file(model, 'w') try: file.write(make_menu(session.app['pages'])) finally: @@ -548,7 +548,7 @@ def create(options): if options.generate_model: model = os.path.join( request.folder, '..', app, 'models', 'db_wizard.py') - file = open(model, 'wb') + file = open_file(model, 'w') try: file.write('### we prepend t_ to tablenames and f_ to fieldnames for disambiguity\n\n') tables = sort_tables(session.app['tables']) @@ -564,7 +564,7 @@ def create(options): if os.path.exists(model): os.unlink(model) if options.populate_database and session.app['tables']: - file = open(model, 'wb') + file = open_file(model, 'w') try: file.write(populate(session.app['tables'])) finally: @@ -574,7 +574,7 @@ def create(options): if options.generate_controller: controller = os.path.join( request.folder, '..', app, 'controllers', 'default.py') - file = open(controller, 'wb') + file = open_file(controller, 'w') try: file.write("""# -*- coding: utf-8 -*- ### required - do no delete @@ -594,7 +594,7 @@ def call(): return service() for page in session.app['pages']: view = os.path.join( request.folder, '..', app, 'views', 'default', page + '.html') - file = open(view, 'wb') + file = open_file(view, 'w') try: file.write( make_view(page, session.app.get('page_' + page, ''))) diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 3060945c..df6c7f94 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -482,7 +482,7 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys = list(ram) # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] ram_keys.remove('ratio') ram_keys.remove('oldest') for key in ram_keys: diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 4d4e965f..213b8683 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -482,7 +482,7 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys = list(ram) # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] ram_keys.remove('ratio') ram_keys.remove('oldest') for key in ram_keys: diff --git a/gluon/contrib/qdb.py b/gluon/contrib/dbg.py similarity index 70% rename from gluon/contrib/qdb.py rename to gluon/contrib/dbg.py index 6e32e6f7..0dcbcf15 100644 --- a/gluon/contrib/qdb.py +++ b/gluon/contrib/dbg.py @@ -1,13 +1,14 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding:utf-8 -"Queues(Pipe)-based independent remote client-server Python Debugger" +"Queues(Pipe)-based independent remote client-server Python Debugger (new-py3)" + from __future__ import print_function __author__ = "Mariano Reingart (reingart@gmail.com)" __copyright__ = "Copyright (C) 2011 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.01b" +__version__ = "1.5.2" # remote debugger queue-based (jsonrpc-like interface): # - bidirectional communication (request - response calls in both ways) @@ -24,13 +25,19 @@ import traceback import cmd import pydoc import threading +import collections + + +# Speed Ups: global variables +breaks = [] class Qdb(bdb.Bdb): "Qdb Debugger Backend" def __init__(self, pipe, redirect_stdio=True, allow_interruptions=False, - skip=[__name__]): + use_speedups=True, skip=[__name__]): + global breaks kwargs = {} if sys.version_info > (2, 7): kwargs['skip'] = skip @@ -38,12 +45,15 @@ class Qdb(bdb.Bdb): self.frame = None self.i = 1 # sequential RPC call id self.waiting = False - self.pipe = pipe # for communication + self.pipe = pipe # for communication self._wait_for_mainpyfile = False self._wait_for_breakpoint = False self.mainpyfile = "" self._lineno = None # last listed line numbre + # ignore filenames (avoid spurious interaction specially on py2) + self.ignore_files = [self.canonic(f) for f in (__file__, bdb.__file__)] # replace system standard input and output (send them thru the pipe) + self.old_stdio = sys.stdin, sys.stdout, sys.stderr if redirect_stdio: sys.stdin = self sys.stdout = self @@ -54,6 +64,10 @@ class Qdb(bdb.Bdb): self.allow_interruptions = allow_interruptions self.burst = 0 # do not send notifications ("burst" mode) self.params = {} # optional parameters for interaction + + # flags to reduce overhead (only stop at breakpoint or interrupt) + self.use_speedups = use_speedups + self.fast_continue = False def pull_actions(self): # receive a remote procedure call from the frontend: @@ -62,14 +76,14 @@ class Qdb(bdb.Bdb): request = self.pipe.recv() if request.get("method") == 'run': return None - response = {'version': '1.1', 'id': request.get('id'), - 'result': None, + response = {'version': '1.1', 'id': request.get('id'), + 'result': None, 'error': None} try: # dispatch message (JSON RPC like) method = getattr(self, request['method']) - response['result'] = method.__call__(*request['args'], - **request.get('kwargs', {})) + response['result'] = method.__call__(*request['args'], + **request.get('kwargs', {})) except Exception as e: response['error'] = {'code': 0, 'message': str(e)} # send the result for normal method calls, not for notifications @@ -83,9 +97,17 @@ class Qdb(bdb.Bdb): # check for non-interaction rpc (set_breakpoint, interrupt) while self.allow_interruptions and self.pipe.poll(): self.pull_actions() + # check for non-interaction rpc (set_breakpoint, interrupt) + while self.pipe.poll(): + self.pull_actions() + if (frame.f_code.co_filename, frame.f_lineno) not in breaks and \ + self.fast_continue: + return self.trace_dispatch # process the frame (see Bdb.trace_dispatch) + ##if self.fast_continue: + ## return self.trace_dispatch if self.quitting: - return # None + return # None if event == 'line': return self.dispatch_line(frame) if event == 'call': @@ -102,13 +124,13 @@ class Qdb(bdb.Bdb): if self._wait_for_mainpyfile or self._wait_for_breakpoint: return if self.stop_here(frame): - self.interaction(frame, None) - + self.interaction(frame) + def user_line(self, frame): """This function is called when we stop or break at this line.""" if self._wait_for_mainpyfile: if (not self.canonic(frame.f_code.co_filename).startswith(self.mainpyfile) - or frame.f_lineno <= 0): + or frame.f_lineno<= 0): return self._wait_for_mainpyfile = 0 if self._wait_for_breakpoint: @@ -125,14 +147,15 @@ class Qdb(bdb.Bdb): extype, exvalue, trace = info # pre-process stack trace as it isn't pickeable (cannot be sent pure) msg = ''.join(traceback.format_exception(extype, exvalue, trace)) - trace = traceback.extract_tb(trace) + # in python3.5, convert FrameSummary to tuples (py2.7+ compatibility) + tb = [tuple(fs) for fs in traceback.extract_tb(trace)] title = traceback.format_exception_only(extype, exvalue)[0] # send an Exception notification - msg = {'method': 'exception', - 'args': (title, extype.__name__, exvalue, trace, msg), + msg = {'method': 'exception', + 'args': (title, extype.__name__, repr(exvalue), tb, msg), 'id': None} self.pipe.send(msg) - self.interaction(frame, info) + self.interaction(frame) def run(self, code, interp=None, *args, **kwargs): try: @@ -151,39 +174,58 @@ class Qdb(bdb.Bdb): # The script has to run in __main__ namespace (clear it) import __main__ import imp + filename = os.path.abspath(filename) __main__.__dict__.clear() - __main__.__dict__.update({"__name__": "__main__", - "__file__": filename, + __main__.__dict__.update({"__name__" : "__main__", + "__file__" : filename, "__builtins__": __builtins__, - "imp": imp, # need for run - }) + "imp" : imp, # need for run + }) - # avoid stopping before we reach the main script + # avoid stopping before we reach the main script self._wait_for_mainpyfile = 1 self.mainpyfile = self.canonic(filename) self._user_requested_quit = 0 - statement = 'imp.load_source("__main__", "%s")' % filename - # notify and wait frontend to set initial params and breakpoints - self.pipe.send({'method': 'startup', 'args': (__version__, )}) + if sys.version_info>(3,0): + statement = 'imp.load_source("__main__", "%s")' % filename + else: + statement = 'execfile(%r)' % filename + self.startup() + self.run(statement) + + def startup(self): + "Notify and wait frontend to set initial params and breakpoints" + # send some useful info to identify session + thread = threading.current_thread() + # get the caller module filename + frame = sys._getframe() + fn = self.canonic(frame.f_code.co_filename) + while frame.f_back and self.canonic(frame.f_code.co_filename) == fn: + frame = frame.f_back + args = [__version__, os.getpid(), thread.name, " ".join(sys.argv), + frame.f_code.co_filename] + self.pipe.send({'method': 'startup', 'args': args}) while self.pull_actions() is not None: pass - self.run(statement) # General interaction function - def interaction(self, frame, info=None): + def interaction(self, frame): # chache frame locals to ensure that modifications are not overwritten self.frame_locals = frame and frame.f_locals or {} # extract current filename and line number code, lineno = frame.f_code, frame.f_lineno - filename = code.co_filename + filename = self.canonic(code.co_filename) basename = os.path.basename(filename) + # check if interaction should be ignored (i.e. debug modules internals) + if filename in self.ignore_files: + return message = "%s:%s" % (basename, lineno) if code.co_name != "?": message = "%s: %s()" % (message, code.co_name) - # wait user events - self.waiting = True + # wait user events + self.waiting = True self.frame = frame try: while self.waiting: @@ -202,11 +244,10 @@ class Qdb(bdb.Bdb): if self.params.get('environment'): kwargs['environment'] = self.do_environment() self.pipe.send({'method': 'interaction', 'id': None, - 'args': (filename, self.frame.f_lineno, line), - 'kwargs': kwargs}) + 'args': (filename, self.frame.f_lineno, line), + 'kwargs': kwargs}) self.pull_actions() - finally: self.waiting = False self.frame = None @@ -228,6 +269,8 @@ class Qdb(bdb.Bdb): frame = sys._getframe().f_back self._wait_for_mainpyfile = frame.f_code.co_filename self._wait_for_breakpoint = 0 + # reinitialize debugger internal settings + self.fast_continue = False bdb.Bdb.set_trace(self, frame) # Command definitions, called by interaction() @@ -235,34 +278,38 @@ class Qdb(bdb.Bdb): def do_continue(self): self.set_continue() self.waiting = False + self.fast_continue = self.use_speedups def do_step(self): self.set_step() self.waiting = False + self.fast_continue = False def do_return(self): self.set_return(self.frame) self.waiting = False + self.fast_continue = False def do_next(self): self.set_next(self.frame) self.waiting = False + self.fast_continue = False def interrupt(self): - self.set_step() + self.set_trace() + self.fast_continue = False def do_quit(self): self.set_quit() self.waiting = False + self.fast_continue = False def do_jump(self, lineno): arg = int(lineno) try: self.frame.f_lineno = arg - return arg except ValueError as e: - print('*** Jump failed:', e) - return False + return str(e) def do_list(self, arg): last = None @@ -272,7 +319,7 @@ class Qdb(bdb.Bdb): else: first = arg elif not self._lineno: - first = max(1, self.frame.f_lineno - 5) + first = max(1, self.frame.f_lineno - 5) else: first = self._lineno + 1 if last is None: @@ -280,11 +327,11 @@ class Qdb(bdb.Bdb): filename = self.frame.f_code.co_filename breaklist = self.get_file_breaks(filename) lines = [] - for lineno in range(first, last + 1): + for lineno in range(first, last+1): line = linecache.getline(filename, lineno, self.frame.f_globals) if not line: - lines.append((filename, lineno, '', current, "\n")) + lines.append((filename, lineno, '', "", "\n")) break else: breakpoint = "B" if lineno in breaklist else "" @@ -297,6 +344,8 @@ class Qdb(bdb.Bdb): return open(filename, "Ur").read() def do_set_breakpoint(self, filename, lineno, temporary=0, cond=None): + global breaks # list for speedups! + breaks.append((filename.replace("\\", "/"), int(lineno))) return self.set_break(filename, int(lineno), temporary, cond) def do_list_breakpoint(self): @@ -304,8 +353,8 @@ class Qdb(bdb.Bdb): if self.breaks: # There's at least one for bp in bdb.Breakpoint.bpbynumber: if bp: - breaks.append((bp.number, bp.file, bp.line, - bp.temporary, bp.enabled, bp.hits, bp.cond, )) + breaks.append((bp.number, bp.file, bp.line, + bp.temporary, bp.enabled, bp.hits, bp.cond, )) return breaks def do_clear_breakpoint(self, filename, lineno): @@ -321,24 +370,33 @@ class Qdb(bdb.Bdb): print('*** DO_CLEAR failed', err) def do_eval(self, arg, safe=True): - ret = eval(arg, self.frame.f_globals, - self.frame_locals) + if self.frame: + ret = eval(arg, self.frame.f_globals, + self.frame_locals) + else: + ret = RPCError("No current frame available to eval") if safe: ret = pydoc.cram(repr(ret), 255) return ret - def do_exec(self, arg): - locals = self.frame_locals - globals = self.frame.f_globals - code = compile(arg + '\n', '', 'single') - save_displayhook = sys.displayhook - self.displayhook_value = None - try: - sys.displayhook = self.displayhook - exec code in globals, locals - finally: - sys.displayhook = save_displayhook - return self.displayhook_value + def do_exec(self, arg, safe=True): + if not self.frame: + ret = RPCError("No current frame available to exec") + else: + locals = self.frame_locals + globals = self.frame.f_globals + code = compile(arg + '\n', '', 'single') + save_displayhook = sys.displayhook + self.displayhook_value = None + try: + sys.displayhook = self.displayhook + exec(code, globals, locals) + ret = self.displayhook_value + finally: + sys.displayhook = save_displayhook + if safe: + ret = pydoc.cram(repr(ret), 255) + return ret def do_where(self): "print_stack_trace" @@ -355,29 +413,33 @@ class Qdb(bdb.Bdb): env = {'locals': {}, 'globals': {}} # converts the frame global and locals to a short text representation: if self.frame: - for name, value in self.frame_locals.items(): - env['locals'][name] = pydoc.cram(repr( - value), 255), repr(type(value)) - for name, value in self.frame.f_globals.items(): - env['globals'][name] = pydoc.cram(repr( - value), 20), repr(type(value)) + for scope, max_length, vars in ( + ("locals", 255, list(self.frame_locals.items())), + ("globals", 20, list(self.frame.f_globals.items())), ): + for (name, value) in vars: + try: + short_repr = pydoc.cram(repr(value), max_length) + except Exception as e: + # some objects cannot be represented... + short_repr = "**exception** %s" % repr(e) + env[scope][name] = (short_repr, repr(type(value))) return env def get_autocomplete_list(self, expression): "Return list of auto-completion options for expression" try: - obj = self.do_eval(expression) + obj = self.do_eval(expression, safe=False) except: return [] else: return dir(obj) - + def get_call_tip(self, expression): "Return list of auto-completion options for expression" try: obj = self.do_eval(expression) except Exception as e: - return ('', '', str(e)) + return ('', '', str(e)) else: name = '' try: @@ -405,7 +467,7 @@ class Qdb(bdb.Bdb): break if f is not None: drop_self = 1 - elif callable(obj): + elif isinstance(obj, collections.Callable): # use the obj as a function by default f = obj # Get the __call__ method instead. @@ -416,7 +478,7 @@ class Qdb(bdb.Bdb): if f: argspec = inspect.formatargspec(*inspect.getargspec(f)) doc = '' - if callable(obj): + if isinstance(obj, collections.Callable): try: doc = inspect.getdoc(obj) except: @@ -442,15 +504,21 @@ class Qdb(bdb.Bdb): self.waiting = False self.frame = None - def post_mortem(self, t=None): + def post_mortem(self, info=None): + "Debug an un-handled python exception" + # check if post mortem mode is enabled: + if not self.params.get('postmortem', True): + return # handling the default - if t is None: + if info is None: # sys.exc_info() returns (type, value, traceback) if an exception is # being handled, otherwise it returns None - t = sys.exc_info()[2] - if t is None: - raise ValueError("A valid traceback must be passed if no " - "exception is being handled") + info = sys.exc_info() + # extract the traceback object: + t = info[2] + if t is None: + raise ValueError("A valid traceback must be passed if no " + "exception is being handled") self.reset() # get last frame: while t is not None: @@ -460,9 +528,24 @@ class Qdb(bdb.Bdb): filename = code.co_filename line = linecache.getline(filename, lineno) #(filename, lineno, "", current, line, )} + # SyntaxError doesn't execute even one line, so avoid mainpyfile check + self._wait_for_mainpyfile = False + # send exception information & request interaction + self.user_exception(frame, info) - self.interaction(frame) - + def ping(self): + "Minimal method to test that the pipe (connection) is alive" + try: + # get some non-trivial data to compare: + args = (id(object()), ) + msg = {'method': 'ping', 'args': args, 'id': None} + self.pipe.send(msg) + msg = self.pipe.recv() + # check if data received is ok (alive and synchonized!) + return msg['result'] == args + except (IOError, EOFError): + return None + # console file-like object emulation def readline(self): "Replacement for stdin.readline()" @@ -483,9 +566,9 @@ class Qdb(bdb.Bdb): "Replacement for stdout.write()" msg = {'method': 'write', 'args': (text, ), 'id': None} self.pipe.send(msg) - + def writelines(self, l): - map(self.write, l) + list(map(self.write, l)) def flush(self): pass @@ -493,10 +576,25 @@ class Qdb(bdb.Bdb): def isatty(self): return 0 + def encoding(self): + return None # use default, 'utf-8' should be better... + + def close(self): + # revert redirections and close connection + if sys: + sys.stdin, sys.stdout, sys.stderr = self.old_stdio + try: + self.pipe.close() + except: + pass + + def __del__(self): + self.close() + class QueuePipe(object): "Simulated pipe for threads (using two queues)" - + def __init__(self, name, in_queue, out_queue): self.__name = name self.in_queue = in_queue @@ -520,12 +618,13 @@ class RPCError(RuntimeError): "Remote Error (not user exception)" pass - + class Frontend(object): "Qdb generic Frontend interface" - + def __init__(self, pipe): self.i = 1 + self.info = () self.pipe = pipe self.notifies = [] self.read_lock = threading.RLock() @@ -545,12 +644,13 @@ class Frontend(object): finally: self.write_lock.release() - def startup(self): + def startup(self, version, pid, thread_name, argv, filename): + self.info = (version, pid, thread_name, argv, filename) self.send({'method': 'run', 'args': (), 'id': None}) def interaction(self, filename, lineno, line, *kwargs): raise NotImplementedError - + def exception(self, title, extype, exvalue, trace, request): "Show a user_exception" raise NotImplementedError @@ -558,7 +658,7 @@ class Frontend(object): def write(self, text): "Console output (print)" raise NotImplementedError - + def readline(self, text): "Console input/rawinput" raise NotImplementedError @@ -570,10 +670,10 @@ class Frontend(object): # wait for a message... request = self.recv() else: - # process an asyncronus notification received earlier + # process an asyncronus notification received earlier request = self.notifies.pop(0) return self.process_message(request) - + def process_message(self, request): if request: result = None @@ -584,17 +684,19 @@ class Frontend(object): elif request.get('method') == 'interaction': self.interaction(*request.get("args"), **request.get("kwargs")) elif request.get('method') == 'startup': - self.startup() + self.startup(*request['args']) elif request.get('method') == 'exception': self.exception(*request['args']) elif request.get('method') == 'write': self.write(*request.get("args")) elif request.get('method') == 'readline': result = self.readline() + elif request.get('method') == 'ping': + result = request['args'] if result: - response = {'version': '1.1', 'id': request.get('id'), - 'result': result, - 'error': None} + response = {'version': '1.1', 'id': request.get('id'), + 'result': result, + 'error': None} self.send(response) return True @@ -603,18 +705,17 @@ class Frontend(object): req = {'method': method, 'args': args, 'id': self.i} self.send(req) self.i += 1 # increment the id - while True: + while 1: # wait until command acknowledge (response id match the request) res = self.recv() if 'id' not in res or not res['id']: - # nested notification received (i.e. write)! process it! - self.process_message(res) + # nested notification received (i.e. write)! process it later... + self.notifies.append(res) elif 'result' not in res: # nested request received (i.e. readline)! process it! self.process_message(res) - elif long(req['id']) != long(res['id']): - print("DEBUGGER wrong packet received: expecting id", req[ - 'id'], res['id']) + elif int(req['id']) != int(res['id']): + print("DEBUGGER wrong packet received: expecting id", req['id'], res['id']) # protocol state is unknown elif 'error' in res and res['error']: raise RPCError(res['error']['message']) @@ -624,23 +725,23 @@ class Frontend(object): def do_step(self, arg=None): "Execute the current line, stop at the first possible occasion" self.call('do_step') - + def do_next(self, arg=None): "Execute the current line, do not stop at function calls" self.call('do_next') - def do_continue(self, arg=None): + def do_continue(self, arg=None): "Continue execution, only stop when a breakpoint is encountered." self.call('do_continue') - - def do_return(self, arg=None): + + def do_return(self, arg=None): "Continue execution until the current function returns" self.call('do_return') - def do_jump(self, arg): - "Set the next line that will be executed." + def do_jump(self, arg): + "Set the next line that will be executed (None if sucess or message)" res = self.call('do_jump', arg) - print(res) + return res def do_where(self, arg=None): "Print a stack trace, with the most recent frame at the bottom." @@ -649,7 +750,7 @@ class Frontend(object): def do_quit(self, arg=None): "Quit from the debugger. The program being executed is aborted." self.call('do_quit') - + def do_eval(self, expr): "Inspect the value of the expression" return self.call('do_eval', expr) @@ -677,11 +778,11 @@ class Frontend(object): def do_clear_file_breakpoints(self, filename): "Remove all breakpoints at filename" self.call('do_clear_breakpoints', filename, lineno) - + def do_list_breakpoint(self): "List all breakpoints" return self.call('do_list_breakpoint') - + def do_exec(self, statement): return self.call('do_exec', statement) @@ -690,7 +791,7 @@ class Frontend(object): def get_call_tip(self, expression): return self.call('get_call_tip', expression) - + def interrupt(self): "Immediately stop at the first possible occasion (outside interaction)" # this is a notification!, do not expect a response @@ -700,7 +801,7 @@ class Frontend(object): def set_burst(self, value): req = {'method': 'set_burst', 'args': (value, )} self.send(req) - + def set_params(self, params): req = {'method': 'set_params', 'args': (params, )} self.send(req) @@ -708,15 +809,15 @@ class Frontend(object): class Cli(Frontend, cmd.Cmd): "Qdb Front-end command line interface" - + def __init__(self, pipe, completekey='tab', stdin=None, stdout=None, skip=None): cmd.Cmd.__init__(self, completekey, stdin, stdout) Frontend.__init__(self, pipe) # redefine Frontend methods: - + def run(self): - while True: + while 1: try: Frontend.run(self) except KeyboardInterrupt: @@ -736,31 +837,30 @@ class Cli(Frontend, cmd.Cmd): def write(self, text): print(text, end=' ') - + def readline(self): - return raw_input() - + return input() + def postcmd(self, stop, line): - return not line.startswith("h") # stop + return not line.startswith("h") # stop do_h = cmd.Cmd.do_help - + do_s = Frontend.do_step do_n = Frontend.do_next - do_c = Frontend.do_continue + do_c = Frontend.do_continue do_r = Frontend.do_return - do_j = Frontend.do_jump do_q = Frontend.do_quit def do_eval(self, args): "Inspect the value of the expression" print(Frontend.do_eval(self, args)) - + def do_list(self, args): "List source code for the current file" lines = Frontend.do_list(self, eval(args, {}, {}) if args else None) self.print_lines(lines) - + def do_where(self, args): "Print a stack trace, with the most recent frame at the bottom." lines = Frontend.do_where(self) @@ -772,7 +872,7 @@ class Cli(Frontend, cmd.Cmd): print("=" * 78) print(key.capitalize()) print("-" * 78) - for name, value in env[key].items(): + for name, value in list(env[key].items()): print("%-12s = %s" % (name, value)) def do_list_breakpoint(self, arg=None): @@ -794,11 +894,18 @@ class Cli(Frontend, cmd.Cmd): else: self.do_list_breakpoint() + def do_jump(self, args): + "Jump to the selected line" + ret = Frontend.do_jump(self, args) + if ret: # show error message if failed + print("cannot jump:", ret) + do_b = do_set_breakpoint do_l = do_list do_p = do_eval do_w = do_where do_e = do_environment + do_j = do_jump def default(self, line): "Default command" @@ -813,61 +920,85 @@ class Cli(Frontend, cmd.Cmd): print() +# WORKAROUND for python3 server using pickle's HIGHEST_PROTOCOL (now 3) +# but python2 client using pickles's protocol version 2 +if sys.version_info[0] > 2: + + import multiprocessing.reduction # forking in py2 + + class ForkingPickler2(multiprocessing.reduction.ForkingPickler): + def __init__(self, file, protocol=None, fix_imports=True): + # downgrade to protocol ver 2 + protocol = 2 + super().__init__(file, protocol, fix_imports) + + multiprocessing.reduction.ForkingPickler = ForkingPickler2 + + +def f(pipe): + "test function to be debugged" + print("creating debugger") + qdb_test = Qdb(pipe=pipe, redirect_stdio=False, allow_interruptions=True) + print("set trace") + + my_var = "Mariano!" + qdb_test.set_trace() + print("hello world!") + for i in range(100000): + pass + print("good by!") + + def test(): - def f(pipe): - print("creating debugger") - qdb = Qdb(pipe=pipe, redirect_stdio=False) - print("set trace") - - my_var = "Mariano!" - qdb.set_trace() - print("hello world!") - print("good by!") - saraza - + "Create a backend/frontend and time it" if '--process' in sys.argv: from multiprocessing import Process, Pipe - pipe, child_conn = Pipe() + front_conn, child_conn = Pipe() p = Process(target=f, args=(child_conn,)) else: from threading import Thread - from Queue import Queue + from queue import Queue parent_queue, child_queue = Queue(), Queue() front_conn = QueuePipe("parent", parent_queue, child_queue) child_conn = QueuePipe("child", child_queue, parent_queue) p = Thread(target=f, args=(child_conn,)) - + p.start() import time class Test(Frontend): def interaction(self, *args): print("interaction!", args) - + ##self.do_next() def exception(self, *args): print("exception", args) - #raise RuntimeError("exception %s" % repr(args)) - qdb = Test(front_conn) - time.sleep(5) - - while True: - print("running...") - Frontend.run(qdb) - time.sleep(1) - print("do_next") - qdb.do_next() + qdb_test = Test(front_conn) + time.sleep(1) + t0 = time.time() + + print("running...") + while front_conn.poll(): + Frontend.run(qdb_test) + qdb_test.do_continue() p.join() + t1 = time.time() + print("took", t1 - t0, "seconds") + sys.exit(0) -def connect(host="localhost", port=6000, authkey='secret password'): - "Connect to a running debugger backend" - +def start(host="localhost", port=6000, authkey='secret password'): + "Start the CLI server and wait connection from a running debugger backend" + address = (host, port) - from multiprocessing.connection import Client - - print("qdb debugger fronted: waiting for connection to", address) - conn = Client(address, authkey=authkey) + from multiprocessing.connection import Listener + address = (host, port) # family is deduced to be 'AF_INET' + if isinstance(authkey, str): + authkey = authkey.encode("utf8") + listener = Listener(address, authkey=authkey) + print("qdb debugger backend: waiting for connection at", address) + conn = listener.accept() + print('qdb debugger backend: connected to', listener.last_accepted) try: Cli(conn).run() except EOFError: @@ -877,13 +1008,13 @@ def connect(host="localhost", port=6000, authkey='secret password'): def main(host='localhost', port=6000, authkey='secret password'): - "Debug a script and accept a remote frontend" - + "Debug a script (running under the backend) and connect to remote frontend" + if not sys.argv[1:] or sys.argv[1] in ("--help", "-h"): print("usage: pdb.py scriptfile [arg] ...") sys.exit(2) - mainpyfile = sys.argv[1] # Get script filename + mainpyfile = sys.argv[1] # Get script filename if not os.path.exists(mainpyfile): print('Error:', mainpyfile, 'does not exist') sys.exit(1) @@ -893,15 +1024,8 @@ def main(host='localhost', port=6000, authkey='secret password'): # Replace pdb's dir with script's dir in front of module search path. sys.path[0] = os.path.dirname(mainpyfile) - from multiprocessing.connection import Listener - address = (host, port) # family is deduced to be 'AF_INET' - listener = Listener(address, authkey=authkey) - print("qdb debugger backend: waiting for connection at", address) - conn = listener.accept() - print('qdb debugger backend: connected to', listener.last_accepted) - # create the backend - qdb = Qdb(conn, redirect_stdio=True, allow_interruptions=True) + init(host, port, authkey) try: print("running", mainpyfile) qdb._runscript(mainpyfile) @@ -911,32 +1035,57 @@ def main(host='localhost', port=6000, authkey='secret password'): print("The program exited via sys.exit(). Exit status: ", end=' ') print(sys.exc_info()[1]) raise - except: - raise - - conn.close() - listener.close() + except Exception: + traceback.print_exc() + print("Uncaught exception. Entering post mortem debugging") + info = sys.exc_info() + qdb.post_mortem(info) + print("Program terminated!") + finally: + conn.close() + print("qdb debbuger backend: connection closed") +# "singleton" to store a unique backend per process qdb = None -def set_trace(host='localhost', port=6000, authkey='secret password'): +def init(host='localhost', port=6000, authkey='secret password', redirect=True): "Simplified interface to debug running programs" global qdb, listener, conn - - from multiprocessing.connection import Listener + + # destroy the debugger if the previous connection is lost (i.e. broken pipe) + if qdb and not qdb.ping(): + qdb.close() + qdb = None + + from multiprocessing.connection import Client # only create it if not currently instantiated if not qdb: address = (host, port) # family is deduced to be 'AF_INET' - listener = Listener(address, authkey=authkey) - conn = listener.accept() - + print("qdb debugger backend: waiting for connection to", address) + if isinstance(authkey, str): + authkey = authkey.encode("utf8") + conn = Client(address, authkey=authkey) + print('qdb debugger backend: connected to', address) # create the backend - qdb = Qdb(conn) + qdb = Qdb(conn, redirect_stdio=redirect, allow_interruptions=True) + # initial hanshake + qdb.startup() + + +def set_trace(host='localhost', port=6000, authkey='secret password'): + "Simplified interface to start debugging immediately" + init(host, port, authkey) # start debugger backend: qdb.set_trace() +def debug(host='localhost', port=6000, authkey='secret password'): + "Simplified interface to start debugging immediately (no stop)" + init(host, port, authkey) + # start debugger backend: + qdb.do_debug() + def quit(): "Remove trace and quit" @@ -948,24 +1097,25 @@ def quit(): conn.close() conn = None if listener: - listener.close() + listener.close() listener = None if __name__ == '__main__': # When invoked as main program: - if '--test' in sys.argv: + if '--test1' in sys.argv: test() # Check environment for configuration parameters: kwargs = {} for param in 'host', 'port', 'authkey': - if 'QDB_%s' % param.upper() in os.environ: - kwargs[param] = os.environ['QDB_%s' % param.upper()] + if 'DBG_%s' % param.upper() in os.environ: + kwargs[param] = os.environ['DBG_%s' % param.upper()] if not sys.argv[1:]: # connect to a remote debbuger - connect(**kwargs) + start(**kwargs) else: # start the debugger on a script # reimport as global __main__ namespace is destroyed - import qdb - qdb.main(**kwargs) + import dbg + dbg.main(**kwargs) + diff --git a/gluon/contrib/populate.py b/gluon/contrib/populate.py index 92c1fec6..6977e8d5 100644 --- a/gluon/contrib/populate.py +++ b/gluon/contrib/populate.py @@ -1,6 +1,7 @@ from __future__ import print_function +from gluon._compat import pickle, unicodeT +from gluon.fileutils import open_file import re -import cPickle import random import datetime @@ -32,10 +33,10 @@ class Learner: self.db[item][nextitem] += 1 def save(self, filename): - cPickle.dump(self.db, open(filename, 'wb')) + pickle.dump(self.db, open_file(filename, 'wb')) def load(self, filename): - self.loadd(cPickle.load(open(filename, 'rb'))) + self.loadd(pickle.load(open_file(filename, 'rb'))) def loadd(self, db): self.db = db @@ -43,7 +44,7 @@ class Learner: def generate(self, length=10000, prefix=False): replacements2 = {' ,': ',', ' \.': '.\n', ' :': ':', ' ;': ';', '\n\s+': '\n'} - keys = self.db.keys() + keys = list(self.db.keys()) key = keys[random.randint(0, len(keys) - 1)] words = key words = words.capitalize() @@ -130,7 +131,7 @@ def populate_generator(table, default=True, compute=False, contents={}): continue # if user supplied it, let it be. field = table[fieldname] - if not isinstance(field.type, (str, unicode)): + if not isinstance(field.type, (str, unicodeT)): continue elif field.type == 'id': continue diff --git a/gluon/debug.py b/gluon/debug.py index a4432590..6887f27f 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -87,9 +87,9 @@ def communicate(command=None): return ''.join(result) -# New debugger implementation using qdb and a web UI +# New debugger implementation using dbg and a web UI -import gluon.contrib.qdb as qdb +import gluon.contrib.dbg as dbg from threading import RLock interact_lock = RLock() @@ -109,11 +109,11 @@ def check_interaction(fn): return check_fn -class WebDebugger(qdb.Frontend): +class WebDebugger(dbg.Frontend): """Qdb web2py interface""" def __init__(self, pipe, completekey='tab', stdin=None, stdout=None): - qdb.Frontend.__init__(self, pipe) + dbg.Frontend.__init__(self, pipe) self.clear_interaction() def clear_interaction(self): @@ -128,7 +128,7 @@ class WebDebugger(qdb.Frontend): run_lock.acquire() try: while self.pipe.poll(): - qdb.Frontend.run(self) + dbg.Frontend.run(self) finally: run_lock.release() @@ -149,23 +149,23 @@ class WebDebugger(qdb.Frontend): @check_interaction def do_continue(self): - qdb.Frontend.do_continue(self) + dbg.Frontend.do_continue(self) @check_interaction def do_step(self): - qdb.Frontend.do_step(self) + dbg.Frontend.do_step(self) @check_interaction def do_return(self): - qdb.Frontend.do_return(self) + dbg.Frontend.do_return(self) @check_interaction def do_next(self): - qdb.Frontend.do_next(self) + dbg.Frontend.do_next(self) @check_interaction def do_quit(self): - qdb.Frontend.do_quit(self) + dbg.Frontend.do_quit(self) def do_exec(self, statement): interact_lock.acquire() @@ -175,22 +175,22 @@ class WebDebugger(qdb.Frontend): # avoid spurious interaction notifications: self.set_burst(2) # execute the statement in the remote debugger: - return qdb.Frontend.do_exec(self, statement) + return dbg.Frontend.do_exec(self, statement) finally: interact_lock.release() # create the connection between threads: parent_queue, child_queue = Queue.Queue(), Queue.Queue() -front_conn = qdb.QueuePipe("parent", parent_queue, child_queue) -child_conn = qdb.QueuePipe("child", child_queue, parent_queue) +front_conn = dbg.QueuePipe("parent", parent_queue, child_queue) +child_conn = dbg.QueuePipe("child", child_queue, parent_queue) web_debugger = WebDebugger(front_conn) # frontend -qdb_debugger = qdb.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend -dbg = qdb_debugger +dbg_debugger = dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend +dbg = dbg_debugger # enable getting context (stack, globals/locals) at interaction -qdb_debugger.set_params(dict(call_stack=True, environment=True)) +dbg_debugger.set_params(dict(call_stack=True, environment=True)) import gluon.main gluon.main.global_settings.debugging = True From 3f96c521f3677e3f91c68e8f113f4f21568499e1 Mon Sep 17 00:00:00 2001 From: Mathieu Clabaut Date: Sat, 8 Oct 2016 16:42:22 +0200 Subject: [PATCH 145/436] Allow cookie to contains several '=' close #1500 --- gluon/contrib/webclient.py | 18 ++++++++++++------ gluon/tests/test_web.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index c5e7f3c7..7f6e288b 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -46,6 +46,17 @@ class WebClient(object): self.default_headers = default_headers self.sessions = {} self.session_regex = session_regex and re.compile(session_regex) + self.headers = {} + + def _parse_headers_in_cookies(self): + self.cookies = {} + if 'set-cookie' in self.headers: + for item in self.headers['set-cookie'].split(','): + cookie = item[:item.find(';')] + pos = cookie.find('=') + key = cookie[:pos] + value = cookie[pos+1:] + self.cookies[key.strip()] = value.strip() def get(self, url, cookies=None, headers=None, auth=None): return self.post(url, data=None, cookies=cookies, @@ -149,12 +160,7 @@ class WebClient(object): else: raise error - # parse headers into cookies - self.cookies = {} - if 'set-cookie' in self.headers: - for item in self.headers['set-cookie'].split(','): - key, value = item[:item.find(';')].split('=') - self.cookies[key.strip()] = value.strip() + self._parse_headers_in_cookies() # check is a new session id has been issued, symptom of broken session if self.session_regex is not None: diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index 4a3ef3bf..adac0f28 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -49,6 +49,24 @@ def stopwebserver(): webserverprocess.terminate() +class Cookie(unittest.TestCase): + def testParseMultipleEquals(self): + """ Test for issue #1500. + Ensure that a cookie containing one or more '=' is correctly parsed + """ + client = WebClient() + client.headers['set-cookie'] = "key = value with one =;" + client._parse_headers_in_cookies() + self.assertIn("key", client.cookies) + self.assertEqual(client.cookies['key'], "value with one =") + + client.headers['set-cookie'] = "key = value with one = and another one =;" + client._parse_headers_in_cookies() + client._parse_headers_in_cookies() + self.assertIn("key", client.cookies) + self.assertEqual(client.cookies['key'], "value with one = and another one =") + + class LiveTest(unittest.TestCase): @classmethod From f8d62423e5b29a25d4d5541cabb1759e3fc16e12 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Mon, 10 Oct 2016 20:53:21 -0700 Subject: [PATCH 146/436] Update custom 503 page code to be less...idiotic --- gluon/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/main.py b/gluon/main.py index 2c887c1c..97b49c9a 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -390,9 +390,9 @@ def wsgibase(environ, responder): % 'invalid request', web2py_error='invalid application') elif not request.is_local and exists(disabled): - if os.path.exists(os.path.join(request.folder,'static','503.html')): - import urllib - raise HTTP(503, urllib.urlopen(os.path.join(request.folder,'static','503.html')).read()) + five0three = os.path.join(request.folder,'static','503.html') + if os.path.exists(five0three): + raise HTTP(503, file(five0three, 'r').read()) else: raise HTTP(503, "

      Temporarily down for maintenance

      ") From 63c494c24d422a6b9fbd325291624335f7463941 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Mon, 10 Oct 2016 20:59:31 -0700 Subject: [PATCH 147/436] Fixed accidental module-to-import swap --- gluon/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/main.py b/gluon/main.py index 97b49c9a..24327a89 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -25,7 +25,7 @@ import socket import random import string -from pydal._compat import Cookie, urllib2 +from gluon._compat import Cookie, urllib2 #from thread import allocate_lock from gluon.fileutils import abspath, write_file From 7615848f8340286460433f31a37aa745042add41 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Tue, 18 Oct 2016 18:27:37 -0700 Subject: [PATCH 148/436] Add a fired event after the component has finished loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes we may want to know when the component is finished with it’s loading process entirely to trigger animations or something similar. I added this for my own website, and I think that others may want to use this. It’s a small addition, but I hope that it will be helpful to others! --- applications/admin/static/js/web2py.js | 1 + applications/examples/static/js/web2py.js | 1 + applications/welcome/static/js/web2py.js | 1 + 3 files changed, 3 insertions(+) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 7853e8cb..7b9d9802 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -338,6 +338,7 @@ web2py.trap_form(action, target); web2py.ajax_init('#' + target); web2py.after_ajax(xhr); + web2py.fire(element, 'w2p:componentComplete', [xhr, status], target); // Let us know the component is finished loading } }); } diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 7853e8cb..7b9d9802 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -338,6 +338,7 @@ web2py.trap_form(action, target); web2py.ajax_init('#' + target); web2py.after_ajax(xhr); + web2py.fire(element, 'w2p:componentComplete', [xhr, status], target); // Let us know the component is finished loading } }); } diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 7853e8cb..7b9d9802 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -338,6 +338,7 @@ web2py.trap_form(action, target); web2py.ajax_init('#' + target); web2py.after_ajax(xhr); + web2py.fire(element, 'w2p:componentComplete', [xhr, status], target); // Let us know the component is finished loading } }); } From 4226b6d0e1bce12549320c40fe4669ff094d486c Mon Sep 17 00:00:00 2001 From: Chris Garcia Date: Fri, 28 Oct 2016 15:23:18 -0500 Subject: [PATCH 149/436] Corrected use of 'lowercase' and 'uppercase' to fix #1515 --- applications/admin/languages/nl.py | 4 ++-- applications/admin/languages/uk.py | 4 ++-- applications/admin/static/codemirror/keymap/vim.js | 6 +++--- gluon/tests/test_validators.py | 8 ++++---- gluon/validators.py | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/applications/admin/languages/nl.py b/applications/admin/languages/nl.py index 55d95be4..9998ee62 100755 --- a/applications/admin/languages/nl.py +++ b/applications/admin/languages/nl.py @@ -352,9 +352,9 @@ 'modules': 'modules', 'Multi User Mode': 'Multi User Mode', 'Must include at least %s %s': 'Moet ten minste bevatten %s %s', -'Must include at least %s lower case': 'Moet ten minste bevatten %s lower case', +'Must include at least %s lowercase': 'Moet ten minste bevatten %s kleine letter', 'Must include at least %s of the following : %s': 'Moet ten minste bevatten %s van het volgende : %s', -'Must include at least %s upper case': 'Moet ten minste bevatten %s upper case', +'Must include at least %s uppercase': 'Moet ten minste bevatten %s hoofdletter', 'new application "%s" created': 'nieuwe applicatie "%s" gemaakt', 'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'Nieuwe Applicatie Wizard', diff --git a/applications/admin/languages/uk.py b/applications/admin/languages/uk.py index a45124c6..e1462848 100755 --- a/applications/admin/languages/uk.py +++ b/applications/admin/languages/uk.py @@ -356,9 +356,9 @@ 'modules': 'Š¼Š¾Š“ŃƒŠ»Ń–', 'Multi User Mode': 'Multi User Mode', 'Must include at least %s %s': 'ŠœŠ°Ń” Š²Š¼Ń–Ń‰ŃƒŠ²Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s %s', -'Must include at least %s lower case': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s малих букв', +'Must include at least %s lowercase': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s малих букв', 'Must include at least %s of the following : %s': 'ŠœŠ°Ń” Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø не менше %s таких символів : %s', -'Must include at least %s upper case': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s великих букв', +'Must include at least %s uppercase': 'Повинен Š²ŠŗŠ»ŃŽŃ‡Š°Ń‚Šø Ń‰Š¾Š½Š°Š¹Š¼ŠµŠ½ŃˆŠµ %s великих букв', 'new application "%s" created': 'новий ГоГаток "%s" створено', 'new application "%s" imported': 'new application "%s" imported', 'New Application Wizard': 'ŠœŠ°Š¹ŃŃ‚ŠµŃ€ ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń нового Š“Š¾Š“Š°Ń‚ŠŗŃƒ', diff --git a/applications/admin/static/codemirror/keymap/vim.js b/applications/admin/static/codemirror/keymap/vim.js index 682eb7a8..a4c31910 100644 --- a/applications/admin/static/codemirror/keymap/vim.js +++ b/applications/admin/static/codemirror/keymap/vim.js @@ -314,8 +314,8 @@ // Vim does not support modifier only keys. return false; } - // TODO: Current bindings expect the character to be lower case, but - // it looks like vim key notation uses upper case. + // TODO: Current bindings expect the character to be lowercase, but + // it looks like vim key notation uses uppercase. if (isUpperCase(lastPiece)) { pieces[pieces.length - 1] = lastPiece.toLowerCase(); } @@ -3644,7 +3644,7 @@ * Extract the regular expression from the query and return a Regexp object. * Returns null if the query is blank. * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. - * If smartCase is passed in, and the query contains upper case letters, + * If smartCase is passed in, and the query contains uppercase letters, * then ignoreCase is overridden, and the 'i' flag will not be set. * If the query contains the /i in the flag part of the regular expression, * then both ignoreCase and smartCase are ignored, and 'i' will be passed diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index c98b4c26..49d4f905 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -843,7 +843,7 @@ class TestValidators(unittest.TestCase): '|'.join(['Minimum length is 8', 'Maximum length is 4', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'Must include at least 1 upper case', + 'Must include at least 1 uppercase', 'Must include at least 1 number'])) ) rtn = IS_STRONG(es=True)('abcde') @@ -851,7 +851,7 @@ class TestValidators(unittest.TestCase): ('abcde', '|'.join(['Minimum length is 8', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'Must include at least 1 upper case', + 'Must include at least 1 uppercase', 'Must include at least 1 number'])) ) rtn = IS_STRONG(upper=0, lower=0, number=0, es=True)('Abcde1') @@ -859,8 +859,8 @@ class TestValidators(unittest.TestCase): ('Abcde1', '|'.join(['Minimum length is 8', 'Must include at least 1 of the following: ~!@#$%^&*()_+-=?<>,.:;{}[]|', - 'May not include any upper case letters', - 'May not include any lower case letters', + 'May not include any uppercase letters', + 'May not include any lowercase letters', 'May not include any numbers'])) ) diff --git a/gluon/validators.py b/gluon/validators.py index 4181c226..22ad1a75 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2470,7 +2470,7 @@ class IS_LIST_OF(Validator): class IS_LOWER(Validator): """ - Converts to lower case:: + Converts to lowercase:: >>> IS_LOWER()('ABC') ('abc', None) @@ -2485,7 +2485,7 @@ class IS_LOWER(Validator): class IS_UPPER(Validator): """ - Converts to upper case:: + Converts to uppercase:: >>> IS_UPPER()('abc') ('ABC', None) @@ -3025,22 +3025,22 @@ class IS_STRONG(object): all_upper = re.findall("[A-Z]", value) if self.upper > 0: if not len(all_upper) >= self.upper: - failures.append(translate("Must include at least %s upper case") + failures.append(translate("Must include at least %s uppercase") % str(self.upper)) else: if len(all_upper) > 0: failures.append( - translate("May not include any upper case letters")) + translate("May not include any uppercase letters")) if isinstance(self.lower, int): all_lower = re.findall("[a-z]", value) if self.lower > 0: if not len(all_lower) >= self.lower: - failures.append(translate("Must include at least %s lower case") + failures.append(translate("Must include at least %s lowercase") % str(self.lower)) else: if len(all_lower) > 0: failures.append( - translate("May not include any lower case letters")) + translate("May not include any lowercase letters")) if isinstance(self.number, int): all_number = re.findall("[0-9]", value) if self.number > 0: From 2d4817841f467f9a51f1b1af1fff2703b353b802 Mon Sep 17 00:00:00 2001 From: Mathieu Clabaut Date: Tue, 1 Nov 2016 11:31:01 +0100 Subject: [PATCH 150/436] Allow for firstname and lastname in verify_email message --- gluon/tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index eb4a61f9..db6a9840 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3417,7 +3417,9 @@ class Auth(object): link = self.url( self.settings.function, args=('verify_email', key), scheme=True) d = dict(form.vars) - d.update(dict(key=key, link=link, username=form.vars[username])) + d.update(dict(key=key, link=link, username=form.vars[username], + firstname=form.vars['firstname'], + lastname=form.vars['lastname'])) if not (self.settings.mailer and self.settings.mailer.send( to=form.vars.email, subject=self.messages.verify_email_subject, From 8e1630843a696d0fc1d096142ebf245bc493ee68 Mon Sep 17 00:00:00 2001 From: Michele Comitini Date: Wed, 2 Nov 2016 00:41:40 +0100 Subject: [PATCH 151/436] fix wrong import in default controller of admin app --- applications/admin/controllers/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 6ad8b332..94fc91ce 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -1960,7 +1960,7 @@ def git_push(): def plugins(): app = request.args(0) - from serializers import loads_json + from gluon.serializers import loads_json if not session.plugins: try: rawlist = urlopen("http://www.web2pyslices.com/" + From 6b1225da029993b5231beaa83b86fa2b4b86702b Mon Sep 17 00:00:00 2001 From: Michele Comitini Date: Fri, 4 Nov 2016 09:59:49 +0100 Subject: [PATCH 152/436] keep the _next while doing the redirect --- gluon/tools.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index eb4a61f9..db0c2e81 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3670,7 +3670,12 @@ class Auth(object): key = request.args[-1] if key: session._reset_password_key = key - redirect(self.url(args='confirm_registration')) + if next: + redirect_vars = {'_next': next} + else: + redirect_vars = {} + redirect(self.url(args='confirm_registration', + vars=redirect_vars)) else: key = session._reset_password_key else: @@ -7065,4 +7070,4 @@ class Config(object): if __name__ == '__main__': import doctest - doctest.testmod() + doctest.testmod() \ No newline at end of file From 02f0bdb8d3815e313f03a239b719fa02244fada7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:37:22 +0000 Subject: [PATCH 153/436] Auth refactor, extracted many methods into a base class for more generic auth mechanisms. Partially addresses #1526 Includes a solution for IS_LOWER and IS_UPPER validator problems I mentioned in #1353 --- gluon/authapi.py | 1020 +++++++++++++++++++++++++++++++ gluon/tests/__init__.py | 1 + gluon/tests/test_authapi.py | 171 ++++++ gluon/tests/test_validators.py | 8 +- gluon/tools.py | 1034 +++++++------------------------- gluon/validators.py | 17 +- 6 files changed, 1438 insertions(+), 813 deletions(-) create mode 100644 gluon/authapi.py create mode 100644 gluon/tests/test_authapi.py diff --git a/gluon/authapi.py b/gluon/authapi.py new file mode 100644 index 00000000..981a314c --- /dev/null +++ b/gluon/authapi.py @@ -0,0 +1,1020 @@ +# -*- coding: utf-8 -*- +""" +| This file is part of the web2py Web Framework +| Copyrighted by Massimo Di Pierro +| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) +""" +from gluon import current +from gluon.storage import Messages, Settings, Storage +from gluon.utils import web2py_uuid +from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, IS_NOT_IN_DB +from pydal.objects import Table, Field, Row +import datetime +from gluon.settings import global_settings + +DEFAULT = lambda: None + + +class AuthAPI(object): + """ + AuthAPI is a barebones Auth implementation which does not have a concept of + HTML forms or redirects, emailing or even an URL, you are responsible for + all that if you use it. + The main Auth functions such as login, logout, register, profile are designed + in a Dict In -> Dict Out logic so, for instance, if you set + registration_requires_verification you are responsible for sending the key to + the user and even rolling back the transaction if you can't do it. + + NOTES: * It does not support all the callbacks Traditional Auth does yet. + Some of the callbacks will not be supported. + Check the method signatures to find out which ones are supported. + * register_fields and profile_fields settings are ignored for now. + + WARNING: No builtin CSRF protection whatsoever. + """ + + default_settings = { + 'create_user_groups': 'user_%(id)s', + 'email_case_sensitive': False, + 'everybody_group_id': None, + 'expiration': 3600, + 'keep_session_onlogin': True, + 'keep_session_onlogout': False, + 'logging_enabled': True, + 'login_after_registration': False, + 'login_email_validate': True, + 'login_userfield': None, + 'logout_onlogout': None, + 'long_expiration': 3600 * 24 * 30, + 'ondelete': 'CASCADE', + 'password_field': 'password', + 'password_min_length': 4, + 'registration_requires_approval': False, + 'registration_requires_verification': False, + 'renew_session_onlogin': True, + 'renew_session_onlogout': True, + 'table_event_name': 'auth_event', + 'table_group_name': 'auth_group', + 'table_membership_name': 'auth_membership', + 'table_permission_name': 'auth_permission', + 'table_user_name': 'auth_user', + 'use_username': False, + 'username_case_sensitive': True + } + + default_messages = { + 'add_group_log': 'Group %(group_id)s created', + 'add_membership_log': None, + 'add_permission_log': None, + 'change_password_log': 'User %(id)s Password changed', + 'del_group_log': 'Group %(group_id)s deleted', + 'del_membership_log': None, + 'del_permission_log': None, + 'email_taken': 'This email already has an account', + 'group_description': 'Group uniquely assigned to user %(id)s', + 'has_membership_log': None, + 'has_permission_log': None, + 'invalid_email': 'Invalid email', + 'key_verified': 'Key verified', + 'invalid_login': 'Invalid login', + 'invalid_password': 'Invalid password', + 'invalid_user': 'Invalid user', + 'invalid_key': 'Invalid key', + 'invalid_username': 'Invalid username', + 'logged_in': 'Logged in', + 'logged_out': 'Logged out', + 'login_failed_log': None, + 'login_log': 'User %(id)s Logged-in', + 'logout_log': 'User %(id)s Logged-out', + 'mismatched_password': "Password fields don't match", + 'password_changed': 'Password changed', + 'profile_log': 'User %(id)s Profile updated', + 'profile_updated': 'Profile updated', + 'register_log': 'User %(id)s Registered', + 'registration_pending': 'Registration is pending approval', + 'registration_successful': 'Registration successful', + 'registration_verifying': 'Registration needs verification', + 'username_taken': 'Username already taken', + 'verify_log': 'User %(id)s verified registration key' + } + + def __init__(self, db=None, hmac_key=None, signature=True): + self.db = db + session = current.session + auth = session.auth + self.user_groups = auth and auth.user_groups or {} + now = current.request.now + # if we have auth info + # if not expired it, used it + # if expired, clear the session + # else, only clear auth info in the session + if auth: + delta = datetime.timedelta(days=0, seconds=auth.expiration) + if auth.last_visit and auth.last_visit + delta > now: + self.user = auth.user + # this is a trick to speed up sessions to avoid many writes + if (now - auth.last_visit).seconds > (auth.expiration / 10): + auth.last_visit = now + else: + self.user = None + if session.auth: + del session.auth + session.renew(clear_session=True) + else: + self.user = None + if session.auth: + del session.auth + + settings = self.settings = Settings(self.__class__.default_settings) + settings.update( + extra_fields={}, + hmac_key=hmac_key, + ) + settings.lock_keys = True + messages = self.messages = Messages(current.T) + messages.update(self.default_messages) + messages.lock_keys = True + if signature: + self.define_signature() + else: + self.signature = None + + def __validate(self, value, requires): + if not isinstance(requires, (list, tuple)): + requires = [requires] + for validator in requires: + (value, error) = validator(value) + if error: + return (value, error) + return (value, None) + + def _get_migrate(self, tablename, migrate=True): + + if type(migrate).__name__ == 'str': + return (migrate + tablename + '.table') + elif migrate == False: + return False + else: + return True + + def _get_user_id(self): + """accessor for auth.user_id""" + return self.user and self.user.id or None + + user_id = property(_get_user_id, doc="user.id or None") + + def table_user(self): + return self.db[self.settings.table_user_name] + + def table_group(self): + return self.db[self.settings.table_group_name] + + def table_membership(self): + return self.db[self.settings.table_membership_name] + + def table_permission(self): + return self.db[self.settings.table_permission_name] + + def table_event(self): + return self.db[self.settings.table_event_name] + + def define_signature(self): + db = self.db + settings = self.settings + request = current.request + T = current.T + reference_user = 'reference %s' % settings.table_user_name + + def lazy_user(auth=self): + return auth.user_id + + def represent(id, record=None, s=settings): + try: + user = s.table_user(id) + return '%s %s' % (user.get("first_name", user.get("email")), + user.get("last_name", '')) + except: + return id + ondelete = self.settings.ondelete + self.signature = Table( + self.db, 'auth_signature', + Field('is_active', 'boolean', + default=True, + readable=False, writable=False, + label=T('Is Active')), + Field('created_on', 'datetime', + default=request.now, + writable=False, readable=False, + label=T('Created On')), + Field('created_by', + reference_user, + default=lazy_user, represent=represent, + writable=False, readable=False, + label=T('Created By'), ondelete=ondelete), + Field('modified_on', 'datetime', + update=request.now, default=request.now, + writable=False, readable=False, + label=T('Modified On')), + Field('modified_by', + reference_user, represent=represent, + default=lazy_user, update=lazy_user, + writable=False, readable=False, + label=T('Modified By'), ondelete=ondelete)) + + def define_tables(self, username=None, signature=None, migrate=None, + fake_migrate=None): + """ + To be called unless tables are defined manually + + Examples: + Use as:: + + # defines all needed tables and table files + # 'myprefix_auth_user.table', ... + auth.define_tables(migrate='myprefix_') + + # defines all needed tables without migration/table files + auth.define_tables(migrate=False) + + """ + + db = self.db + if migrate is None: + migrate = db._migrate + if fake_migrate is None: + fake_migrate = db._fake_migrate + settings = self.settings + if username is None: + username = settings.use_username + else: + settings.use_username = username + if not self.signature: + self.define_signature() + if signature: + signature_list = [self.signature] + elif not signature: + signature_list = [] + elif isinstance(signature, Table): + signature_list = [signature] + else: + signature_list = signature + is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) + is_crypted = CRYPT(key=settings.hmac_key, + min_length=settings.password_min_length) + is_unique_email = [ + IS_EMAIL(error_message=self.messages.invalid_email), + IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name, + error_message=self.messages.email_taken)] + if not settings.email_case_sensitive: + is_unique_email.insert(1, IS_LOWER()) + if settings.table_user_name not in db.tables: + passfield = settings.password_field + extra_fields = settings.extra_fields.get( + settings.table_user_name, []) + signature_list + if username or settings.cas_provider: # cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables + is_unique_username = \ + [IS_MATCH('[\w\.\-]+', strict=True, + error_message=self.messages.invalid_username), + IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name, + error_message=self.messages.username_taken)] + if not settings.username_case_sensitive: + is_unique_username.insert(1, IS_LOWER()) + db.define_table( + settings.table_user_name, + Field('first_name', length=128, default='', + label=self.messages.label_first_name, + requires=is_not_empty), + Field('last_name', length=128, default='', + label=self.messages.label_last_name, + requires=is_not_empty), + Field('email', length=512, default='', + label=self.messages.label_email, + requires=is_unique_email), + Field('username', length=128, default='', + label=self.messages.label_username, + requires=is_unique_username), + Field(passfield, 'password', length=512, + readable=False, label=self.messages.label_password, + requires=[is_crypted]), + Field('registration_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_key), + Field('reset_password_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_reset_password_key), + Field('registration_id', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_id), + *extra_fields, + **dict( + migrate=self._get_migrate(settings.table_user_name, + migrate), + fake_migrate=fake_migrate, + format='%(username)s')) + else: + db.define_table( + settings.table_user_name, + Field('first_name', length=128, default='', + label=self.messages.label_first_name, + requires=is_not_empty), + Field('last_name', length=128, default='', + label=self.messages.label_last_name, + requires=is_not_empty), + Field('email', length=512, default='', + label=self.messages.label_email, + requires=is_unique_email), + Field(passfield, 'password', length=512, + readable=False, label=self.messages.label_password, + requires=[is_crypted]), + Field('registration_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_key), + Field('reset_password_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_reset_password_key), + Field('registration_id', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_id), + *extra_fields, + **dict( + migrate=self._get_migrate(settings.table_user_name, + migrate), + fake_migrate=fake_migrate, + format='%(first_name)s %(last_name)s (%(id)s)')) + reference_table_user = 'reference %s' % settings.table_user_name + if settings.table_group_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_group_name, []) + signature_list + db.define_table( + settings.table_group_name, + Field('role', length=512, default='', + label=self.messages.label_role, + requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)), + Field('description', 'text', + label=self.messages.label_description), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_group_name, migrate), + fake_migrate=fake_migrate, + format='%(role)s (%(id)s)')) + reference_table_group = 'reference %s' % settings.table_group_name + if settings.table_membership_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_membership_name, []) + signature_list + db.define_table( + settings.table_membership_name, + Field('user_id', reference_table_user, + label=self.messages.label_user_id), + Field('group_id', reference_table_group, + label=self.messages.label_group_id), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_membership_name, migrate), + fake_migrate=fake_migrate)) + if settings.table_permission_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_permission_name, []) + signature_list + db.define_table( + settings.table_permission_name, + Field('group_id', reference_table_group, + label=self.messages.label_group_id), + Field('name', default='default', length=512, + label=self.messages.label_name, + requires=is_not_empty), + Field('table_name', length=512, + label=self.messages.label_table_name), + Field('record_id', 'integer', default=0, + label=self.messages.label_record_id, + requires=IS_INT_IN_RANGE(0, 10 ** 9)), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_permission_name, migrate), + fake_migrate=fake_migrate)) + if settings.table_event_name not in db.tables: + db.define_table( + settings.table_event_name, + Field('time_stamp', 'datetime', + default=current.request.now, + label=self.messages.label_time_stamp), + Field('client_ip', + default=current.request.client, + label=self.messages.label_client_ip), + Field('user_id', reference_table_user, default=None, + label=self.messages.label_user_id), + Field('origin', default='auth', length=512, + label=self.messages.label_origin, + requires=is_not_empty), + Field('description', 'text', default='', + label=self.messages.label_description, + requires=is_not_empty), + *settings.extra_fields.get(settings.table_event_name, []), + **dict( + migrate=self._get_migrate( + settings.table_event_name, migrate), + fake_migrate=fake_migrate)) + + return self + + def log_event(self, description, vars=None, origin='auth'): + """ + Examples: + Use as:: + + auth.log_event(description='this happened', origin='auth') + + """ + if not self.settings.logging_enabled or not description: + return + elif self.is_logged_in(): + user_id = self.user.id + else: + user_id = None # user unknown + vars = vars or {} + # log messages should not be translated + if type(description).__name__ == 'lazyT': + description = description.m + self.table_event().insert(description=str(description % vars), origin=origin, user_id=user_id) + + def id_group(self, role): + """ + Returns the group_id of the group specified by the role + """ + rows = self.db(self.table_group().role == role).select() + if not rows: + return None + return rows[0].id + + def user_group(self, user_id=None): + """ + Returns the group_id of the group uniquely associated to this user + i.e. `role=user:[user_id]` + """ + return self.id_group(self.user_group_role(user_id)) + + def user_group_role(self, user_id=None): + if not self.settings.create_user_groups: + return None + if user_id: + user = self.table_user()[user_id] + else: + user = self.user + return self.settings.create_user_groups % user + + def add_group(self, role, description=''): + """ + Creates a group associated to a role + """ + group_id = self.table_group().insert(role=role, description=description) + self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role)) + return group_id + + def del_group(self, group_id): + """ + Deletes a group + """ + self.db(self.table_group().id == group_id).delete() + self.db(self.table_membership().group_id == group_id).delete() + self.db(self.table_permission().group_id == group_id).delete() + if group_id in self.user_groups: + del self.user_groups[group_id] + self.log_event(self.messages.del_group_log, dict(group_id=group_id)) + + def update_groups(self): + if not self.user: + return + user_groups = self.user_groups = {} + if current.session.auth: + current.session.auth.user_groups = self.user_groups + table_group = self.table_group() + table_membership = self.table_membership() + memberships = self.db( + table_membership.user_id == self.user.id).select() + for membership in memberships: + group = table_group(membership.group_id) + if group: + user_groups[membership.group_id] = group.role + + def add_membership(self, group_id=None, user_id=None, role=None): + """ + Gives user_id membership of group_id or role + if user is None than user_id is that of current logged in user + """ + + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + if not group_id: + raise ValueError('group_id not provided or invalid') + if not user_id: + raise ValueError('user_id not provided or invalid') + membership = self.table_membership() + db = membership._db + record = db((membership.user_id == user_id) & + (membership.group_id == group_id), + ignore_common_filters=True).select().first() + if record: + if hasattr(record, 'is_active') and not record.is_active: + record.update_record(is_active=True) + return record.id + else: + id = membership.insert(group_id=group_id, user_id=user_id) + if role: + self.user_groups[group_id] = role + else: + self.update_groups() + self.log_event(self.messages['add_membership_log'], + dict(user_id=user_id, group_id=group_id)) + return id + + def del_membership(self, group_id=None, user_id=None, role=None): + """ + Revokes membership from group_id to user_id + if user_id is None than user_id is that of current logged in user + """ + + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + membership = self.table_membership() + self.log_event(self.messages['del_membership_log'], + dict(user_id=user_id, group_id=group_id)) + ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete() + if group_id in self.user_groups: + del self.user_groups[group_id] + return ret + + def has_membership(self, group_id=None, user_id=None, role=None): + """ + Checks if user is member of group_id or role + """ + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + membership = self.table_membership() + if group_id and user_id and self.db((membership.user_id == user_id) & + (membership.group_id == group_id)).select(): + r = True + else: + r = False + self.log_event(self.messages['has_membership_log'], + dict(user_id=user_id, group_id=group_id, check=r)) + return r + + def add_permission(self, + group_id, + name='any', + table_name='', + record_id=0, + ): + """ + Gives group_id 'name' access to 'table_name' and 'record_id' + """ + + permission = self.table_permission() + if group_id == 0: + group_id = self.user_group() + record = self.db((permission.group_id == group_id) & + (permission.name == name) & + (permission.table_name == str(table_name)) & + (permission.record_id == long(record_id)), + ignore_common_filters=True + ).select(limitby=(0, 1), orderby_on_limitby=False).first() + if record: + if hasattr(record, 'is_active') and not record.is_active: + record.update_record(is_active=True) + id = record.id + else: + id = permission.insert(group_id=group_id, name=name, + table_name=str(table_name), + record_id=long(record_id)) + self.log_event(self.messages['add_permission_log'], + dict(permission_id=id, group_id=group_id, + name=name, table_name=table_name, + record_id=record_id)) + return id + + def del_permission(self, + group_id, + name='any', + table_name='', + record_id=0, + ): + """ + Revokes group_id 'name' access to 'table_name' and 'record_id' + """ + + permission = self.table_permission() + self.log_event(self.messages['del_permission_log'], + dict(group_id=group_id, name=name, + table_name=table_name, record_id=record_id)) + return self.db(permission.group_id == + group_id)(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + long(record_id)).delete() + + def has_permission(self, + name='any', + table_name='', + record_id=0, + user_id=None, + group_id=None, + ): + """ + Checks if user_id or current logged in user is member of a group + that has 'name' permission on 'table_name' and 'record_id' + if group_id is passed, it checks whether the group has the permission + """ + + if not group_id and self.settings.everybody_group_id and \ + self.has_permission(name, table_name, record_id, user_id=None, + group_id=self.settings.everybody_group_id): + return True + + if not user_id and not group_id and self.user: + user_id = self.user.id + if user_id: + membership = self.table_membership() + rows = self.db(membership.user_id == user_id).select(membership.group_id) + groups = set([row.group_id for row in rows]) + if group_id and group_id not in groups: + return False + else: + groups = set([group_id]) + permission = self.table_permission() + rows = self.db(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + record_id).select(permission.group_id) + groups_required = set([row.group_id for row in rows]) + if record_id: + rows = self.db(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + 0).select(permission.group_id) + groups_required = groups_required.union(set([row.group_id for row in rows])) + if groups.intersection(groups_required): + r = True + else: + r = False + if user_id: + self.log_event(self.messages['has_permission_log'], + dict(user_id=user_id, name=name, + table_name=table_name, record_id=record_id)) + return r + + def is_logged_in(self): + """ + Checks if the user is logged in and returns True/False. + If so user is in auth.user as well as in session.auth.user + """ + if self.user: + return True + return False + + def login_user(self, user): + """ + Logins the `user = db.auth_user(id)` + """ + if global_settings.web2py_runtime_gae: + user = Row(self.table_user()._filter_fields(user, id=True)) + delattr(user, 'password') + else: + user = Row(user) + for key in list(user.keys()): + value = user[key] + if callable(value) or key == 'password': + delattr(user, key) + if self.settings.renew_session_onlogin: + current.session.renew(clear_session=not self.settings.keep_session_onlogin) + current.session.auth = Storage(user=user, + last_visit=current.request.now, + expiration=self.settings.expiration, + hmac_key=web2py_uuid()) + self.user = user + self.update_groups() + + def login(self, log=DEFAULT, **kwargs): + """ + Login a user + + Keyword Args: + username/email/name_of_your_username_field (string) - username + password/name_of_your_passfield (string) - user's password + remember_me (boolean) - extend the duration of the login to settings.long_expiration + """ + settings = self.settings + session = current.session + table_user = self.table_user() + + if 'username' in table_user.fields or \ + not settings.login_email_validate: + userfield_validator = IS_NOT_EMPTY(error_message=self.messages.is_empty) + if not settings.username_case_sensitive: + userfield_validator = [IS_LOWER(), userfield_validator] + else: + userfield_validator = IS_EMAIL(error_message=self.messages.invalid_email) + if not settings.email_case_sensitive: + userfield_validator = [IS_LOWER(), userfield_validator] + + passfield = settings.password_field + + if log is DEFAULT: + log = self.messages['login_log'] + + user = None + + # Setup the default field used for the userfield + if self.settings.login_userfield: + userfield = self.settings.login_userfield + else: + if 'username' in table_user.fields: + userfield = 'username' + else: + userfield = 'email' + + # Get the userfield from kwargs and validate it + userfield_value = kwargs.get(userfield) + if userfield_value is None: + raise KeyError('%s not found in kwargs' % userfield) + + validated, error = self.__validate(userfield_value, userfield_validator) + + if error: + return {'errors': {userfield: error}, 'message': self.messages.invalid_login, 'user': None} + + # Get the user for this userfield and check it + user = table_user(**{userfield: validated}) + + if user is None: + return {'errors': {userfield: self.messages.invalid_user}, 'message': self.messages.invalid_login, 'user': None} + + if (user.registration_key or '').startswith('pending'): + return {'errors': None, 'message': self.messages.registration_pending, 'user': None} + elif user.registration_key in ('disabled', 'blocked'): + return {'errors': None, 'message': self.messages.login_disabled, 'user': None} + elif (user.registration_key is not None and user.registration_key.strip()): + return {'errors': None, 'message': self.messages.registration_verifying, 'user': None} + + # Finally verify the password + passfield = settings.password_field + password = table_user[passfield].validate(kwargs.get(passfield, ''))[0] + + if password == user[passfield]: + self.login_user(user) + session.auth.expiration = \ + kwargs.get('remember_me', False) and \ + settings.long_expiration or \ + settings.expiration + session.auth.remember_me = kwargs.get('remember_me', False) + self.log_event(log, user) + return {'errors': None, 'message': self.messages.logged_in, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + else: + self.log_event(self.messages['login_failed_log'], kwargs) + return {'errors': {passfield: self.messages.invalid_password}, 'message': self.messages.invalid_login, 'user': None} + + def logout(self, log=DEFAULT, onlogout=DEFAULT, **kwargs): + """ + Logs out user + """ + settings = self.settings + session = current.session + + if onlogout is DEFAULT: + onlogout = settings.logout_onlogout + if onlogout: + onlogout(self.user) + if log is DEFAULT: + log = self.messages['logout_log'] + if self.user: + self.log_event(log, self.user) + + session.auth = None + self.user = None + if settings.renew_session_onlogout: + session.renew(clear_session=not settings.keep_session_onlogout) + + return {'errors': None, 'message': self.messages.logged_out, 'user': None} + + def register(self, log=DEFAULT, **kwargs): + """ + Register a user. + """ + + table_user = self.table_user() + settings = self.settings + + if self.is_logged_in(): + raise AssertionError('User trying to register is logged in') + + if log is DEFAULT: + log = self.messages['register_log'] + + if self.settings.login_userfield: + userfield = self.settings.login_userfield + elif 'username' in table_user.fields: + userfield = 'username' + else: + userfield = 'email' + + # Ensure the username field is unique. + unique_validator = IS_NOT_IN_DB(self.db, table_user[userfield]) + userfield_validator = table_user[userfield].requires + if userfield_validator is None: + userfield_validator = unique_validator + elif isinstance(userfield_validator, (list, tuple)): + if not any([isinstance(validator, IS_NOT_IN_DB) for validator in + userfield_validator]): + if isinstance(userfield_validator, list): + userfield_validator.append(unique_validator) + else: + userfield_validator += (unique_validator, ) + elif not isinstance(userfield_validator, IS_NOT_IN_DB): + userfield_validator = [userfield_validator, unique_validator] + table_user[userfield].requires = userfield_validator + + passfield = settings.password_field + + try: # Make sure we have our original minimum length + table_user[passfield].requires[-1].min_length = settings.password_min_length + except: + pass + + key = web2py_uuid() + if settings.registration_requires_approval: + key = 'pending-' + key + + table_user.registration_key.default = key + + result = table_user.validate_and_insert(**kwargs) + if result.errors: + return {'errors': result.errors.as_dict(), 'message': None, 'user': None} + + user = table_user[result.id] + + message = self.messages.registration_successful + + if settings.create_user_groups: + d = user.as_dict() + description = self.messages.group_description % d + group_id = self.add_group(settings.create_user_groups % d, description) + self.add_membership(group_id, result.id) + + if self.settings.everybody_group_id: + self.add_membership(self.settings.everybody_group_id, result) + + if settings.registration_requires_verification: + d = {k: user[k] for k in table_user.fields if table_user[k].readable} + d['key']= key + if settings.login_after_registration and not settings.registration_requires_approval: + self.login_user(user) + return {'errors': None, 'message': None, 'user': d} + + if settings.registration_requires_approval: + user.update_record(registration_key='pending') + message = self.messages.registration_pending + elif settings.login_after_registration: + user.update_record(registration_key='') + self.login_user(user) + message = self.messages.logged_in + + self.log_event(log, user) + + return {'errors': None, 'message': message, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + def profile(self, log=DEFAULT, **kwargs): + """ + Lets the user change his/her profile + """ + + table_user = self.table_user() + settings = self.settings + table_user[settings.password_field].writable = False + + if not self.is_logged_in(): + raise AssertionError('User is not logged in') + + if not kwargs: + user = table_user[self.user.id] + return {'errors': None, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + result = self.db(table_user.id == self.user.id).validate_and_update(**kwargs) + user = table_user[self.user.id] + + if result.errors: + return {'errors': result.errors, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + if log is DEFAULT: + log = self.messages['profile_log'] + + self.log_event(log, user) + self.user.update(**kwargs) + return {'errors': None, 'message': self.messages.profile_updated, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + def change_password(self, log=DEFAULT, **kwargs): + """ + Lets the user change password + + Keyword Args: + old_password (string) - User's current password + new_password (string) - User's new password + new_password2 (string) - Verify the new password + """ + settings = self.settings + messages = self.messages + + if not self.is_logged_in(): + raise AssertionError('User is not logged in') + + db = self.db + table_user = self.table_user() + s = db(table_user.id == self.user.id) + + request = current.request + session = current.session + passfield = settings.password_field + + requires = table_user[passfield].requires + if not isinstance(requires, (list, tuple)): + requires = [requires] + requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) + if requires: + requires[0].min_length = 0 + + old_password = kwargs.get('old_password', '') + new_password = kwargs.get('new_password', '') + new_password2 = kwargs.get('new_password2', '') + + validator_old = requires + validator_pass2 = IS_EQUAL_TO(new_password, error_message=messages.mismatched_password) + + old_password, error_old = self.__validate(old_password, validator_old) + new_password2, error_new2 = self.__validate(new_password2, validator_pass2) + + errors = {} + if error_old: + errors['old_password'] = error_old + if error_new2: + errors['new_password2'] = error_new2 + if errors: + return {'errors': errors, 'message': None} + + current_user = s.select(limitby=(0, 1), orderby_on_limitby=False).first() + if not old_password == current_user[passfield]: + return {'errors': {'old_password': messages.invalid_password}, 'message': None} + else: + d = {passfield: new_password} + resp = s.validate_and_update(**d) + if resp.errors: + return {'errors': {'new_password': resp.errors[passfield]}, 'message': None} + if log is DEFAULT: + log = messages['change_password_log'] + self.log_event(log, self.user) + return {'errors': None, 'message': messages.password_changed} + + def verify_key(self, + key=None, + ignore_approval=False, + log=DEFAULT, + ): + """ + Verify a given registration_key actually exists in the user table. + Resets the key to empty string '' or 'pending' if + setttings.registration_requires_approval is true. + + Keyword Args: + key (string) - User's registration key + """ + table_user = self.table_user() + user = table_user(registration_key=key) + if (user is None) or (key is None): + return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key } + + if self.settings.registration_requires_approval: + user.update_record(registration_key='pending') + result = {'errors': None, 'message': self.messages.registration_pending} + else: + user.update_record(registration_key='') + result = {'errors': None, 'message': self.messages.key_verified} + # make sure session has same user.registration_key as db record + if current.session.auth and current.session.auth.user: + current.session.auth.user.registration_key = user.registration_key + if log is DEFAULT: + log = self.messages['verify_log'] + self.log_event(log, user) + return result diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 2f111fe3..a0e3fa56 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -13,6 +13,7 @@ from .test_contribs import * from .test_routes import * from .test_router import * from .test_validators import * +from .test_authapi import * from .test_tools import * from .test_utils import * from .test_serializers import * diff --git a/gluon/tests/test_authapi.py b/gluon/tests/test_authapi.py new file mode 100644 index 00000000..cd00afb6 --- /dev/null +++ b/gluon/tests/test_authapi.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Unit tests for authapi """ +import os +import unittest +from gluon.globals import Request, Response, Session +from gluon.languages import translator +from gluon.dal import DAL, Field +from gluon.authapi import AuthAPI +from gluon.storage import Storage +from gluon._compat import to_bytes, to_native, add_charset + +DEFAULT_URI = os.getenv('DB', 'sqlite:memory') + + +class TestAuthAPI(unittest.TestCase): + + def setUp(self): + self.request = Request(env={}) + self.request.application = 'a' + self.request.controller = 'c' + self.request.function = 'f' + self.request.folder = 'applications/admin' + self.response = Response() + self.session = Session() + T = translator('', 'en') + self.session.connect(self.request, self.response) + from gluon.globals import current + self.current = current + self.current.request = self.request + self.current.response = self.response + self.current.session = self.session + self.current.T = T + self.db = DAL(DEFAULT_URI, check_reserved=['all']) + self.auth = AuthAPI(self.db) + self.auth.define_tables(username=True, signature=False) + # Create a user + self.auth.table_user().validate_and_insert(first_name='Bart', + last_name='Simpson', + username='bart', + email='bart@simpson.com', + password='bart_password', + registration_key='', + registration_id='' + ) + self.db.commit() + + def test_login(self): + result = self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + self.assertTrue(result['user']['email'] == 'bart@simpson.com') + self.auth.logout() + self.assertFalse(self.auth.is_logged_in()) + self.auth.settings.username_case_sensitive = False + result = self.auth.login(**{'username': 'BarT', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + + def test_logout(self): + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.logout() + self.assertTrue(not self.auth.is_logged_in()) + self.assertTrue(result['user'] is None) + + def test_register(self): + self.auth.settings.login_after_registration = True + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.assertTrue(result['user']['email'] == 'lisa@simpson.com') + self.assertTrue(self.auth.is_logged_in()) + with self.assertRaises(AssertionError): # Can't register if you're logged in + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.auth.logout() + self.auth.settings.login_after_registration = False + result = self.auth.register(**{ + 'username': 'barney', + 'first_name': 'Barney', + 'last_name': 'Gumble', + 'email': 'barney@simpson.com', + 'password': 'barney_password' + }) + self.assertTrue(result['user']['email'] == 'barney@simpson.com') + self.assertFalse(self.auth.is_logged_in()) + self.auth.settings.login_userfield = 'email' + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.assertTrue(result['errors']['email'] == self.auth.messages.email_taken) + self.assertTrue(result['user'] is None) + self.auth.settings.registration_requires_verification = True + result = self.auth.register(**{ + 'username': 'homer', + 'first_name': 'Homer', + 'last_name': 'Simpson', + 'email': 'homer@simpson.com', + 'password': 'homer_password' + }) + self.assertTrue('key' in result['user']) + + def test_profile(self): + with self.assertRaises(AssertionError): + # We are not logged in + self.auth.profile() + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.profile(email='bartolo@simpson.com') + self.assertTrue(result['user']['email'] == 'bartolo@simpson.com') + self.assertTrue(self.auth.table_user()[result['user']['id']].email == 'bartolo@simpson.com') + + def test_change_password(self): + with self.assertRaises(AssertionError): + # We are not logged in + self.auth.change_password() + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234') + self.auth.logout() + self.assertTrue(not self.auth.is_logged_in()) + self.auth.login(username='bart', password='1234') + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='5678') + self.assertTrue('new_password2' in result['errors']) + result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234') + self.assertTrue('old_password' in result['errors']) + + def test_verify_key(self): + self.auth.settings.registration_requires_verification = True + result = self.auth.register(**{ + 'username': 'homer', + 'first_name': 'Homer', + 'last_name': 'Simpson', + 'email': 'homer@simpson.com', + 'password': 'homer_password' + }) + self.assertTrue('key' in result['user']) + homer_id = result['user']['id'] + homers_key = result['user']['key'] + result = self.auth.verify_key(key=None) + self.assertTrue(result['errors'] is not None) + result = self.auth.verify_key(key='12345') + self.assertTrue(result['errors'] is not None) + result = self.auth.verify_key(key=homers_key) + self.assertTrue(result['errors'] is None) + self.assertEqual(self.auth.table_user()[homer_id].registration_key, '') + self.auth.settings.registration_requires_approval = True + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + lisa_id = result['user']['id'] + result = self.auth.verify_key(key=result['user']['key']) + self.assertEqual(self.auth.table_user()[lisa_id].registration_key, 'pending') diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index c98b4c26..a7a35e90 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -702,15 +702,19 @@ class TestValidators(unittest.TestCase): def test_IS_LOWER(self): rtn = IS_LOWER()('ABC') + self.assertEqual(rtn, ('abc', None)) + rtn = IS_LOWER()(b'ABC') self.assertEqual(rtn, (b'abc', None)) rtn = IS_LOWER()('Ƒ') - self.assertEqual(rtn, (b'\xc3\xb1', None)) + self.assertEqual(rtn, ('Ʊ', None)) def test_IS_UPPER(self): rtn = IS_UPPER()('abc') + self.assertEqual(rtn, ('ABC', None)) + rtn = IS_UPPER()(b'abc') self.assertEqual(rtn, (b'ABC', None)) rtn = IS_UPPER()('Ʊ') - self.assertEqual(rtn, (b'\xc3\x91', None)) + self.assertEqual(rtn, ('Ƒ', None)) def test_IS_SLUG(self): rtn = IS_SLUG()('abc123') diff --git a/gluon/tools.py b/gluon/tools.py index eb4a61f9..471f0166 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -36,6 +36,7 @@ import json from email import message_from_string +from gluon.authapi import AuthAPI from gluon.contenttype import contenttype from gluon.storage import Storage, StorageList, Settings, Messages from gluon.utils import web2py_uuid, compare @@ -55,7 +56,7 @@ Field = DAL.Field __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] -### mind there are two loggers here (logger and crud.settings.logger)! +# mind there are two loggers here (logger and crud.settings.logger)! logger = logging.getLogger("web2py") DEFAULT = lambda: None @@ -245,7 +246,7 @@ class Mail(object): if filename is None: raise Exception('Missing attachment name') payload = payload.read() - #FIXME PY3 can be used to_native? + # FIXME PY3 can be used to_native? filename = filename.encode(encoding) if content_type is None: content_type = contenttype(filename) @@ -633,13 +634,13 @@ class Mail(object): x509_sign_chainfile = x509_sign_chainfile or self.settings.x509_sign_chainfile x509_sign_certfile = x509_sign_certfile or self.settings.x509_sign_certfile or \ - x509_sign_keyfile or self.settings.x509_sign_certfile + x509_sign_keyfile or self.settings.x509_sign_certfile # crypt certfiles could be a string or a list x509_crypt_certfiles = x509_crypt_certfiles or self.settings.x509_crypt_certfiles x509_nocerts = x509_nocerts or\ - self.settings.x509_nocerts + self.settings.x509_nocerts # need m2crypto try: @@ -697,8 +698,8 @@ class Mail(object): # make an encryption cert's stack for crypt_certfile in x509_crypt_certfiles: certfile = X509.load_cert(crypt_certfile)\ - if os.path.isfile(crypt_certfile)\ - else X509.load_cert_string(crypt_certfile) + if os.path.isfile(crypt_certfile)\ + else X509.load_cert_string(crypt_certfile) sk.push(certfile) s.set_x509_stack(sk) @@ -768,10 +769,10 @@ class Mail(object): xcc['reply_to'] = reply_to from google.appengine.api import mail attachments = attachments and [mail.Attachment( - a.my_filename, - a.my_payload, - content_id='' % k - ) for k, a in enumerate(attachments) if not raw] + a.my_filename, + a.my_payload, + content_id='' % k + ) for k, a in enumerate(attachments) if not raw] if attachments: result = mail.send_mail( sender=sender, to=origTo, @@ -841,7 +842,7 @@ class Recaptcha(DIV): options='', comment='', ajax=False - ): + ): request = request or current.request self.request_vars = request and request.vars or current.request.vars self.remote_addr = request.env.remote_addr @@ -1217,7 +1218,7 @@ class AuthJWT(object): salt=None, additional_payload=None, before_authorization=None, - max_header_length=4*1024, + max_header_length=4 * 1024, ): self.secret_key = secret_key self.auth = auth @@ -1318,9 +1319,9 @@ class AuthJWT(object): while sticking to a somewhat-stable Auth API. """ # TODO: Check the following comment - ## is the following safe or should we use - ## calendar.timegm(datetime.datetime.utcnow().timetuple()) - ## result seem to be the same (seconds since epoch, in UTC) + # is the following safe or should we use + # calendar.timegm(datetime.datetime.utcnow().timetuple()) + # result seem to be the same (seconds since epoch, in UTC) now = time.mktime(datetime.datetime.now().timetuple()) expires = now + self.expiration payload = dict( @@ -1484,172 +1485,119 @@ class AuthJWT(object): return decorator -class Auth(object): - default_settings = dict( - hideerror=False, - password_min_length=4, - cas_maps=None, - reset_password_requires_verification=False, - registration_requires_verification=False, - registration_requires_approval=False, - bulk_register_enabled=False, - login_after_registration=False, - login_after_password_change=True, - alternate_requires_registration=False, - create_user_groups="user_%(id)s", - everybody_group_id=None, - manager_actions={}, - auth_manager_role=None, - two_factor_authentication_group=None, - auth_two_factor_enabled=False, - auth_two_factor_tries_left=3, - login_captcha=None, - register_captcha=None, - pre_registration_div=None, - retrieve_username_captcha=None, - retrieve_password_captcha=None, - captcha=None, - prevent_open_redirect_attacks=True, - prevent_password_reset_attacks=True, - expiration=3600, # one hour - long_expiration=3600 * 30 * 24, # one month - remember_me_form=True, - allow_basic_login=False, - allow_basic_login_only=False, - on_failed_authentication=lambda x: redirect(x), - formstyle=None, - label_separator=None, - logging_enabled=True, - allow_delete_accounts=False, - password_field='password', - table_user_name='auth_user', - table_group_name='auth_group', - table_membership_name='auth_membership', - table_permission_name='auth_permission', - table_event_name='auth_event', - table_cas_name='auth_cas', - table_token_name='auth_token', - table_user=None, - table_group=None, - table_membership=None, - table_permission=None, - table_event=None, - table_cas=None, - showid=False, - use_username=False, - login_email_validate=True, - login_userfield=None, - multi_login=False, - logout_onlogout=None, - register_fields=None, - register_verify_password=True, - profile_fields=None, - email_case_sensitive=True, - username_case_sensitive=True, - update_fields=['email'], - ondelete="CASCADE", - client_side=True, - renew_session_onlogin=True, - renew_session_onlogout=True, - keep_session_onlogin=True, - keep_session_onlogout=False, - wiki=Settings(), - ) - # ## these are messages that can be customized - default_messages = dict( - login_button='Log In', - register_button='Sign Up', - password_reset_button='Request reset password', - password_change_button='Change password', - profile_save_button='Apply changes', - submit_button='Submit', - verify_password='Verify Password', - delete_label='Check to delete', - function_disabled='Function disabled', - access_denied='Insufficient privileges', - registration_verifying='Registration needs verification', - registration_pending='Registration is pending approval', - email_taken='This email already has an account', - invalid_username='Invalid username', - username_taken='Username already taken', - login_disabled='Login disabled by administrator', - logged_in='Logged in', - email_sent='Email sent', - unable_to_send_email='Unable to send email', - email_verified='Email verified', - logged_out='Logged out', - registration_successful='Registration successful', - invalid_email='Invalid email', - invalid_login='Invalid login', - invalid_user='Invalid user', - invalid_password='Invalid password', - invalid_two_factor_code = 'Incorrect code. {0} more attempt(s) remaining.', - is_empty="Cannot be empty", - mismatched_password="Password fields don't match", - verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email', - verify_email_subject='Email verification', - username_sent='Your username was emailed to you', - new_password_sent='A new password was emailed to you', - password_changed='Password changed', - retrieve_username='Your username is: %(username)s', - retrieve_username_subject='Username retrieve', - retrieve_password='Your password is: %(password)s', - retrieve_password_subject='Password retrieve', - reset_password='Click on the link %(link)s to reset your password', - reset_password_subject='Password reset', - bulk_invite_subject='Invitation to join %(site)s', - retrieve_two_factor_code='Your temporary login code is {0}', - retrieve_two_factor_code_subject='Two-step Login Authentication Code', - bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', - invalid_reset_password='Invalid reset password', - profile_updated='Profile updated', - new_password='New password', - old_password='Old password', - group_description='Group uniquely assigned to user %(id)s', - register_log='User %(id)s Registered', - login_log='User %(id)s Logged-in', - login_failed_log=None, - logout_log='User %(id)s Logged-out', - profile_log='User %(id)s Profile updated', - verify_email_log='User %(id)s Verification email sent', - retrieve_username_log='User %(id)s Username retrieved', - retrieve_password_log='User %(id)s Password retrieved', - reset_password_log='User %(id)s Password reset', - change_password_log='User %(id)s Password changed', - add_group_log='Group %(group_id)s created', - del_group_log='Group %(group_id)s deleted', - add_membership_log=None, - del_membership_log=None, - has_membership_log=None, - add_permission_log=None, - del_permission_log=None, - has_permission_log=None, - impersonate_log='User %(id)s is impersonating %(other_id)s', - label_first_name='First name', - label_last_name='Last name', - label_username='Username', - label_email='E-mail', - label_password='Password', - label_registration_key='Registration key', - label_reset_password_key='Reset Password key', - label_registration_id='Registration identifier', - label_role='Role', - label_description='Description', - label_user_id='User ID', - label_group_id='Group ID', - label_name='Name', - label_table_name='Object or table name', - label_record_id='Record ID', - label_time_stamp='Timestamp', - label_client_ip='Client IP', - label_origin='Origin', - label_remember_me="Remember me (for 30 days)", - label_two_factor='Authentication code', - two_factor_comment = 'This code was emailed to you and is required for login.', - verify_password_comment='please input your password again', - ) +class Auth(AuthAPI): + default_settings = dict(AuthAPI.default_settings, + allow_basic_login=False, + allow_basic_login_only=False, + allow_delete_accounts=False, + alternate_requires_registration=False, + auth_manager_role=None, + auth_two_factor_enabled=False, + auth_two_factor_tries_left=3, + bulk_register_enabled=False, + captcha=None, + cas_maps=None, + client_side=True, + formstyle=None, + hideerror=False, + label_separator=None, + login_after_password_change=True, + login_after_registration=False, + login_captcha=None, + long_expiration=3600 * 30 * 24, # one month + mailer=None, + manager_actions={}, + multi_login=False, + on_failed_authentication=lambda x: redirect(x), + pre_registration_div=None, + prevent_open_redirect_attacks=True, + prevent_password_reset_attacks=True, + profile_fields=None, + register_captcha=None, + register_fields=None, + register_verify_password=True, + remember_me_form=True, + reset_password_requires_verification=False, + retrieve_password_captcha=None, + retrieve_username_captcha=None, + showid=False, + table_cas=None, + table_cas_name='auth_cas', + table_event=None, + table_group=None, + table_membership=None, + table_permission=None, + table_token_name='auth_token', + table_user=None, + two_factor_authentication_group=None, + update_fields=['email'], + wiki=Settings() + ) + # ## these are messages that can be customized + default_messages = dict(AuthAPI.default_messages, + access_denied='Insufficient privileges', + bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', + bulk_invite_subject='Invitation to join %(site)s', + delete_label='Check to delete', + email_sent='Email sent', + email_verified='Email verified', + function_disabled='Function disabled', + impersonate_log='User %(id)s is impersonating %(other_id)s', + invalid_reset_password='Invalid reset password', + invalid_two_factor_code='Incorrect code. {0} more attempt(s) remaining.', + is_empty="Cannot be empty", + label_client_ip='Client IP', + label_description='Description', + label_email='E-mail', + label_first_name='First name', + label_group_id='Group ID', + label_last_name='Last name', + label_name='Name', + label_origin='Origin', + label_password='Password', + label_record_id='Record ID', + label_registration_id='Registration identifier', + label_registration_key='Registration key', + label_remember_me="Remember me (for 30 days)", + label_reset_password_key='Reset Password key', + label_role='Role', + label_table_name='Object or table name', + label_time_stamp='Timestamp', + label_two_factor='Authentication code', + label_user_id='User ID', + label_username='Username', + login_button='Log In', + login_disabled='Login disabled by administrator', + new_password='New password', + new_password_sent='A new password was emailed to you', + old_password='Old password', + password_change_button='Change password', + password_reset_button='Request reset password', + profile_save_button='Apply changes', + register_button='Sign Up', + reset_password='Click on the link %(link)s to reset your password', + reset_password_log='User %(id)s Password reset', + reset_password_subject='Password reset', + retrieve_password='Your password is: %(password)s', + retrieve_password_log='User %(id)s Password retrieved', + retrieve_password_subject='Password retrieve', + retrieve_two_factor_code='Your temporary login code is {0}', + retrieve_two_factor_code_subject='Two-step Login Authentication Code', + retrieve_username='Your username is: %(username)s', + retrieve_username_log='User %(id)s Username retrieved', + retrieve_username_subject='Username retrieve', + submit_button='Submit', + two_factor_comment='This code was emailed to you and is required for login.', + unable_send_email='Unable to send email', + username_sent='Your username was emailed to you', + verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email', + verify_email_log='User %(id)s Verification email sent', + verify_email_subject='Email verification', + verify_password='Verify Password', + verify_password_comment='please input your password again' + ) """ Class for authentication, authorization, role based access control. @@ -1816,7 +1764,7 @@ class Auth(object): self.user = auth.user # this is a trick to speed up sessions to avoid many writes if (now - auth.last_visit).seconds > (auth.expiration / 10): - auth.last_visit = request.now + auth.last_visit = now else: self.user = None if session.auth: @@ -1893,15 +1841,14 @@ class Auth(object): # ## these are messages that can be customized messages = self.messages = Messages(current.T) messages.update(Auth.default_messages) - messages.update(ajax_failed_authentication= - DIV(H4('NOT AUTHORIZED'), - 'Please ', - A('login', - _href=self.settings.login_url + - ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) - if current.request.env.http_web2py_component_location else ''), - ' to view this content.', - _class='not-authorized alert alert-block')) + messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'), + 'Please ', + A('login', + _href=self.settings.login_url + + ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) + if current.request.env.http_web2py_component_location else ''), + ' to view this content.', + _class='not-authorized alert alert-block')) messages.lock_keys = True # for "remember me" option @@ -1932,31 +1879,10 @@ class Auth(object): parts = next.split('/') if not ':' in parts[0]: return next - elif len(parts)>2 and parts[0].endswith(':') and parts[1:3]==['', host]: + elif len(parts) > 2 and parts[0].endswith(':') and parts[1:3] == ['', host]: return next return None - def _get_user_id(self): - """accessor for auth.user_id""" - return self.user and self.user.id or None - - user_id = property(_get_user_id, doc="user.id or None") - - def table_user(self): - return self.db[self.settings.table_user_name] - - def table_group(self): - return self.db[self.settings.table_group_name] - - def table_membership(self): - return self.db[self.settings.table_membership_name] - - def table_permission(self): - return self.db[self.settings.table_permission_name] - - def table_event(self): - return self.db[self.settings.table_event_name] - def table_cas(self): return self.db[self.settings.table_cas_name] @@ -2062,7 +1988,7 @@ class Auth(object): if callable(user_identifier): user_identifier = user_identifier(self.user) elif ((isinstance(user_identifier, str) or - type(user_identifier).__name__ == 'lazyT') and + type(user_identifier).__name__ == 'lazyT') and re.search(r'%\(.+\)s', user_identifier)): user_identifier = user_identifier % self.user if not user_identifier: @@ -2090,13 +2016,13 @@ class Auth(object): self.bar[0][3].append((item['name'], False, item['href'])) def bootstrap3(): # Default web2py scaffolding - def rename(icon): return icon+' '+icon.replace('icon', 'glyphicon') - self.bar = UL(LI(Anr(I(_class=rename('icon '+items[0]['icon'])), + def rename(icon): return icon + ' ' + icon.replace('icon', 'glyphicon') + self.bar = UL(LI(Anr(I(_class=rename('icon ' + items[0]['icon'])), ' ' + items[0]['name'], _href=items[0]['href'])), _class='dropdown-menu') del items[0] for item in items: - self.bar.insert(-1, LI(Anr(I(_class=rename('icon '+item['icon'])), + self.bar.insert(-1, LI(Anr(I(_class=rename('icon ' + item['icon'])), ' ' + item['name'], _href=item['href']))) self.bar.insert(-1, LI('', _class='divider')) @@ -2213,15 +2139,6 @@ class Auth(object): return self.bar - def __get_migrate(self, tablename, migrate=True): - - if type(migrate).__name__ == 'str': - return (migrate + tablename + '.table') - elif migrate == False: - return False - else: - return True - def enable_record_versioning(self, tables, archive_db=None, @@ -2260,56 +2177,13 @@ class Auth(object): for table in tables: fieldnames = table.fields() if ('id' in fieldnames and - 'modified_on' in fieldnames and - not current_record in fieldnames): + 'modified_on' in fieldnames and + not current_record in fieldnames): table._enable_record_versioning(archive_db=archive_db, archive_name=archive_names, current_record=current_record, current_record_label=current_record_label) - def define_signature(self): - db = self.db - settings = self.settings - request = current.request - T = current.T - reference_user = 'reference %s' % settings.table_user_name - - def lazy_user(auth=self): - return auth.user_id - - def represent(id, record=None, s=settings): - try: - user = s.table_user(id) - return '%s %s' % (user.get("first_name", user.get("email")), - user.get("last_name", '')) - except: - return id - ondelete = self.settings.ondelete - self.signature = Table( - self.db, 'auth_signature', - Field('is_active', 'boolean', - default=True, - readable=False, writable=False, - label=T('Is Active')), - Field('created_on', 'datetime', - default=request.now, - writable=False, readable=False, - label=T('Created On')), - Field('created_by', - reference_user, - default=lazy_user, represent=represent, - writable=False, readable=False, - label=T('Created By'), ondelete=ondelete), - Field('modified_on', 'datetime', - update=request.now, default=request.now, - writable=False, readable=False, - label=T('Modified On')), - Field('modified_by', - reference_user, represent=represent, - default=lazy_user, update=lazy_user, - writable=False, readable=False, - label=T('Modified By'), ondelete=ondelete)) - def define_tables(self, username=None, signature=None, enable_tokens=False, migrate=None, fake_migrate=None): """ @@ -2333,179 +2207,11 @@ class Auth(object): if fake_migrate is None: fake_migrate = db._fake_migrate settings = self.settings - if username is None: - username = settings.use_username - else: - settings.use_username = username settings.enable_tokens = enable_tokens - if not self.signature: - self.define_signature() - if signature: - signature_list = [self.signature] - elif not signature: - signature_list = [] - elif isinstance(signature, Table): - signature_list = [signature] - else: - signature_list = signature - is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) - is_crypted = CRYPT(key=settings.hmac_key, - min_length=settings.password_min_length) - is_unique_email = [ - IS_EMAIL(error_message=self.messages.invalid_email), - IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name, - error_message=self.messages.email_taken)] - if not settings.email_case_sensitive: - is_unique_email.insert(1, IS_LOWER()) - if settings.table_user_name not in db.tables: - passfield = settings.password_field - extra_fields = settings.extra_fields.get( - settings.table_user_name, []) + signature_list - if username or settings.cas_provider: - is_unique_username = \ - [IS_MATCH('[\w\.\-]+', strict=True, - error_message=self.messages.invalid_username), - IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name, - error_message=self.messages.username_taken)] - if not settings.username_case_sensitive: - is_unique_username.insert(1, IS_LOWER()) - db.define_table( - settings.table_user_name, - Field('first_name', length=128, default='', - label=self.messages.label_first_name, - requires=is_not_empty), - Field('last_name', length=128, default='', - label=self.messages.label_last_name, - requires=is_not_empty), - Field('email', length=512, default='', - label=self.messages.label_email, - requires=is_unique_email), - Field('username', length=128, default='', - label=self.messages.label_username, - requires=is_unique_username), - Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password, - requires=[is_crypted]), - Field('registration_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_key), - Field('reset_password_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_reset_password_key), - Field('registration_id', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_id), - *extra_fields, - **dict( - migrate=self.__get_migrate(settings.table_user_name, - migrate), - fake_migrate=fake_migrate, - format='%(username)s')) - else: - db.define_table( - settings.table_user_name, - Field('first_name', length=128, default='', - label=self.messages.label_first_name, - requires=is_not_empty), - Field('last_name', length=128, default='', - label=self.messages.label_last_name, - requires=is_not_empty), - Field('email', length=512, default='', - label=self.messages.label_email, - requires=is_unique_email), - Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password, - requires=[is_crypted]), - Field('registration_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_key), - Field('reset_password_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_reset_password_key), - Field('registration_id', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_id), - *extra_fields, - **dict( - migrate=self.__get_migrate(settings.table_user_name, - migrate), - fake_migrate=fake_migrate, - format='%(first_name)s %(last_name)s (%(id)s)')) - reference_table_user = 'reference %s' % settings.table_user_name - if settings.table_group_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_group_name, []) + signature_list - db.define_table( - settings.table_group_name, - Field('role', length=512, default='', - label=self.messages.label_role, - requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)), - Field('description', 'text', - label=self.messages.label_description), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_group_name, migrate), - fake_migrate=fake_migrate, - format='%(role)s (%(id)s)')) - reference_table_group = 'reference %s' % settings.table_group_name - if settings.table_membership_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_membership_name, []) + signature_list - db.define_table( - settings.table_membership_name, - Field('user_id', reference_table_user, - label=self.messages.label_user_id), - Field('group_id', reference_table_group, - label=self.messages.label_group_id), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_membership_name, migrate), - fake_migrate=fake_migrate)) - if settings.table_permission_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_permission_name, []) + signature_list - db.define_table( - settings.table_permission_name, - Field('group_id', reference_table_group, - label=self.messages.label_group_id), - Field('name', default='default', length=512, - label=self.messages.label_name, - requires=is_not_empty), - Field('table_name', length=512, - label=self.messages.label_table_name), - Field('record_id', 'integer', default=0, - label=self.messages.label_record_id, - requires=IS_INT_IN_RANGE(0, 10 ** 9)), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_permission_name, migrate), - fake_migrate=fake_migrate)) - if settings.table_event_name not in db.tables: - db.define_table( - settings.table_event_name, - Field('time_stamp', 'datetime', - default=current.request.now, - label=self.messages.label_time_stamp), - Field('client_ip', - default=current.request.client, - label=self.messages.label_client_ip), - Field('user_id', reference_table_user, default=None, - label=self.messages.label_user_id), - Field('origin', default='auth', length=512, - label=self.messages.label_origin, - requires=is_not_empty), - Field('description', 'text', default='', - label=self.messages.label_description, - requires=is_not_empty), - *settings.extra_fields.get(settings.table_event_name, []), - **dict( - migrate=self.__get_migrate( - settings.table_event_name, migrate), - fake_migrate=fake_migrate)) + super(Auth, self).define_tables(username, signature, migrate, fake_migrate) + now = current.request.now + reference_table_user = 'reference %s' % settings.table_user_name if settings.cas_domains: if settings.table_cas_name not in db.tables: db.define_table( @@ -2518,7 +2224,7 @@ class Auth(object): Field('renew', 'boolean', default=False), *settings.extra_fields.get(settings.table_cas_name, []), **dict( - migrate=self.__get_migrate( + migrate=self._get_migrate( settings.table_cas_name, migrate), fake_migrate=fake_migrate)) if settings.enable_tokens: @@ -2532,7 +2238,7 @@ class Auth(object): Field('expires_on', 'datetime', default=datetime.datetime(2999, 12, 31)), Field('token', writable=False, default=web2py_uuid, unique=True), *extra_fields, - **dict(migrate=self.__get_migrate(settings.table_token_name, migrate), + **dict(migrate=self._get_migrate(settings.table_token_name, migrate), fake_migrate=fake_migrate)) if not db._lazy_tables: settings.table_user = db[settings.table_user_name] @@ -2566,26 +2272,6 @@ class Auth(object): maps=maps) return self - def log_event(self, description, vars=None, origin='auth'): - """ - Examples: - Use as:: - - auth.log_event(description='this happened', origin='auth') - - """ - if not self.settings.logging_enabled or not description: - return - elif self.is_logged_in(): - user_id = self.user.id - else: - user_id = None # user unknown - vars = vars or {} - # log messages should not be translated - if type(description).__name__ == 'lazyT': - description = description.m - self.table_event().insert(description=str(description % vars), origin=origin, user_id=user_id) - def get_or_create_user(self, keys, update_fields=['email'], login=True, get=True): """ @@ -2683,29 +2369,6 @@ class Auth(object): raise http_401 return (True, True, is_valid_user) - def login_user(self, user): - """ - Logins the `user = db.auth_user(id)` - """ - from gluon.settings import global_settings - if global_settings.web2py_runtime_gae: - user = Row(self.table_user()._filter_fields(user, id=True)) - delattr(user, 'password') - else: - user = Row(user) - for key in list(user.keys()): - value = user[key] - if callable(value) or key == 'password': - delattr(user, key) - if self.settings.renew_session_onlogin: - current.session.renew(clear_session=not self.settings.keep_session_onlogin) - current.session.auth = Storage(user=user, - last_visit=current.request.now, - expiration=self.settings.expiration, - hmac_key=web2py_uuid()) - self.user = user - self.update_groups() - def _get_login_settings(self): table_user = self.table_user() userfield = self.settings.login_userfield or 'username' \ @@ -2726,7 +2389,7 @@ class Auth(object): settings.passfield].validate(password)[0] if ((user.registration_key is None or not user.registration_key.strip()) and - password == user[settings.passfield]): + password == user[settings.passfield]): self.login_user(user) return user else: @@ -3368,7 +3031,7 @@ class Auth(object): if self.settings.register_fields is None: self.settings.register_fields = [f.name for f in table_user if f.writable] k = self.settings.register_fields.index(passfield) - self.settings.register_fields.insert(k+1, "password_two") + self.settings.register_fields.insert(k + 1, "password_two") extra_fields = [ Field("password_two", "password", requires=IS_EQUAL_TO(request.post_vars.get(passfield, None), @@ -3447,16 +3110,6 @@ class Auth(object): return form - def is_logged_in(self): - """ - Checks if the user is logged in and returns True/False. - If so user is in auth.user as well as in session.auth.user - """ - - if self.user: - return True - return False - def verify_email(self, next=DEFAULT, onaccept=DEFAULT, @@ -3508,7 +3161,7 @@ class Auth(object): response = current.response session = current.session captcha = self.settings.retrieve_username_captcha or \ - (self.settings.retrieve_username_captcha is not False and self.settings.captcha) + (self.settings.retrieve_username_captcha is not False and self.settings.captcha) if not self.settings.mailer: response.flash = self.messages.function_disabled return '' @@ -3627,7 +3280,7 @@ class Auth(object): d = { passfield: str(table_user[passfield].validate(password)[0]), 'registration_key': '' - } + } user.update_record(**d) if self.settings.mailer and \ self.settings.mailer.send(to=form.vars.email, @@ -3684,7 +3337,6 @@ class Auth(object): raise Exception except Exception as e: session.flash = self.messages.invalid_reset_password - redirect(self.url('login', vars=dict(test=e))) redirect(next, client_side=self.settings.client_side) passfield = self.settings.password_field form = SQLFORM.factory( @@ -3768,9 +3420,9 @@ class Auth(object): else: emails_fail.append(email) emails_fail += emails[max_emails:] - form = DIV(H4('Emails sent'), UL(*[A(x, _href='mailto:'+x) for x in emails_sent]), - H4('Emails failed'), UL(*[A(x, _href='mailto:'+x) for x in emails_fail]), - H4('Emails existing'), UL(*[A(x, _href='mailto:'+x) for x in emails_exist])) + form = DIV(H4('Emails sent'), UL(*[A(x, _href='mailto:' + x) for x in emails_sent]), + H4('Emails failed'), UL(*[A(x, _href='mailto:' + x) for x in emails_fail]), + H4('Emails existing'), UL(*[A(x, _href='mailto:' + x) for x in emails_exist])) return form def manage_tokens(self): @@ -3874,7 +3526,7 @@ class Auth(object): response = current.response session = current.session captcha = self.settings.retrieve_password_captcha or \ - (self.settings.retrieve_password_captcha is not False and self.settings.captcha) + (self.settings.retrieve_password_captcha is not False and self.settings.captcha) if next is DEFAULT: next = self.get_vars_next() or self.settings.request_reset_password_next @@ -4077,7 +3729,7 @@ class Auth(object): formstyle=self.settings.formstyle, separator=self.settings.label_separator, deletable=self.settings.allow_delete_accounts, - ) + ) if form.accepts(request, session, formname='profile', onvalidation=onvalidation, @@ -4205,21 +3857,6 @@ class Auth(object): else: return SQLFORM(table_user, user.id, readonly=True) - def update_groups(self): - if not self.user: - return - user_groups = self.user_groups = {} - if current.session.auth: - current.session.auth.user_groups = self.user_groups - table_group = self.table_group() - table_membership = self.table_membership() - memberships = self.db( - table_membership.user_id == self.user.id).select() - for membership in memberships: - group = table_group(membership.group_id) - if group: - user_groups[membership.group_id] = group.role - def groups(self): """ Displays the groups and their roles for the logged in user @@ -4337,6 +3974,7 @@ class Auth(object): If role is provided instead of group_id then the group_id is calculated. """ + def has_membership(self=self, group_id=group_id, role=role): return self.has_membership(group_id=group_id, role=role) return self.requires(has_membership, otherwise=otherwise) @@ -4348,6 +3986,7 @@ class Auth(object): if user logged in is not a member of any group (role) that has 'name' access to 'table_name', 'record_id'. """ + def has_permission(self=self, name=name, table_name=table_name, record_id=record_id): return self.has_permission(name, table_name, record_id) return self.requires(has_permission, otherwise=otherwise) @@ -4363,231 +4002,6 @@ class Auth(object): return URL.verify(current.request, user_signature=True, hash_vars=hash_vars) return self.requires(verify, otherwise) - def add_group(self, role, description=''): - """ - Creates a group associated to a role - """ - group_id = self.table_group().insert(role=role, description=description) - self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role)) - return group_id - - def del_group(self, group_id): - """ - Deletes a group - """ - self.db(self.table_group().id == group_id).delete() - self.db(self.table_membership().group_id == group_id).delete() - self.db(self.table_permission().group_id == group_id).delete() - if group_id in self.user_groups: - del self.user_groups[group_id] - self.log_event(self.messages.del_group_log, dict(group_id=group_id)) - - def id_group(self, role): - """ - Returns the group_id of the group specified by the role - """ - rows = self.db(self.table_group().role == role).select() - if not rows: - return None - return rows[0].id - - def user_group(self, user_id=None): - """ - Returns the group_id of the group uniquely associated to this user - i.e. `role=user:[user_id]` - """ - return self.id_group(self.user_group_role(user_id)) - - def user_group_role(self, user_id=None): - if not self.settings.create_user_groups: - return None - if user_id: - user = self.table_user()[user_id] - else: - user = self.user - return self.settings.create_user_groups % user - - def has_membership(self, group_id=None, user_id=None, role=None): - """ - Checks if user is member of group_id or role - """ - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - membership = self.table_membership() - if group_id and user_id and self.db((membership.user_id == user_id) & - (membership.group_id == group_id)).select(): - r = True - else: - r = False - self.log_event(self.messages['has_membership_log'], - dict(user_id=user_id, group_id=group_id, check=r)) - return r - - def add_membership(self, group_id=None, user_id=None, role=None): - """ - Gives user_id membership of group_id or role - if user is None than user_id is that of current logged in user - """ - - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - if not group_id: - raise ValueError('group_id not provided or invalid') - if not user_id: - raise ValueError('user_id not provided or invalid') - membership = self.table_membership() - db = membership._db - record = db((membership.user_id == user_id) & - (membership.group_id == group_id), - ignore_common_filters=True).select().first() - if record: - if hasattr(record, 'is_active') and not record.is_active: - record.update_record(is_active=True) - return record.id - else: - id = membership.insert(group_id=group_id, user_id=user_id) - if role: - self.user_groups[group_id] = role - else: - self.update_groups() - self.log_event(self.messages['add_membership_log'], - dict(user_id=user_id, group_id=group_id)) - return id - - def del_membership(self, group_id=None, user_id=None, role=None): - """ - Revokes membership from group_id to user_id - if user_id is None than user_id is that of current logged in user - """ - - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - membership = self.table_membership() - self.log_event(self.messages['del_membership_log'], - dict(user_id=user_id, group_id=group_id)) - ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete() - if group_id in self.user_groups: - del self.user_groups[group_id] - return ret - - def has_permission(self, - name='any', - table_name='', - record_id=0, - user_id=None, - group_id=None, - ): - """ - Checks if user_id or current logged in user is member of a group - that has 'name' permission on 'table_name' and 'record_id' - if group_id is passed, it checks whether the group has the permission - """ - - if not group_id and self.settings.everybody_group_id and \ - self.has_permission(name, table_name, record_id, user_id=None, - group_id=self.settings.everybody_group_id): - return True - - if not user_id and not group_id and self.user: - user_id = self.user.id - if user_id: - membership = self.table_membership() - rows = self.db(membership.user_id == user_id).select(membership.group_id) - groups = set([row.group_id for row in rows]) - if group_id and group_id not in groups: - return False - else: - groups = set([group_id]) - permission = self.table_permission() - rows = self.db(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - record_id).select(permission.group_id) - groups_required = set([row.group_id for row in rows]) - if record_id: - rows = self.db(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - 0).select(permission.group_id) - groups_required = groups_required.union(set([row.group_id for row in rows])) - if groups.intersection(groups_required): - r = True - else: - r = False - if user_id: - self.log_event(self.messages['has_permission_log'], - dict(user_id=user_id, name=name, - table_name=table_name, record_id=record_id)) - return r - - def add_permission(self, - group_id, - name='any', - table_name='', - record_id=0, - ): - """ - Gives group_id 'name' access to 'table_name' and 'record_id' - """ - - permission = self.table_permission() - if group_id == 0: - group_id = self.user_group() - record = self.db((permission.group_id == group_id) & - (permission.name == name) & - (permission.table_name == str(table_name)) & - (permission.record_id == long(record_id)), - ignore_common_filters=True - ).select(limitby=(0, 1), orderby_on_limitby=False).first() - if record: - if hasattr(record, 'is_active') and not record.is_active: - record.update_record(is_active=True) - id = record.id - else: - id = permission.insert(group_id=group_id, name=name, - table_name=str(table_name), - record_id=long(record_id)) - self.log_event(self.messages['add_permission_log'], - dict(permission_id=id, group_id=group_id, - name=name, table_name=table_name, - record_id=record_id)) - return id - - def del_permission(self, - group_id, - name='any', - table_name='', - record_id=0, - ): - """ - Revokes group_id 'name' access to 'table_name' and 'record_id' - """ - - permission = self.table_permission() - self.log_event(self.messages['del_permission_log'], - dict(group_id=group_id, name=name, - table_name=table_name, record_id=record_id)) - return self.db(permission.group_id == - group_id)(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - long(record_id)).delete() - def accessible_query(self, name, table, user_id=None): """ Returns a query with all accessible records for user_id or @@ -4622,16 +4036,16 @@ class Auth(object): permission = self.table_permission() query = table.id.belongs( db(membership.user_id == user_id) - (membership.group_id == permission.group_id) - (permission.name == name) - (permission.table_name == table) - ._select(permission.record_id)) + (membership.group_id == permission.group_id) + (permission.name == name) + (permission.table_name == table) + ._select(permission.record_id)) if self.settings.everybody_group_id: query |= table.id.belongs( db(permission.group_id == self.settings.everybody_group_id) - (permission.name == name) - (permission.table_name == table) - ._select(permission.record_id)) + (permission.name == name) + (permission.table_name == table) + ._select(permission.record_id)) return query @staticmethod @@ -4769,12 +4183,12 @@ class Auth(object): def wikimenu(self): """To be used in menu.py for app wide wiki menus""" if (hasattr(self, "_wiki") and - self._wiki.settings.controller and - self._wiki.settings.function): + self._wiki.settings.controller and + self._wiki.settings.function): self._wiki.automenu() -class Crud(object): # pragma: no cover +class Crud(object): # pragma: no cover def url(self, f=None, args=None, vars=None): """ @@ -4947,7 +4361,7 @@ class Crud(object): # pragma: no cover formstyle=self.settings.formstyle, separator=self.settings.label_separator, **attributes # contains hidden - ) + ) self.accepted = False self.deleted = False captcha = self.settings.update_captcha or self.settings.captcha @@ -5044,7 +4458,7 @@ class Crud(object): # pragma: no cover showid=self.settings.showid, formstyle=self.settings.formstyle, separator=self.settings.label_separator - ) + ) if current.request.extension not in ('html', 'load'): return table._filter_fields(form.record, id=True) return form @@ -5702,6 +5116,7 @@ class Service(object): self.error() class JsonRpcException(Exception): + def __init__(self, code, info): jrpc_error = Service.jsonrpc_errors.get(code) if jrpc_error: @@ -6218,6 +5633,7 @@ class PluginManager(object): class Expose(object): + def __init__(self, base=None, basename=None, extensions=None, allow_download=True, follow_symlink_out=False): """ @@ -6245,7 +5661,8 @@ class Expose(object): """ # why would this not be callable? but otherwise tests do not pass - if current.session and callable(current.session.forget): current.session.forget() + if current.session and callable(current.session.forget): + current.session.forget() self.follow_symlink_out = follow_symlink_out self.base = self.normalize_path( base or os.path.join(current.request.folder, 'static')) @@ -6305,7 +5722,7 @@ class Expose(object): @staticmethod def __in_base(subdir, basedir, sep=os.path.sep): """True if subdir/ is under basedir/""" - s = lambda f: '%s%s' % (f.rstrip(sep), sep) # f -> f/ + s = lambda f: '%s%s' % (f.rstrip(sep), sep) # f -> f/ # The trailing '/' is for the case of '/foobar' in_base of '/foo': # - becase '/foobar' starts with '/foo' # - but '/foobar/' doesn't start with '/foo/' @@ -6470,46 +5887,46 @@ class Wiki(object): table_definitions = [ ('wiki_page', { - 'args': [ - Field('slug', - requires=[IS_SLUG(), - IS_NOT_IN_DB(db, 'wiki_page.slug')], - writable=False), - Field('title', length=255, unique=True), - Field('body', 'text', notnull=True), - Field('tags', 'list:string'), - Field('can_read', 'list:string', - writable=perms, - readable=perms, - default=[Wiki.everybody]), - Field('can_edit', 'list:string', - writable=perms, readable=perms, - default=[Wiki.everybody]), - Field('changelog'), - Field('html', 'text', - compute=self.get_renderer(), - readable=False, writable=False), - Field('render', default="markmin", - readable=show_engine, - writable=show_engine, - requires=IS_EMPTY_OR( - IS_IN_SET(engines))), - auth.signature], - 'vars': {'format': '%(title)s', 'migrate': migrate}}), + 'args': [ + Field('slug', + requires=[IS_SLUG(), + IS_NOT_IN_DB(db, 'wiki_page.slug')], + writable=False), + Field('title', length=255, unique=True), + Field('body', 'text', notnull=True), + Field('tags', 'list:string'), + Field('can_read', 'list:string', + writable=perms, + readable=perms, + default=[Wiki.everybody]), + Field('can_edit', 'list:string', + writable=perms, readable=perms, + default=[Wiki.everybody]), + Field('changelog'), + Field('html', 'text', + compute=self.get_renderer(), + readable=False, writable=False), + Field('render', default="markmin", + readable=show_engine, + writable=show_engine, + requires=IS_EMPTY_OR( + IS_IN_SET(engines))), + auth.signature], + 'vars': {'format': '%(title)s', 'migrate': migrate}}), ('wiki_tag', { - 'args': [ - Field('name'), - Field('wiki_page', 'reference wiki_page'), - auth.signature], - 'vars':{'format': '%(title)s', 'migrate': migrate}}), + 'args': [ + Field('name'), + Field('wiki_page', 'reference wiki_page'), + auth.signature], + 'vars':{'format': '%(title)s', 'migrate': migrate}}), ('wiki_media', { - 'args': [ - Field('wiki_page', 'reference wiki_page'), - Field('title', required=True), - Field('filename', 'upload', required=True), - auth.signature], - 'vars': {'format': '%(title)s', 'migrate': migrate}}), - ] + 'args': [ + Field('wiki_page', 'reference wiki_page'), + Field('title', required=True), + Field('filename', 'upload', required=True), + auth.signature], + 'vars': {'format': '%(title)s', 'migrate': migrate}}), + ] # define only non-existent tables for key, value in table_definitions: @@ -6567,8 +5984,8 @@ class Wiki(object): elif self.auth.user: groups = self.settings.groups if ('wiki_editor' in groups or - set(groups).intersection(set(page.can_read + page.can_edit)) or - page.created_by == self.auth.user.id): + set(groups).intersection(set(page.can_read + page.can_edit)) or + page.created_by == self.auth.user.id): return True return False @@ -6814,7 +6231,7 @@ class Wiki(object): options.insert(0, OPTION('', _value='')) fields = [Field("slug", default=current.request.args(1) or self.settings.force_prefix, - requires=(IS_SLUG(), IS_NOT_IN_DB(db, db.wiki_page.slug))),] + requires=(IS_SLUG(), IS_NOT_IN_DB(db, db.wiki_page.slug))), ] if self.settings.templates: fields.append( Field("from_template", "reference wiki_page", @@ -6980,14 +6397,14 @@ class Wiki(object): def link(t): return A(t, _href=URL(args='_search', vars=dict(q=t))) items = [DIV(H3(A(p.wiki_page.title, _href=URL( - args=p.wiki_page.slug))), - MARKMIN(self.first_paragraph(p.wiki_page)) - if preview else '', - DIV(_class='w2p_wiki_tags', - *[link(t.strip()) for t in - p.wiki_page.tags or [] if t.strip()]), - _class='w2p_wiki_search_item') - for p in pages] + args=p.wiki_page.slug))), + MARKMIN(self.first_paragraph(p.wiki_page)) + if preview else '', + DIV(_class='w2p_wiki_tags', + *[link(t.strip()) for t in + p.wiki_page.tags or [] if t.strip()]), + _class='w2p_wiki_search_item') + for p in pages] content.append(DIV(_class='w2p_wiki_pages', *items)) else: cloud = False @@ -7032,6 +6449,7 @@ class Wiki(object): class Config(object): + def __init__( self, filename, diff --git a/gluon/validators.py b/gluon/validators.py index 4181c226..779acab4 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2478,9 +2478,14 @@ class IS_LOWER(Validator): ('\\xc3\\xb1', None) """ - def __call__(self, value): - return (to_bytes(to_unicode(value).lower()), None) + cast_back = lambda x: x + if isinstance(value, str): + cast_back = to_native + elif isinstance(value, bytes): + cast_back = to_bytes + value = to_unicode(value).lower() + return (cast_back(value), None) class IS_UPPER(Validator): @@ -2495,7 +2500,13 @@ class IS_UPPER(Validator): """ def __call__(self, value): - return (to_bytes(to_unicode(value).upper()), None) + cast_back = lambda x: x + if isinstance(value, str): + cast_back = to_native + elif isinstance(value, bytes): + cast_back = to_bytes + value = to_unicode(value).upper() + return (cast_back(value), None) def urlify(s, maxlen=80, keep_underscores=False): From d1dfc4a06a30af1d64976e0be35b522d96980edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:51:54 +0000 Subject: [PATCH 154/436] use _compat's long --- gluon/authapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/authapi.py b/gluon/authapi.py index 981a314c..dc3860ac 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -4,6 +4,7 @@ | Copyrighted by Massimo Di Pierro | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ +from gluo._compat import long from gluon import current from gluon.storage import Messages, Settings, Storage from gluon.utils import web2py_uuid From 85c68e6876a320bb6f5c8ad4e377aaecfbe0ae5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:52:42 +0000 Subject: [PATCH 155/436] typo --- gluon/authapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index dc3860ac..c4507931 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -4,7 +4,7 @@ | Copyrighted by Massimo Di Pierro | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ -from gluo._compat import long +from gluon._compat import long from gluon import current from gluon.storage import Messages, Settings, Storage from gluon.utils import web2py_uuid From 8e79e49ae3b19f816310858f0f04016d54c6f2bb Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Thu, 10 Nov 2016 01:19:06 -0800 Subject: [PATCH 156/436] Mobile (iOS) fix for dismissing flashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS devices don’t like listening to clicks on most objects. They typically prefer a and button objects. This fix replaces the #closeflash span with a link to ā€œ#ā€ instead (while also inheriting text-decoration and color styling), so that mobile (iOS) devices will allow you to close the flash. --- applications/admin/static/js/web2py.js | 2 +- applications/examples/static/js/web2py.js | 2 +- applications/welcome/static/js/web2py.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 7b9d9802..bbeb0a9c 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 7b9d9802..bbeb0a9c 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 7b9d9802..bbeb0a9c 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); From c64a9192d49ee735981615a25ae2c5e9900f22ed Mon Sep 17 00:00:00 2001 From: Marvi Date: Fri, 11 Nov 2016 10:12:11 +0100 Subject: [PATCH 157/436] Update extract_mysql_models.py --- scripts/extract_mysql_models.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/extract_mysql_models.py b/scripts/extract_mysql_models.py index 4b4d4281..dfaae02c 100644 --- a/scripts/extract_mysql_models.py +++ b/scripts/extract_mysql_models.py @@ -18,7 +18,7 @@ legacy_db(legacy_db.mytable.id>0).select() If the script crashes this is might be due to that fact that the data_type_map dictionary below is incomplete. Please complete it, improve it and continue. -Created by Falko Krause, minor modifications by Massimo Di Pierro and Ron McOuat +Created by Falko Krause, minor modifications by Massimo Di Pierro, Ron McOuat and Marvi Benedet ''' import subprocess import re @@ -52,10 +52,11 @@ data_type_map = dict( ) -def mysql(database_name, username, password): +def mysql(database_name, username, password, host): p = subprocess.Popen(['mysql', '--user=%s' % username, '--password=%s' % password, + '--host=%s' % host, '--execute=show tables;', database_name], stdin=subprocess.PIPE, @@ -64,14 +65,15 @@ def mysql(database_name, username, password): sql_showtables, stderr = p.communicate() tables = [re.sub( '\|\s+([^\|*])\s+.*', '\1', x) for x in sql_showtables.split()[1:]] - connection_string = "legacy_db = DAL('mysql://%s:%s@localhost/%s')" % ( - username, password, database_name) + connection_string = "legacy_db = DAL('mysql://%s:%s@%s/%s')" % ( + username, password, host, database_name) legacy_db_table_web2py_code = [] for table_name in tables: #get the sql create statement p = subprocess.Popen(['mysqldump', '--user=%s' % username, '--password=%s' % password, + '--host=%s' % host, '--skip-add-drop-table', '--no-data', database_name, table_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -106,9 +108,13 @@ def mysql(database_name, username, password): legacy_db_table_web2py_code) return legacy_db_web2py_code -regex = re.compile('(.*?):(.*?)@(.*)') +regex = re.compile('(.*):(.*)@((.*)/)?(.*)') if len(sys.argv) < 2 or not regex.match(sys.argv[1]): - print 'USAGE:\n\n extract_mysql_models.py username:password@data_basename\n\n' + print 'USAGE:\n\n extract_mysql_models.py username:password@[host/]data_basename\n\n' else: m = regex.match(sys.argv[1]) - print mysql(m.group(3), m.group(1), m.group(2)) + username = m.group(1) + password = m.group(2) + host = m.group(4) or 'localhost' + db_name = m.group(5) + print mysql(database_name = db_name, username = username, password = password, host = host) From 75c1d8082407574bafd5ea9a6708cf7b90446eca Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 03:30:16 -0800 Subject: [PATCH 158/436] Replace "#" link with "javascript:null;" --- applications/admin/static/js/web2py.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index bbeb0a9c..6eeef9ca 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +767,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ From f7adfbde76875ce5dc0665fed6d4a2e1f1d334f4 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 03:30:50 -0800 Subject: [PATCH 159/436] Replace "#" link with "javascript:null;" --- applications/examples/static/js/web2py.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index bbeb0a9c..6eeef9ca 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +767,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ From 0a07af55f95beef0120df01114b875f357409e78 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 03:31:10 -0800 Subject: [PATCH 160/436] Replace "#" link with "javascript:null;" --- applications/welcome/static/js/web2py.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index bbeb0a9c..6eeef9ca 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -563,7 +563,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +767,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ From 870d9d3e57c9a7f83bac73202cb57a36a7205371 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 13:52:43 -0800 Subject: [PATCH 161/436] Fix .DS_Store ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 92f69758..de1273c4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.orig Thumbs.db .DS_Store +*.DS_Store index.yaml routes.py logging.conf From d51ea90e1838ffde0481137b708f2538c4ff3eb8 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 15:49:28 -0800 Subject: [PATCH 162/436] Code fix to allow adding markmin translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playing with this file made me realize that the quotes defining the strings themselves are added to the regex searches, and that my previous addition caused invalid syntax. So, I decided to fix this by placing the first quote before the markmin decorator. (Triple quotes won’t work, however…) --- gluon/languages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/languages.py b/gluon/languages.py index afc5b332..ff9ff4cf 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -962,7 +962,7 @@ def findT(path, language=DEFAULT_LANGUAGE): + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): data = to_native(read_locked(filename)) items = regex_translate.findall(data) - items += ["@markmin\x01%s" %x for x in regex_translate_m.findall(data)] + for x in regex_translate_m.findall(data): items.append("%s@markmin\x01%s" %(x[0], x[1:])) for item in items: try: message = safe_eval(item) From 90ee6f3754c324709073bf858b3c5c1234255b5e Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 15:51:01 -0800 Subject: [PATCH 163/436] Add three-quote support on markmin --- gluon/languages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/languages.py b/gluon/languages.py index ff9ff4cf..0faedf33 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -962,7 +962,9 @@ def findT(path, language=DEFAULT_LANGUAGE): + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): data = to_native(read_locked(filename)) items = regex_translate.findall(data) - for x in regex_translate_m.findall(data): items.append("%s@markmin\x01%s" %(x[0], x[1:])) + for x in regex_translate_m.findall(data): + if x[0:3] in ["'''", '"""']: items.append("%s@markmin\x01%s" %(x[0:3], x[3:])) + else: items.append("%s@markmin\x01%s" %(x[0], x[1:])) for item in items: try: message = safe_eval(item) From e19435dbcf4b81083f7cb70f0c1b763c18d89bad Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Fri, 11 Nov 2016 18:51:38 -0800 Subject: [PATCH 164/436] Prevent Firefox from loading the page "javascript:null;" --- applications/admin/static/js/web2py.js | 7 ++++--- applications/examples/static/js/web2py.js | 7 ++++--- applications/welcome/static/js/web2py.js | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 6eeef9ca..c8b5e585 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -168,7 +168,8 @@ * and require no dom manipulations */ var doc = $(document); - doc.on('click', '.w2p_flash', function () { + doc.on('click', '.w2p_flash', function (event) { + event.preventDefault(); var t = $(this); if (t.css('top') == '0px') t.slideUp('slow'); else t.fadeOut(); @@ -563,7 +564,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +768,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ +/* compatibility code - end*/ \ No newline at end of file diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 6eeef9ca..c8b5e585 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -168,7 +168,8 @@ * and require no dom manipulations */ var doc = $(document); - doc.on('click', '.w2p_flash', function () { + doc.on('click', '.w2p_flash', function (event) { + event.preventDefault(); var t = $(this); if (t.css('top') == '0px') t.slideUp('slow'); else t.fadeOut(); @@ -563,7 +564,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +768,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ +/* compatibility code - end*/ \ No newline at end of file diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 6eeef9ca..c8b5e585 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -168,7 +168,8 @@ * and require no dom manipulations */ var doc = $(document); - doc.on('click', '.w2p_flash', function () { + doc.on('click', '.w2p_flash', function (event) { + event.preventDefault(); var t = $(this); if (t.css('top') == '0px') t.slideUp('slow'); else t.fadeOut(); @@ -563,7 +564,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ').slideDown(); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -767,4 +768,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ +/* compatibility code - end*/ \ No newline at end of file From 2c70a858f14a0fed504a3a10306e7b24ec128b60 Mon Sep 17 00:00:00 2001 From: Giuseppe Chiesa Date: Thu, 17 Nov 2016 13:30:11 +0100 Subject: [PATCH 165/436] implemented base support for CASv3 --- gluon/tools.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 664f2f56..053dc9ad 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2005,6 +2005,12 @@ class Auth(object): return self.cas_validate(version=2, proxy=False) elif args(1) == self.settings.cas_actions['proxyvalidate']: return self.cas_validate(version=2, proxy=True) + elif (args(1) == 'p3' + and args(2) == self.settings.cas_actions['servicevalidate']): + return self.cas_validate(version=3, proxy=False) + elif (args(1) == 'p3' + and args(2) == self.settings.cas_actions['proxyvalidate']): + return self.cas_validate(version=3, proxy=True) elif args(1) == self.settings.cas_actions['logout']: return self.logout(next=request.vars.service or DEFAULT) else: @@ -2830,6 +2836,15 @@ class Auth(object): if success: if version == 1: message = 'yes\n%s' % user[userfield] + elif version == 3: + username = user.get('username', user[userfield]) + message = build_response( + TAG['cas:authenticationSuccess']( + TAG['cas:user'](username), + TAG['cas:attributes']( + *[TAG['cas:' + field.name](user[field.name]) + for field in self.table_user() + if field.readable]))) else: # assume version 2 username = user.get('username', user[userfield]) message = build_response( @@ -7072,4 +7087,4 @@ class Config(object): if __name__ == '__main__': import doctest - doctest.testmod() \ No newline at end of file + doctest.testmod() From bf5ec0d7cfb83a1a39dc254905c46db4eea7b319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 19:38:21 +0000 Subject: [PATCH 166/436] Fixed a long standing bug in login_user which was using 'password' instead of settings.password_field Fixes #636 --- gluon/authapi.py | 17 ++++++++++------- gluon/tools.py | 9 ++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c4507931..6ff67ab3 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -688,19 +688,22 @@ class AuthAPI(object): return True return False - def login_user(self, user): - """ - Logins the `user = db.auth_user(id)` - """ + def _update_session_user(self, user): if global_settings.web2py_runtime_gae: user = Row(self.table_user()._filter_fields(user, id=True)) - delattr(user, 'password') + delattr(user, self.settings.password_field) else: user = Row(user) for key in list(user.keys()): value = user[key] - if callable(value) or key == 'password': + if callable(value) or key == self.settings.password_field: delattr(user, key) + + def login_user(self, user): + """ + Logins the `user = db.auth_user(id)` + """ + self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) current.session.auth = Storage(user=user, @@ -924,7 +927,7 @@ class AuthAPI(object): log = self.messages['profile_log'] self.log_event(log, user) - self.user.update(**kwargs) + self._update_session_user(user) return {'errors': None, 'message': self.messages.profile_updated, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} def change_password(self, log=DEFAULT, **kwargs): diff --git a/gluon/tools.py b/gluon/tools.py index 471f0166..f002bfd4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3734,7 +3734,14 @@ class Auth(AuthAPI): formname='profile', onvalidation=onvalidation, hideerror=self.settings.hideerror): - self.user.update(table_user._filter_fields(form.vars)) + extra_fields = self.settings.extra_fields.get(self.settings.table_user_name, []) + if any(f.compute for f in extra_fields): + user = table_user[self.user.id] + self._update_session_user(user) + else: + self.user.update(table_user._filter_fields(form.vars)) + + session.flash = self.messages.profile_updated self.log_event(log, self.user) callback(onaccept, form) From 7b66ec0ae3b33aa28c1c0328dc0a62361b780124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 19:51:51 +0000 Subject: [PATCH 167/436] Fixes #1506 --- gluon/authapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 6ff67ab3..c4934b83 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -135,10 +135,10 @@ class AuthAPI(object): messages = self.messages = Messages(current.T) messages.update(self.default_messages) messages.lock_keys = True - if signature: + if signature is True: self.define_signature() else: - self.signature = None + self.signature = signature or None def __validate(self, value, requires): if not isinstance(requires, (list, tuple)): @@ -251,7 +251,7 @@ class AuthAPI(object): settings.use_username = username if not self.signature: self.define_signature() - if signature: + if signature is True: signature_list = [self.signature] elif not signature: signature_list = [] From 757d46274ee43108d5c67bd9eca03624a0aa01da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 20:00:51 +0000 Subject: [PATCH 168/436] fix bug with new login_user --- gluon/authapi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c4934b83..c9f425fc 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -698,12 +698,13 @@ class AuthAPI(object): value = user[key] if callable(value) or key == self.settings.password_field: delattr(user, key) + return user def login_user(self, user): """ Logins the `user = db.auth_user(id)` """ - self._update_session_user(user) + user = self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) current.session.auth = Storage(user=user, From 920ab72415998ca9f2a6f406c64dcd0ad815f059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 20:11:45 +0000 Subject: [PATCH 169/436] fixed _update_session_user not really updating the session --- gluon/authapi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c9f425fc..1937972f 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -698,6 +698,10 @@ class AuthAPI(object): value = user[key] if callable(value) or key == self.settings.password_field: delattr(user, key) + current.session.auth = Storage(user=user, + last_visit=current.request.now, + expiration=self.settings.expiration, + hmac_key=web2py_uuid()) return user def login_user(self, user): @@ -707,10 +711,6 @@ class AuthAPI(object): user = self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) - current.session.auth = Storage(user=user, - last_visit=current.request.now, - expiration=self.settings.expiration, - hmac_key=web2py_uuid()) self.user = user self.update_groups() From 79e256b1d73ea0b9fecc3274cf061fb4283a34a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 21 Nov 2016 15:53:10 +0000 Subject: [PATCH 170/436] require sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 472896fd..3bc79dcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -sudo: false +sudo: required cache: pip From 0b8ecea4dcd667689e835ab82286ec079b903191 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 21 Nov 2016 10:27:16 -0600 Subject: [PATCH 171/436] sudo: required, thanks Leonel --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 472896fd..3bc79dcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -sudo: false +sudo: required cache: pip From a23b264a370ad44fcd8080770bad89a1f830f679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 21 Nov 2016 19:48:41 +0000 Subject: [PATCH 172/436] make sudo false again --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3bc79dcc..472896fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -sudo: required +sudo: false cache: pip From fc0add67b511c662fa3d2e5c790a34419e8dd9cb Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Tue, 29 Nov 2016 14:05:17 -0800 Subject: [PATCH 173/436] Add web2py componentBegin event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Like the ā€œw2p:componentCompleteā€ event I added a while back, this is another event that I use in my website, and I thought it would be helpful to share it with others. This way, you can easily catch when a web2py component starts loading, and write a handler for whatever reason (in my case, determining when the user is navigating to a new page through a smooth loading function piggybacking off of the web2py component system) --- applications/admin/static/js/web2py.js | 1 + applications/examples/static/js/web2py.js | 1 + applications/welcome/static/js/web2py.js | 1 + 3 files changed, 3 insertions(+) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index c8b5e585..1228961c 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -317,6 +317,7 @@ 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); + web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target); return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false }, 'success': function (data, status, xhr) { diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index c8b5e585..1228961c 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -317,6 +317,7 @@ 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); + web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target); return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false }, 'success': function (data, status, xhr) { diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index c8b5e585..1228961c 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -317,6 +317,7 @@ 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); + web2py.fire(element, 'w2p:componentBegin', [xhr, settings], target); return web2py.fire(element, 'ajax:beforeSend', [xhr, settings], target); //test a usecase, should stop here if returns false }, 'success': function (data, status, xhr) { From 52fe4407b8237743ab006dd50d827c995ef1e2aa Mon Sep 17 00:00:00 2001 From: sean Date: Thu, 1 Dec 2016 10:29:23 -0500 Subject: [PATCH 174/436] when mkdir of missing app folders, handle broken links properly --- gluon/admin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gluon/admin.py b/gluon/admin.py index 9615956f..3d8d13c8 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -438,7 +438,11 @@ def add_path_first(path): def try_mkdir(path): if not os.path.exists(path): try: - os.mkdir(path) + if os.path.islink(path): + # path is a broken link, try to mkdir the target of the link instead of the link itself. + os.mkdir(os.path.realpath(path)) + else: + os.mkdir(path) except OSError as e: if e.strerror == 'File exists': # In case of race condition. pass From 637579f53106b7f65fbf35ad81e43768fb39d936 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 3 Dec 2016 08:32:44 +0100 Subject: [PATCH 175/436] replaced ipaddr with ipaddress.py (backported from py3) --- gluon/_compat.py | 2 + gluon/contrib/ipaddr.py | 1923 ------------------------- gluon/contrib/ipaddress.py | 2425 ++++++++++++++++++++++++++++++++ gluon/tests/test_validators.py | 5 +- gluon/validators.py | 15 +- 5 files changed, 2434 insertions(+), 1936 deletions(-) delete mode 100644 gluon/contrib/ipaddr.py create mode 100644 gluon/contrib/ipaddress.py diff --git a/gluon/_compat.py b/gluon/_compat.py index a87a5c94..f7562b12 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -32,6 +32,7 @@ if PY2: import cgi import cookielib from xmlrpclib import ProtocolError + from gluon.contrib import ipaddress BytesIO = StringIO reduce = reduce hashlib_md5 = hashlib.md5 @@ -97,6 +98,7 @@ else: from http import cookiejar as cookielib from xmlrpc.client import ProtocolError import html # warning, this is the python3 module and not the web2py html module + import ipaddress hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) iterkeys = lambda d: iter(d.keys()) itervalues = lambda d: iter(d.values()) diff --git a/gluon/contrib/ipaddr.py b/gluon/contrib/ipaddr.py deleted file mode 100644 index c30f2984..00000000 --- a/gluon/contrib/ipaddr.py +++ /dev/null @@ -1,1923 +0,0 @@ -#!/usr/bin/python -# -# Copyright 2007 Google Inc. -# Licensed to PSF under a Contributor Agreement. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. - -"""A fast, lightweight IPv4/IPv6 manipulation library in Python. - -This library is used to create/poke/manipulate IPv4 and IPv6 addresses -and networks. - -""" - -__version__ = 'trunk' - -import struct - -IPV4LENGTH = 32 -IPV6LENGTH = 128 - - -class AddressValueError(ValueError): - """A Value Error related to the address.""" - - -class NetmaskValueError(ValueError): - """A Value Error related to the netmask.""" - - -def IPAddress(address, version=None): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP address. Either IPv4 or - IPv6 addresses may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - version: An Integer, 4 or 6. If set, don't try to automatically - determine what the IP address type is. important for things - like IPAddress(1), which could be IPv4, '0.0.0.1', or IPv6, - '::1'. - - Returns: - An IPv4Address or IPv6Address object. - - Raises: - ValueError: if the string passed isn't either a v4 or a v6 - address. - - """ - if version: - if version == 4: - return IPv4Address(address) - elif version == 6: - return IPv6Address(address) - - try: - return IPv4Address(address) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Address(address) - except (AddressValueError, NetmaskValueError): - pass - - raise ValueError('%r does not appear to be an IPv4 or IPv6 address' % - address) - - -def IPNetwork(address, version=None, strict=False): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP address. Either IPv4 or - IPv6 addresses may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - version: An Integer, if set, don't try to automatically - determine what the IP address type is. important for things - like IPNetwork(1), which could be IPv4, '0.0.0.1/32', or IPv6, - '::1/128'. - - Returns: - An IPv4Network or IPv6Network object. - - Raises: - ValueError: if the string passed isn't either a v4 or a v6 - address. Or if a strict network was requested and a strict - network wasn't given. - - """ - if version: - if version == 4: - return IPv4Network(address, strict) - elif version == 6: - return IPv6Network(address, strict) - - try: - return IPv4Network(address, strict) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Network(address, strict) - except (AddressValueError, NetmaskValueError): - pass - - raise ValueError('%r does not appear to be an IPv4 or IPv6 network' % - address) - - -def v4_int_to_packed(address): - """The binary representation of this address. - - Args: - address: An integer representation of an IPv4 IP address. - - Returns: - The binary representation of this address. - - Raises: - ValueError: If the integer is too large to be an IPv4 IP - address. - """ - if address > _BaseV4._ALL_ONES: - raise ValueError('Address too large for IPv4') - return Bytes(struct.pack('!I', address)) - - -def v6_int_to_packed(address): - """The binary representation of this address. - - Args: - address: An integer representation of an IPv6 IP address. - - Returns: - The binary representation of this address. - """ - return Bytes(struct.pack('!QQ', address >> 64, address & (2**64 - 1))) - - -def _find_address_range(addresses): - """Find a sequence of addresses. - - Args: - addresses: a list of IPv4 or IPv6 addresses. - - Returns: - A tuple containing the first and last IP addresses in the sequence, - and the index of the last IP address in the sequence. - - """ - first = last = addresses[0] - last_index = 0 - for ip in addresses[1:]: - if ip._ip == last._ip + 1: - last = ip - last_index += 1 - else: - break - return (first, last, last_index) - -def _get_prefix_length(number1, number2, bits): - """Get the number of leading bits that are same for two numbers. - - Args: - number1: an integer. - number2: another integer. - bits: the maximum number of bits to compare. - - Returns: - The number of leading bits that are the same for two numbers. - - """ - for i in range(bits): - if number1 >> i == number2 >> i: - return bits - i - return 0 - -def _count_righthand_zero_bits(number, bits): - """Count the number of zero bits on the right hand side. - - Args: - number: an integer. - bits: maximum number of bits to count. - - Returns: - The number of zero bits on the right hand side of the number. - - """ - if number == 0: - return bits - for i in range(bits): - if (number >> i) % 2: - return i - -def summarize_address_range(first, last): - """Summarize a network range given the first and last IP addresses. - - Example: - >>> summarize_address_range(IPv4Address('1.1.1.0'), - IPv4Address('1.1.1.130')) - [IPv4Network('1.1.1.0/25'), IPv4Network('1.1.1.128/31'), - IPv4Network('1.1.1.130/32')] - - Args: - first: the first IPv4Address or IPv6Address in the range. - last: the last IPv4Address or IPv6Address in the range. - - Returns: - The address range collapsed to a list of IPv4Network's or - IPv6Network's. - - Raise: - TypeError: - If the first and last objects are not IP addresses. - If the first and last objects are not the same version. - ValueError: - If the last object is not greater than the first. - If the version is not 4 or 6. - - """ - if not (isinstance(first, _BaseIP) and isinstance(last, _BaseIP)): - raise TypeError('first and last must be IP addresses, not networks') - if first.version != last.version: - raise TypeError("%s and %s are not of the same version" % ( - str(first), str(last))) - if first > last: - raise ValueError('last IP address must be greater than first') - - networks = [] - - if first.version == 4: - ip = IPv4Network - elif first.version == 6: - ip = IPv6Network - else: - raise ValueError('unknown IP version') - - ip_bits = first._max_prefixlen - first_int = first._ip - last_int = last._ip - while first_int <= last_int: - nbits = _count_righthand_zero_bits(first_int, ip_bits) - current = None - while nbits >= 0: - addend = 2**nbits - 1 - current = first_int + addend - nbits -= 1 - if current <= last_int: - break - prefix = _get_prefix_length(first_int, current, ip_bits) - net = ip('%s/%d' % (str(first), prefix)) - networks.append(net) - if current == ip._ALL_ONES: - break - first_int = current + 1 - first = IPAddress(first_int, version=first._version) - return networks - -def _collapse_address_list_recursive(addresses): - """Loops through the addresses, collapsing concurrent netblocks. - - Example: - - ip1 = IPv4Network('1.1.0.0/24') - ip2 = IPv4Network('1.1.1.0/24') - ip3 = IPv4Network('1.1.2.0/24') - ip4 = IPv4Network('1.1.3.0/24') - ip5 = IPv4Network('1.1.4.0/24') - ip6 = IPv4Network('1.1.0.1/22') - - _collapse_address_list_recursive([ip1, ip2, ip3, ip4, ip5, ip6]) -> - [IPv4Network('1.1.0.0/22'), IPv4Network('1.1.4.0/24')] - - This shouldn't be called directly; it is called via - collapse_address_list([]). - - Args: - addresses: A list of IPv4Network's or IPv6Network's - - Returns: - A list of IPv4Network's or IPv6Network's depending on what we were - passed. - - """ - ret_array = [] - optimized = False - - for cur_addr in addresses: - if not ret_array: - ret_array.append(cur_addr) - continue - if cur_addr in ret_array[-1]: - optimized = True - elif cur_addr == ret_array[-1].supernet().subnet()[1]: - ret_array.append(ret_array.pop().supernet()) - optimized = True - else: - ret_array.append(cur_addr) - - if optimized: - return _collapse_address_list_recursive(ret_array) - - return ret_array - - -def collapse_address_list(addresses): - """Collapse a list of IP objects. - - Example: - collapse_address_list([IPv4('1.1.0.0/24'), IPv4('1.1.1.0/24')]) -> - [IPv4('1.1.0.0/23')] - - Args: - addresses: A list of IPv4Network or IPv6Network objects. - - Returns: - A list of IPv4Network or IPv6Network objects depending on what we - were passed. - - Raises: - TypeError: If passed a list of mixed version objects. - - """ - i = 0 - addrs = [] - ips = [] - nets = [] - - # split IP addresses and networks - for ip in addresses: - if isinstance(ip, _BaseIP): - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - str(ip), str(ips[-1]))) - ips.append(ip) - elif ip._prefixlen == ip._max_prefixlen: - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - str(ip), str(ips[-1]))) - ips.append(ip.ip) - else: - if nets and nets[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - str(ip), str(nets[-1]))) - nets.append(ip) - - # sort and dedup - ips = sorted(set(ips)) - nets = sorted(set(nets)) - - while i < len(ips): - (first, last, last_index) = _find_address_range(ips[i:]) - i += last_index + 1 - addrs.extend(summarize_address_range(first, last)) - - return _collapse_address_list_recursive(sorted( - addrs + nets, key=_BaseNet._get_networks_key)) - -# backwards compatibility -CollapseAddrList = collapse_address_list - -# We need to distinguish between the string and packed-bytes representations -# of an IP address. For example, b'0::1' is the IPv4 address 48.58.58.49, -# while '0::1' is an IPv6 address. -# -# In Python 3, the native 'bytes' type already provides this functionality, -# so we use it directly. For earlier implementations where bytes is not a -# distinct type, we create a subclass of str to serve as a tag. -# -# Usage example (Python 2): -# ip = ipaddr.IPAddress(ipaddr.Bytes('xxxx')) -# -# Usage example (Python 3): -# ip = ipaddr.IPAddress(b'xxxx') -try: - if bytes is str: - raise TypeError("bytes is not a distinct type") - Bytes = bytes -except (NameError, TypeError): - class Bytes(str): - def __repr__(self): - return 'Bytes(%s)' % str.__repr__(self) - -def get_mixed_type_key(obj): - """Return a key suitable for sorting between networks and addresses. - - Address and Network objects are not sortable by default; they're - fundamentally different so the expression - - IPv4Address('1.1.1.1') <= IPv4Network('1.1.1.1/24') - - doesn't make any sense. There are some times however, where you may wish - to have ipaddr sort these for you anyway. If you need to do this, you - can use this function as the key= argument to sorted(). - - Args: - obj: either a Network or Address object. - Returns: - appropriate key. - - """ - if isinstance(obj, _BaseNet): - return obj._get_networks_key() - elif isinstance(obj, _BaseIP): - return obj._get_address_key() - return NotImplemented - -class _IPAddrBase(object): - - """The mother class.""" - - def __index__(self): - return self._ip - - def __int__(self): - return self._ip - - def __hex__(self): - return hex(self._ip) - - @property - def exploded(self): - """Return the longhand version of the IP address as a string.""" - return self._explode_shorthand_ip_string() - - @property - def compressed(self): - """Return the shorthand version of the IP address as a string.""" - return str(self) - - -class _BaseIP(_IPAddrBase): - - """A generic IP object. - - This IP class contains the version independent methods which are - used by single IP addresses. - - """ - - def __eq__(self, other): - try: - return (self._ip == other._ip - and self._version == other._version) - except AttributeError: - return NotImplemented - - def __ne__(self, other): - eq = self.__eq__(other) - if eq is NotImplemented: - return NotImplemented - return not eq - - def __le__(self, other): - gt = self.__gt__(other) - if gt is NotImplemented: - return NotImplemented - return not gt - - def __ge__(self, other): - lt = self.__lt__(other) - if lt is NotImplemented: - return NotImplemented - return not lt - - def __lt__(self, other): - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - str(self), str(other))) - if not isinstance(other, _BaseIP): - raise TypeError('%s and %s are not of the same type' % ( - str(self), str(other))) - if self._ip != other._ip: - return self._ip < other._ip - return False - - def __gt__(self, other): - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - str(self), str(other))) - if not isinstance(other, _BaseIP): - raise TypeError('%s and %s are not of the same type' % ( - str(self), str(other))) - if self._ip != other._ip: - return self._ip > other._ip - return False - - # Shorthand for Integer addition and subtraction. This is not - # meant to ever support addition/subtraction of addresses. - def __add__(self, other): - if not isinstance(other, int): - return NotImplemented - return IPAddress(int(self) + other, version=self._version) - - def __sub__(self, other): - if not isinstance(other, int): - return NotImplemented - return IPAddress(int(self) - other, version=self._version) - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, str(self)) - - def __str__(self): - return '%s' % self._string_from_ip_int(self._ip) - - def __hash__(self): - return hash(hex(long(self._ip))) - - def _get_address_key(self): - return (self._version, self) - - @property - def version(self): - raise NotImplementedError('BaseIP has no version') - - -class _BaseNet(_IPAddrBase): - - """A generic IP object. - - This IP class contains the version independent methods which are - used by networks. - - """ - - def __init__(self, address): - self._cache = {} - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, str(self)) - - def iterhosts(self): - """Generate Iterator over usable hosts in a network. - - This is like __iter__ except it doesn't return the network - or broadcast addresses. - - """ - cur = int(self.network) + 1 - bcast = int(self.broadcast) - 1 - while cur <= bcast: - cur += 1 - yield IPAddress(cur - 1, version=self._version) - - def __iter__(self): - cur = int(self.network) - bcast = int(self.broadcast) - while cur <= bcast: - cur += 1 - yield IPAddress(cur - 1, version=self._version) - - def __getitem__(self, n): - network = int(self.network) - broadcast = int(self.broadcast) - if n >= 0: - if network + n > broadcast: - raise IndexError - return IPAddress(network + n, version=self._version) - else: - n += 1 - if broadcast + n < network: - raise IndexError - return IPAddress(broadcast + n, version=self._version) - - def __lt__(self, other): - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - str(self), str(other))) - if not isinstance(other, _BaseNet): - raise TypeError('%s and %s are not of the same type' % ( - str(self), str(other))) - if self.network != other.network: - return self.network < other.network - if self.netmask != other.netmask: - return self.netmask < other.netmask - return False - - def __gt__(self, other): - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - str(self), str(other))) - if not isinstance(other, _BaseNet): - raise TypeError('%s and %s are not of the same type' % ( - str(self), str(other))) - if self.network != other.network: - return self.network > other.network - if self.netmask != other.netmask: - return self.netmask > other.netmask - return False - - def __le__(self, other): - gt = self.__gt__(other) - if gt is NotImplemented: - return NotImplemented - return not gt - - def __ge__(self, other): - lt = self.__lt__(other) - if lt is NotImplemented: - return NotImplemented - return not lt - - def __eq__(self, other): - try: - return (self._version == other._version - and self.network == other.network - and int(self.netmask) == int(other.netmask)) - except AttributeError: - if isinstance(other, _BaseIP): - return (self._version == other._version - and self._ip == other._ip) - - def __ne__(self, other): - eq = self.__eq__(other) - if eq is NotImplemented: - return NotImplemented - return not eq - - def __str__(self): - return '%s/%s' % (str(self.ip), - str(self._prefixlen)) - - def __hash__(self): - return hash(int(self.network) ^ int(self.netmask)) - - def __contains__(self, other): - # always false if one is v4 and the other is v6. - if self._version != other._version: - return False - # dealing with another network. - if isinstance(other, _BaseNet): - return (self.network <= other.network and - self.broadcast >= other.broadcast) - # dealing with another address - else: - return (int(self.network) <= int(other._ip) <= - int(self.broadcast)) - - def overlaps(self, other): - """Tell if self is partly contained in other.""" - return self.network in other or self.broadcast in other or ( - other.network in self or other.broadcast in self) - - @property - def network(self): - x = self._cache.get('network') - if x is None: - x = IPAddress(self._ip & int(self.netmask), version=self._version) - self._cache['network'] = x - return x - - @property - def broadcast(self): - x = self._cache.get('broadcast') - if x is None: - x = IPAddress(self._ip | int(self.hostmask), version=self._version) - self._cache['broadcast'] = x - return x - - @property - def hostmask(self): - x = self._cache.get('hostmask') - if x is None: - x = IPAddress(int(self.netmask) ^ self._ALL_ONES, - version=self._version) - self._cache['hostmask'] = x - return x - - @property - def with_prefixlen(self): - return '%s/%d' % (str(self.ip), self._prefixlen) - - @property - def with_netmask(self): - return '%s/%s' % (str(self.ip), str(self.netmask)) - - @property - def with_hostmask(self): - return '%s/%s' % (str(self.ip), str(self.hostmask)) - - @property - def numhosts(self): - """Number of hosts in the current subnet.""" - return int(self.broadcast) - int(self.network) + 1 - - @property - def version(self): - raise NotImplementedError('BaseNet has no version') - - @property - def prefixlen(self): - return self._prefixlen - - def address_exclude(self, other): - """Remove an address from a larger block. - - For example: - - addr1 = IPNetwork('10.1.1.0/24') - addr2 = IPNetwork('10.1.1.0/26') - addr1.address_exclude(addr2) = - [IPNetwork('10.1.1.64/26'), IPNetwork('10.1.1.128/25')] - - or IPv6: - - addr1 = IPNetwork('::1/32') - addr2 = IPNetwork('::1/128') - addr1.address_exclude(addr2) = [IPNetwork('::0/128'), - IPNetwork('::2/127'), - IPNetwork('::4/126'), - IPNetwork('::8/125'), - ... - IPNetwork('0:0:8000::/33')] - - Args: - other: An IPvXNetwork object of the same type. - - Returns: - A sorted list of IPvXNetwork objects addresses which is self - minus other. - - Raises: - TypeError: If self and other are of difffering address - versions, or if other is not a network object. - ValueError: If other is not completely contained by self. - - """ - if not self._version == other._version: - raise TypeError("%s and %s are not of the same version" % ( - str(self), str(other))) - - if not isinstance(other, _BaseNet): - raise TypeError("%s is not a network object" % str(other)) - - if other not in self: - raise ValueError('%s not contained in %s' % (str(other), - str(self))) - if other == self: - return [] - - ret_addrs = [] - - # Make sure we're comparing the network of other. - other = IPNetwork('%s/%s' % (str(other.network), str(other.prefixlen)), - version=other._version) - - s1, s2 = self.subnet() - while s1 != other and s2 != other: - if other in s1: - ret_addrs.append(s2) - s1, s2 = s1.subnet() - elif other in s2: - ret_addrs.append(s1) - s1, s2 = s2.subnet() - else: - # If we got here, there's a bug somewhere. - assert True == False, ('Error performing exclusion: ' - 's1: %s s2: %s other: %s' % - (str(s1), str(s2), str(other))) - if s1 == other: - ret_addrs.append(s2) - elif s2 == other: - ret_addrs.append(s1) - else: - # If we got here, there's a bug somewhere. - assert True == False, ('Error performing exclusion: ' - 's1: %s s2: %s other: %s' % - (str(s1), str(s2), str(other))) - - return sorted(ret_addrs, key=_BaseNet._get_networks_key) - - def compare_networks(self, other): - """Compare two IP objects. - - This is only concerned about the comparison of the integer - representation of the network addresses. This means that the - host bits aren't considered at all in this method. If you want - to compare host bits, you can easily enough do a - 'HostA._ip < HostB._ip' - - Args: - other: An IP object. - - Returns: - If the IP versions of self and other are the same, returns: - - -1 if self < other: - eg: IPv4('1.1.1.0/24') < IPv4('1.1.2.0/24') - IPv6('1080::200C:417A') < IPv6('1080::200B:417B') - 0 if self == other - eg: IPv4('1.1.1.1/24') == IPv4('1.1.1.2/24') - IPv6('1080::200C:417A/96') == IPv6('1080::200C:417B/96') - 1 if self > other - eg: IPv4('1.1.1.0/24') > IPv4('1.1.0.0/24') - IPv6('1080::1:200C:417A/112') > - IPv6('1080::0:200C:417A/112') - - If the IP versions of self and other are different, returns: - - -1 if self._version < other._version - eg: IPv4('10.0.0.1/24') < IPv6('::1/128') - 1 if self._version > other._version - eg: IPv6('::1/128') > IPv4('255.255.255.0/24') - - """ - if self._version < other._version: - return -1 - if self._version > other._version: - return 1 - # self._version == other._version below here: - if self.network < other.network: - return -1 - if self.network > other.network: - return 1 - # self.network == other.network below here: - if self.netmask < other.netmask: - return -1 - if self.netmask > other.netmask: - return 1 - # self.network == other.network and self.netmask == other.netmask - return 0 - - def _get_networks_key(self): - """Network-only key function. - - Returns an object that identifies this address' network and - netmask. This function is a suitable "key" argument for sorted() - and list.sort(). - - """ - return (self._version, self.network, self.netmask) - - def _ip_int_from_prefix(self, prefixlen): - """Turn the prefix length into a bitwise netmask. - - Args: - prefixlen: An integer, the prefix length. - - Returns: - An integer. - - """ - return self._ALL_ONES ^ (self._ALL_ONES >> prefixlen) - - def _prefix_from_ip_int(self, ip_int): - """Return prefix length from a bitwise netmask. - - Args: - ip_int: An integer, the netmask in expanded bitwise format. - - Returns: - An integer, the prefix length. - - Raises: - NetmaskValueError: If the input is not a valid netmask. - - """ - prefixlen = self._max_prefixlen - while prefixlen: - if ip_int & 1: - break - ip_int >>= 1 - prefixlen -= 1 - - if ip_int == (1 << prefixlen) - 1: - return prefixlen - else: - raise NetmaskValueError('Bit pattern does not match /1*0*/') - - def _prefix_from_prefix_int(self, prefixlen): - """Validate and return a prefix length integer. - - Args: - prefixlen: An integer containing the prefix length. - - Returns: - The input, possibly converted from long to int. - - Raises: - NetmaskValueError: If the input is not an integer, or out of range. - """ - if not isinstance(prefixlen, (int, long)): - raise NetmaskValueError('%r is not an integer' % prefixlen) - prefixlen = int(prefixlen) - if not (0 <= prefixlen <= self._max_prefixlen): - raise NetmaskValueError('%d is not a valid prefix length' % - prefixlen) - return prefixlen - - def _prefix_from_prefix_string(self, prefixlen_str): - """Turn a prefix length string into an integer. - - Args: - prefixlen_str: A decimal string containing the prefix length. - - Returns: - The prefix length as an integer. - - Raises: - NetmaskValueError: If the input is malformed or out of range. - - """ - try: - if not _BaseV4._DECIMAL_DIGITS.issuperset(prefixlen_str): - raise ValueError - prefixlen = int(prefixlen_str) - except ValueError: - raise NetmaskValueError('%s is not a valid prefix length' % - prefixlen_str) - return self._prefix_from_prefix_int(prefixlen) - - def _prefix_from_ip_string(self, ip_str): - """Turn a netmask/hostmask string into a prefix length. - - Args: - ip_str: A netmask or hostmask, formatted as an IP address. - - Returns: - The prefix length as an integer. - - Raises: - NetmaskValueError: If the input is not a netmask or hostmask. - - """ - # Parse the netmask/hostmask like an IP address. - try: - ip_int = self._ip_int_from_string(ip_str) - except AddressValueError: - raise NetmaskValueError('%s is not a valid netmask' % ip_str) - - # Try matching a netmask (this would be /1*0*/ as a bitwise regexp). - # Note that the two ambiguous cases (all-ones and all-zeroes) are - # treated as netmasks. - try: - return self._prefix_from_ip_int(ip_int) - except NetmaskValueError: - pass - - # Invert the bits, and try matching a /0+1+/ hostmask instead. - ip_int ^= self._ALL_ONES - try: - return self._prefix_from_ip_int(ip_int) - except NetmaskValueError: - raise NetmaskValueError('%s is not a valid netmask' % ip_str) - - def iter_subnets(self, prefixlen_diff=1, new_prefix=None): - """The subnets which join to make the current subnet. - - In the case that self contains only one IP - (self._prefixlen == 32 for IPv4 or self._prefixlen == 128 - for IPv6), return a list with just ourself. - - Args: - prefixlen_diff: An integer, the amount the prefix length - should be increased by. This should not be set if - new_prefix is also set. - new_prefix: The desired new prefix length. This must be a - larger number (smaller prefix) than the existing prefix. - This should not be set if prefixlen_diff is also set. - - Returns: - An iterator of IPv(4|6) objects. - - Raises: - ValueError: The prefixlen_diff is too small or too large. - OR - prefixlen_diff and new_prefix are both set or new_prefix - is a smaller number than the current prefix (smaller - number means a larger network) - - """ - if self._prefixlen == self._max_prefixlen: - yield self - return - - if new_prefix is not None: - if new_prefix < self._prefixlen: - raise ValueError('new prefix must be longer') - if prefixlen_diff != 1: - raise ValueError('cannot set prefixlen_diff and new_prefix') - prefixlen_diff = new_prefix - self._prefixlen - - if prefixlen_diff < 0: - raise ValueError('prefix length diff must be > 0') - new_prefixlen = self._prefixlen + prefixlen_diff - - if new_prefixlen > self._max_prefixlen: - raise ValueError( - 'prefix length diff %d is invalid for netblock %s' % ( - new_prefixlen, str(self))) - - first = IPNetwork('%s/%s' % (str(self.network), - str(self._prefixlen + prefixlen_diff)), - version=self._version) - - yield first - current = first - while True: - broadcast = current.broadcast - if broadcast == self.broadcast: - return - new_addr = IPAddress(int(broadcast) + 1, version=self._version) - current = IPNetwork('%s/%s' % (str(new_addr), str(new_prefixlen)), - version=self._version) - - yield current - - def masked(self): - """Return the network object with the host bits masked out.""" - return IPNetwork('%s/%d' % (self.network, self._prefixlen), - version=self._version) - - def subnet(self, prefixlen_diff=1, new_prefix=None): - """Return a list of subnets, rather than an iterator.""" - return list(self.iter_subnets(prefixlen_diff, new_prefix)) - - def supernet(self, prefixlen_diff=1, new_prefix=None): - """The supernet containing the current network. - - Args: - prefixlen_diff: An integer, the amount the prefix length of - the network should be decreased by. For example, given a - /24 network and a prefixlen_diff of 3, a supernet with a - /21 netmask is returned. - - Returns: - An IPv4 network object. - - Raises: - ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have a - negative prefix length. - OR - If prefixlen_diff and new_prefix are both set or new_prefix is a - larger number than the current prefix (larger number means a - smaller network) - - """ - if self._prefixlen == 0: - return self - - if new_prefix is not None: - if new_prefix > self._prefixlen: - raise ValueError('new prefix must be shorter') - if prefixlen_diff != 1: - raise ValueError('cannot set prefixlen_diff and new_prefix') - prefixlen_diff = self._prefixlen - new_prefix - - - if self.prefixlen - prefixlen_diff < 0: - raise ValueError( - 'current prefixlen is %d, cannot have a prefixlen_diff of %d' % - (self.prefixlen, prefixlen_diff)) - return IPNetwork('%s/%s' % (str(self.network), - str(self.prefixlen - prefixlen_diff)), - version=self._version) - - # backwards compatibility - Subnet = subnet - Supernet = supernet - AddressExclude = address_exclude - CompareNetworks = compare_networks - Contains = __contains__ - - -class _BaseV4(object): - - """Base IPv4 object. - - The following methods are used by IPv4 objects in both single IP - addresses and networks. - - """ - - # Equivalent to 255.255.255.255 or 32 bits of 1's. - _ALL_ONES = (2**IPV4LENGTH) - 1 - _DECIMAL_DIGITS = frozenset('0123456789') - - def __init__(self, address): - self._version = 4 - self._max_prefixlen = IPV4LENGTH - - def _explode_shorthand_ip_string(self): - return str(self) - - def _ip_int_from_string(self, ip_str): - """Turn the given IP string into an integer for comparison. - - Args: - ip_str: A string, the IP ip_str. - - Returns: - The IP ip_str as an integer. - - Raises: - AddressValueError: if ip_str isn't a valid IPv4 Address. - - """ - octets = ip_str.split('.') - if len(octets) != 4: - raise AddressValueError(ip_str) - - packed_ip = 0 - for oc in octets: - try: - packed_ip = (packed_ip << 8) | self._parse_octet(oc) - except ValueError: - raise AddressValueError(ip_str) - return packed_ip - - def _parse_octet(self, octet_str): - """Convert a decimal octet into an integer. - - Args: - octet_str: A string, the number to parse. - - Returns: - The octet as an integer. - - Raises: - ValueError: if the octet isn't strictly a decimal from [0..255]. - - """ - # Whitelist the characters, since int() allows a lot of bizarre stuff. - if not self._DECIMAL_DIGITS.issuperset(octet_str): - raise ValueError - octet_int = int(octet_str, 10) - # Disallow leading zeroes, because no clear standard exists on - # whether these should be interpreted as decimal or octal. - if octet_int > 255 or (octet_str[0] == '0' and len(octet_str) > 1): - raise ValueError - return octet_int - - def _string_from_ip_int(self, ip_int): - """Turns a 32-bit integer into dotted decimal notation. - - Args: - ip_int: An integer, the IP address. - - Returns: - The IP address as a string in dotted decimal notation. - - """ - octets = [] - for _ in xrange(4): - octets.insert(0, str(ip_int & 0xFF)) - ip_int >>= 8 - return '.'.join(octets) - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def packed(self): - """The binary representation of this address.""" - return v4_int_to_packed(self._ip) - - @property - def version(self): - return self._version - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - - Returns: - A boolean, True if the address is within the - reserved IPv4 Network range. - - """ - return self in IPv4Network('240.0.0.0/4') - - @property - def is_private(self): - """Test if this address is allocated for private networks. - - Returns: - A boolean, True if the address is reserved per RFC 1918. - - """ - return (self in IPv4Network('10.0.0.0/8') or - self in IPv4Network('172.16.0.0/12') or - self in IPv4Network('192.168.0.0/16')) - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - - Returns: - A boolean, True if the address is multicast. - See RFC 3171 for details. - - """ - return self in IPv4Network('224.0.0.0/4') - - @property - def is_unspecified(self): - """Test if the address is unspecified. - - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 5735 3. - - """ - return self in IPv4Network('0.0.0.0') - - @property - def is_loopback(self): - """Test if the address is a loopback address. - - Returns: - A boolean, True if the address is a loopback per RFC 3330. - - """ - return self in IPv4Network('127.0.0.0/8') - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - - Returns: - A boolean, True if the address is link-local per RFC 3927. - - """ - return self in IPv4Network('169.254.0.0/16') - - -class IPv4Address(_BaseV4, _BaseIP): - - """Represent and manipulate single IPv4 Addresses.""" - - def __init__(self, address): - - """ - Args: - address: A string or integer representing the IP - '192.168.1.1' - - Additionally, an integer can be passed, so - IPv4Address('192.168.1.1') == IPv4Address(3232235777). - or, more generally - IPv4Address(int(IPv4Address('192.168.1.1'))) == - IPv4Address('192.168.1.1') - - Raises: - AddressValueError: If ipaddr isn't a valid IPv4 address. - - """ - _BaseV4.__init__(self, address) - - # Efficient copy constructor. - if isinstance(address, IPv4Address): - self._ip = address._ip - return - - # Efficient constructor from integer. - if isinstance(address, (int, long)): - self._ip = address - if address < 0 or address > self._ALL_ONES: - raise AddressValueError(address) - return - - # Constructing from a packed address - if isinstance(address, Bytes): - try: - self._ip, = struct.unpack('!I', address) - except struct.error: - raise AddressValueError(address) # Wrong length. - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = str(address) - self._ip = self._ip_int_from_string(addr_str) - - -class IPv4Network(_BaseV4, _BaseNet): - - """This class represents and manipulates 32-bit IPv4 networks. - - Attributes: [examples for IPv4Network('1.2.3.4/27')] - ._ip: 16909060 - .ip: IPv4Address('1.2.3.4') - .network: IPv4Address('1.2.3.0') - .hostmask: IPv4Address('0.0.0.31') - .broadcast: IPv4Address('1.2.3.31') - .netmask: IPv4Address('255.255.255.224') - .prefixlen: 27 - - """ - - def __init__(self, address, strict=False): - """Instantiate a new IPv4 network object. - - Args: - address: The IPv4 network as a string, 2-tuple, or any format - supported by the IPv4Address constructor. - - Strings typically use CIDR format, such as '192.0.2.0/24'. - If a dotted-quad is provided after the '/', it is treated as - a netmask if it starts with a nonzero bit (e.g. 255.0.0.0 == /8) - or a hostmask if it starts with a zero bit - (e.g. /0.0.0.255 == /8), with the single exception of an all-zero - mask which is treated as /0. - - The 2-tuple format consists of an (ip, prefixlen), where ip is any - format recognized by the IPv4Address constructor, and prefixlen is - an integer from 0 through 32. - - A plain IPv4 address (in any format) will be forwarded to the - IPv4Address constructor, with an implied prefixlen of 32. - - For example, the following inputs are equivalent: - IPv4Network('192.0.2.1/32') - IPv4Network('192.0.2.1/255.255.255.255') - IPv4Network('192.0.2.1') - IPv4Network(0xc0000201) - IPv4Network(IPv4Address('192.0.2.1')) - IPv4Network(('192.0.2.1', 32)) - IPv4Network((0xc0000201, 32)) - IPv4Network((IPv4Address('192.0.2.1'), 32)) - - strict: A boolean. If true, ensure that we have been passed - A true network address, eg, 192.168.1.0/24 and not an - IP address on a network, eg, 192.168.1.1/24. - - Raises: - AddressValueError: If ipaddr isn't a valid IPv4 address. - NetmaskValueError: If the netmask isn't valid for - an IPv4 address. - ValueError: If strict was True and a network address was not - supplied. - - """ - _BaseNet.__init__(self, address) - _BaseV4.__init__(self, address) - - # Constructing from a single IP address. - if isinstance(address, (int, long, Bytes, IPv4Address)): - self.ip = IPv4Address(address) - self._ip = self.ip._ip - self._prefixlen = self._max_prefixlen - self.netmask = IPv4Address(self._ALL_ONES) - return - - # Constructing from an (ip, prefixlen) tuple. - if isinstance(address, tuple): - try: - ip, prefixlen = address - except ValueError: - raise AddressValueError(address) - self.ip = IPv4Address(ip) - self._ip = self.ip._ip - self._prefixlen = self._prefix_from_prefix_int(prefixlen) - - else: - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = str(address).split('/') - - if len(addr) > 2: - raise AddressValueError(address) - - self._ip = self._ip_int_from_string(addr[0]) - self.ip = IPv4Address(self._ip) - - if len(addr) == 2: - try: - # Check for a netmask in prefix length form. - self._prefixlen = self._prefix_from_prefix_string(addr[1]) - except NetmaskValueError: - # Check for a netmask or hostmask in dotted-quad form. - # This may raise NetmaskValueError. - self._prefixlen = self._prefix_from_ip_string(addr[1]) - else: - self._prefixlen = self._max_prefixlen - - self.netmask = IPv4Address(self._ip_int_from_prefix(self._prefixlen)) - - if strict: - if self.ip != self.network: - raise ValueError('%s has host bits set' % self.ip) - if self._prefixlen == (self._max_prefixlen - 1): - self.iterhosts = self.__iter__ - - # backwards compatibility - IsRFC1918 = lambda self: self.is_private - IsMulticast = lambda self: self.is_multicast - IsLoopback = lambda self: self.is_loopback - IsLinkLocal = lambda self: self.is_link_local - - -class _BaseV6(object): - - """Base IPv6 object. - - The following methods are used by IPv6 objects in both single IP - addresses and networks. - - """ - - _ALL_ONES = (2**IPV6LENGTH) - 1 - _HEXTET_COUNT = 8 - _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef') - - def __init__(self, address): - self._version = 6 - self._max_prefixlen = IPV6LENGTH - - def _ip_int_from_string(self, ip_str): - """Turn an IPv6 ip_str into an integer. - - Args: - ip_str: A string, the IPv6 ip_str. - - Returns: - A long, the IPv6 ip_str. - - Raises: - AddressValueError: if ip_str isn't a valid IPv6 Address. - - """ - parts = ip_str.split(':') - - # An IPv6 address needs at least 2 colons (3 parts). - if len(parts) < 3: - raise AddressValueError(ip_str) - - # If the address has an IPv4-style suffix, convert it to hexadecimal. - if '.' in parts[-1]: - ipv4_int = IPv4Address(parts.pop())._ip - parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF)) - parts.append('%x' % (ipv4_int & 0xFFFF)) - - # An IPv6 address can't have more than 8 colons (9 parts). - if len(parts) > self._HEXTET_COUNT + 1: - raise AddressValueError(ip_str) - - # Disregarding the endpoints, find '::' with nothing in between. - # This indicates that a run of zeroes has been skipped. - try: - skip_index, = ( - [i for i in xrange(1, len(parts) - 1) if not parts[i]] or - [None]) - except ValueError: - # Can't have more than one '::' - raise AddressValueError(ip_str) - - # parts_hi is the number of parts to copy from above/before the '::' - # parts_lo is the number of parts to copy from below/after the '::' - if skip_index is not None: - # If we found a '::', then check if it also covers the endpoints. - parts_hi = skip_index - parts_lo = len(parts) - skip_index - 1 - if not parts[0]: - parts_hi -= 1 - if parts_hi: - raise AddressValueError(ip_str) # ^: requires ^:: - if not parts[-1]: - parts_lo -= 1 - if parts_lo: - raise AddressValueError(ip_str) # :$ requires ::$ - parts_skipped = self._HEXTET_COUNT - (parts_hi + parts_lo) - if parts_skipped < 1: - raise AddressValueError(ip_str) - else: - # Otherwise, allocate the entire address to parts_hi. The endpoints - # could still be empty, but _parse_hextet() will check for that. - if len(parts) != self._HEXTET_COUNT: - raise AddressValueError(ip_str) - parts_hi = len(parts) - parts_lo = 0 - parts_skipped = 0 - - try: - # Now, parse the hextets into a 128-bit integer. - ip_int = 0L - for i in xrange(parts_hi): - ip_int <<= 16 - ip_int |= self._parse_hextet(parts[i]) - ip_int <<= 16 * parts_skipped - for i in xrange(-parts_lo, 0): - ip_int <<= 16 - ip_int |= self._parse_hextet(parts[i]) - return ip_int - except ValueError: - raise AddressValueError(ip_str) - - def _parse_hextet(self, hextet_str): - """Convert an IPv6 hextet string into an integer. - - Args: - hextet_str: A string, the number to parse. - - Returns: - The hextet as an integer. - - Raises: - ValueError: if the input isn't strictly a hex number from [0..FFFF]. - - """ - # Whitelist the characters, since int() allows a lot of bizarre stuff. - if not self._HEX_DIGITS.issuperset(hextet_str): - raise ValueError - if len(hextet_str) > 4: - raise ValueError - hextet_int = int(hextet_str, 16) - if hextet_int > 0xFFFF: - raise ValueError - return hextet_int - - def _compress_hextets(self, hextets): - """Compresses a list of hextets. - - Compresses a list of strings, replacing the longest continuous - sequence of "0" in the list with "" and adding empty strings at - the beginning or at the end of the string such that subsequently - calling ":".join(hextets) will produce the compressed version of - the IPv6 address. - - Args: - hextets: A list of strings, the hextets to compress. - - Returns: - A list of strings. - - """ - best_doublecolon_start = -1 - best_doublecolon_len = 0 - doublecolon_start = -1 - doublecolon_len = 0 - for index in range(len(hextets)): - if hextets[index] == '0': - doublecolon_len += 1 - if doublecolon_start == -1: - # Start of a sequence of zeros. - doublecolon_start = index - if doublecolon_len > best_doublecolon_len: - # This is the longest sequence of zeros so far. - best_doublecolon_len = doublecolon_len - best_doublecolon_start = doublecolon_start - else: - doublecolon_len = 0 - doublecolon_start = -1 - - if best_doublecolon_len > 1: - best_doublecolon_end = (best_doublecolon_start + - best_doublecolon_len) - # For zeros at the end of the address. - if best_doublecolon_end == len(hextets): - hextets += [''] - hextets[best_doublecolon_start:best_doublecolon_end] = [''] - # For zeros at the beginning of the address. - if best_doublecolon_start == 0: - hextets = [''] + hextets - - return hextets - - def _string_from_ip_int(self, ip_int=None): - """Turns a 128-bit integer into hexadecimal notation. - - Args: - ip_int: An integer, the IP address. - - Returns: - A string, the hexadecimal representation of the address. - - Raises: - ValueError: The address is bigger than 128 bits of all ones. - - """ - if not ip_int and ip_int != 0: - ip_int = int(self._ip) - - if ip_int > self._ALL_ONES: - raise ValueError('IPv6 address is too large') - - hex_str = '%032x' % ip_int - hextets = [] - for x in range(0, 32, 4): - hextets.append('%x' % int(hex_str[x:x+4], 16)) - - hextets = self._compress_hextets(hextets) - return ':'.join(hextets) - - def _explode_shorthand_ip_string(self): - """Expand a shortened IPv6 address. - - Args: - ip_str: A string, the IPv6 address. - - Returns: - A string, the expanded IPv6 address. - - """ - if isinstance(self, _BaseNet): - ip_str = str(self.ip) - else: - ip_str = str(self) - - ip_int = self._ip_int_from_string(ip_str) - parts = [] - for i in xrange(self._HEXTET_COUNT): - parts.append('%04x' % (ip_int & 0xFFFF)) - ip_int >>= 16 - parts.reverse() - if isinstance(self, _BaseNet): - return '%s/%d' % (':'.join(parts), self.prefixlen) - return ':'.join(parts) - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def packed(self): - """The binary representation of this address.""" - return v6_int_to_packed(self._ip) - - @property - def version(self): - return self._version - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - - Returns: - A boolean, True if the address is a multicast address. - See RFC 2373 2.7 for details. - - """ - return self in IPv6Network('ff00::/8') - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - - Returns: - A boolean, True if the address is within one of the - reserved IPv6 Network ranges. - - """ - return (self in IPv6Network('::/8') or - self in IPv6Network('100::/8') or - self in IPv6Network('200::/7') or - self in IPv6Network('400::/6') or - self in IPv6Network('800::/5') or - self in IPv6Network('1000::/4') or - self in IPv6Network('4000::/3') or - self in IPv6Network('6000::/3') or - self in IPv6Network('8000::/3') or - self in IPv6Network('A000::/3') or - self in IPv6Network('C000::/3') or - self in IPv6Network('E000::/4') or - self in IPv6Network('F000::/5') or - self in IPv6Network('F800::/6') or - self in IPv6Network('FE00::/9')) - - @property - def is_unspecified(self): - """Test if the address is unspecified. - - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 2373 2.5.2. - - """ - return self._ip == 0 and getattr(self, '_prefixlen', 128) == 128 - - @property - def is_loopback(self): - """Test if the address is a loopback address. - - Returns: - A boolean, True if the address is a loopback address as defined in - RFC 2373 2.5.3. - - """ - return self._ip == 1 and getattr(self, '_prefixlen', 128) == 128 - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - - Returns: - A boolean, True if the address is reserved per RFC 4291. - - """ - return self in IPv6Network('fe80::/10') - - @property - def is_site_local(self): - """Test if the address is reserved for site-local. - - Note that the site-local address space has been deprecated by RFC 3879. - Use is_private to test if this address is in the space of unique local - addresses as defined by RFC 4193. - - Returns: - A boolean, True if the address is reserved per RFC 3513 2.5.6. - - """ - return self in IPv6Network('fec0::/10') - - @property - def is_private(self): - """Test if this address is allocated for private networks. - - Returns: - A boolean, True if the address is reserved per RFC 4193. - - """ - return self in IPv6Network('fc00::/7') - - @property - def ipv4_mapped(self): - """Return the IPv4 mapped address. - - Returns: - If the IPv6 address is a v4 mapped address, return the - IPv4 mapped address. Return None otherwise. - - """ - if (self._ip >> 32) != 0xFFFF: - return None - return IPv4Address(self._ip & 0xFFFFFFFF) - - @property - def teredo(self): - """Tuple of embedded teredo IPs. - - Returns: - Tuple of the (server, client) IPs or None if the address - doesn't appear to be a teredo address (doesn't start with - 2001::/32) - - """ - if (self._ip >> 96) != 0x20010000: - return None - return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF), - IPv4Address(~self._ip & 0xFFFFFFFF)) - - @property - def sixtofour(self): - """Return the IPv4 6to4 embedded address. - - Returns: - The IPv4 6to4-embedded address if present or None if the - address doesn't appear to contain a 6to4 embedded address. - - """ - if (self._ip >> 112) != 0x2002: - return None - return IPv4Address((self._ip >> 80) & 0xFFFFFFFF) - - -class IPv6Address(_BaseV6, _BaseIP): - - """Represent and manipulate single IPv6 Addresses. - """ - - def __init__(self, address): - """Instantiate a new IPv6 address object. - - Args: - address: A string or integer representing the IP - - Additionally, an integer can be passed, so - IPv6Address('2001:4860::') == - IPv6Address(42541956101370907050197289607612071936L). - or, more generally - IPv6Address(IPv6Address('2001:4860::')._ip) == - IPv6Address('2001:4860::') - - Raises: - AddressValueError: If address isn't a valid IPv6 address. - - """ - _BaseV6.__init__(self, address) - - # Efficient copy constructor. - if isinstance(address, IPv6Address): - self._ip = address._ip - return - - # Efficient constructor from integer. - if isinstance(address, (int, long)): - self._ip = address - if address < 0 or address > self._ALL_ONES: - raise AddressValueError(address) - return - - # Constructing from a packed address - if isinstance(address, Bytes): - try: - hi, lo = struct.unpack('!QQ', address) - except struct.error: - raise AddressValueError(address) # Wrong length. - self._ip = (hi << 64) | lo - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = str(address) - self._ip = self._ip_int_from_string(addr_str) - - -class IPv6Network(_BaseV6, _BaseNet): - - """This class represents and manipulates 128-bit IPv6 networks. - - Attributes: [examples for IPv6('2001:658:22A:CAFE:200::1/64')] - .ip: IPv6Address('2001:658:22a:cafe:200::1') - .network: IPv6Address('2001:658:22a:cafe::') - .hostmask: IPv6Address('::ffff:ffff:ffff:ffff') - .broadcast: IPv6Address('2001:658:22a:cafe:ffff:ffff:ffff:ffff') - .netmask: IPv6Address('ffff:ffff:ffff:ffff::') - .prefixlen: 64 - - """ - - - def __init__(self, address, strict=False): - """Instantiate a new IPv6 network object. - - Args: - address: The IPv6 network as a string, 2-tuple, or any format - supported by the IPv6Address constructor. - - Strings should be in CIDR format, such as '2001:db8::/32'. - - The 2-tuple format consists of an (ip, prefixlen), where ip is any - format recognized by the IPv6Address constructor, and prefixlen is - an integer from 0 through 128. - - A plain IPv6 address (in any format) will be forwarded to the - IPv6Address constructor, with an implied prefixlen of 128. - - For example, the following inputs are equivalent: - IPv6Network('2001:db8::/128') - IPv6Network('2001:db8:0:0:0:0:0:0/128') - IPv6Network('2001:db8::') - IPv6Network(0x20010db8 << 96) - IPv6Network(IPv6Address('2001:db8::')) - IPv6Network(('2001:db8::', 128)) - IPv6Network((0x20010db8 << 96, 128)) - IPv6Network((IPv6Address('2001:db8::'), 128)) - - strict: A boolean. If true, ensure that we have been passed - A true network address, eg, 2001:db8::/32 and not an - IP address on a network, eg, 2001:db8::1/32. - - Raises: - AddressValueError: If address isn't a valid IPv6 address. - NetmaskValueError: If the netmask isn't valid for - an IPv6 address. - ValueError: If strict was True and a network address was not - supplied. - - """ - _BaseNet.__init__(self, address) - _BaseV6.__init__(self, address) - - # Constructing from a single IP address. - if isinstance(address, (int, long, Bytes, IPv6Address)): - self.ip = IPv6Address(address) - self._ip = self.ip._ip - self._prefixlen = self._max_prefixlen - self.netmask = IPv6Address(self._ALL_ONES) - return - - # Constructing from an (ip, prefixlen) tuple. - if isinstance(address, tuple): - try: - ip, prefixlen = address - except ValueError: - raise AddressValueError(address) - self.ip = IPv6Address(ip) - self._ip = self.ip._ip - self._prefixlen = self._prefix_from_prefix_int(prefixlen) - - else: - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = str(address).split('/') - - if len(addr) > 2: - raise AddressValueError(address) - - self._ip = self._ip_int_from_string(addr[0]) - self.ip = IPv6Address(self._ip) - - if len(addr) == 2: - # This may raise NetmaskValueError - self._prefixlen = self._prefix_from_prefix_string(addr[1]) - else: - self._prefixlen = self._max_prefixlen - - self.netmask = IPv6Address(self._ip_int_from_prefix(self._prefixlen)) - - if strict: - if self.ip != self.network: - raise ValueError('%s has host bits set' % - self.ip) - if self._prefixlen == (self._max_prefixlen - 1): - self.iterhosts = self.__iter__ - - @property - def with_netmask(self): - return self.with_prefixlen diff --git a/gluon/contrib/ipaddress.py b/gluon/contrib/ipaddress.py new file mode 100644 index 00000000..9cf71a74 --- /dev/null +++ b/gluon/contrib/ipaddress.py @@ -0,0 +1,2425 @@ +# Copyright 2007 Google Inc. +# Licensed to PSF under a Contributor Agreement. + +"""A fast, lightweight IPv4/IPv6 manipulation library in Python. + +This library is used to create/poke/manipulate IPv4 and IPv6 addresses +and networks. + +""" + +from __future__ import unicode_literals + + +import itertools +import struct + +__version__ = '1.0.17' + +# Compatibility functions +_compat_int_types = (int,) +try: + _compat_int_types = (int, long) +except NameError: + pass +try: + _compat_str = unicode +except NameError: + _compat_str = str + assert bytes != str +if b'\0'[0] == 0: # Python 3 semantics + def _compat_bytes_to_byte_vals(byt): + return byt +else: + def _compat_bytes_to_byte_vals(byt): + return [struct.unpack(b'!B', b)[0] for b in byt] +try: + _compat_int_from_byte_vals = int.from_bytes +except AttributeError: + def _compat_int_from_byte_vals(bytvals, endianess): + assert endianess == 'big' + res = 0 + for bv in bytvals: + assert isinstance(bv, _compat_int_types) + res = (res << 8) + bv + return res + + +def _compat_to_bytes(intval, length, endianess): + assert isinstance(intval, _compat_int_types) + assert endianess == 'big' + if length == 4: + if intval < 0 or intval >= 2 ** 32: + raise struct.error("integer out of range for 'I' format code") + return struct.pack(b'!I', intval) + elif length == 16: + if intval < 0 or intval >= 2 ** 128: + raise struct.error("integer out of range for 'QQ' format code") + return struct.pack(b'!QQ', intval >> 64, intval & 0xffffffffffffffff) + else: + raise NotImplementedError() +if hasattr(int, 'bit_length'): + # Not int.bit_length , since that won't work in 2.7 where long exists + def _compat_bit_length(i): + return i.bit_length() +else: + def _compat_bit_length(i): + for res in itertools.count(): + if i >> res == 0: + return res + + +def _compat_range(start, end, step=1): + assert step > 0 + i = start + while i < end: + yield i + i += step + + +class _TotalOrderingMixin(object): + __slots__ = () + + # Helper that derives the other comparison operations from + # __lt__ and __eq__ + # We avoid functools.total_ordering because it doesn't handle + # NotImplemented correctly yet (http://bugs.python.org/issue10042) + def __eq__(self, other): + raise NotImplementedError + + def __ne__(self, other): + equal = self.__eq__(other) + if equal is NotImplemented: + return NotImplemented + return not equal + + def __lt__(self, other): + raise NotImplementedError + + def __le__(self, other): + less = self.__lt__(other) + if less is NotImplemented or not less: + return self.__eq__(other) + return less + + def __gt__(self, other): + less = self.__lt__(other) + if less is NotImplemented: + return NotImplemented + equal = self.__eq__(other) + if equal is NotImplemented: + return NotImplemented + return not (less or equal) + + def __ge__(self, other): + less = self.__lt__(other) + if less is NotImplemented: + return NotImplemented + return not less + + +IPV4LENGTH = 32 +IPV6LENGTH = 128 + + +class AddressValueError(ValueError): + """A Value Error related to the address.""" + + +class NetmaskValueError(ValueError): + """A Value Error related to the netmask.""" + + +def ip_address(address): + """Take an IP string/int and return an object of the correct type. + + Args: + address: A string or integer, the IP address. Either IPv4 or + IPv6 addresses may be supplied; integers less than 2**32 will + be considered to be IPv4 by default. + + Returns: + An IPv4Address or IPv6Address object. + + Raises: + ValueError: if the *address* passed isn't either a v4 or a v6 + address + + """ + try: + return IPv4Address(address) + except (AddressValueError, NetmaskValueError): + pass + + try: + return IPv6Address(address) + except (AddressValueError, NetmaskValueError): + pass + + if isinstance(address, bytes): + raise AddressValueError( + '%r does not appear to be an IPv4 or IPv6 address. ' + 'Did you pass in a bytes (str in Python 2) instead of' + ' a unicode object?' % address) + + raise ValueError('%r does not appear to be an IPv4 or IPv6 address' % + address) + + +def ip_network(address, strict=True): + """Take an IP string/int and return an object of the correct type. + + Args: + address: A string or integer, the IP network. Either IPv4 or + IPv6 networks may be supplied; integers less than 2**32 will + be considered to be IPv4 by default. + + Returns: + An IPv4Network or IPv6Network object. + + Raises: + ValueError: if the string passed isn't either a v4 or a v6 + address. Or if the network has host bits set. + + """ + try: + return IPv4Network(address, strict) + except (AddressValueError, NetmaskValueError): + pass + + try: + return IPv6Network(address, strict) + except (AddressValueError, NetmaskValueError): + pass + + if isinstance(address, bytes): + raise AddressValueError( + '%r does not appear to be an IPv4 or IPv6 network. ' + 'Did you pass in a bytes (str in Python 2) instead of' + ' a unicode object?' % address) + + raise ValueError('%r does not appear to be an IPv4 or IPv6 network' % + address) + + +def ip_interface(address): + """Take an IP string/int and return an object of the correct type. + + Args: + address: A string or integer, the IP address. Either IPv4 or + IPv6 addresses may be supplied; integers less than 2**32 will + be considered to be IPv4 by default. + + Returns: + An IPv4Interface or IPv6Interface object. + + Raises: + ValueError: if the string passed isn't either a v4 or a v6 + address. + + Notes: + The IPv?Interface classes describe an Address on a particular + Network, so they're basically a combination of both the Address + and Network classes. + + """ + try: + return IPv4Interface(address) + except (AddressValueError, NetmaskValueError): + pass + + try: + return IPv6Interface(address) + except (AddressValueError, NetmaskValueError): + pass + + raise ValueError('%r does not appear to be an IPv4 or IPv6 interface' % + address) + + +def v4_int_to_packed(address): + """Represent an address as 4 packed bytes in network (big-endian) order. + + Args: + address: An integer representation of an IPv4 IP address. + + Returns: + The integer address packed as 4 bytes in network (big-endian) order. + + Raises: + ValueError: If the integer is negative or too large to be an + IPv4 IP address. + + """ + try: + return _compat_to_bytes(address, 4, 'big') + except (struct.error, OverflowError): + raise ValueError("Address negative or too large for IPv4") + + +def v6_int_to_packed(address): + """Represent an address as 16 packed bytes in network (big-endian) order. + + Args: + address: An integer representation of an IPv6 IP address. + + Returns: + The integer address packed as 16 bytes in network (big-endian) order. + + """ + try: + return _compat_to_bytes(address, 16, 'big') + except (struct.error, OverflowError): + raise ValueError("Address negative or too large for IPv6") + + +def _split_optional_netmask(address): + """Helper to split the netmask and raise AddressValueError if needed""" + addr = _compat_str(address).split('/') + if len(addr) > 2: + raise AddressValueError("Only one '/' permitted in %r" % address) + return addr + + +def _find_address_range(addresses): + """Find a sequence of sorted deduplicated IPv#Address. + + Args: + addresses: a list of IPv#Address objects. + + Yields: + A tuple containing the first and last IP addresses in the sequence. + + """ + it = iter(addresses) + first = last = next(it) + for ip in it: + if ip._ip != last._ip + 1: + yield first, last + first = ip + last = ip + yield first, last + + +def _count_righthand_zero_bits(number, bits): + """Count the number of zero bits on the right hand side. + + Args: + number: an integer. + bits: maximum number of bits to count. + + Returns: + The number of zero bits on the right hand side of the number. + + """ + if number == 0: + return bits + return min(bits, _compat_bit_length(~number & (number - 1))) + + +def summarize_address_range(first, last): + """Summarize a network range given the first and last IP addresses. + + Example: + >>> list(summarize_address_range(IPv4Address('192.0.2.0'), + ... IPv4Address('192.0.2.130'))) + ... #doctest: +NORMALIZE_WHITESPACE + [IPv4Network('192.0.2.0/25'), IPv4Network('192.0.2.128/31'), + IPv4Network('192.0.2.130/32')] + + Args: + first: the first IPv4Address or IPv6Address in the range. + last: the last IPv4Address or IPv6Address in the range. + + Returns: + An iterator of the summarized IPv(4|6) network objects. + + Raise: + TypeError: + If the first and last objects are not IP addresses. + If the first and last objects are not the same version. + ValueError: + If the last object is not greater than the first. + If the version of the first address is not 4 or 6. + + """ + if (not (isinstance(first, _BaseAddress) and + isinstance(last, _BaseAddress))): + raise TypeError('first and last must be IP addresses, not networks') + if first.version != last.version: + raise TypeError("%s and %s are not of the same version" % ( + first, last)) + if first > last: + raise ValueError('last IP address must be greater than first') + + if first.version == 4: + ip = IPv4Network + elif first.version == 6: + ip = IPv6Network + else: + raise ValueError('unknown IP version') + + ip_bits = first._max_prefixlen + first_int = first._ip + last_int = last._ip + while first_int <= last_int: + nbits = min(_count_righthand_zero_bits(first_int, ip_bits), + _compat_bit_length(last_int - first_int + 1) - 1) + net = ip((first_int, ip_bits - nbits)) + yield net + first_int += 1 << nbits + if first_int - 1 == ip._ALL_ONES: + break + + +def _collapse_addresses_internal(addresses): + """Loops through the addresses, collapsing concurrent netblocks. + + Example: + + ip1 = IPv4Network('192.0.2.0/26') + ip2 = IPv4Network('192.0.2.64/26') + ip3 = IPv4Network('192.0.2.128/26') + ip4 = IPv4Network('192.0.2.192/26') + + _collapse_addresses_internal([ip1, ip2, ip3, ip4]) -> + [IPv4Network('192.0.2.0/24')] + + This shouldn't be called directly; it is called via + collapse_addresses([]). + + Args: + addresses: A list of IPv4Network's or IPv6Network's + + Returns: + A list of IPv4Network's or IPv6Network's depending on what we were + passed. + + """ + # First merge + to_merge = list(addresses) + subnets = {} + while to_merge: + net = to_merge.pop() + supernet = net.supernet() + existing = subnets.get(supernet) + if existing is None: + subnets[supernet] = net + elif existing != net: + # Merge consecutive subnets + del subnets[supernet] + to_merge.append(supernet) + # Then iterate over resulting networks, skipping subsumed subnets + last = None + for net in sorted(subnets.values()): + if last is not None: + # Since they are sorted, + # last.network_address <= net.network_address is a given. + if last.broadcast_address >= net.broadcast_address: + continue + yield net + last = net + + +def collapse_addresses(addresses): + """Collapse a list of IP objects. + + Example: + collapse_addresses([IPv4Network('192.0.2.0/25'), + IPv4Network('192.0.2.128/25')]) -> + [IPv4Network('192.0.2.0/24')] + + Args: + addresses: An iterator of IPv4Network or IPv6Network objects. + + Returns: + An iterator of the collapsed IPv(4|6)Network objects. + + Raises: + TypeError: If passed a list of mixed version objects. + + """ + addrs = [] + ips = [] + nets = [] + + # split IP addresses and networks + for ip in addresses: + if isinstance(ip, _BaseAddress): + if ips and ips[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + ip, ips[-1])) + ips.append(ip) + elif ip._prefixlen == ip._max_prefixlen: + if ips and ips[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + ip, ips[-1])) + try: + ips.append(ip.ip) + except AttributeError: + ips.append(ip.network_address) + else: + if nets and nets[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + ip, nets[-1])) + nets.append(ip) + + # sort and dedup + ips = sorted(set(ips)) + + # find consecutive address ranges in the sorted sequence and summarize them + if ips: + for first, last in _find_address_range(ips): + addrs.extend(summarize_address_range(first, last)) + + return _collapse_addresses_internal(addrs + nets) + + +def get_mixed_type_key(obj): + """Return a key suitable for sorting between networks and addresses. + + Address and Network objects are not sortable by default; they're + fundamentally different so the expression + + IPv4Address('192.0.2.0') <= IPv4Network('192.0.2.0/24') + + doesn't make any sense. There are some times however, where you may wish + to have ipaddress sort these for you anyway. If you need to do this, you + can use this function as the key= argument to sorted(). + + Args: + obj: either a Network or Address object. + Returns: + appropriate key. + + """ + if isinstance(obj, _BaseNetwork): + return obj._get_networks_key() + elif isinstance(obj, _BaseAddress): + return obj._get_address_key() + return NotImplemented + + +class _IPAddressBase(_TotalOrderingMixin): + + """The mother class.""" + + __slots__ = () + + @property + def exploded(self): + """Return the longhand version of the IP address as a string.""" + return self._explode_shorthand_ip_string() + + @property + def compressed(self): + """Return the shorthand version of the IP address as a string.""" + return _compat_str(self) + + @property + def reverse_pointer(self): + """The name of the reverse DNS pointer for the IP address, e.g.: + >>> ipaddress.ip_address("127.0.0.1").reverse_pointer + '1.0.0.127.in-addr.arpa' + >>> ipaddress.ip_address("2001:db8::1").reverse_pointer + '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa' + + """ + return self._reverse_pointer() + + @property + def version(self): + msg = '%200s has no version specified' % (type(self),) + raise NotImplementedError(msg) + + def _check_int_address(self, address): + if address < 0: + msg = "%d (< 0) is not permitted as an IPv%d address" + raise AddressValueError(msg % (address, self._version)) + if address > self._ALL_ONES: + msg = "%d (>= 2**%d) is not permitted as an IPv%d address" + raise AddressValueError(msg % (address, self._max_prefixlen, + self._version)) + + def _check_packed_address(self, address, expected_len): + address_len = len(address) + if address_len != expected_len: + msg = ( + '%r (len %d != %d) is not permitted as an IPv%d address. ' + 'Did you pass in a bytes (str in Python 2) instead of' + ' a unicode object?' + ) + raise AddressValueError(msg % (address, address_len, + expected_len, self._version)) + + @classmethod + def _ip_int_from_prefix(cls, prefixlen): + """Turn the prefix length into a bitwise netmask + + Args: + prefixlen: An integer, the prefix length. + + Returns: + An integer. + + """ + return cls._ALL_ONES ^ (cls._ALL_ONES >> prefixlen) + + @classmethod + def _prefix_from_ip_int(cls, ip_int): + """Return prefix length from the bitwise netmask. + + Args: + ip_int: An integer, the netmask in expanded bitwise format + + Returns: + An integer, the prefix length. + + Raises: + ValueError: If the input intermingles zeroes & ones + """ + trailing_zeroes = _count_righthand_zero_bits(ip_int, + cls._max_prefixlen) + prefixlen = cls._max_prefixlen - trailing_zeroes + leading_ones = ip_int >> trailing_zeroes + all_ones = (1 << prefixlen) - 1 + if leading_ones != all_ones: + byteslen = cls._max_prefixlen // 8 + details = _compat_to_bytes(ip_int, byteslen, 'big') + msg = 'Netmask pattern %r mixes zeroes & ones' + raise ValueError(msg % details) + return prefixlen + + @classmethod + def _report_invalid_netmask(cls, netmask_str): + msg = '%r is not a valid netmask' % netmask_str + raise NetmaskValueError(msg) + + @classmethod + def _prefix_from_prefix_string(cls, prefixlen_str): + """Return prefix length from a numeric string + + Args: + prefixlen_str: The string to be converted + + Returns: + An integer, the prefix length. + + Raises: + NetmaskValueError: If the input is not a valid netmask + """ + # int allows a leading +/- as well as surrounding whitespace, + # so we ensure that isn't the case + if not _BaseV4._DECIMAL_DIGITS.issuperset(prefixlen_str): + cls._report_invalid_netmask(prefixlen_str) + try: + prefixlen = int(prefixlen_str) + except ValueError: + cls._report_invalid_netmask(prefixlen_str) + if not (0 <= prefixlen <= cls._max_prefixlen): + cls._report_invalid_netmask(prefixlen_str) + return prefixlen + + @classmethod + def _prefix_from_ip_string(cls, ip_str): + """Turn a netmask/hostmask string into a prefix length + + Args: + ip_str: The netmask/hostmask to be converted + + Returns: + An integer, the prefix length. + + Raises: + NetmaskValueError: If the input is not a valid netmask/hostmask + """ + # Parse the netmask/hostmask like an IP address. + try: + ip_int = cls._ip_int_from_string(ip_str) + except AddressValueError: + cls._report_invalid_netmask(ip_str) + + # Try matching a netmask (this would be /1*0*/ as a bitwise regexp). + # Note that the two ambiguous cases (all-ones and all-zeroes) are + # treated as netmasks. + try: + return cls._prefix_from_ip_int(ip_int) + except ValueError: + pass + + # Invert the bits, and try matching a /0+1+/ hostmask instead. + ip_int ^= cls._ALL_ONES + try: + return cls._prefix_from_ip_int(ip_int) + except ValueError: + cls._report_invalid_netmask(ip_str) + + def __reduce__(self): + return self.__class__, (_compat_str(self),) + + +class _BaseAddress(_IPAddressBase): + + """A generic IP object. + + This IP class contains the version independent methods which are + used by single IP addresses. + """ + + __slots__ = () + + def __int__(self): + return self._ip + + def __eq__(self, other): + try: + return (self._ip == other._ip and + self._version == other._version) + except AttributeError: + return NotImplemented + + def __lt__(self, other): + if not isinstance(other, _IPAddressBase): + return NotImplemented + if not isinstance(other, _BaseAddress): + raise TypeError('%s and %s are not of the same type' % ( + self, other)) + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + self, other)) + if self._ip != other._ip: + return self._ip < other._ip + return False + + # Shorthand for Integer addition and subtraction. This is not + # meant to ever support addition/subtraction of addresses. + def __add__(self, other): + if not isinstance(other, _compat_int_types): + return NotImplemented + return self.__class__(int(self) + other) + + def __sub__(self, other): + if not isinstance(other, _compat_int_types): + return NotImplemented + return self.__class__(int(self) - other) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, _compat_str(self)) + + def __str__(self): + return _compat_str(self._string_from_ip_int(self._ip)) + + def __hash__(self): + return hash(hex(int(self._ip))) + + def _get_address_key(self): + return (self._version, self) + + def __reduce__(self): + return self.__class__, (self._ip,) + + +class _BaseNetwork(_IPAddressBase): + + """A generic IP network object. + + This IP class contains the version independent methods which are + used by networks. + + """ + def __init__(self, address): + self._cache = {} + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, _compat_str(self)) + + def __str__(self): + return '%s/%d' % (self.network_address, self.prefixlen) + + def hosts(self): + """Generate Iterator over usable hosts in a network. + + This is like __iter__ except it doesn't return the network + or broadcast addresses. + + """ + network = int(self.network_address) + broadcast = int(self.broadcast_address) + for x in _compat_range(network + 1, broadcast): + yield self._address_class(x) + + def __iter__(self): + network = int(self.network_address) + broadcast = int(self.broadcast_address) + for x in _compat_range(network, broadcast + 1): + yield self._address_class(x) + + def __getitem__(self, n): + network = int(self.network_address) + broadcast = int(self.broadcast_address) + if n >= 0: + if network + n > broadcast: + raise IndexError('address out of range') + return self._address_class(network + n) + else: + n += 1 + if broadcast + n < network: + raise IndexError('address out of range') + return self._address_class(broadcast + n) + + def __lt__(self, other): + if not isinstance(other, _IPAddressBase): + return NotImplemented + if not isinstance(other, _BaseNetwork): + raise TypeError('%s and %s are not of the same type' % ( + self, other)) + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + self, other)) + if self.network_address != other.network_address: + return self.network_address < other.network_address + if self.netmask != other.netmask: + return self.netmask < other.netmask + return False + + def __eq__(self, other): + try: + return (self._version == other._version and + self.network_address == other.network_address and + int(self.netmask) == int(other.netmask)) + except AttributeError: + return NotImplemented + + def __hash__(self): + return hash(int(self.network_address) ^ int(self.netmask)) + + def __contains__(self, other): + # always false if one is v4 and the other is v6. + if self._version != other._version: + return False + # dealing with another network. + if isinstance(other, _BaseNetwork): + return False + # dealing with another address + else: + # address + return (int(self.network_address) <= int(other._ip) <= + int(self.broadcast_address)) + + def overlaps(self, other): + """Tell if self is partly contained in other.""" + return self.network_address in other or ( + self.broadcast_address in other or ( + other.network_address in self or ( + other.broadcast_address in self))) + + @property + def broadcast_address(self): + x = self._cache.get('broadcast_address') + if x is None: + x = self._address_class(int(self.network_address) | + int(self.hostmask)) + self._cache['broadcast_address'] = x + return x + + @property + def hostmask(self): + x = self._cache.get('hostmask') + if x is None: + x = self._address_class(int(self.netmask) ^ self._ALL_ONES) + self._cache['hostmask'] = x + return x + + @property + def with_prefixlen(self): + return '%s/%d' % (self.network_address, self._prefixlen) + + @property + def with_netmask(self): + return '%s/%s' % (self.network_address, self.netmask) + + @property + def with_hostmask(self): + return '%s/%s' % (self.network_address, self.hostmask) + + @property + def num_addresses(self): + """Number of hosts in the current subnet.""" + return int(self.broadcast_address) - int(self.network_address) + 1 + + @property + def _address_class(self): + # Returning bare address objects (rather than interfaces) allows for + # more consistent behaviour across the network address, broadcast + # address and individual host addresses. + msg = '%200s has no associated address class' % (type(self),) + raise NotImplementedError(msg) + + @property + def prefixlen(self): + return self._prefixlen + + def address_exclude(self, other): + """Remove an address from a larger block. + + For example: + + addr1 = ip_network('192.0.2.0/28') + addr2 = ip_network('192.0.2.1/32') + list(addr1.address_exclude(addr2)) = + [IPv4Network('192.0.2.0/32'), IPv4Network('192.0.2.2/31'), + IPv4Network('192.0.2.4/30'), IPv4Network('192.0.2.8/29')] + + or IPv6: + + addr1 = ip_network('2001:db8::1/32') + addr2 = ip_network('2001:db8::1/128') + list(addr1.address_exclude(addr2)) = + [ip_network('2001:db8::1/128'), + ip_network('2001:db8::2/127'), + ip_network('2001:db8::4/126'), + ip_network('2001:db8::8/125'), + ... + ip_network('2001:db8:8000::/33')] + + Args: + other: An IPv4Network or IPv6Network object of the same type. + + Returns: + An iterator of the IPv(4|6)Network objects which is self + minus other. + + Raises: + TypeError: If self and other are of differing address + versions, or if other is not a network object. + ValueError: If other is not completely contained by self. + + """ + if not self._version == other._version: + raise TypeError("%s and %s are not of the same version" % ( + self, other)) + + if not isinstance(other, _BaseNetwork): + raise TypeError("%s is not a network object" % other) + + if not other.subnet_of(self): + raise ValueError('%s not contained in %s' % (other, self)) + if other == self: + return + + # Make sure we're comparing the network of other. + other = other.__class__('%s/%s' % (other.network_address, + other.prefixlen)) + + s1, s2 = self.subnets() + while s1 != other and s2 != other: + if other.subnet_of(s1): + yield s2 + s1, s2 = s1.subnets() + elif other.subnet_of(s2): + yield s1 + s1, s2 = s2.subnets() + else: + # If we got here, there's a bug somewhere. + raise AssertionError('Error performing exclusion: ' + 's1: %s s2: %s other: %s' % + (s1, s2, other)) + if s1 == other: + yield s2 + elif s2 == other: + yield s1 + else: + # If we got here, there's a bug somewhere. + raise AssertionError('Error performing exclusion: ' + 's1: %s s2: %s other: %s' % + (s1, s2, other)) + + def compare_networks(self, other): + """Compare two IP objects. + + This is only concerned about the comparison of the integer + representation of the network addresses. This means that the + host bits aren't considered at all in this method. If you want + to compare host bits, you can easily enough do a + 'HostA._ip < HostB._ip' + + Args: + other: An IP object. + + Returns: + If the IP versions of self and other are the same, returns: + + -1 if self < other: + eg: IPv4Network('192.0.2.0/25') < IPv4Network('192.0.2.128/25') + IPv6Network('2001:db8::1000/124') < + IPv6Network('2001:db8::2000/124') + 0 if self == other + eg: IPv4Network('192.0.2.0/24') == IPv4Network('192.0.2.0/24') + IPv6Network('2001:db8::1000/124') == + IPv6Network('2001:db8::1000/124') + 1 if self > other + eg: IPv4Network('192.0.2.128/25') > IPv4Network('192.0.2.0/25') + IPv6Network('2001:db8::2000/124') > + IPv6Network('2001:db8::1000/124') + + Raises: + TypeError if the IP versions are different. + + """ + # does this need to raise a ValueError? + if self._version != other._version: + raise TypeError('%s and %s are not of the same type' % ( + self, other)) + # self._version == other._version below here: + if self.network_address < other.network_address: + return -1 + if self.network_address > other.network_address: + return 1 + # self.network_address == other.network_address below here: + if self.netmask < other.netmask: + return -1 + if self.netmask > other.netmask: + return 1 + return 0 + + def _get_networks_key(self): + """Network-only key function. + + Returns an object that identifies this address' network and + netmask. This function is a suitable "key" argument for sorted() + and list.sort(). + + """ + return (self._version, self.network_address, self.netmask) + + def subnets(self, prefixlen_diff=1, new_prefix=None): + """The subnets which join to make the current subnet. + + In the case that self contains only one IP + (self._prefixlen == 32 for IPv4 or self._prefixlen == 128 + for IPv6), yield an iterator with just ourself. + + Args: + prefixlen_diff: An integer, the amount the prefix length + should be increased by. This should not be set if + new_prefix is also set. + new_prefix: The desired new prefix length. This must be a + larger number (smaller prefix) than the existing prefix. + This should not be set if prefixlen_diff is also set. + + Returns: + An iterator of IPv(4|6) objects. + + Raises: + ValueError: The prefixlen_diff is too small or too large. + OR + prefixlen_diff and new_prefix are both set or new_prefix + is a smaller number than the current prefix (smaller + number means a larger network) + + """ + if self._prefixlen == self._max_prefixlen: + yield self + return + + if new_prefix is not None: + if new_prefix < self._prefixlen: + raise ValueError('new prefix must be longer') + if prefixlen_diff != 1: + raise ValueError('cannot set prefixlen_diff and new_prefix') + prefixlen_diff = new_prefix - self._prefixlen + + if prefixlen_diff < 0: + raise ValueError('prefix length diff must be > 0') + new_prefixlen = self._prefixlen + prefixlen_diff + + if new_prefixlen > self._max_prefixlen: + raise ValueError( + 'prefix length diff %d is invalid for netblock %s' % ( + new_prefixlen, self)) + + start = int(self.network_address) + end = int(self.broadcast_address) + 1 + step = (int(self.hostmask) + 1) >> prefixlen_diff + for new_addr in _compat_range(start, end, step): + current = self.__class__((new_addr, new_prefixlen)) + yield current + + def supernet(self, prefixlen_diff=1, new_prefix=None): + """The supernet containing the current network. + + Args: + prefixlen_diff: An integer, the amount the prefix length of + the network should be decreased by. For example, given a + /24 network and a prefixlen_diff of 3, a supernet with a + /21 netmask is returned. + + Returns: + An IPv4 network object. + + Raises: + ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have + a negative prefix length. + OR + If prefixlen_diff and new_prefix are both set or new_prefix is a + larger number than the current prefix (larger number means a + smaller network) + + """ + if self._prefixlen == 0: + return self + + if new_prefix is not None: + if new_prefix > self._prefixlen: + raise ValueError('new prefix must be shorter') + if prefixlen_diff != 1: + raise ValueError('cannot set prefixlen_diff and new_prefix') + prefixlen_diff = self._prefixlen - new_prefix + + new_prefixlen = self.prefixlen - prefixlen_diff + if new_prefixlen < 0: + raise ValueError( + 'current prefixlen is %d, cannot have a prefixlen_diff of %d' % + (self.prefixlen, prefixlen_diff)) + return self.__class__(( + int(self.network_address) & (int(self.netmask) << prefixlen_diff), + new_prefixlen + )) + + @property + def is_multicast(self): + """Test if the address is reserved for multicast use. + + Returns: + A boolean, True if the address is a multicast address. + See RFC 2373 2.7 for details. + + """ + return (self.network_address.is_multicast and + self.broadcast_address.is_multicast) + + def subnet_of(self, other): + # always false if one is v4 and the other is v6. + if self._version != other._version: + return False + # dealing with another network. + if (hasattr(other, 'network_address') and + hasattr(other, 'broadcast_address')): + return (other.network_address <= self.network_address and + other.broadcast_address >= self.broadcast_address) + # dealing with another address + else: + raise TypeError('Unable to test subnet containment with element ' + 'of type %s' % type(other)) + + def supernet_of(self, other): + # always false if one is v4 and the other is v6. + if self._version != other._version: + return False + # dealing with another network. + if (hasattr(other, 'network_address') and + hasattr(other, 'broadcast_address')): + return (other.network_address >= self.network_address and + other.broadcast_address <= self.broadcast_address) + # dealing with another address + else: + raise TypeError('Unable to test subnet containment with element ' + 'of type %s' % type(other)) + + @property + def is_reserved(self): + """Test if the address is otherwise IETF reserved. + + Returns: + A boolean, True if the address is within one of the + reserved IPv6 Network ranges. + + """ + return (self.network_address.is_reserved and + self.broadcast_address.is_reserved) + + @property + def is_link_local(self): + """Test if the address is reserved for link-local. + + Returns: + A boolean, True if the address is reserved per RFC 4291. + + """ + return (self.network_address.is_link_local and + self.broadcast_address.is_link_local) + + @property + def is_private(self): + """Test if this address is allocated for private networks. + + Returns: + A boolean, True if the address is reserved per + iana-ipv4-special-registry or iana-ipv6-special-registry. + + """ + return (self.network_address.is_private and + self.broadcast_address.is_private) + + @property + def is_global(self): + """Test if this address is allocated for public networks. + + Returns: + A boolean, True if the address is not reserved per + iana-ipv4-special-registry or iana-ipv6-special-registry. + + """ + return not self.is_private + + @property + def is_unspecified(self): + """Test if the address is unspecified. + + Returns: + A boolean, True if this is the unspecified address as defined in + RFC 2373 2.5.2. + + """ + return (self.network_address.is_unspecified and + self.broadcast_address.is_unspecified) + + @property + def is_loopback(self): + """Test if the address is a loopback address. + + Returns: + A boolean, True if the address is a loopback address as defined in + RFC 2373 2.5.3. + + """ + return (self.network_address.is_loopback and + self.broadcast_address.is_loopback) + + +class _BaseV4(object): + + """Base IPv4 object. + + The following methods are used by IPv4 objects in both single IP + addresses and networks. + + """ + + __slots__ = () + _version = 4 + # Equivalent to 255.255.255.255 or 32 bits of 1's. + _ALL_ONES = (2 ** IPV4LENGTH) - 1 + _DECIMAL_DIGITS = frozenset('0123456789') + + # the valid octets for host and netmasks. only useful for IPv4. + _valid_mask_octets = frozenset([255, 254, 252, 248, 240, 224, 192, 128, 0]) + + _max_prefixlen = IPV4LENGTH + # There are only a handful of valid v4 netmasks, so we cache them all + # when constructed (see _make_netmask()). + _netmask_cache = {} + + def _explode_shorthand_ip_string(self): + return _compat_str(self) + + @classmethod + def _make_netmask(cls, arg): + """Make a (netmask, prefix_len) tuple from the given argument. + + Argument can be: + - an integer (the prefix length) + - a string representing the prefix length (e.g. "24") + - a string representing the prefix netmask (e.g. "255.255.255.0") + """ + if arg not in cls._netmask_cache: + if isinstance(arg, _compat_int_types): + prefixlen = arg + else: + try: + # Check for a netmask in prefix length form + prefixlen = cls._prefix_from_prefix_string(arg) + except NetmaskValueError: + # Check for a netmask or hostmask in dotted-quad form. + # This may raise NetmaskValueError. + prefixlen = cls._prefix_from_ip_string(arg) + netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) + cls._netmask_cache[arg] = netmask, prefixlen + return cls._netmask_cache[arg] + + @classmethod + def _ip_int_from_string(cls, ip_str): + """Turn the given IP string into an integer for comparison. + + Args: + ip_str: A string, the IP ip_str. + + Returns: + The IP ip_str as an integer. + + Raises: + AddressValueError: if ip_str isn't a valid IPv4 Address. + + """ + if not ip_str: + raise AddressValueError('Address cannot be empty') + + octets = ip_str.split('.') + if len(octets) != 4: + raise AddressValueError("Expected 4 octets in %r" % ip_str) + + try: + return _compat_int_from_byte_vals( + map(cls._parse_octet, octets), 'big') + except ValueError as exc: + raise AddressValueError("%s in %r" % (exc, ip_str)) + + @classmethod + def _parse_octet(cls, octet_str): + """Convert a decimal octet into an integer. + + Args: + octet_str: A string, the number to parse. + + Returns: + The octet as an integer. + + Raises: + ValueError: if the octet isn't strictly a decimal from [0..255]. + + """ + if not octet_str: + raise ValueError("Empty octet not permitted") + # Whitelist the characters, since int() allows a lot of bizarre stuff. + if not cls._DECIMAL_DIGITS.issuperset(octet_str): + msg = "Only decimal digits permitted in %r" + raise ValueError(msg % octet_str) + # We do the length check second, since the invalid character error + # is likely to be more informative for the user + if len(octet_str) > 3: + msg = "At most 3 characters permitted in %r" + raise ValueError(msg % octet_str) + # Convert to integer (we know digits are legal) + octet_int = int(octet_str, 10) + # Any octets that look like they *might* be written in octal, + # and which don't look exactly the same in both octal and + # decimal are rejected as ambiguous + if octet_int > 7 and octet_str[0] == '0': + msg = "Ambiguous (octal/decimal) value in %r not permitted" + raise ValueError(msg % octet_str) + if octet_int > 255: + raise ValueError("Octet %d (> 255) not permitted" % octet_int) + return octet_int + + @classmethod + def _string_from_ip_int(cls, ip_int): + """Turns a 32-bit integer into dotted decimal notation. + + Args: + ip_int: An integer, the IP address. + + Returns: + The IP address as a string in dotted decimal notation. + + """ + return '.'.join(_compat_str(struct.unpack(b'!B', b)[0] + if isinstance(b, bytes) + else b) + for b in _compat_to_bytes(ip_int, 4, 'big')) + + def _is_hostmask(self, ip_str): + """Test if the IP string is a hostmask (rather than a netmask). + + Args: + ip_str: A string, the potential hostmask. + + Returns: + A boolean, True if the IP string is a hostmask. + + """ + bits = ip_str.split('.') + try: + parts = [x for x in map(int, bits) if x in self._valid_mask_octets] + except ValueError: + return False + if len(parts) != len(bits): + return False + if parts[0] < parts[-1]: + return True + return False + + def _reverse_pointer(self): + """Return the reverse DNS pointer name for the IPv4 address. + + This implements the method described in RFC1035 3.5. + + """ + reverse_octets = _compat_str(self).split('.')[::-1] + return '.'.join(reverse_octets) + '.in-addr.arpa' + + @property + def max_prefixlen(self): + return self._max_prefixlen + + @property + def version(self): + return self._version + + +class IPv4Address(_BaseV4, _BaseAddress): + + """Represent and manipulate single IPv4 Addresses.""" + + __slots__ = ('_ip', '__weakref__') + + def __init__(self, address): + + """ + Args: + address: A string or integer representing the IP + + Additionally, an integer can be passed, so + IPv4Address('192.0.2.1') == IPv4Address(3221225985). + or, more generally + IPv4Address(int(IPv4Address('192.0.2.1'))) == + IPv4Address('192.0.2.1') + + Raises: + AddressValueError: If ipaddress isn't a valid IPv4 address. + + """ + # Efficient constructor from integer. + if isinstance(address, _compat_int_types): + self._check_int_address(address) + self._ip = address + return + + # Constructing from a packed address + if isinstance(address, bytes): + self._check_packed_address(address, 4) + bvs = _compat_bytes_to_byte_vals(address) + self._ip = _compat_int_from_byte_vals(bvs, 'big') + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP string. + addr_str = _compat_str(address) + if '/' in addr_str: + raise AddressValueError("Unexpected '/' in %r" % address) + self._ip = self._ip_int_from_string(addr_str) + + @property + def packed(self): + """The binary representation of this address.""" + return v4_int_to_packed(self._ip) + + @property + def is_reserved(self): + """Test if the address is otherwise IETF reserved. + + Returns: + A boolean, True if the address is within the + reserved IPv4 Network range. + + """ + return self in self._constants._reserved_network + + @property + def is_private(self): + """Test if this address is allocated for private networks. + + Returns: + A boolean, True if the address is reserved per + iana-ipv4-special-registry. + + """ + return any(self in net for net in self._constants._private_networks) + + @property + def is_global(self): + return ( + self not in self._constants._public_network and + not self.is_private) + + @property + def is_multicast(self): + """Test if the address is reserved for multicast use. + + Returns: + A boolean, True if the address is multicast. + See RFC 3171 for details. + + """ + return self in self._constants._multicast_network + + @property + def is_unspecified(self): + """Test if the address is unspecified. + + Returns: + A boolean, True if this is the unspecified address as defined in + RFC 5735 3. + + """ + return self == self._constants._unspecified_address + + @property + def is_loopback(self): + """Test if the address is a loopback address. + + Returns: + A boolean, True if the address is a loopback per RFC 3330. + + """ + return self in self._constants._loopback_network + + @property + def is_link_local(self): + """Test if the address is reserved for link-local. + + Returns: + A boolean, True if the address is link-local per RFC 3927. + + """ + return self in self._constants._linklocal_network + + +class IPv4Interface(IPv4Address): + + def __init__(self, address): + if isinstance(address, (bytes, _compat_int_types)): + IPv4Address.__init__(self, address) + self.network = IPv4Network(self._ip) + self._prefixlen = self._max_prefixlen + return + + if isinstance(address, tuple): + IPv4Address.__init__(self, address[0]) + if len(address) > 1: + self._prefixlen = int(address[1]) + else: + self._prefixlen = self._max_prefixlen + + self.network = IPv4Network(address, strict=False) + self.netmask = self.network.netmask + self.hostmask = self.network.hostmask + return + + addr = _split_optional_netmask(address) + IPv4Address.__init__(self, addr[0]) + + self.network = IPv4Network(address, strict=False) + self._prefixlen = self.network._prefixlen + + self.netmask = self.network.netmask + self.hostmask = self.network.hostmask + + def __str__(self): + return '%s/%d' % (self._string_from_ip_int(self._ip), + self.network.prefixlen) + + def __eq__(self, other): + address_equal = IPv4Address.__eq__(self, other) + if not address_equal or address_equal is NotImplemented: + return address_equal + try: + return self.network == other.network + except AttributeError: + # An interface with an associated network is NOT the + # same as an unassociated address. That's why the hash + # takes the extra info into account. + return False + + def __lt__(self, other): + address_less = IPv4Address.__lt__(self, other) + if address_less is NotImplemented: + return NotImplemented + try: + return self.network < other.network + except AttributeError: + # We *do* allow addresses and interfaces to be sorted. The + # unassociated address is considered less than all interfaces. + return False + + def __hash__(self): + return self._ip ^ self._prefixlen ^ int(self.network.network_address) + + __reduce__ = _IPAddressBase.__reduce__ + + @property + def ip(self): + return IPv4Address(self._ip) + + @property + def with_prefixlen(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self._prefixlen) + + @property + def with_netmask(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self.netmask) + + @property + def with_hostmask(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self.hostmask) + + +class IPv4Network(_BaseV4, _BaseNetwork): + + """This class represents and manipulates 32-bit IPv4 network + addresses.. + + Attributes: [examples for IPv4Network('192.0.2.0/27')] + .network_address: IPv4Address('192.0.2.0') + .hostmask: IPv4Address('0.0.0.31') + .broadcast_address: IPv4Address('192.0.2.32') + .netmask: IPv4Address('255.255.255.224') + .prefixlen: 27 + + """ + # Class to use when creating address objects + _address_class = IPv4Address + + def __init__(self, address, strict=True): + + """Instantiate a new IPv4 network object. + + Args: + address: A string or integer representing the IP [& network]. + '192.0.2.0/24' + '192.0.2.0/255.255.255.0' + '192.0.0.2/0.0.0.255' + are all functionally the same in IPv4. Similarly, + '192.0.2.1' + '192.0.2.1/255.255.255.255' + '192.0.2.1/32' + are also functionally equivalent. That is to say, failing to + provide a subnetmask will create an object with a mask of /32. + + If the mask (portion after the / in the argument) is given in + dotted quad form, it is treated as a netmask if it starts with a + non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it + starts with a zero field (e.g. 0.255.255.255 == /8), with the + single exception of an all-zero mask which is treated as a + netmask == /0. If no mask is given, a default of /32 is used. + + Additionally, an integer can be passed, so + IPv4Network('192.0.2.1') == IPv4Network(3221225985) + or, more generally + IPv4Interface(int(IPv4Interface('192.0.2.1'))) == + IPv4Interface('192.0.2.1') + + Raises: + AddressValueError: If ipaddress isn't a valid IPv4 address. + NetmaskValueError: If the netmask isn't valid for + an IPv4 address. + ValueError: If strict is True and a network address is not + supplied. + + """ + _BaseNetwork.__init__(self, address) + + # Constructing from a packed address or integer + if isinstance(address, (_compat_int_types, bytes)): + self.network_address = IPv4Address(address) + self.netmask, self._prefixlen = self._make_netmask( + self._max_prefixlen) + # fixme: address/network test here. + return + + if isinstance(address, tuple): + if len(address) > 1: + arg = address[1] + else: + # We weren't given an address[1] + arg = self._max_prefixlen + self.network_address = IPv4Address(address[0]) + self.netmask, self._prefixlen = self._make_netmask(arg) + packed = int(self.network_address) + if packed & int(self.netmask) != packed: + if strict: + raise ValueError('%s has host bits set' % self) + else: + self.network_address = IPv4Address(packed & + int(self.netmask)) + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = _split_optional_netmask(address) + self.network_address = IPv4Address(self._ip_int_from_string(addr[0])) + + if len(addr) == 2: + arg = addr[1] + else: + arg = self._max_prefixlen + self.netmask, self._prefixlen = self._make_netmask(arg) + + if strict: + if (IPv4Address(int(self.network_address) & int(self.netmask)) != + self.network_address): + raise ValueError('%s has host bits set' % self) + self.network_address = IPv4Address(int(self.network_address) & + int(self.netmask)) + + if self._prefixlen == (self._max_prefixlen - 1): + self.hosts = self.__iter__ + + @property + def is_global(self): + """Test if this address is allocated for public networks. + + Returns: + A boolean, True if the address is not reserved per + iana-ipv4-special-registry. + + """ + return (not (self.network_address in IPv4Network('100.64.0.0/10') and + self.broadcast_address in IPv4Network('100.64.0.0/10')) and + not self.is_private) + + +class _IPv4Constants(object): + + _linklocal_network = IPv4Network('169.254.0.0/16') + + _loopback_network = IPv4Network('127.0.0.0/8') + + _multicast_network = IPv4Network('224.0.0.0/4') + + _public_network = IPv4Network('100.64.0.0/10') + + _private_networks = [ + IPv4Network('0.0.0.0/8'), + IPv4Network('10.0.0.0/8'), + IPv4Network('127.0.0.0/8'), + IPv4Network('169.254.0.0/16'), + IPv4Network('172.16.0.0/12'), + IPv4Network('192.0.0.0/29'), + IPv4Network('192.0.0.170/31'), + IPv4Network('192.0.2.0/24'), + IPv4Network('192.168.0.0/16'), + IPv4Network('198.18.0.0/15'), + IPv4Network('198.51.100.0/24'), + IPv4Network('203.0.113.0/24'), + IPv4Network('240.0.0.0/4'), + IPv4Network('255.255.255.255/32'), + ] + + _reserved_network = IPv4Network('240.0.0.0/4') + + _unspecified_address = IPv4Address('0.0.0.0') + + +IPv4Address._constants = _IPv4Constants + + +class _BaseV6(object): + + """Base IPv6 object. + + The following methods are used by IPv6 objects in both single IP + addresses and networks. + + """ + + __slots__ = () + _version = 6 + _ALL_ONES = (2 ** IPV6LENGTH) - 1 + _HEXTET_COUNT = 8 + _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef') + _max_prefixlen = IPV6LENGTH + + # There are only a bunch of valid v6 netmasks, so we cache them all + # when constructed (see _make_netmask()). + _netmask_cache = {} + + @classmethod + def _make_netmask(cls, arg): + """Make a (netmask, prefix_len) tuple from the given argument. + + Argument can be: + - an integer (the prefix length) + - a string representing the prefix length (e.g. "24") + - a string representing the prefix netmask (e.g. "255.255.255.0") + """ + if arg not in cls._netmask_cache: + if isinstance(arg, _compat_int_types): + prefixlen = arg + else: + prefixlen = cls._prefix_from_prefix_string(arg) + netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) + cls._netmask_cache[arg] = netmask, prefixlen + return cls._netmask_cache[arg] + + @classmethod + def _ip_int_from_string(cls, ip_str): + """Turn an IPv6 ip_str into an integer. + + Args: + ip_str: A string, the IPv6 ip_str. + + Returns: + An int, the IPv6 address + + Raises: + AddressValueError: if ip_str isn't a valid IPv6 Address. + + """ + if not ip_str: + raise AddressValueError('Address cannot be empty') + + parts = ip_str.split(':') + + # An IPv6 address needs at least 2 colons (3 parts). + _min_parts = 3 + if len(parts) < _min_parts: + msg = "At least %d parts expected in %r" % (_min_parts, ip_str) + raise AddressValueError(msg) + + # If the address has an IPv4-style suffix, convert it to hexadecimal. + if '.' in parts[-1]: + try: + ipv4_int = IPv4Address(parts.pop())._ip + except AddressValueError as exc: + raise AddressValueError("%s in %r" % (exc, ip_str)) + parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF)) + parts.append('%x' % (ipv4_int & 0xFFFF)) + + # An IPv6 address can't have more than 8 colons (9 parts). + # The extra colon comes from using the "::" notation for a single + # leading or trailing zero part. + _max_parts = cls._HEXTET_COUNT + 1 + if len(parts) > _max_parts: + msg = "At most %d colons permitted in %r" % ( + _max_parts - 1, ip_str) + raise AddressValueError(msg) + + # Disregarding the endpoints, find '::' with nothing in between. + # This indicates that a run of zeroes has been skipped. + skip_index = None + for i in _compat_range(1, len(parts) - 1): + if not parts[i]: + if skip_index is not None: + # Can't have more than one '::' + msg = "At most one '::' permitted in %r" % ip_str + raise AddressValueError(msg) + skip_index = i + + # parts_hi is the number of parts to copy from above/before the '::' + # parts_lo is the number of parts to copy from below/after the '::' + if skip_index is not None: + # If we found a '::', then check if it also covers the endpoints. + parts_hi = skip_index + parts_lo = len(parts) - skip_index - 1 + if not parts[0]: + parts_hi -= 1 + if parts_hi: + msg = "Leading ':' only permitted as part of '::' in %r" + raise AddressValueError(msg % ip_str) # ^: requires ^:: + if not parts[-1]: + parts_lo -= 1 + if parts_lo: + msg = "Trailing ':' only permitted as part of '::' in %r" + raise AddressValueError(msg % ip_str) # :$ requires ::$ + parts_skipped = cls._HEXTET_COUNT - (parts_hi + parts_lo) + if parts_skipped < 1: + msg = "Expected at most %d other parts with '::' in %r" + raise AddressValueError(msg % (cls._HEXTET_COUNT - 1, ip_str)) + else: + # Otherwise, allocate the entire address to parts_hi. The + # endpoints could still be empty, but _parse_hextet() will check + # for that. + if len(parts) != cls._HEXTET_COUNT: + msg = "Exactly %d parts expected without '::' in %r" + raise AddressValueError(msg % (cls._HEXTET_COUNT, ip_str)) + if not parts[0]: + msg = "Leading ':' only permitted as part of '::' in %r" + raise AddressValueError(msg % ip_str) # ^: requires ^:: + if not parts[-1]: + msg = "Trailing ':' only permitted as part of '::' in %r" + raise AddressValueError(msg % ip_str) # :$ requires ::$ + parts_hi = len(parts) + parts_lo = 0 + parts_skipped = 0 + + try: + # Now, parse the hextets into a 128-bit integer. + ip_int = 0 + for i in range(parts_hi): + ip_int <<= 16 + ip_int |= cls._parse_hextet(parts[i]) + ip_int <<= 16 * parts_skipped + for i in range(-parts_lo, 0): + ip_int <<= 16 + ip_int |= cls._parse_hextet(parts[i]) + return ip_int + except ValueError as exc: + raise AddressValueError("%s in %r" % (exc, ip_str)) + + @classmethod + def _parse_hextet(cls, hextet_str): + """Convert an IPv6 hextet string into an integer. + + Args: + hextet_str: A string, the number to parse. + + Returns: + The hextet as an integer. + + Raises: + ValueError: if the input isn't strictly a hex number from + [0..FFFF]. + + """ + # Whitelist the characters, since int() allows a lot of bizarre stuff. + if not cls._HEX_DIGITS.issuperset(hextet_str): + raise ValueError("Only hex digits permitted in %r" % hextet_str) + # We do the length check second, since the invalid character error + # is likely to be more informative for the user + if len(hextet_str) > 4: + msg = "At most 4 characters permitted in %r" + raise ValueError(msg % hextet_str) + # Length check means we can skip checking the integer value + return int(hextet_str, 16) + + @classmethod + def _compress_hextets(cls, hextets): + """Compresses a list of hextets. + + Compresses a list of strings, replacing the longest continuous + sequence of "0" in the list with "" and adding empty strings at + the beginning or at the end of the string such that subsequently + calling ":".join(hextets) will produce the compressed version of + the IPv6 address. + + Args: + hextets: A list of strings, the hextets to compress. + + Returns: + A list of strings. + + """ + best_doublecolon_start = -1 + best_doublecolon_len = 0 + doublecolon_start = -1 + doublecolon_len = 0 + for index, hextet in enumerate(hextets): + if hextet == '0': + doublecolon_len += 1 + if doublecolon_start == -1: + # Start of a sequence of zeros. + doublecolon_start = index + if doublecolon_len > best_doublecolon_len: + # This is the longest sequence of zeros so far. + best_doublecolon_len = doublecolon_len + best_doublecolon_start = doublecolon_start + else: + doublecolon_len = 0 + doublecolon_start = -1 + + if best_doublecolon_len > 1: + best_doublecolon_end = (best_doublecolon_start + + best_doublecolon_len) + # For zeros at the end of the address. + if best_doublecolon_end == len(hextets): + hextets += [''] + hextets[best_doublecolon_start:best_doublecolon_end] = [''] + # For zeros at the beginning of the address. + if best_doublecolon_start == 0: + hextets = [''] + hextets + + return hextets + + @classmethod + def _string_from_ip_int(cls, ip_int=None): + """Turns a 128-bit integer into hexadecimal notation. + + Args: + ip_int: An integer, the IP address. + + Returns: + A string, the hexadecimal representation of the address. + + Raises: + ValueError: The address is bigger than 128 bits of all ones. + + """ + if ip_int is None: + ip_int = int(cls._ip) + + if ip_int > cls._ALL_ONES: + raise ValueError('IPv6 address is too large') + + hex_str = '%032x' % ip_int + hextets = ['%x' % int(hex_str[x:x + 4], 16) for x in range(0, 32, 4)] + + hextets = cls._compress_hextets(hextets) + return ':'.join(hextets) + + def _explode_shorthand_ip_string(self): + """Expand a shortened IPv6 address. + + Args: + ip_str: A string, the IPv6 address. + + Returns: + A string, the expanded IPv6 address. + + """ + if isinstance(self, IPv6Network): + ip_str = _compat_str(self.network_address) + elif isinstance(self, IPv6Interface): + ip_str = _compat_str(self.ip) + else: + ip_str = _compat_str(self) + + ip_int = self._ip_int_from_string(ip_str) + hex_str = '%032x' % ip_int + parts = [hex_str[x:x + 4] for x in range(0, 32, 4)] + if isinstance(self, (_BaseNetwork, IPv6Interface)): + return '%s/%d' % (':'.join(parts), self._prefixlen) + return ':'.join(parts) + + def _reverse_pointer(self): + """Return the reverse DNS pointer name for the IPv6 address. + + This implements the method described in RFC3596 2.5. + + """ + reverse_chars = self.exploded[::-1].replace(':', '') + return '.'.join(reverse_chars) + '.ip6.arpa' + + @property + def max_prefixlen(self): + return self._max_prefixlen + + @property + def version(self): + return self._version + + +class IPv6Address(_BaseV6, _BaseAddress): + + """Represent and manipulate single IPv6 Addresses.""" + + __slots__ = ('_ip', '__weakref__') + + def __init__(self, address): + """Instantiate a new IPv6 address object. + + Args: + address: A string or integer representing the IP + + Additionally, an integer can be passed, so + IPv6Address('2001:db8::') == + IPv6Address(42540766411282592856903984951653826560) + or, more generally + IPv6Address(int(IPv6Address('2001:db8::'))) == + IPv6Address('2001:db8::') + + Raises: + AddressValueError: If address isn't a valid IPv6 address. + + """ + # Efficient constructor from integer. + if isinstance(address, _compat_int_types): + self._check_int_address(address) + self._ip = address + return + + # Constructing from a packed address + if isinstance(address, bytes): + self._check_packed_address(address, 16) + bvs = _compat_bytes_to_byte_vals(address) + self._ip = _compat_int_from_byte_vals(bvs, 'big') + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP string. + addr_str = _compat_str(address) + if '/' in addr_str: + raise AddressValueError("Unexpected '/' in %r" % address) + self._ip = self._ip_int_from_string(addr_str) + + @property + def packed(self): + """The binary representation of this address.""" + return v6_int_to_packed(self._ip) + + @property + def is_multicast(self): + """Test if the address is reserved for multicast use. + + Returns: + A boolean, True if the address is a multicast address. + See RFC 2373 2.7 for details. + + """ + return self in self._constants._multicast_network + + @property + def is_reserved(self): + """Test if the address is otherwise IETF reserved. + + Returns: + A boolean, True if the address is within one of the + reserved IPv6 Network ranges. + + """ + return any(self in x for x in self._constants._reserved_networks) + + @property + def is_link_local(self): + """Test if the address is reserved for link-local. + + Returns: + A boolean, True if the address is reserved per RFC 4291. + + """ + return self in self._constants._linklocal_network + + @property + def is_site_local(self): + """Test if the address is reserved for site-local. + + Note that the site-local address space has been deprecated by RFC 3879. + Use is_private to test if this address is in the space of unique local + addresses as defined by RFC 4193. + + Returns: + A boolean, True if the address is reserved per RFC 3513 2.5.6. + + """ + return self in self._constants._sitelocal_network + + @property + def is_private(self): + """Test if this address is allocated for private networks. + + Returns: + A boolean, True if the address is reserved per + iana-ipv6-special-registry. + + """ + return any(self in net for net in self._constants._private_networks) + + @property + def is_global(self): + """Test if this address is allocated for public networks. + + Returns: + A boolean, true if the address is not reserved per + iana-ipv6-special-registry. + + """ + return not self.is_private + + @property + def is_unspecified(self): + """Test if the address is unspecified. + + Returns: + A boolean, True if this is the unspecified address as defined in + RFC 2373 2.5.2. + + """ + return self._ip == 0 + + @property + def is_loopback(self): + """Test if the address is a loopback address. + + Returns: + A boolean, True if the address is a loopback address as defined in + RFC 2373 2.5.3. + + """ + return self._ip == 1 + + @property + def ipv4_mapped(self): + """Return the IPv4 mapped address. + + Returns: + If the IPv6 address is a v4 mapped address, return the + IPv4 mapped address. Return None otherwise. + + """ + if (self._ip >> 32) != 0xFFFF: + return None + return IPv4Address(self._ip & 0xFFFFFFFF) + + @property + def teredo(self): + """Tuple of embedded teredo IPs. + + Returns: + Tuple of the (server, client) IPs or None if the address + doesn't appear to be a teredo address (doesn't start with + 2001::/32) + + """ + if (self._ip >> 96) != 0x20010000: + return None + return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF), + IPv4Address(~self._ip & 0xFFFFFFFF)) + + @property + def sixtofour(self): + """Return the IPv4 6to4 embedded address. + + Returns: + The IPv4 6to4-embedded address if present or None if the + address doesn't appear to contain a 6to4 embedded address. + + """ + if (self._ip >> 112) != 0x2002: + return None + return IPv4Address((self._ip >> 80) & 0xFFFFFFFF) + + +class IPv6Interface(IPv6Address): + + def __init__(self, address): + if isinstance(address, (bytes, _compat_int_types)): + IPv6Address.__init__(self, address) + self.network = IPv6Network(self._ip) + self._prefixlen = self._max_prefixlen + return + if isinstance(address, tuple): + IPv6Address.__init__(self, address[0]) + if len(address) > 1: + self._prefixlen = int(address[1]) + else: + self._prefixlen = self._max_prefixlen + self.network = IPv6Network(address, strict=False) + self.netmask = self.network.netmask + self.hostmask = self.network.hostmask + return + + addr = _split_optional_netmask(address) + IPv6Address.__init__(self, addr[0]) + self.network = IPv6Network(address, strict=False) + self.netmask = self.network.netmask + self._prefixlen = self.network._prefixlen + self.hostmask = self.network.hostmask + + def __str__(self): + return '%s/%d' % (self._string_from_ip_int(self._ip), + self.network.prefixlen) + + def __eq__(self, other): + address_equal = IPv6Address.__eq__(self, other) + if not address_equal or address_equal is NotImplemented: + return address_equal + try: + return self.network == other.network + except AttributeError: + # An interface with an associated network is NOT the + # same as an unassociated address. That's why the hash + # takes the extra info into account. + return False + + def __lt__(self, other): + address_less = IPv6Address.__lt__(self, other) + if address_less is NotImplemented: + return NotImplemented + try: + return self.network < other.network + except AttributeError: + # We *do* allow addresses and interfaces to be sorted. The + # unassociated address is considered less than all interfaces. + return False + + def __hash__(self): + return self._ip ^ self._prefixlen ^ int(self.network.network_address) + + __reduce__ = _IPAddressBase.__reduce__ + + @property + def ip(self): + return IPv6Address(self._ip) + + @property + def with_prefixlen(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self._prefixlen) + + @property + def with_netmask(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self.netmask) + + @property + def with_hostmask(self): + return '%s/%s' % (self._string_from_ip_int(self._ip), + self.hostmask) + + @property + def is_unspecified(self): + return self._ip == 0 and self.network.is_unspecified + + @property + def is_loopback(self): + return self._ip == 1 and self.network.is_loopback + + +class IPv6Network(_BaseV6, _BaseNetwork): + + """This class represents and manipulates 128-bit IPv6 networks. + + Attributes: [examples for IPv6('2001:db8::1000/124')] + .network_address: IPv6Address('2001:db8::1000') + .hostmask: IPv6Address('::f') + .broadcast_address: IPv6Address('2001:db8::100f') + .netmask: IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0') + .prefixlen: 124 + + """ + + # Class to use when creating address objects + _address_class = IPv6Address + + def __init__(self, address, strict=True): + """Instantiate a new IPv6 Network object. + + Args: + address: A string or integer representing the IPv6 network or the + IP and prefix/netmask. + '2001:db8::/128' + '2001:db8:0000:0000:0000:0000:0000:0000/128' + '2001:db8::' + are all functionally the same in IPv6. That is to say, + failing to provide a subnetmask will create an object with + a mask of /128. + + Additionally, an integer can be passed, so + IPv6Network('2001:db8::') == + IPv6Network(42540766411282592856903984951653826560) + or, more generally + IPv6Network(int(IPv6Network('2001:db8::'))) == + IPv6Network('2001:db8::') + + strict: A boolean. If true, ensure that we have been passed + A true network address, eg, 2001:db8::1000/124 and not an + IP address on a network, eg, 2001:db8::1/124. + + Raises: + AddressValueError: If address isn't a valid IPv6 address. + NetmaskValueError: If the netmask isn't valid for + an IPv6 address. + ValueError: If strict was True and a network address was not + supplied. + + """ + _BaseNetwork.__init__(self, address) + + # Efficient constructor from integer or packed address + if isinstance(address, (bytes, _compat_int_types)): + self.network_address = IPv6Address(address) + self.netmask, self._prefixlen = self._make_netmask( + self._max_prefixlen) + return + + if isinstance(address, tuple): + if len(address) > 1: + arg = address[1] + else: + arg = self._max_prefixlen + self.netmask, self._prefixlen = self._make_netmask(arg) + self.network_address = IPv6Address(address[0]) + packed = int(self.network_address) + if packed & int(self.netmask) != packed: + if strict: + raise ValueError('%s has host bits set' % self) + else: + self.network_address = IPv6Address(packed & + int(self.netmask)) + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = _split_optional_netmask(address) + + self.network_address = IPv6Address(self._ip_int_from_string(addr[0])) + + if len(addr) == 2: + arg = addr[1] + else: + arg = self._max_prefixlen + self.netmask, self._prefixlen = self._make_netmask(arg) + + if strict: + if (IPv6Address(int(self.network_address) & int(self.netmask)) != + self.network_address): + raise ValueError('%s has host bits set' % self) + self.network_address = IPv6Address(int(self.network_address) & + int(self.netmask)) + + if self._prefixlen == (self._max_prefixlen - 1): + self.hosts = self.__iter__ + + def hosts(self): + """Generate Iterator over usable hosts in a network. + + This is like __iter__ except it doesn't return the + Subnet-Router anycast address. + + """ + network = int(self.network_address) + broadcast = int(self.broadcast_address) + for x in _compat_range(network + 1, broadcast + 1): + yield self._address_class(x) + + @property + def is_site_local(self): + """Test if the address is reserved for site-local. + + Note that the site-local address space has been deprecated by RFC 3879. + Use is_private to test if this address is in the space of unique local + addresses as defined by RFC 4193. + + Returns: + A boolean, True if the address is reserved per RFC 3513 2.5.6. + + """ + return (self.network_address.is_site_local and + self.broadcast_address.is_site_local) + + +class _IPv6Constants(object): + + _linklocal_network = IPv6Network('fe80::/10') + + _multicast_network = IPv6Network('ff00::/8') + + _private_networks = [ + IPv6Network('::1/128'), + IPv6Network('::/128'), + IPv6Network('::ffff:0:0/96'), + IPv6Network('100::/64'), + IPv6Network('2001::/23'), + IPv6Network('2001:2::/48'), + IPv6Network('2001:db8::/32'), + IPv6Network('2001:10::/28'), + IPv6Network('fc00::/7'), + IPv6Network('fe80::/10'), + ] + + _reserved_networks = [ + IPv6Network('::/8'), IPv6Network('100::/8'), + IPv6Network('200::/7'), IPv6Network('400::/6'), + IPv6Network('800::/5'), IPv6Network('1000::/4'), + IPv6Network('4000::/3'), IPv6Network('6000::/3'), + IPv6Network('8000::/3'), IPv6Network('A000::/3'), + IPv6Network('C000::/3'), IPv6Network('E000::/4'), + IPv6Network('F000::/5'), IPv6Network('F800::/6'), + IPv6Network('FE00::/9'), + ] + + _sitelocal_network = IPv6Network('fec0::/10') + + +IPv6Address._constants = _IPv6Constants diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 49d4f905..502b193d 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -1029,12 +1029,11 @@ this is the content of the fake file self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address')) rtn = IS_IPV6(is_multicast=True)('ff00::126c:8ffa:fe22:b3af') self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', None)) - # TODO: # with py3.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private # with py2.ipaddress '2001::126c:8ffa:fe22:b3af' is considered private # with gluon.contrib.ipaddr(both current and trunk) is not considered private - # rtn = IS_IPV6(is_routeable=True)('2001::126c:8ffa:fe22:b3af') - # self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None)) + rtn = IS_IPV6(is_routeable=False)('2001::126c:8ffa:fe22:b3af') + self.assertEqual(rtn, ('2001::126c:8ffa:fe22:b3af', None)) rtn = IS_IPV6(is_routeable=True)('ff00::126c:8ffa:fe22:b3af') self.assertEqual(rtn, ('ff00::126c:8ffa:fe22:b3af', 'Enter valid IPv6 address')) rtn = IS_IPV6(subnets='2001::/32')('2001::8ffa:fe22:b3af') diff --git a/gluon/validators.py b/gluon/validators.py index 22ad1a75..0aa1ec37 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -3505,10 +3505,7 @@ class IS_IPV6(Validator): self.error_message = error_message def __call__(self, value): - try: - import ipaddress - except ImportError: - from gluon.contrib import ipaddr as ipaddress + from gluon._compat import ipaddress try: ip = ipaddress.IPv6Address(to_unicode(value)) @@ -3732,12 +3729,10 @@ class IS_IPADDRESS(Validator): self.error_message = error_message def __call__(self, value): - try: - from ipaddress import ip_address as IPAddress - from ipaddress import IPv6Address, IPv4Address - except ImportError: - from gluon.contrib.ipaddr import (IPAddress, IPv4Address, - IPv6Address) + from gluon._compat import ipaddress + IPAddress = ipaddress.ip_address + IPv6Address = ipaddress.IPv6Address + IPv4Address = ipaddress.IPv4Address try: ip = IPAddress(to_unicode(value)) From efff27ffe49cf276d780115f33088703c1376aeb Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 7 Dec 2016 23:02:56 -0600 Subject: [PATCH 176/436] improved fabfile --- fabfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index 00a52c02..e4dbbe60 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,7 +1,8 @@ from fabric.api import * from fabric.operations import put, get -from fabric.contrib.files import exists +from fabric.contrib.files import exists, append, uncomment import os +import crypt import datetime import getpass @@ -19,11 +20,11 @@ def create_user(username): """fab -H root@host create_user:username""" password = getpass.getpass('password for %s> ' % username) run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username)) + local('ssh-copy-id %s' % env.hosts[0]) run('cp /etc/sudoers /tmp/sudoers.new') append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True) run('visudo -c -f /tmp/sudoers.new') run('EDITOR="cp /tmp/sudoers.new" visudo') - local('ssh-copy-id %s' % env.hosts[0]) uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes') def install_web2py(): From 867f93b634252438685cf3f683685af34e8e5ff1 Mon Sep 17 00:00:00 2001 From: wish7 Date: Fri, 9 Dec 2016 17:21:04 +0100 Subject: [PATCH 177/436] Fix for #1443 - currently emperor.uwsgi.service is not started on boot, PR will enable emperor.uwsgi.service on boot (behaviour as in old versions of this script) --- scripts/setup-web2py-nginx-uwsgi-ubuntu.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh index 3669f055..9d7fcee8 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -216,6 +216,7 @@ fi /etc/init.d/nginx start systemctl start emperor.uwsgi.service +systemctl enable emperor.uwsgi.service echo < Date: Tue, 20 Dec 2016 18:54:20 +0100 Subject: [PATCH 178/436] Add Auth and Crud messages when updating language files --- gluon/languages.py | 24 +++++++++++++++--------- gluon/tools.py | 24 +++++++++++++----------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index 0faedf33..b626457c 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -951,12 +951,23 @@ def findT(path, language=DEFAULT_LANGUAGE): Note: Must be run by the admin app """ + from gluon.tools import Auth, Crud lang_file = pjoin(path, 'languages', language + '.py') sentences = read_dict(lang_file) mp = pjoin(path, 'models') cp = pjoin(path, 'controllers') vp = pjoin(path, 'views') mop = pjoin(path, 'modules') + def add_message(message): + if not message.startswith('#') and not '\n' in message: + tokens = message.rsplit('##', 1) + else: + # this allows markmin syntax in translations + tokens = [message] + if len(tokens) == 2: + message = tokens[0].strip() + '##' + tokens[1].strip() + if message and not message in sentences: + sentences[message] = message.replace("@markmin\x01", "") for filename in \ listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\ + listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0): @@ -970,15 +981,10 @@ def findT(path, language=DEFAULT_LANGUAGE): message = safe_eval(item) except: continue # silently ignore inproperly formatted strings - if not message.startswith('#') and not '\n' in message: - tokens = message.rsplit('##', 1) - else: - # this allows markmin syntax in translations - tokens = [message] - if len(tokens) == 2: - message = tokens[0].strip() + '##' + tokens[1].strip() - if message and not message in sentences: - sentences[message] = message.replace("@markmin\x01", "") + add_message(message) + gluon_msg = [Auth.default_messages, Crud.default_messages] + for item in [x for m in gluon_msg for x in m.values() if x is not None]: + add_message(item) if not '!langcode!' in sentences: sentences['!langcode!'] = ( DEFAULT_LANGUAGE if language in ('default', DEFAULT_LANGUAGE) else language) diff --git a/gluon/tools.py b/gluon/tools.py index 053dc9ad..8a5b5aaa 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -4798,6 +4798,18 @@ class Auth(object): class Crud(object): # pragma: no cover + default_messages = dict( + submit_button = 'Submit', + delete_label = 'Check to delete', + record_created = 'Record Created', + record_updated = 'Record Updated', + record_deleted = 'Record Deleted', + update_log = 'Record %(id)s updated', + create_log = 'Record %(id)s created', + read_log = 'Record %(id)s read', + delete_log = 'Record %(id)s deleted', + ) + def url(self, f=None, args=None, vars=None): """ This should point to the controller that exposes @@ -4846,17 +4858,7 @@ class Crud(object): # pragma: no cover settings.lock_keys = True messages = self.messages = Messages(current.T) - messages.submit_button = 'Submit' - messages.delete_label = 'Check to delete' - messages.record_created = 'Record Created' - messages.record_updated = 'Record Updated' - messages.record_deleted = 'Record Deleted' - - messages.update_log = 'Record %(id)s updated' - messages.create_log = 'Record %(id)s created' - messages.read_log = 'Record %(id)s read' - messages.delete_log = 'Record %(id)s deleted' - + messages.update(Crud.default_messages) messages.lock_keys = True def __call__(self): From 28b0385d9bd98a5380735cc4e206d22dd0f84542 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Dec 2016 15:49:30 -0600 Subject: [PATCH 179/436] changes to fabfile --- fabfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fabfile.py b/fabfile.py index 00a52c02..e4dbbe60 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,7 +1,8 @@ from fabric.api import * from fabric.operations import put, get -from fabric.contrib.files import exists +from fabric.contrib.files import exists, append, uncomment import os +import crypt import datetime import getpass @@ -19,11 +20,11 @@ def create_user(username): """fab -H root@host create_user:username""" password = getpass.getpass('password for %s> ' % username) run('useradd -m -G www-data -s /bin/bash -p %s %s' % (crypt.crypt(password, 'salt'), username)) + local('ssh-copy-id %s' % env.hosts[0]) run('cp /etc/sudoers /tmp/sudoers.new') append('/tmp/sudoers.new', '%s ALL=(ALL) NOPASSWD:ALL' % username, use_sudo=True) run('visudo -c -f /tmp/sudoers.new') run('EDITOR="cp /tmp/sudoers.new" visudo') - local('ssh-copy-id %s' % env.hosts[0]) uncomment('~%s/.bashrc' % username, '#force_color_prompt=yes') def install_web2py(): From 4acd9f8f2bba01e6030f2a155fc898309d7dc9ff Mon Sep 17 00:00:00 2001 From: Erik Montes Date: Wed, 21 Dec 2016 11:17:12 -0800 Subject: [PATCH 180/436] typo fix --- applications/welcome/views/generic.jsonp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/welcome/views/generic.jsonp b/applications/welcome/views/generic.jsonp index 8b09fc2b..b46aaf09 100644 --- a/applications/welcome/views/generic.jsonp +++ b/applications/welcome/views/generic.jsonp @@ -7,7 +7,7 @@ # it is not safe to use as a generic.jsonp because of security implications. if response.view == 'generic.jsonp': - raise HTTP(501,'generic.jsonp diasbled for security reasons') + raise HTTP(501,'generic.jsonp disabled for security reasons') try: from gluon.serializers import json @@ -20,4 +20,4 @@ except ImportError: raise HTTP(405, 'JSON not available') except: raise HTTP(405, 'JSON error') -}} \ No newline at end of file +}} From 7e5277030afce432b03161a7bf456b5851d5f9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eenol=20AKKA=C5=9E?= Date: Mon, 26 Dec 2016 14:21:41 +0300 Subject: [PATCH 181/436] fix css validation error --- .../admin/static/css/bootstrap_essentials.css | 962 ++++++++++++++---- 1 file changed, 760 insertions(+), 202 deletions(-) diff --git a/applications/admin/static/css/bootstrap_essentials.css b/applications/admin/static/css/bootstrap_essentials.css index 4343a844..4d3a1648 100644 --- a/applications/admin/static/css/bootstrap_essentials.css +++ b/applications/admin/static/css/bootstrap_essentials.css @@ -1,340 +1,814 @@ - /*============================================================= GENERAL ==============================================================*/ - body { /*remember to account for the hidden area underneath + +body { + /*remember to account for the hidden area underneath fixed navbar by adding at least 40px padding to the . Be sure to add this after the core Bootstrap CSS and before the optional responsive CSS. -An alternative solution is to set top-margin to div#main padding-top:60px; comment this for alternative solution*/ height:auto; /*uncomment this for alternative solution*/ } +An alternative solution is to set top-margin to div#main padding-top:60px; comment this for alternative solution*/ + height: auto; + /*uncomment this for alternative solution*/ +} + /*============================================================= BOOTSTRAP ICONS FOLDER FIX ==============================================================*/ - [class^="icon-"], [class*=" icon-"] { /* right folder for bootstrap black images/icons*/ background-image:url("../images/glyphicons-halflings.png") } -.icon-white, .nav-tabs>.active >a>[class^="icon-"], .nav-tabs>.active>a>[class*=" icon-"], .nav-pills>.active>a>[class^="icon-"], .nav-pills>.active>a>[class*=" icon-"], .nav-list>.active>a>[class^="icon-"], .nav-list>.active>a>[class*=" icon-"], .navbar-inverse .nav>.active>a>[class^="icon-"], .navbar-inverse .nav>.active>a>[class*=" icon-"], .dropdown-menu>li>a:hover>[class^="icon-"], .dropdown-menu>li>a:hover>[class*=" icon-"], .dropdown-menu>.active>a>[class^="icon-"], .dropdown-menu>.active>a>[class*=" icon-"] { /* right folder for bootstrap white images/icons*/ background-image:url("../images/glyphicons-halflings-white.png"); } +[class^="icon-"], +[class*=" icon-"] { + /* right folder for bootstrap black images/icons*/ + background-image: url("../images/glyphicons-halflings.png") +} + +.icon-white, +.nav-tabs>.active>a>[class^="icon-"], +.nav-tabs>.active>a>[class*=" icon-"], +.nav-pills>.active>a>[class^="icon-"], +.nav-pills>.active>a>[class*=" icon-"], +.nav-list>.active>a>[class^="icon-"], +.nav-list>.active>a>[class*=" icon-"], +.navbar-inverse .nav>.active>a>[class^="icon-"], +.navbar-inverse .nav>.active>a>[class*=" icon-"], +.dropdown-menu>li>a:hover>[class^="icon-"], +.dropdown-menu>li>a:hover>[class*=" icon-"], +.dropdown-menu>.active>a>[class^="icon-"], +.dropdown-menu>.active>a>[class*=" icon-"] { + /* right folder for bootstrap white images/icons*/ + background-image: url("../images/glyphicons-halflings-white.png"); +} + /*============================================================= INPUT BORDER HIGHLIGHT WHEN INPUT IS FOCUSED ==============================================================*/ - textarea:focus, input[type="text"]:focus, input[type="password"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus, input[type="file"]:focus, select:focus, .uneditable-input:focus { /* outline color*/ border-color:rgba(232, 149, 60, 0.8); outline:0; /*outline:thin dotted \9;*/ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); } -.web2py_grid .dropdown-menu li > a:hover, .web2py_grid .dropdown-menu li > a:focus { filter:progid:DXImageTransform.Microsoft.gradient(enabled=false); /* IE6-9*/ background-image:none; background-color:#E8953C; } +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +input[type="file"]:focus, +select:focus, +.uneditable-input:focus { + /* outline color*/ + border-color: rgba(232, 149, 60, 0.8); + outline: 0; + /*outline:thin dotted \9;*/ + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); +} + +.web2py_grid .dropdown-menu li>a:hover, +.web2py_grid .dropdown-menu li>a:focus { + filter: progid: DXImageTransform.Microsoft.gradient(enabled=false); + /* IE6-9*/ + background-image: none; + background-color: #E8953C; +} + /*============================================================= COLOR OF LINKS ==============================================================*/ - a, a:hover { color:#E8953C; text-decoration:none; } -a:hover { color:#e2821b; } +a, +a:hover { + color: #E8953C; + text-decoration: none; +} + +a:hover { + color: #e2821b; +} + /*============================================================= CONTROLS and CONTAINERS ==============================================================*/ - .row-buttons .btn { margin-bottom:7px; } -.sidebar .box { clear:right; margin-top:2em; border-top:1px solid #d1d1d1; padding:0 1em; } +.row-buttons .btn { + margin-bottom: 7px; +} -.pwdchange>.button { margin-bottom:10px; } +.sidebar .box { + clear: right; + margin-top: 2em; + border-top: 1px solid #d1d1d1; + padding: 0 1em; +} -input[type="file"] { margin-bottom:9px; } +.pwdchange>.button { + margin-bottom: 10px; +} -.form-inline input[type="file"] { margin-bottom:0px; } +input[type="file"] { + margin-bottom: 9px; +} -input + .help-block { margin-top:-10px; margin-bottom:4px; } +.form-inline input[type="file"] { + margin-bottom: 0px; +} -#confirm_form input.btn, .generatedbyw2p input { margin-right:4px; } +input+.help-block { + margin-top: -10px; + margin-bottom: 4px; +} + +#confirm_form input.btn, +.generatedbyw2p input { + margin-right: 4px; +} + +a[rel='tooltip'] span, +div[rel='tooltip'] span { + display: none; + margin-left: -9999px; +} -a[rel='tooltip'] span, div[rel='tooltip'] span { display:none; margin-left:-9999px; } /*in-page browsing*/ - [rel="pagebookmark"] { position:relative; } -[rel="pagebookmark"]>.component { cursor:pointer; } +[rel="pagebookmark"] { + position: relative; +} + +[rel="pagebookmark"]>.component { + cursor: pointer; +} + +[rel="pagebookmark"]>.hashstick { + position: absolute; + top: -54px; + left: -9999px; + visibility: visible; +} -[rel="pagebookmark"]>.hashstick { position:absolute; top:-54px; left:-9999px; visibility:visible; } /* following 2 rules set the style of a small button for going to top of page*/ - .tophashlink.btn { padding:2px 3px; visibility:hidden; } -.hashstick:target+.tophashlink.btn { visibility:visible; } +.tophashlink.btn { + padding: 2px 3px; + visibility: hidden; +} -ul.act_edit { margin-top:4px; margin-left:20px; } +.hashstick:target+.tophashlink.btn { + visibility: visible; +} -ul.act_edit .btn { margin-top:4px; margin-bottom:4px; } +ul.act_edit { + margin-top: 4px; + margin-left: 20px; +} -ul.act_edit .file>a { white-space:pre; } +ul.act_edit .btn { + margin-top: 4px; + margin-bottom: 4px; +} -.right-full { text-align:right; } +ul.act_edit .file>a { + white-space: pre; +} -.searchbox, .searchbox label, .searchbox input { display:inline-block; } +.right-full { + text-align: right; +} -.buttons-row .btn { margin-bottom:9px; } +.searchbox, +.searchbox label, +.searchbox input { + display: inline-block; +} -.li-controls { display:inline-block; width:180px; vertical-align:middle; } +.buttons-row .btn { + margin-bottom: 9px; +} -.celled { display:inline-block; padding: 0 0 0 4px; vertical-align:top; margin-top:4px; width:700px; } +.li-controls { + display: inline-block; + width: 180px; + vertical-align: middle; +} -.folder { list-style-type:none; #border-left: 1px dotted #AAA; } +.celled { + display: inline-block; + padding: 0 0 0 4px; + vertical-align: top; + margin-top: 4px; + width: 700px; +} -.folder li { list-style-type:none; } +.folder { + list-style-type: none; + border-left: 1px dotted #AAA; +} -.folder>i { display:inline-block; width:5px; height:5px; border:1px solid; background-color:#FAA732; margin-left:-4px; margin-top:-2px; border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border-radius:1px; } +.folder li { + list-style-type: none; +} -.folder>i+a { padding-left:0.5em; } +.folder>i { + display: inline-block; + width: 5px; + height: 5px; + border: 1px solid; + background-color: #FAA732; + margin-left: -4px; + margin-top: -2px; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-radius: 1px; +} -.folder ul { margin-top:0.5em; margin-bottom:0.5em; } +.folder>i+a { + padding-left: 0.5em; +} -.controls-inline .btn { margin-right:5px; } +.folder ul { + margin-top: 0.5em; + margin-bottom: 0.5em; +} -div.web2py_counter.span6 { min-height:24px; text-align:right; } +.controls-inline .btn { + margin-right: 5px; +} -.pagination { margin:0; } +div.web2py_counter.span6 { + min-height: 24px; + text-align: right; +} -.table { margin-bottom:10px; } +.pagination { + margin: 0; +} -.row_buttons .btn { margin-right:4px; } +.table { + margin-bottom: 10px; +} -.editor-bar-column { display:inline-block; vertical-align:top; margin-right:4px; } +.row_buttons .btn { + margin-right: 4px; +} -.editor-bar-column .input-long { width:270px; } +.editor-bar-column { + display: inline-block; + vertical-align: top; + margin-right: 4px; +} -.editor-bar-column .input-normal { width:206px; } +.editor-bar-column .input-long { + width: 270px; +} -.keybindings li { margin-bottom:0.5em; } +.editor-bar-column .input-normal { + width: 206px; +} -.keybindings span { padding:0.3em; border:1px solid transparent; vertical-align:middle; } +.keybindings li { + margin-bottom: 0.5em; +} -.teletype-text { font-family:monospace; font-weight:bold; font-style:normal; border-color:#999; background:#333; color:#DDD; -moz-border-radius:0.3em; border-radius:0.3em; } +.keybindings span { + padding: 0.3em; + border: 1px solid transparent; + vertical-align: middle; +} -.edit_language .tab_row div { display:inline-block; vertical-align:top; margin-right:4px; } +.teletype-text { + font-family: monospace; + font-weight: bold; + font-style: normal; + border-color: #999; + background: #333; + color: #DDD; + -moz-border-radius: 0.3em; + border-radius: 0.3em; +} -.edit_language .fake-input { height:18px; padding:4px; font-size:13px; line-height:18px; overflow:hidden; white-space:nowrap; display:inline-block; margin-bottom:9px; } +.edit_language .tab_row div { + display: inline-block; + vertical-align: top; + margin-right: 4px; +} -.test h3 { padding-left:9px; margin:0; font-size:16px; line-height:1; border-left:9px solid transparent; } +.edit_language .fake-input { + height: 18px; + padding: 4px; + font-size: 13px; + line-height: 18px; + overflow: hidden; + white-space: nowrap; + display: inline-block; + margin-bottom: 9px; +} -.test h3.passed { border-color:#009900; } +.test h3 { + padding-left: 9px; + margin: 0; + font-size: 16px; + line-height: 1; + border-left: 9px solid transparent; +} -.test h3.failed { border-color:#CC0000; } +.test h3.passed { + border-color: #009900; +} -.test h3.nodoctests { border-color:#CCCC99; } +.test h3.failed { + border-color: #CC0000; +} -.test .test_report { width:100%; overflow:auto; } +.test h3.nodoctests { + border-color: #CCCC99; +} -.test_report pre { white-space:pre; } +.test .test_report { + width: 100%; + overflow: auto; +} -.test div[id^="output_"]>h2 { font-size:18px; line-height:1; color:grey; } +.test_report pre { + white-space: pre; +} -div.center { text-align:center; } +.test div[id^="output_"]>h2 { + font-size: 18px; + line-height: 1; + color: grey; +} + +div.center { + text-align: center; +} + +.delete h2 { + word-wrap: break-word; +} -.delete h2 { word-wrap:break-word; } /*============================================================= SHELL ==============================================================*/ - .shell .output-wrapper { width:100%; height:30em; border:1px solid #333; } -.shell .prompt-wrapper { float:left; width:100%; overflow:hidden; height:auto; border:1px solid #333; } +.shell .output-wrapper { + width: 100%; + height: 30em; + border: 1px solid #333; +} -.shell .prompt-container { margin-left:2.5em; } +.shell .prompt-wrapper { + float: left; + width: 100%; + overflow: hidden; + height: auto; + border: 1px solid #333; +} -.shell #caret { width:2.5em; float:left; margin-left:-100%; } +.shell .prompt-container { + margin-left: 2.5em; +} -.shell #shellwrapper { background:white; color:#E8953C; width:100%; margin:1em 0; border:0; } +.shell #caret { + width: 2.5em; + float: left; + margin-left: -100%; +} -.shell #output, .shell .prompt { color:#E8953C; background:white; resize:none; border:none; width:100%; height:100%; cursor:default; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } +.shell #shellwrapper { + background: white; + color: #E8953C; + width: 100%; + margin: 1em 0; + border: 0; +} -.shell #output:focus, .shell .prompt:focus { border-color:transparent; outline:0; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } +.shell #output, +.shell .prompt { + color: #E8953C; + background: white; + resize: none; + border: none; + width: 100%; + height: 100%; + cursor: default; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} -.shell #output pre { color: #E8953C; } +.shell #output:focus, +.shell .prompt:focus { + border-color: transparent; + outline: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} -.shell #autoscroll { cursor:pointer; float:right; } +.shell #output pre { + color: #E8953C; +} -.shell .prompt, .shell #output, .shell #caret { font-size: 11pt; padding: 6px; padding-right: 0em; } +.shell #autoscroll { + cursor: pointer; + float: right; +} -.shell #caret { padding-top:9px; } +.shell .prompt, +.shell #output, +.shell #caret { + font-size: 11pt; + padding: 6px; + padding-right: 0em; +} -.shell .prompt, .shell #output, .shell pre, .shell #caret { font-family: monospace; } +.shell #caret { + padding-top: 9px; +} + +.shell .prompt, +.shell #output, +.shell pre, +.shell #caret { + font-family: monospace; +} + +.shell a[rel="tooltip"] { + margin-left: 8px; +} -.shell a[rel="tooltip"] { margin-left:8px; } /*============================================================= PEEK ==============================================================*/ - .peek .code-wrapper { width:100%; overflow:auto; white-space:pre; } -.peek table td pre { word-break:normal; white-space:pre; } +.peek .code-wrapper { + width: 100%; + overflow: auto; + white-space: pre; +} + +.peek table td pre { + word-break: normal; + white-space: pre; +} + /*============================================================= FOOTER ==============================================================*/ - #footer { border-top:1px solid; text-align:center; padding:1em 0; } -#footer span, #footer select { display:inline-block; margin-bottom:0; vertical-align:middle; } +#footer { + border-top: 1px solid; + text-align: center; + padding: 1em 0; +} + +#footer span, +#footer select { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; +} + +#footer select { + width: auto; +} -#footer select { width:auto; } /*============================================================= MAIN ==============================================================*/ - #main { margin-top:60px; /*uncomment this for alternative solution to hidden area underneath fixed navbar issue*/ margin-bottom:60px; } + +#main { + margin-top: 60px; + /*uncomment this for alternative solution to hidden area underneath fixed navbar issue*/ + margin-bottom: 60px; +} + /*============================================================= WIZARD ==============================================================*/ - #wizard_nav .box { border-bottom:1px dotted; } -#wizard_nav li { margin-left:1em; margin-top:0.5em; } +#wizard_nav .box { + border-bottom: 1px dotted; +} -.step textarea { width:auto; } +#wizard_nav li { + margin-left: 1em; + margin-top: 0.5em; +} -select[name='layout_theme'] { vertical-align:top; } +.step textarea { + width: auto; +} + +select[name='layout_theme'] { + vertical-align: top; +} + +img#preview { + margin-bottom: 9px; +} -img#preview { margin-bottom:9px; } /* multiselect customization*/ - .ms-container { margin-bottom:5px; } -.ms-selectable, .step .ms-selection { text-align:center; } +.ms-container { + margin-bottom: 5px; +} -.ms-list { text-align:left; background:white; } +.ms-selectable, +.step .ms-selection { + text-align: center; +} -.ms-container li.ms-elem-selectable:not(.disabled).ms-hover, .ms-container .ms-selection li:not(.disabled).ms-hover { background-color:#E8953C; } +.ms-list { + text-align: left; + background: white; +} -.ms-container .ms-selectable { margin-right:25px; } +.ms-container li.ms-elem-selectable:not(.disabled).ms-hover, +.ms-container .ms-selection li:not(.disabled).ms-hover { + background-color: #E8953C; +} -.ms-container .ms-selectable, .ms-container .ms-selection { background:transparent; } +.ms-container .ms-selectable { + margin-right: 25px; +} + +.ms-container .ms-selectable, +.ms-container .ms-selection { + background: transparent; +} + +.ms-container .ms-list.ms-focus { + border-color: rgba(232, 149, 60, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); +} -.ms-container .ms-list.ms-focus { border-color:rgba(232, 149, 60, 0.8); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(232, 149, 60, 0.6); } /* grow_input*/ - ul[id$="_grow_input"] { margin-left:0; } + +ul[id$="_grow_input"] { + margin-left: 0; +} + /* generate_form*/ - #generate_form .control-group { margin-bottom:0; } -#generate_form .control-label { text-align:left; } +#generate_form .control-group { + margin-bottom: 0; +} -#generate_form .controls { padding-left:18px; margin-left:0; } +#generate_form .control-label { + text-align: left; +} -#generate_form .control-label.empty { width:142px; } +#generate_form .controls { + padding-left: 18px; + margin-left: 0; +} + +#generate_form .control-label.empty { + width: 142px; +} + +.step [rel="pagebookmark"]>.hashstick { + display: none; +} -.step [rel="pagebookmark"]>.hashstick { display:none; } /*generated page*/ - .generated iframe { border:1px inset #e3e3e3; } + +.generated iframe { + border: 1px inset #e3e3e3; +} + /*============================================================= ERRORS TABLE / TICKET PAGE ==============================================================*/ - .tablebar { margin:7px 0 7px 0; } -.tablebar input { margin-right:27px; } +.tablebar { + margin: 7px 0 7px 0; +} -.tablebar span { vertical-align:bottom; } +.tablebar input { + margin-right: 27px; +} -.table th { background: #e9e9e9; background: -moz-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #FAFAFA), color-stop(100%, #E9E9E9)); background: -webkit-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); background: -o-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); background: -ms-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); background: linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FAFAFA', endColorstr='#E9E9E9'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#FAFAFA', endColorstr='#E9E9E9')"; /*font-size:10px; color:#444; text-transform:uppercase;*/ } +.tablebar span { + vertical-align: bottom; +} -td.cbcentered, th.cbcentered { text-align:center; } +.table th { + background: #e9e9e9; + background: -moz-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #FAFAFA), color-stop(100%, #E9E9E9)); + background: -webkit-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); + background: -o-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); + background: -ms-linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); + background: linear-gradient(top, #FAFAFA 0%, #E9E9E9 100%); + filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#FAFAFA', endColorstr='#E9E9E9'); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#FAFAFA', endColorstr='#E9E9E9')"; + /*font-size:10px; color:#444; text-transform:uppercase;*/ +} -td.cbcentered>input, th.cbcentered>input { margin-top:-1px; } +td.cbcentered, +th.cbcentered { + text-align: center; +} -.traceback div { } +td.cbcentered>input, +th.cbcentered>input { + margin-top: -1px; +} -.ticket_code>table td:first-child { border-left:0; } +.traceback div {} -#trck_errors table td pre { word-break:normal; white-space:pre; } +.ticket_code>table td:first-child { + border-left: 0; +} -.inspect pre, .errorsource pre { word-break:normal; white-space:pre; } +#trck_errors table td pre { + word-break: normal; + white-space: pre; +} -.ticket_code { background-color:lightyellow; } +.inspect pre, +.errorsource pre { + word-break: normal; + white-space: pre; +} -.ticket_code table, .ticket_code td { border-width:0px; border-collapse:collapse; width:100%; } +.ticket_code { + background-color: lightyellow; +} + +.ticket_code table, +.ticket_code td { + border-width: 0px; + border-collapse: collapse; + width: 100%; +} + +.ticket_code tbody tr:hover td { + background-color: transparent; +} -.ticket_code tbody tr:hover td { background-color:transparent; } /*============================================================= FLOT GRAPHS ==============================================================*/ - .about #placeholder { width:auto; max-width:600px; height:300px; position:relative; margin:0 auto; /* for centering*/ } + +.about #placeholder { + width: auto; + max-width: 600px; + height: 300px; + position: relative; + margin: 0 auto; + /* for centering*/ +} + /*============================================================= THE GRID ==============================================================*/ - #w2p_query_panel { min-width:20px; min-height:20px; padding:10px; margin-top:1em; background-color:#f5f5f5; border: 1px solid #e3e3e3; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); } -#w2p_query_panel select, #w2p_query_panel input { margin-bottom:0; margin-right:4px; } +#w2p_query_panel { + min-width: 20px; + min-height: 20px; + padding: 10px; + margin-top: 1em; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} -.web2py_grid .hidden { visibility:visible; } +#w2p_query_panel select, +#w2p_query_panel input { + margin-bottom: 0; + margin-right: 4px; +} -.qry_pnl_btns { display:inline-block; } +.web2py_grid .hidden { + visibility: visible; +} -#w2p_grid_addbtn, #w2p_search-form { margin-top:9px; margin-bottom:9px; } +.qry_pnl_btns { + display: inline-block; +} -#w2p_search-form { margin-bottom:0; } +#w2p_grid_addbtn, +#w2p_search-form { + margin-top: 9px; + margin-bottom: 9px; +} + +#w2p_search-form { + margin-bottom: 0; +} + +#w2p_search-form form { + margin-bottom: 0; +} -#w2p_search-form form { margin-bottom:0; } /*----- translate page ---*/ - .languageform input { margin-bottom:0; } -.languageform input.untranslated { background-color:#FC0; } +.languageform input { + margin-bottom: 0; +} + +.languageform input.untranslated { + background-color: #FC0; +} + /*============================================================= MASKED UPLOAD INPUT (NO BOOTSTRAP RELATED) ==============================================================*/ + #appupdate_file.masked { -margin: 0; -opacity: 0; --ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* IE 8 */ -filter: alpha(opacity=0); /* IE 7 */ -font-size: 100px; -position: absolute; -top: 0; -right: 0; -z-index: 410; + margin: 0; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + /* IE 8 */ + filter: alpha(opacity=0); + /* IE 7 */ + font-size: 100px; + position: absolute; + top: 0; + right: 0; + z-index: 410; } #fileselect { -padding: 4px 6px; -border: 1px solid #ccc; -border-radius: 4px; -color: #555; -cursor: default; -position: relative; -z-index: 400; -font-size: 14px; -background-color: #fff; -margin-bottom: 10px; -overflow: hidden; + padding: 4px 6px; + border: 1px solid #ccc; + border-radius: 4px; + color: #555; + cursor: default; + position: relative; + z-index: 400; + font-size: 14px; + background-color: #fff; + margin-bottom: 10px; + overflow: hidden; } #fileselect span { -position: absolute; -left: 6px; -top: 4px; + position: absolute; + left: 6px; + top: 4px; } .uploadbtn { -position: absolute; -top: 3px; -right: 3px; + position: absolute; + top: 3px; + right: 3px; } .txtPlaceholder { -font-style: italic; -color: #ccc; + font-style: italic; + color: #ccc; } + /*============================================================= EDIT PAGE SLIDING FILES MENU ==============================================================*/ + @media (max-width: 979px) { - body.edit div#header {position:relative; z-index: 1030 !important;} + body.edit div#header { + position: relative; + z-index: 1030 !important; + } } -#editor_area, #edit_placeholder { +#editor_area, +#edit_placeholder { margin: 0; padding: 0; } @@ -361,11 +835,13 @@ EDIT PAGE SLIDING FILES MENU transition: all 0.4s; } -#files:hover, #files:focus { +#files:hover, +#files:focus { left: 0px !important; } -#files, .files-toggle { +#files, +.files-toggle { background: #1b1b1b; opacity: 0.98; } @@ -410,80 +886,162 @@ EDIT PAGE SLIDING FILES MENU text-shadow: none; } + /*============================================================= MEDIA QUERIES ==============================================================*/ - @media (max-width: 800px) { .step [rel="pagebookmark"]>.hashstick { /*top:-54px;*/ display:block; } - } +@media (max-width: 800px) { + .step [rel="pagebookmark"]>.hashstick { + /*top:-54px;*/ + display: block; + } +} -@media (max-width: 767px) { [rel="pagebookmark"]>.hashstick { top:0; } - -/*----------------------------------- +@media (max-width: 767px) { + [rel="pagebookmark"]>.hashstick { + top: 0; + } + /*----------------------------------- main -------------------------------------*/ - #main { margin-top:0; } - -/*----------------------------------- + #main { + margin-top: 0; + } + /*----------------------------------- footer -------------------------------------*/ - #footer { margin-left: -20px; margin-right: -20px; padding-left: 20px; padding-right: 20px; } - -/*----------------------------------- + #footer { + margin-left: -20px; + margin-right: -20px; + padding-left: 20px; + padding-right: 20px; + } + /*----------------------------------- errors page -------------------------------------*/ - #trck_errors { table-layout:fixed; } + #trck_errors { + table-layout: fixed; + } + #trck_errors .column1 { + width: 20px; + } + #trck_errors .column2 { + width: 45px; + } + #trck_errors .column3 { + width: 150px; + } + #trck_errors .columnN { + width: 55px; + } + #trck_errors .columnN1 { + width: 138px; + } + .ticket_code, + .inspect.resp1, + .inspect.controls pre, + .errorsource { + width: 100%; + overflow: auto; + } + .ticket_code>table { + width: 100%; + } + .celled { + width: 320px; + } +} -#trck_errors .column1 { width:20px; } - -#trck_errors .column2 { width:45px; } - -#trck_errors .column3 { width:150px; } - -#trck_errors .columnN { width:55px; } - -#trck_errors .columnN1 { width:138px; } - -.ticket_code, .inspect.resp1, .inspect.controls pre, .errorsource { width:100%; overflow:auto; } - -.ticket_code>table { width:100%; } - -.celled { width:320px; } - - } - -@media (max-width: 480px) { .qry_pnl_btns { display:block; margin-top:4px; } - -/*----------------------------------- +@media (max-width: 480px) { + .qry_pnl_btns { + display: block; + margin-top: 4px; + } + /*----------------------------------- wizard -------------------------------------*/ - #generate_form .control-label { float:left; width:160px; padding-top:5px; } + #generate_form .control-label { + float: left; + width: 160px; + padding-top: 5px; + } + .inspect>code { + display: block; + white-space: normal; + } + .li-controls {} + .celled { + width: 165px; + } +} -.inspect>code { display:block; white-space:normal; } - -.li-controls { } - -.celled { width:165px; } - - } /*----------------------------------- miscellaneous -------------------------------------*/ -h4.editableapp, h4.currentapp { padding: 5px 0 5px 54px; display: inline; } -h4.editableapp { background: #fff url(../images/folder.png) no-repeat; } +h4.editableapp, +h4.currentapp { + padding: 5px 0 5px 54px; + display: inline; +} -h4.currentapp { background: #fff url(../images/folder_locked.png) no-repeat; } +h4.editableapp { + background: #fff url(../images/folder.png) no-repeat; +} -.w2p_flash { position:fixed; width:50%; top:49px; left:25%; right:25%; cursor:default; text-align:center; z-index:5620; } -span#closeflash {position:absolute; top:1px; right:-1px; font-size:150%; border:1px solid black; border-color: transparent transparent #fbeed5 #fbeed5; border-radius: 0 0 0 4px; width:22px; } -span#closeflash:hover {font-weight:bold; cursor:pointer; } +h4.currentapp { + background: #fff url(../images/folder_locked.png) no-repeat; +} -table.twitter{ background-color: transparent; } -table.twitter tr td {vertical-align: top; padding: 5px; } -table.twitter tr { border-bottom: 1px solid #a0a0a0; } +.w2p_flash { + position: fixed; + width: 50%; + top: 49px; + left: 25%; + right: 25%; + cursor: default; + text-align: center; + z-index: 5620; +} -div.error_wrapper {margin-top:-10px; margin-bottom:8px; padding-left:2px; color:#d62e2b; } +span#closeflash { + position: absolute; + top: 1px; + right: -1px; + font-size: 150%; + border: 1px solid black; + border-color: transparent transparent #fbeed5 #fbeed5; + border-radius: 0 0 0 4px; + width: 22px; +} -.twitter-timeline >iframe{padding: 1em 0;} +span#closeflash:hover { + font-weight: bold; + cursor: pointer; +} + +table.twitter { + background-color: transparent; +} + +table.twitter tr td { + vertical-align: top; + padding: 5px; +} + +table.twitter tr { + border-bottom: 1px solid #a0a0a0; +} + +div.error_wrapper { + margin-top: -10px; + margin-bottom: 8px; + padding-left: 2px; + color: #d62e2b; +} + +.twitter-timeline>iframe { + padding: 1em 0; +} From 46b7716b1db562d193b99da6d5640fa6645b2c51 Mon Sep 17 00:00:00 2001 From: maxslimmer Date: Wed, 4 Jan 2017 18:02:23 -0800 Subject: [PATCH 182/436] get cache.disk statistics for update --- applications/examples/controllers/appadmin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 73303f53..a602a95c 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -465,6 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: + value = cache.disk.storage[key] if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] From 0165cfadef968b27a6fda494af8a826557cdc8ea Mon Sep 17 00:00:00 2001 From: maxslimmer Date: Wed, 4 Jan 2017 18:03:37 -0800 Subject: [PATCH 183/436] get cache.disk statistics value for update and display --- applications/welcome/controllers/appadmin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 73303f53..a602a95c 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -465,6 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: + value = cache.disk.storage[key] if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] From d862da354364f680cdedbe387a71f874bece6025 Mon Sep 17 00:00:00 2001 From: maxslimmer Date: Wed, 4 Jan 2017 18:08:57 -0800 Subject: [PATCH 184/436] revert part of fix to displaying cache statistics (#1) --- applications/admin/controllers/appadmin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 73303f53..a602a95c 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -465,6 +465,7 @@ def ccache(): ram['keys'].append((key, GetInHMS(time.time() - value[0]))) for key in cache.disk.storage: + value = cache.disk.storage[key] if key == 'web2py_cache_statistics' and isinstance(value[1], dict): disk['hits'] = value[1]['hit_total'] - value[1]['misses'] disk['misses'] = value[1]['misses'] From bd526452a8b0e9cbca57440bfd29f54271e509a6 Mon Sep 17 00:00:00 2001 From: abastardi Date: Fri, 13 Jan 2017 15:52:42 -0500 Subject: [PATCH 185/436] Fix router functions check When the router checks whether the requested route matches a function in the functions list, it does not strip the extension, causing failures for URLs with extensions. This change strips the extension before comparing to the list of functions. --- gluon/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/rewrite.py b/gluon/rewrite.py index fc4d1153..1fef465a 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -1040,7 +1040,7 @@ class MapUrlIn(object): else: default_function = self.router.default_function # str or None default_function = self.domain_function or default_function - if not arg0 or functions and arg0 not in functions: + if not arg0 or functions and arg0.split('.')[0] not in functions: self.function = default_function or "" self.pop_arg_if(arg0 and self.function == arg0) else: From 7b3fe560edd0dc40002ab62cd6c07bb8352e20f6 Mon Sep 17 00:00:00 2001 From: abastardi Date: Sat, 14 Jan 2017 13:14:04 -0500 Subject: [PATCH 186/436] Update test_router_functions to test extensions --- gluon/tests/test_router.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/gluon/tests/test_router.py b/gluon/tests/test_router.py index 20b58358..ad454fcb 100644 --- a/gluon/tests/test_router.py +++ b/gluon/tests/test_router.py @@ -873,6 +873,27 @@ class TestRouter(unittest.TestCase): self.assertEqual(str(URL(a='app2', c='default', f='index', args=['ctr'])), "/app2/index/ctr") + # outbound - with extensions + self.assertEqual( + str(URL(a='app', c='default', f='index.json')), "/index.json") + self.assertEqual( + str(URL(a='app', c='default', f='index.json', args=['arg1'])), "/index.json/arg1") + self.assertEqual(str( + URL(a='app', c='default', f='user.json')), "/user.json") + self.assertEqual(str( + URL(a='app', c='default', f='user.json', args=['arg1'])), "/user.json/arg1") + self.assertEqual(str(URL( + a='app', c='default', f='user.json', args=['index'])), "/user.json/index") + self.assertEqual(str( + URL(a='app', c='default', f='index.json', args=['ctr'])), "/index.json/ctr") + self.assertEqual( + str(URL(a='app', c='ctr', f='ctrf1.json', args=['arg'])), "/ctr/ctrf1.json/arg") + + self.assertEqual(str(URL( + a='app2', c='default', f='index.json', args=['arg1'])), "/app2/index.json/arg1") + self.assertEqual(str(URL( + a='app2', c='ctr', f='index.json', args=['arg1'])), "/app2/ctr/index.json/arg1") + # inbound self.assertEqual( filter_url('http://d.com/arg'), "/app/default/index ['arg']") @@ -896,6 +917,30 @@ class TestRouter(unittest.TestCase): self.assertEqual( filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg") + # inbound - with extensions + self.assertEqual( + filter_url('http://d.com/index.json'), "/app/default/index.json") + self.assertEqual(filter_url('http://d.com/user.json'), "/app/default/user.json") + self.assertEqual( + filter_url('http://d.com/user.json/arg'), "/app/default/user.json ['arg']") + self.assertEqual( + filter_url('http://d.com/user.json/index'), "/app/default/user.json ['index']") + self.assertEqual( + filter_url('http://d.com/index.json/ctr'), "/app/default/index.json ['ctr']") + self.assertEqual( + filter_url('http://d.com/ctr/ctrf1.json/arg'), "/app/ctr/ctrf1.json ['arg']") + + self.assertEqual(filter_url( + 'http://d.com/app2/index.json/arg'), "/app2/default/index.json ['arg']") + self.assertEqual( + filter_url('http://d.com/app2/user.json'), "/app2/default/user.json") + self.assertEqual(filter_url( + 'http://d.com/app2/user.json/arg'), "/app2/default/user.json ['arg']") + self.assertEqual(filter_url( + 'http://d.com/app2/ctr/index.json/arg'), "/app2/ctr/index.json ['arg']") + self.assertEqual( + filter_url('http://d.com/app2/ctr/arg'), "/app2/ctr/arg") + def test_router_functions2(self): ''' Test more functions=[something] From b2548f56312a7ad3cf90fa34b436048baf0423cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Bo=C3=9F?= Date: Sun, 22 Jan 2017 20:57:24 +0100 Subject: [PATCH 187/436] don't write to /dev/urandom on Windows platforms --- gluon/utils.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/gluon/utils.py b/gluon/utils.py index a7ffdeb6..85d7d49f 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -17,6 +17,7 @@ import random import inspect import time import os +import sys import re import logging import socket @@ -299,19 +300,20 @@ def initialize_urandom(): try: os.urandom(1) have_urandom = True - try: - # try to add process-specific entropy - frandom = open('/dev/urandom', 'wb') + if sys.platform != 'win32': try: - if PY2: - frandom.write(''.join(chr(t) for t in ctokens)) - else: - frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) - finally: - frandom.close() - except IOError: - # works anyway - pass + # try to add process-specific entropy + frandom = open('/dev/urandom', 'wb') + try: + if PY2: + frandom.write(''.join(chr(t) for t in ctokens)) + else: + frandom.write(bytes([]).join(bytes([t]) for t in ctokens)) + finally: + frandom.close() + except IOError: + # works anyway + pass except NotImplementedError: have_urandom = False logger.warning( From b6e5e165265b708d58d03ddbb3d34b01602867b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 25 Jan 2017 20:10:03 +0000 Subject: [PATCH 188/436] Update compileapp.py The "current" may have been altered by the controller. This is the case for admin/controllers/shell.py, in callback(), where a new shell environment is created (from gluon/shell) If this happens, the incorrect "response" is being referenced and the output of the controller is lost. --- gluon/compileapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 03529434..69a29068 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -640,7 +640,7 @@ def run_controller_in(controller, function, environment): ccode = getcfs(layer, filename, lambda: compile2(code, layer)) restricted(ccode, environment, layer=filename) - response = current.response + response = environment["response"] vars = response._vars if response.postprocessing: vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) From 215abc9e4fc44679aeee079ab807cce0b6bfaffa Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 30 Jan 2017 12:46:50 -0600 Subject: [PATCH 189/436] download over https for security --- applications/examples/views/default/download.html | 12 ++++++------ gluon/packages/dal | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/applications/examples/views/default/download.html b/applications/examples/views/default/download.html index 99acd476..587a1e8b 100644 --- a/applications/examples/views/default/download.html +++ b/applications/examples/views/default/download.html @@ -17,10 +17,10 @@ - For Windows + For Windows - For Windows + For Windows Git Repository @@ -28,19 +28,19 @@ - For Mac + For Mac - For Mac + For Mac - Source Code + Source Code - Source Code + Source Code Source code docs diff --git a/gluon/packages/dal b/gluon/packages/dal index 12bc6d97..e49dd9bf 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 12bc6d97402acce462c1193f57bbba4afde7c3c3 +Subproject commit e49dd9bf97c10cbaa90908cbb3ca1957c4a7e385 From 55592e7c6e54747e71f13e49330ad06f46b28acf Mon Sep 17 00:00:00 2001 From: BrenBarn Date: Sat, 28 Jan 2017 13:49:16 -0800 Subject: [PATCH 190/436] Add FlexibleService, which allows @service-style methods that accept varargs --- gluon/tools.py | 390 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) diff --git a/gluon/tools.py b/gluon/tools.py index 8a5b5aaa..67fd5ac1 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -6053,6 +6053,396 @@ class Service(object): def error(self): raise HTTP(404, "Object does not exist") +class FlexibleService(Service): + # __init__ inherited + + # we override csv and xml to fix a bug that caused these procedures to be stored under run_procedures instead of csv_procedures or xml_procedures + # other service decorators (run, json, etc.) are inherited + + def csv(self, f): + """ + Example: + Use as:: + + service = Service() + @service.csv + def myfunction(a, b): + return a + b + def call(): + return service() + + Then call it with:: + + wget http://..../app/default/call/csv/myfunction?a=3&b=4 + + """ + self.csv_procedures[f.__name__] = f + return f + + # fix bug where stuff was put in run_procedures instead of xml_procedures + def xml(self, f): + """ + Example: + Use as:: + + service = Service() + @service.xml + def myfunction(a, b): + return a + b + def call(): + return service() + + Then call it with:: + + wget http://..../app/default/call/xml/myfunction?a=3&b=4 + + """ + self.xml_procedures[f.__name__] = f + return f + + def serve_run(self, args=None): + request = current.request + if not args: + args = request.args + if args and args[0] in self.run_procedures: + return str(self.call_service_function(self.run_procedures[args[0]], + *args[1:], **dict(request.vars))) + self.error() + + def serve_csv(self, args=None): + request = current.request + response = current.response + response.headers['Content-Type'] = 'text/x-csv' + if not args: + args = request.args + + def none_exception(value): + if isinstance(value, unicodeT): + return value.encode('utf8') + if hasattr(value, 'isoformat'): + return value.isoformat()[:19].replace('T', ' ') + if value is None: + return '' + return value + if args and args[0] in self.csv_procedures: + import types + r = self.call_service_function(self.csv_procedures[args[0]], + *args[1:], **dict(request.vars)) + s = StringIO() + if hasattr(r, 'export_to_csv_file'): + r.export_to_csv_file(s) + elif r and not isinstance(r, types.GeneratorType) and isinstance(r[0], (dict, Storage)): + import csv + writer = csv.writer(s) + writer.writerow(r[0].keys()) + for line in r: + writer.writerow([none_exception(v) + for v in line.values()]) + else: + import csv + writer = csv.writer(s) + for line in r: + writer.writerow(line) + return s.getvalue() + self.error() + + def serve_xml(self, args=None): + request = current.request + response = current.response + response.headers['Content-Type'] = 'text/xml' + if not args: + args = request.args + if args and args[0] in self.xml_procedures: + s = self.call_service_function(self.xml_procedures[args[0]], + *args[1:], **dict(request.vars)) + if hasattr(s, 'as_list'): + s = s.as_list() + return serializers.xml(s, quote=False) + self.error() + + def serve_rss(self, args=None): + request = current.request + response = current.response + if not args: + args = request.args + if args and args[0] in self.rss_procedures: + feed = self.call_service_function(self.rss_procedures[args[0]], + *args[1:], **dict(request.vars)) + else: + self.error() + response.headers['Content-Type'] = 'application/rss+xml' + return serializers.rss(feed) + + def serve_json(self, args=None): + request = current.request + response = current.response + response.headers['Content-Type'] = 'application/json; charset=utf-8' + if not args: + args = request.args + d = dict(request.vars) + if args and args[0] in self.json_procedures: + s = self.call_service_function(self.json_procedures[args[0]], *args[1:], **d) + if hasattr(s, 'as_list'): + s = s.as_list() + return response.json(s) + self.error() + + def serve_jsonrpc(self): + def return_response(id, result): + return serializers.json({'version': '1.1', 'id': id, 'result': result, 'error': None}) + + def return_error(id, code, message, data=None): + error = {'name': 'JSONRPCError', + 'code': code, 'message': message} + if data is not None: + error['data'] = data + return serializers.json({'id': id, + 'version': '1.1', + 'error': error, + }) + + request = current.request + response = current.response + response.headers['Content-Type'] = 'application/json; charset=utf-8' + methods = self.jsonrpc_procedures + data = json.loads(request.body.read()) + jsonrpc_2 = data.get('jsonrpc') + if jsonrpc_2: # hand over to version 2 of the protocol + return self.serve_jsonrpc2(data) + id, method, params = data.get('id'), data.get('method'), data.get('params', []) + if id is None: + return return_error(0, 100, 'missing id') + if method not in methods: + return return_error(id, 100, 'method "%s" does not exist' % method) + try: + if isinstance(params, dict): + s = methods[method](**params) + else: + s = methods[method](*params) + if hasattr(s, 'as_list'): + s = s.as_list() + return return_response(id, s) + except Service.JsonRpcException as e: + return return_error(id, e.code, e.info) + except: + etype, eval, etb = sys.exc_info() + message = '%s: %s' % (etype.__name__, eval) + data = request.is_local and traceback.format_tb(etb) + logger.warning('jsonrpc exception %s\n%s' % (message, traceback.format_tb(etb))) + return return_error(id, 100, message, data) + + def serve_jsonrpc2(self, data=None, batch_element=False): + + def return_response(id, result): + if not must_respond: + return None + return serializers.json({'jsonrpc': '2.0', 'id': id, 'result': result}) + + def return_error(id, code, message=None, data=None): + error = {'code': code} + if code in Service.jsonrpc_errors: + error['message'] = Service.jsonrpc_errors[code][0] + error['data'] = Service.jsonrpc_errors[code][1] + if message is not None: + error['message'] = message + if data is not None: + error['data'] = data + return serializers.json({'jsonrpc': '2.0', 'id': id, 'error': error}) + + def validate(data): + """ + Validate request as defined in: http://www.jsonrpc.org/specification#request_object. + + Args: + data(str): The json object. + + Returns: + - True -- if successful + - False -- if no error should be reported (i.e. data is missing 'id' member) + + Raises: + JsonRPCException + + """ + + iparms = set(data.keys()) + mandatory_args = set(['jsonrpc', 'method']) + missing_args = mandatory_args - iparms + + if missing_args: + raise Service.JsonRpcException(-32600, 'Missing arguments %s.' % list(missing_args)) + if data['jsonrpc'] != '2.0': + raise Service.JsonRpcException(-32603, 'Unsupported jsonrpc version "%s"' % data['jsonrpc']) + if 'id' not in iparms: + return False + + return True + + request = current.request + response = current.response + if not data: + response.headers['Content-Type'] = 'application/json; charset=utf-8' + try: + data = json.loads(request.body.read()) + except ValueError: # decoding error in json lib + return return_error(None, -32700) + + # Batch handling + if isinstance(data, list) and not batch_element: + retlist = [] + for c in data: + retstr = self.serve_jsonrpc2(c, batch_element=True) + if retstr: # do not add empty responses + retlist.append(retstr) + if len(retlist) == 0: # return nothing + return '' + else: + return "[" + ','.join(retlist) + "]" + methods = self.jsonrpc2_procedures + methods.update(self.jsonrpc_procedures) + + try: + must_respond = validate(data) + except Service.JsonRpcException as e: + return return_error(None, e.code, e.info) + + id, method, params = data.get('id'), data['method'], data.get('params', '') + if method not in methods: + return return_error(id, -32601, data='Method "%s" does not exist' % method) + try: + if isinstance(params, dict): + s = methods[method](**params) + else: + s = methods[method](*params) + if hasattr(s, 'as_list'): + s = s.as_list() + if must_respond: + return return_response(id, s) + else: + return '' + except HTTP as e: + raise e + except Service.JsonRpcException as e: + return return_error(id, e.code, e.info) + except: + etype, eval, etb = sys.exc_info() + data = '%s: %s\n' % (etype.__name__, eval) + str(request.is_local and traceback.format_tb(etb)) + logger.warning('%s: %s\n%s' % (etype.__name__, eval, traceback.format_tb(etb))) + return return_error(id, -32099, data=data) + + def serve_xmlrpc(self): + request = current.request + response = current.response + services = self.xmlrpc_procedures.values() + return response.xmlrpc(request, services) + + def serve_amfrpc(self, version=0): + try: + import pyamf + import pyamf.remoting.gateway + except: + return "pyamf not installed or not in Python sys.path" + request = current.request + response = current.response + if version == 3: + services = self.amfrpc3_procedures + base_gateway = pyamf.remoting.gateway.BaseGateway(services) + pyamf_request = pyamf.remoting.decode(request.body) + else: + services = self.amfrpc_procedures + base_gateway = pyamf.remoting.gateway.BaseGateway(services) + context = pyamf.get_context(pyamf.AMF0) + pyamf_request = pyamf.remoting.decode(request.body, context) + pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion) + for name, message in pyamf_request: + pyamf_response[name] = base_gateway.getProcessor(message)(message) + response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE + if version == 3: + return pyamf.remoting.encode(pyamf_response).getvalue() + else: + return pyamf.remoting.encode(pyamf_response, context).getvalue() + + def serve_soap(self, version="1.1"): + try: + from gluon.contrib.pysimplesoap.server import SoapDispatcher + except: + return "pysimplesoap not installed in contrib" + request = current.request + response = current.response + procedures = self.soap_procedures + + location = "%s://%s%s" % (request.env.wsgi_url_scheme, + request.env.http_host, + URL(r=request, f="call/soap", vars={})) + namespace = 'namespace' in response and response.namespace or location + documentation = response.description or '' + dispatcher = SoapDispatcher( + name=response.title, + location=location, + action=location, # SOAPAction + namespace=namespace, + prefix='pys', + documentation=documentation, + ns=True) + for method, (function, returns, args, doc) in iteritems(procedures): + dispatcher.register_function(method, function, returns, args, doc) + if request.env.request_method == 'POST': + fault = {} + # Process normal Soap Operation + response.headers['Content-Type'] = 'text/xml' + xml = dispatcher.dispatch(request.body.read(), fault=fault) + if fault: + # May want to consider populating a ticket here... + response.status = 500 + # return the soap response + return xml + elif 'WSDL' in request.vars: + # Return Web Service Description + response.headers['Content-Type'] = 'text/xml' + return dispatcher.wsdl() + elif 'op' in request.vars: + # Return method help webpage + response.headers['Content-Type'] = 'text/html' + method = request.vars['op'] + sample_req_xml, sample_res_xml, doc = dispatcher.help(method) + body = [H1("Welcome to Web2Py SOAP webservice gateway"), + A("See all webservice operations", + _href=URL(r=request, f="call/soap", vars={})), + H2(method), + P(doc), + UL(LI("Location: %s" % dispatcher.location), + LI("Namespace: %s" % dispatcher.namespace), + LI("SoapAction: %s" % dispatcher.action), + ), + H3("Sample SOAP XML Request Message:"), + CODE(sample_req_xml, language="xml"), + H3("Sample SOAP XML Response Message:"), + CODE(sample_res_xml, language="xml"), + ] + return {'body': body} + else: + # Return general help and method list webpage + response.headers['Content-Type'] = 'text/html' + body = [H1("Welcome to Web2Py SOAP webservice gateway"), + P(response.description), + P("The following operations are available"), + A("See WSDL for webservice description", + _href=URL(r=request, f="call/soap", vars={"WSDL": None})), + UL([LI(A("%s: %s" % (method, doc or ''), + _href=URL(r=request, f="call/soap", vars={'op': method}))) + for method, doc in dispatcher.list_methods()]), + ] + return {'body': body} + + # inherited __call__ method + + # inherited error() method + + # we make this a method so that subclasses can override it if they want to do more specific argument-checking + # but the default implmentation is the simplest: just pass the arguments we got, with no checking + def call_service_function(self, f, *a, **b): + return f(*a, **b) + def completion(callback): """ From 86a2c529b9adf3ab050985aef7fe0c51462985a2 Mon Sep 17 00:00:00 2001 From: BrenBarn Date: Tue, 31 Jan 2017 14:13:43 -0800 Subject: [PATCH 191/436] Change to modify Service instead of adding FlexibleService --- gluon/tools.py | 415 ++----------------------------------------------- 1 file changed, 17 insertions(+), 398 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 67fd5ac1..331fa713 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -5386,7 +5386,9 @@ def universal_caller(f, *a, **b): class Service(object): - def __init__(self, environment=None): + def __init__(self, environment=None, check_args=False): + self.check_args = check_args + self.run_procedures = {} self.csv_procedures = {} self.xml_procedures = {} @@ -5436,7 +5438,7 @@ class Service(object): wget http://..../app/default/call/csv/myfunction?a=3&b=4 """ - self.run_procedures[f.__name__] = f + self.csv_procedures[f.__name__] = f return f def xml(self, f): @@ -5456,7 +5458,7 @@ class Service(object): wget http://..../app/default/call/xml/myfunction?a=3&b=4 """ - self.run_procedures[f.__name__] = f + self.xml_procedures[f.__name__] = f return f def rss(self, f): @@ -5643,7 +5645,7 @@ class Service(object): if not args: args = request.args if args and args[0] in self.run_procedures: - return str(universal_caller(self.run_procedures[args[0]], + return str(self.call_service_function(self.run_procedures[args[0]], *args[1:], **dict(request.vars))) self.error() @@ -5662,9 +5664,9 @@ class Service(object): if value is None: return '' return value - if args and args[0] in self.run_procedures: + if args and args[0] in self.csv_procedures: import types - r = universal_caller(self.run_procedures[args[0]], + r = self.call_service_function(self.csv_procedures[args[0]], *args[1:], **dict(request.vars)) s = StringIO() if hasattr(r, 'export_to_csv_file'): @@ -5690,8 +5692,8 @@ class Service(object): response.headers['Content-Type'] = 'text/xml' if not args: args = request.args - if args and args[0] in self.run_procedures: - s = universal_caller(self.run_procedures[args[0]], + if args and args[0] in self.xml_procedures: + s = self.call_service_function(self.xml_procedures[args[0]], *args[1:], **dict(request.vars)) if hasattr(s, 'as_list'): s = s.as_list() @@ -5704,7 +5706,7 @@ class Service(object): if not args: args = request.args if args and args[0] in self.rss_procedures: - feed = universal_caller(self.rss_procedures[args[0]], + feed = self.call_service_function(self.rss_procedures[args[0]], *args[1:], **dict(request.vars)) else: self.error() @@ -5719,7 +5721,7 @@ class Service(object): args = request.args d = dict(request.vars) if args and args[0] in self.json_procedures: - s = universal_caller(self.json_procedures[args[0]], *args[1:], **d) + s = self.call_service_function(self.json_procedures[args[0]], *args[1:], **d) if hasattr(s, 'as_list'): s = s.as_list() return response.json(s) @@ -6052,398 +6054,15 @@ class Service(object): def error(self): raise HTTP(404, "Object does not exist") - -class FlexibleService(Service): - # __init__ inherited - # we override csv and xml to fix a bug that caused these procedures to be stored under run_procedures instead of csv_procedures or xml_procedures - # other service decorators (run, json, etc.) are inherited - - def csv(self, f): - """ - Example: - Use as:: - - service = Service() - @service.csv - def myfunction(a, b): - return a + b - def call(): - return service() - - Then call it with:: - - wget http://..../app/default/call/csv/myfunction?a=3&b=4 - - """ - self.csv_procedures[f.__name__] = f - return f - - # fix bug where stuff was put in run_procedures instead of xml_procedures - def xml(self, f): - """ - Example: - Use as:: - - service = Service() - @service.xml - def myfunction(a, b): - return a + b - def call(): - return service() - - Then call it with:: - - wget http://..../app/default/call/xml/myfunction?a=3&b=4 - - """ - self.xml_procedures[f.__name__] = f - return f - - def serve_run(self, args=None): - request = current.request - if not args: - args = request.args - if args and args[0] in self.run_procedures: - return str(self.call_service_function(self.run_procedures[args[0]], - *args[1:], **dict(request.vars))) - self.error() - - def serve_csv(self, args=None): - request = current.request - response = current.response - response.headers['Content-Type'] = 'text/x-csv' - if not args: - args = request.args - - def none_exception(value): - if isinstance(value, unicodeT): - return value.encode('utf8') - if hasattr(value, 'isoformat'): - return value.isoformat()[:19].replace('T', ' ') - if value is None: - return '' - return value - if args and args[0] in self.csv_procedures: - import types - r = self.call_service_function(self.csv_procedures[args[0]], - *args[1:], **dict(request.vars)) - s = StringIO() - if hasattr(r, 'export_to_csv_file'): - r.export_to_csv_file(s) - elif r and not isinstance(r, types.GeneratorType) and isinstance(r[0], (dict, Storage)): - import csv - writer = csv.writer(s) - writer.writerow(r[0].keys()) - for line in r: - writer.writerow([none_exception(v) - for v in line.values()]) - else: - import csv - writer = csv.writer(s) - for line in r: - writer.writerow(line) - return s.getvalue() - self.error() - - def serve_xml(self, args=None): - request = current.request - response = current.response - response.headers['Content-Type'] = 'text/xml' - if not args: - args = request.args - if args and args[0] in self.xml_procedures: - s = self.call_service_function(self.xml_procedures[args[0]], - *args[1:], **dict(request.vars)) - if hasattr(s, 'as_list'): - s = s.as_list() - return serializers.xml(s, quote=False) - self.error() - - def serve_rss(self, args=None): - request = current.request - response = current.response - if not args: - args = request.args - if args and args[0] in self.rss_procedures: - feed = self.call_service_function(self.rss_procedures[args[0]], - *args[1:], **dict(request.vars)) - else: - self.error() - response.headers['Content-Type'] = 'application/rss+xml' - return serializers.rss(feed) - - def serve_json(self, args=None): - request = current.request - response = current.response - response.headers['Content-Type'] = 'application/json; charset=utf-8' - if not args: - args = request.args - d = dict(request.vars) - if args and args[0] in self.json_procedures: - s = self.call_service_function(self.json_procedures[args[0]], *args[1:], **d) - if hasattr(s, 'as_list'): - s = s.as_list() - return response.json(s) - self.error() - - def serve_jsonrpc(self): - def return_response(id, result): - return serializers.json({'version': '1.1', 'id': id, 'result': result, 'error': None}) - - def return_error(id, code, message, data=None): - error = {'name': 'JSONRPCError', - 'code': code, 'message': message} - if data is not None: - error['data'] = data - return serializers.json({'id': id, - 'version': '1.1', - 'error': error, - }) - - request = current.request - response = current.response - response.headers['Content-Type'] = 'application/json; charset=utf-8' - methods = self.jsonrpc_procedures - data = json.loads(request.body.read()) - jsonrpc_2 = data.get('jsonrpc') - if jsonrpc_2: # hand over to version 2 of the protocol - return self.serve_jsonrpc2(data) - id, method, params = data.get('id'), data.get('method'), data.get('params', []) - if id is None: - return return_error(0, 100, 'missing id') - if method not in methods: - return return_error(id, 100, 'method "%s" does not exist' % method) - try: - if isinstance(params, dict): - s = methods[method](**params) - else: - s = methods[method](*params) - if hasattr(s, 'as_list'): - s = s.as_list() - return return_response(id, s) - except Service.JsonRpcException as e: - return return_error(id, e.code, e.info) - except: - etype, eval, etb = sys.exc_info() - message = '%s: %s' % (etype.__name__, eval) - data = request.is_local and traceback.format_tb(etb) - logger.warning('jsonrpc exception %s\n%s' % (message, traceback.format_tb(etb))) - return return_error(id, 100, message, data) - - def serve_jsonrpc2(self, data=None, batch_element=False): - - def return_response(id, result): - if not must_respond: - return None - return serializers.json({'jsonrpc': '2.0', 'id': id, 'result': result}) - - def return_error(id, code, message=None, data=None): - error = {'code': code} - if code in Service.jsonrpc_errors: - error['message'] = Service.jsonrpc_errors[code][0] - error['data'] = Service.jsonrpc_errors[code][1] - if message is not None: - error['message'] = message - if data is not None: - error['data'] = data - return serializers.json({'jsonrpc': '2.0', 'id': id, 'error': error}) - - def validate(data): - """ - Validate request as defined in: http://www.jsonrpc.org/specification#request_object. - - Args: - data(str): The json object. - - Returns: - - True -- if successful - - False -- if no error should be reported (i.e. data is missing 'id' member) - - Raises: - JsonRPCException - - """ - - iparms = set(data.keys()) - mandatory_args = set(['jsonrpc', 'method']) - missing_args = mandatory_args - iparms - - if missing_args: - raise Service.JsonRpcException(-32600, 'Missing arguments %s.' % list(missing_args)) - if data['jsonrpc'] != '2.0': - raise Service.JsonRpcException(-32603, 'Unsupported jsonrpc version "%s"' % data['jsonrpc']) - if 'id' not in iparms: - return False - - return True - - request = current.request - response = current.response - if not data: - response.headers['Content-Type'] = 'application/json; charset=utf-8' - try: - data = json.loads(request.body.read()) - except ValueError: # decoding error in json lib - return return_error(None, -32700) - - # Batch handling - if isinstance(data, list) and not batch_element: - retlist = [] - for c in data: - retstr = self.serve_jsonrpc2(c, batch_element=True) - if retstr: # do not add empty responses - retlist.append(retstr) - if len(retlist) == 0: # return nothing - return '' - else: - return "[" + ','.join(retlist) + "]" - methods = self.jsonrpc2_procedures - methods.update(self.jsonrpc_procedures) - - try: - must_respond = validate(data) - except Service.JsonRpcException as e: - return return_error(None, e.code, e.info) - - id, method, params = data.get('id'), data['method'], data.get('params', '') - if method not in methods: - return return_error(id, -32601, data='Method "%s" does not exist' % method) - try: - if isinstance(params, dict): - s = methods[method](**params) - else: - s = methods[method](*params) - if hasattr(s, 'as_list'): - s = s.as_list() - if must_respond: - return return_response(id, s) - else: - return '' - except HTTP as e: - raise e - except Service.JsonRpcException as e: - return return_error(id, e.code, e.info) - except: - etype, eval, etb = sys.exc_info() - data = '%s: %s\n' % (etype.__name__, eval) + str(request.is_local and traceback.format_tb(etb)) - logger.warning('%s: %s\n%s' % (etype.__name__, eval, traceback.format_tb(etb))) - return return_error(id, -32099, data=data) - - def serve_xmlrpc(self): - request = current.request - response = current.response - services = self.xmlrpc_procedures.values() - return response.xmlrpc(request, services) - - def serve_amfrpc(self, version=0): - try: - import pyamf - import pyamf.remoting.gateway - except: - return "pyamf not installed or not in Python sys.path" - request = current.request - response = current.response - if version == 3: - services = self.amfrpc3_procedures - base_gateway = pyamf.remoting.gateway.BaseGateway(services) - pyamf_request = pyamf.remoting.decode(request.body) - else: - services = self.amfrpc_procedures - base_gateway = pyamf.remoting.gateway.BaseGateway(services) - context = pyamf.get_context(pyamf.AMF0) - pyamf_request = pyamf.remoting.decode(request.body, context) - pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion) - for name, message in pyamf_request: - pyamf_response[name] = base_gateway.getProcessor(message)(message) - response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE - if version == 3: - return pyamf.remoting.encode(pyamf_response).getvalue() - else: - return pyamf.remoting.encode(pyamf_response, context).getvalue() - - def serve_soap(self, version="1.1"): - try: - from gluon.contrib.pysimplesoap.server import SoapDispatcher - except: - return "pysimplesoap not installed in contrib" - request = current.request - response = current.response - procedures = self.soap_procedures - - location = "%s://%s%s" % (request.env.wsgi_url_scheme, - request.env.http_host, - URL(r=request, f="call/soap", vars={})) - namespace = 'namespace' in response and response.namespace or location - documentation = response.description or '' - dispatcher = SoapDispatcher( - name=response.title, - location=location, - action=location, # SOAPAction - namespace=namespace, - prefix='pys', - documentation=documentation, - ns=True) - for method, (function, returns, args, doc) in iteritems(procedures): - dispatcher.register_function(method, function, returns, args, doc) - if request.env.request_method == 'POST': - fault = {} - # Process normal Soap Operation - response.headers['Content-Type'] = 'text/xml' - xml = dispatcher.dispatch(request.body.read(), fault=fault) - if fault: - # May want to consider populating a ticket here... - response.status = 500 - # return the soap response - return xml - elif 'WSDL' in request.vars: - # Return Web Service Description - response.headers['Content-Type'] = 'text/xml' - return dispatcher.wsdl() - elif 'op' in request.vars: - # Return method help webpage - response.headers['Content-Type'] = 'text/html' - method = request.vars['op'] - sample_req_xml, sample_res_xml, doc = dispatcher.help(method) - body = [H1("Welcome to Web2Py SOAP webservice gateway"), - A("See all webservice operations", - _href=URL(r=request, f="call/soap", vars={})), - H2(method), - P(doc), - UL(LI("Location: %s" % dispatcher.location), - LI("Namespace: %s" % dispatcher.namespace), - LI("SoapAction: %s" % dispatcher.action), - ), - H3("Sample SOAP XML Request Message:"), - CODE(sample_req_xml, language="xml"), - H3("Sample SOAP XML Response Message:"), - CODE(sample_res_xml, language="xml"), - ] - return {'body': body} - else: - # Return general help and method list webpage - response.headers['Content-Type'] = 'text/html' - body = [H1("Welcome to Web2Py SOAP webservice gateway"), - P(response.description), - P("The following operations are available"), - A("See WSDL for webservice description", - _href=URL(r=request, f="call/soap", vars={"WSDL": None})), - UL([LI(A("%s: %s" % (method, doc or ''), - _href=URL(r=request, f="call/soap", vars={'op': method}))) - for method, doc in dispatcher.list_methods()]), - ] - return {'body': body} - - # inherited __call__ method - - # inherited error() method - # we make this a method so that subclasses can override it if they want to do more specific argument-checking # but the default implmentation is the simplest: just pass the arguments we got, with no checking def call_service_function(self, f, *a, **b): - return f(*a, **b) - - + if self.check_args: + return universal_caller(f, *a, **b) + else: + return f(*a, **b) + def completion(callback): """ Executes a task on completion of the called action. From d7fb270e7e3a75ee7d2ab0a53f81ffad15ab97ea Mon Sep 17 00:00:00 2001 From: Erik Montes Date: Tue, 7 Feb 2017 10:46:22 -0800 Subject: [PATCH 192/436] Remove ! from tag --- applications/admin/views/default.mobile/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/views/default.mobile/layout.html b/applications/admin/views/default.mobile/layout.html index 8f004ced..b17e0d70 100644 --- a/applications/admin/views/default.mobile/layout.html +++ b/applications/admin/views/default.mobile/layout.html @@ -34,7 +34,7 @@ - {{include 'web2py_ajax.html'}} From 48a0683dd108216ca876ad2456ba550dc4773c72 Mon Sep 17 00:00:00 2001 From: Erik Montes Date: Tue, 7 Feb 2017 10:47:26 -0800 Subject: [PATCH 193/436] Removed ! from tag --- applications/admin/views/plugin_jqmobile/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/views/plugin_jqmobile/layout.html b/applications/admin/views/plugin_jqmobile/layout.html index 8910c2f6..1f6419cd 100644 --- a/applications/admin/views/plugin_jqmobile/layout.html +++ b/applications/admin/views/plugin_jqmobile/layout.html @@ -34,7 +34,7 @@ - {{include 'web2py_ajax.html'}} From f0382d646cc9c0ca4f24f855b5daac2b5745f2fe Mon Sep 17 00:00:00 2001 From: Nico de Groot Date: Sun, 26 Feb 2017 13:21:44 +0100 Subject: [PATCH 194/436] minor cleanup removed unused class 'navbar-ex1-collapse' see https://github.com/twbs/bootstrap/issues/10948 --- applications/welcome/views/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/welcome/views/layout.html b/applications/welcome/views/layout.html index a52ff188..07f674d4 100644 --- a/applications/welcome/views/layout.html +++ b/applications/welcome/views/layout.html @@ -59,7 +59,7 @@ {{=response.logo or ''}}
    -
      diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index e72b3dab..bd4d2966 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -12,12 +12,6 @@ import gluon.contenttype import gluon.fileutils from gluon._compat import iteritems -# d3_graph_model added but leaving pygraphviz code as is for initial tests. -try: - import pygraphviz as pgv -except ImportError: - pgv = None - is_gae = request.env.web2py_runtime_gae or False # ## critical --- make a copy of the environment @@ -566,60 +560,6 @@ def table_template(table): _cellborder=0, _cellspacing=0) ).xml() - -# d3_graph_model added but leaving pygraphviz code as is for initial tests. -# The Graph Model button in admin app views/default/design.html has been redirected -# to the d3_graph_model function. -def bg_graph_model(): - graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR') - - subgraphs = dict() - for tablename in db.tables: - if hasattr(db[tablename],'_meta_graphmodel'): - meta_graphmodel = db[tablename]._meta_graphmodel - else: - meta_graphmodel = dict(group=request.application, color='#ECECEC') - - group = meta_graphmodel['group'].replace(' ', '') - if group not in subgraphs: - subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - - graph.add_node(tablename, name=tablename, shape='plaintext', - label=table_template(tablename)) - - for n, key in enumerate(subgraphs.iterkeys()): - graph.subgraph(nbunch=subgraphs[key]['tables'], - name='cluster%d' % n, - style='filled', - color=subgraphs[key]['meta']['color'], - label=subgraphs[key]['meta']['group']) - - for tablename in db.tables: - for field in db[tablename]: - f_type = field.type - if isinstance(f_type,str) and ( - f_type.startswith('reference') or - f_type.startswith('list:reference')): - referenced_table = f_type.split()[1].split('.')[0] - n1 = graph.get_node(tablename) - n2 = graph.get_node(referenced_table) - graph.add_edge(n1, n2, color="#4C4C4C", label='') - - graph.layout() - if not request.args: - response.headers['Content-Type'] = 'image/png' - return graph.draw(format='png', prog='dot') - else: - response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0) - if request.args(0) == 'dot': - return graph.string() - else: - return graph.draw(format=request.args(0), prog='dot') - -def graph_model(): - return dict(databases=databases, pgv=pgv) - def manage(): tables = manager_action['tables'] if isinstance(tables[0], str): diff --git a/applications/examples/views/appadmin.html b/applications/examples/views/appadmin.html index c92e4058..5330b163 100644 --- a/applications/examples/views/appadmin.html +++ b/applications/examples/views/appadmin.html @@ -233,43 +233,22 @@
      {{pass}} -{{if request.function=='graph_model':}} -

      {{=T("Graph Model")}}

      - {{if not pgv:}} - {{=T('pygraphviz library not found')}} - {{elif not databases:}} - {{=T("No databases in this application")}} - {{else:}} - -
      - {{=IMG(_src=URL('appadmin', 'bg_graph_model'))}} - {{pass}} -{{pass}} - {{if request.function=='d3_graph_model':}}

      {{=T("Graph Model")}}

      -
      - - -{{pass}} + {{if not databases:}} + {{=T("No databases in this application")}} + {{else:}} +
      + + + {{pass}} +{{pass}} {{if request.function == 'manage':}}

      {{=heading}}

      diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index e72b3dab..bd4d2966 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -12,12 +12,6 @@ import gluon.contenttype import gluon.fileutils from gluon._compat import iteritems -# d3_graph_model added but leaving pygraphviz code as is for initial tests. -try: - import pygraphviz as pgv -except ImportError: - pgv = None - is_gae = request.env.web2py_runtime_gae or False # ## critical --- make a copy of the environment @@ -566,60 +560,6 @@ def table_template(table): _cellborder=0, _cellspacing=0) ).xml() - -# d3_graph_model added but leaving pygraphviz code as is for initial tests. -# The Graph Model button in admin app views/default/design.html has been redirected -# to the d3_graph_model function. -def bg_graph_model(): - graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR') - - subgraphs = dict() - for tablename in db.tables: - if hasattr(db[tablename],'_meta_graphmodel'): - meta_graphmodel = db[tablename]._meta_graphmodel - else: - meta_graphmodel = dict(group=request.application, color='#ECECEC') - - group = meta_graphmodel['group'].replace(' ', '') - if group not in subgraphs: - subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - - graph.add_node(tablename, name=tablename, shape='plaintext', - label=table_template(tablename)) - - for n, key in enumerate(subgraphs.iterkeys()): - graph.subgraph(nbunch=subgraphs[key]['tables'], - name='cluster%d' % n, - style='filled', - color=subgraphs[key]['meta']['color'], - label=subgraphs[key]['meta']['group']) - - for tablename in db.tables: - for field in db[tablename]: - f_type = field.type - if isinstance(f_type,str) and ( - f_type.startswith('reference') or - f_type.startswith('list:reference')): - referenced_table = f_type.split()[1].split('.')[0] - n1 = graph.get_node(tablename) - n2 = graph.get_node(referenced_table) - graph.add_edge(n1, n2, color="#4C4C4C", label='') - - graph.layout() - if not request.args: - response.headers['Content-Type'] = 'image/png' - return graph.draw(format='png', prog='dot') - else: - response.headers['Content-Disposition']='attachment;filename=graph.%s'%request.args(0) - if request.args(0) == 'dot': - return graph.string() - else: - return graph.draw(format=request.args(0), prog='dot') - -def graph_model(): - return dict(databases=databases, pgv=pgv) - def manage(): tables = manager_action['tables'] if isinstance(tables[0], str): diff --git a/applications/welcome/views/appadmin.html b/applications/welcome/views/appadmin.html index c92e4058..5330b163 100644 --- a/applications/welcome/views/appadmin.html +++ b/applications/welcome/views/appadmin.html @@ -233,43 +233,22 @@
      {{pass}} -{{if request.function=='graph_model':}} -

      {{=T("Graph Model")}}

      - {{if not pgv:}} - {{=T('pygraphviz library not found')}} - {{elif not databases:}} - {{=T("No databases in this application")}} - {{else:}} - -
      - {{=IMG(_src=URL('appadmin', 'bg_graph_model'))}} - {{pass}} -{{pass}} - {{if request.function=='d3_graph_model':}}

      {{=T("Graph Model")}}

      -
      - - -{{pass}} + {{if not databases:}} + {{=T("No databases in this application")}} + {{else:}} +
      + + + {{pass}} +{{pass}} {{if request.function == 'manage':}}

      {{=heading}}

      From 6f256be1f156bf3afc99834c836ccfb0066260d1 Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Mon, 8 May 2017 07:02:31 +1200 Subject: [PATCH 227/436] Added databases variable check --- applications/admin/controllers/appadmin.py | 2 +- applications/examples/controllers/appadmin.py | 2 +- applications/welcome/controllers/appadmin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index bd4d2966..2935959d 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -692,4 +692,4 @@ def d3_graph_model(): # d3 v4 allows individual modules to be specified. The complete d3 library is included below. response.files.append(URL('static','js/d3.min.js')) response.files.append(URL('static','js/d3_graph.js')) - return dict(nodes=nodes, links=links) \ No newline at end of file + return dict(databases=databases, nodes=nodes, links=links) \ No newline at end of file diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index bd4d2966..2935959d 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -692,4 +692,4 @@ def d3_graph_model(): # d3 v4 allows individual modules to be specified. The complete d3 library is included below. response.files.append(URL('static','js/d3.min.js')) response.files.append(URL('static','js/d3_graph.js')) - return dict(nodes=nodes, links=links) \ No newline at end of file + return dict(databases=databases, nodes=nodes, links=links) \ No newline at end of file diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index bd4d2966..2935959d 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -692,4 +692,4 @@ def d3_graph_model(): # d3 v4 allows individual modules to be specified. The complete d3 library is included below. response.files.append(URL('static','js/d3.min.js')) response.files.append(URL('static','js/d3_graph.js')) - return dict(nodes=nodes, links=links) \ No newline at end of file + return dict(databases=databases, nodes=nodes, links=links) \ No newline at end of file From f76437703b4c5d9c617a59207e87759d3f52b49e Mon Sep 17 00:00:00 2001 From: ilvalle Date: Thu, 11 May 2017 20:28:35 +0200 Subject: [PATCH 228/436] fix web debugger, close #1629 --- applications/admin/controllers/debug.py | 3 ++- gluon/compileapp.py | 2 +- gluon/debug.py | 26 ++++++++++++------------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 447948d6..8306bebe 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -6,6 +6,7 @@ import gluon.validators import code from gluon.debug import communicate, web_debugger, dbg_debugger from gluon._compat import thread +from gluon.fileutils import open_file import pydoc @@ -54,7 +55,7 @@ def interact(): if filename: # prevent IOError 2 on some circuntances (EAFP instead of os.access) try: - lines = open(filename).readlines() + lines = open_file(filename, 'r').readlines() except: lines = "" lines = dict([(i + 1, l) for (i, l) in enumerate( diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 69a29068..3dc08bcb 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -637,7 +637,7 @@ def run_controller_in(controller, function, environment): web2py_error=badf) code = "%s\nresponse._vars=response._caller(%s)" % (code, function) layer = "%s:%s" % (filename, function) - ccode = getcfs(layer, filename, lambda: compile2(code, layer)) + ccode = getcfs(layer, filename, lambda: compile2(code, filename)) restricted(ccode, environment, layer=filename) response = environment["response"] diff --git a/gluon/debug.py b/gluon/debug.py index 6887f27f..8f75c5d8 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -89,7 +89,7 @@ def communicate(command=None): # New debugger implementation using dbg and a web UI -import gluon.contrib.dbg as dbg +import gluon.contrib.dbg as c_dbg from threading import RLock interact_lock = RLock() @@ -109,11 +109,11 @@ def check_interaction(fn): return check_fn -class WebDebugger(dbg.Frontend): +class WebDebugger(c_dbg.Frontend): """Qdb web2py interface""" def __init__(self, pipe, completekey='tab', stdin=None, stdout=None): - dbg.Frontend.__init__(self, pipe) + c_dbg.Frontend.__init__(self, pipe) self.clear_interaction() def clear_interaction(self): @@ -128,7 +128,7 @@ class WebDebugger(dbg.Frontend): run_lock.acquire() try: while self.pipe.poll(): - dbg.Frontend.run(self) + c_dbg.Frontend.run(self) finally: run_lock.release() @@ -149,23 +149,23 @@ class WebDebugger(dbg.Frontend): @check_interaction def do_continue(self): - dbg.Frontend.do_continue(self) + c_dbg.Frontend.do_continue(self) @check_interaction def do_step(self): - dbg.Frontend.do_step(self) + c_dbg.Frontend.do_step(self) @check_interaction def do_return(self): - dbg.Frontend.do_return(self) + c_dbg.Frontend.do_return(self) @check_interaction def do_next(self): - dbg.Frontend.do_next(self) + c_dbg.Frontend.do_next(self) @check_interaction def do_quit(self): - dbg.Frontend.do_quit(self) + c_dbg.Frontend.do_quit(self) def do_exec(self, statement): interact_lock.acquire() @@ -175,18 +175,18 @@ class WebDebugger(dbg.Frontend): # avoid spurious interaction notifications: self.set_burst(2) # execute the statement in the remote debugger: - return dbg.Frontend.do_exec(self, statement) + return c_dbg.Frontend.do_exec(self, statement) finally: interact_lock.release() # create the connection between threads: parent_queue, child_queue = Queue.Queue(), Queue.Queue() -front_conn = dbg.QueuePipe("parent", parent_queue, child_queue) -child_conn = dbg.QueuePipe("child", child_queue, parent_queue) +front_conn = c_dbg.QueuePipe("parent", parent_queue, child_queue) +child_conn = c_dbg.QueuePipe("child", child_queue, parent_queue) web_debugger = WebDebugger(front_conn) # frontend -dbg_debugger = dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend +dbg_debugger = c_dbg.Qdb(pipe=child_conn, redirect_stdio=False, skip=None) # backend dbg = dbg_debugger # enable getting context (stack, globals/locals) at interaction From 6342bf0ddb303c7d756e1faac2bad28b9382c94e Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Mon, 15 May 2017 20:12:58 +1200 Subject: [PATCH 229/436] Consistent set of d3 js and css across admin, examples, and welcome --- applications/admin/static/css/d3_graph.css | 33 ++++ applications/admin/static/js/d3.min.js | 8 + applications/admin/static/js/d3_graph.js | 181 ++++++++++++++++++ applications/examples/static/css/d3_graph.css | 33 ++++ applications/examples/static/js/d3.min.js | 8 + applications/examples/static/js/d3_graph.js | 181 ++++++++++++++++++ 6 files changed, 444 insertions(+) create mode 100644 applications/admin/static/css/d3_graph.css create mode 100644 applications/admin/static/js/d3.min.js create mode 100644 applications/admin/static/js/d3_graph.js create mode 100644 applications/examples/static/css/d3_graph.css create mode 100644 applications/examples/static/js/d3.min.js create mode 100644 applications/examples/static/js/d3_graph.js diff --git a/applications/admin/static/css/d3_graph.css b/applications/admin/static/css/d3_graph.css new file mode 100644 index 00000000..fd0ab8fb --- /dev/null +++ b/applications/admin/static/css/d3_graph.css @@ -0,0 +1,33 @@ + .node {fill: steelblue; + stroke: #636363; + stroke-width: 1px;} + + .auth {fill: lightgrey;} + + .table {r: 10;} + + .link {stroke: #bbbbbb; + stroke-width: 2px;} + td {padding: 4px;} + + div.tooltip { + position: absolute; + text-align: left; + /* width: 140px; */ + /* height: 28px;*/ + padding: 0px 5px 0px 5px; + padding-top: 0px; + font: 12px sans-serif; + background: #fff7bc; + border: solid 1px #aaa; + border-radius: 6px; + pointer-events: none;} + + h5 { font: 14px sans-serif; + background : #ec7014; + color: #ffffe5; + padding: 5px 2px 5px 2px; + margin-top: 1px;} + path { + fill: #aaaaaa;} + diff --git a/applications/admin/static/js/d3.min.js b/applications/admin/static/js/d3.min.js new file mode 100644 index 00000000..6ba6b41e --- /dev/null +++ b/applications/admin/static/js/d3.min.js @@ -0,0 +1,8 @@ +// https://d3js.org Version 4.7.4. Copyright 2017 Mike Bostock. +(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t){return function(n,e){return Ls(t(n),e)}}function e(t,n){return[t,n]}function r(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Js?i*=10:o>=Qs?i*=5:o>=Ks&&(i*=2),n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function g(t,n){for(var e,r=0,i=t.length;r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function S(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function q(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function D(t,n){return function(){this.setAttribute(t,n)}}function O(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function F(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function Y(t){return function(){this.style.removeProperty(t)}}function B(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t){return function(){delete this[t]}}function X(t,n){return function(){this[t]=n}}function V(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function $(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new Z(t)}function Z(t){this._node=t,this._names=$(t.getAttribute("class")||"")}function G(t,n){for(var e=W(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Rl.exec(t))?kt(parseInt(n[1],16)):(n=ql.exec(t))?new At(n[1],n[2],n[3],1):(n=Ul.exec(t))?new At(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Dl.exec(t))?St(n[1],n[2],n[3],n[4]):(n=Ol.exec(t))?St(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Fl.exec(t))?Ct(n[1],n[2]/100,n[3]/100,1):(n=Il.exec(t))?Ct(n[1],n[2]/100,n[3]/100,n[4]):Yl.hasOwnProperty(t)?kt(Yl[t]):"transparent"===t?new At(NaN,NaN,NaN,0):null}function kt(t){return new At(t>>16&255,t>>8&255,255&t,1)}function St(t,n,e,r){return r<=0&&(t=n=e=NaN),new At(t,n,e,r)}function Nt(t){return t instanceof Mt||(t=Tt(t)),t?(t=t.rgb(),new At(t.r,t.g,t.b,t.opacity)):new At}function Et(t,n,e,r){return 1===arguments.length?Nt(t):new At(t,n,e,null==r?1:r)}function At(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ct(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Lt(t,n,e,r)}function zt(t){if(t instanceof Lt)return new Lt(t.h,t.s,t.l,t.opacity);if(t instanceof Mt||(t=Tt(t)),!t)return new Lt;if(t instanceof Lt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&c<1?0:u,new Lt(u,a,c,t.opacity)}function Pt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function qt(t){if(t instanceof Dt)return new Dt(t.l,t.a,t.b,t.opacity);if(t instanceof Ht){var n=t.h*Bl;return new Dt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof At||(t=Nt(t));var e=Yt(t.r),r=Yt(t.g),i=Yt(t.b),o=Ot((.4124564*e+.3575761*r+.1804375*i)/Hl),u=Ot((.2126729*e+.7151522*r+.072175*i)/Xl);return new Dt(116*u-16,500*(o-u),200*(u-Ot((.0193339*e+.119192*r+.9503041*i)/Vl)),t.opacity)}function Ut(t,n,e,r){return 1===arguments.length?qt(t):new Dt(t,n,e,null==r?1:r)}function Dt(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Ot(t){return t>Gl?Math.pow(t,1/3):t/Zl+$l}function Ft(t){return t>Wl?t*t*t:Zl*(t-$l)}function It(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Yt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bt(t){if(t instanceof Ht)return new Ht(t.h,t.c,t.l,t.opacity);t instanceof Dt||(t=qt(t));var n=Math.atan2(t.b,t.a)*jl;return new Ht(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function jt(t,n,e,r){return 1===arguments.length?Bt(t):new Ht(t,n,e,null==r?1:r)}function Ht(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Xt(t){if(t instanceof $t)return new $t(t.h,t.s,t.l,t.opacity);t instanceof At||(t=Nt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(ih*r+eh*n-rh*e)/(ih+eh-rh),o=r-i,u=(nh*(e-i)-Kl*o)/th,a=Math.sqrt(u*u+o*o)/(nh*i*(1-i)),c=a?Math.atan2(u,o)*jl-120:NaN;return new $t(c<0?c+360:c,a,i,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Xt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function Zt(t,n){return function(e){return t+e*n}}function Gt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Jt(t,n){var e=n-t;return e?Zt(t,e>180||e<-180?e-360*Math.round(e/360):e):ph(isNaN(t)?n:t)}function Qt(t){return 1==(t=+t)?Kt:function(n,e){return e-n?Gt(n,e,t):ph(isNaN(n)?e:n)}}function Kt(t,n){var e=n-t;return e?Zt(t,e):ph(isNaN(t)?n:t)}function tn(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:mh(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:mh(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:mh(t,e)},{i:a-2,x:mh(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e=0&&n._call.call(null,t),n=n._next;--Ih}function mn(){Xh=(Hh=$h.now())+Vh,Ih=Yh=0;try{yn()}finally{Ih=0,bn(),Xh=0}}function xn(){var t=$h.now(),n=t-Hh;n>jh&&(Vh-=n,Hh=t)}function bn(){for(var t,n,e=sh,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:sh=n);fh=t,wn(r)}function wn(t){if(!Ih){Yh&&(Yh=clearTimeout(Yh));var n=t-Xh;n>24?(t<1/0&&(Yh=setTimeout(mn,n)),Bh&&(Bh=clearInterval(Bh))):(Bh||(Hh=Xh,Bh=setInterval(xn,jh)),Ih=1,Wh(mn))}}function Mn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Kh)throw new Error("too late");return e}function Tn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>np)throw new Error("too late");return e}function kn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function Sn(t,n,e){function r(t){e.state=tp,e.timer.restart(i,e.delay,e.time),e.delay<=t&&i(t-e.delay)}function i(r){var s,f,l,h;if(e.state!==tp)return u();for(s in c)if(h=c[s],h.name===e.name){if(h.state===ep)return Zh(i);h.state===rp?(h.state=op,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete c[s]):+s=0&&(t=t.slice(0,n)),!t||"start"===t})}function Hn(t,n,e){var r,i,o=jn(n)?Mn:Tn;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function Xn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Vn(t,n){var e,r,i;return function(){var o=al(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function $n(t){return function(){this.style.removeProperty(t)}}function Wn(t,n,e){var r,i;return function(){var o=al(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function Zn(t,n,e){var r,i,o;return function(){var u=al(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Gn(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function Jn(t){return function(){this.textContent=t}}function Qn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Kn(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function te(t){return vt().transition(t)}function ne(){return++Ep}function ee(t){return+t}function re(t){return t*t}function ie(t){return t*(2-t)}function oe(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function ue(t){return t*t*t}function ae(t){return--t*t*t+1}function ce(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function se(t){return 1-Math.cos(t*Rp)}function fe(t){return Math.sin(t*Rp)}function le(t){return(1-Math.cos(Lp*t))/2}function he(t){return Math.pow(2,10*t-10)}function pe(t){return 1-Math.pow(2,-10*t)}function de(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function ve(t){return 1-Math.sqrt(1-t*t)}function _e(t){return Math.sqrt(1- --t*t)}function ge(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ye(t){return 1-me(1-t)}function me(t){return(t=+t)Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,id(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],S){case ud:case od:N&&(m=Math.max(P-l,Math.min(R-v,m)),h=l+m,_=v+m),E&&(x=Math.max(L-p,Math.min(q-g,x)),d=p+x,y=g+x);break;case ad:N<0?(m=Math.max(P-l,Math.min(R-l,m)),h=l+m,_=v):N>0&&(m=Math.max(P-v,Math.min(R-v,m)),h=l,_=v+m),E<0?(x=Math.max(L-p,Math.min(q-p,x)),d=p+x,y=g):E>0&&(x=Math.max(L-g,Math.min(q-g,x)),d=p,y=g+x);break;case cd:N&&(h=Math.max(P,Math.min(R,l-m*N)),_=Math.max(P,Math.min(R,v+m*N))),E&&(d=Math.max(L,Math.min(q,p-x*E)),y=Math.max(L,Math.min(q,g+x*E)))}_0&&(l=h-m),E<0?g=y-x:E>0&&(p=d-x),S=ud,Y.attr("cursor",hd.selection),o());break;default:return}id()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:S===cd&&(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad,o());break;case 32:S===ud&&(t.event.altKey?(N&&(v=_-m*N,l=h+m*N),E&&(g=y-x*E,p=d+x*E),S=cd):(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad),Y.attr("cursor",hd[k]),o());break;default:return}id()}if(t.event.touches){if(t.event.changedTouches.length=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u}while((l=f<<1|s)==(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function Qe(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;ef&&(f=r),il&&(l=i));for(f",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Sv[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function pr(n){ +return Ev=zv(n),t.format=Ev.format,t.formatPrefix=Ev.formatPrefix,Ev}function dr(){this.reset()}function vr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function _r(t){return t>1?0:t<-1?v_:Math.acos(t)}function gr(t){return t>1?__:t<-1?-__:Math.asin(t)}function yr(t){return(t=A_(t/2))*t}function mr(){}function xr(t,n){t&&R_.hasOwnProperty(t.type)&&R_[t.type](t,n)}function br(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=T_(n),u=A_(n),a=Iv*u,c=Fv*o+a*T_(i),s=a*r*A_(i);U_.add(M_(s,c)),Ov=t,Fv=o,Iv=u}function Nr(t){return[M_(t[1],t[0]),gr(t[2])]}function Er(t){var n=t[0],e=t[1],r=T_(e);return[r*T_(n),r*A_(n),A_(e)]}function Ar(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Cr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function zr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Pr(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Lr(t){var n=z_(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Rr(t,n){Zv.push(Gv=[Yv=t,jv=t]),nHv&&(Hv=n)}function qr(t,n){var e=Er([t*x_,n*x_]);if(Wv){var r=Cr(Wv,e),i=[r[1],-r[0],0],o=Cr(i,r);Lr(o),o=Nr(o);var u,a=t-Xv,c=a>0?1:-1,s=o[0]*m_*c,f=b_(a)>180;f^(c*XvHv&&(Hv=u):(s=(s+360)%360-180,f^(c*XvHv&&(Hv=n))),f?tYr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t):jv>=Yv?(tjv&&(jv=t)):t>Xv?Yr(Yv,t)>Yr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t)}else Zv.push(Gv=[Yv=t,jv=t]);nHv&&(Hv=n),Wv=e,Xv=t}function Ur(){Y_.point=qr}function Dr(){Gv[0]=Yv,Gv[1]=jv,Y_.point=Rr,Wv=null}function Or(t,n){if(Wv){var e=t-Xv;I_.add(b_(e)>180?e+(e>0?360:-360):e)}else Vv=t,$v=n;O_.point(t,n),qr(t,n)}function Fr(){O_.lineStart()}function Ir(){Or(Vv,$v),O_.lineEnd(),b_(I_)>d_&&(Yv=-(jv=180)),Gv[0]=Yv,Gv[1]=jv,Wv=null}function Yr(t,n){return(n-=t)<0?n+360:n}function Br(t,n){return t[0]-n[0]}function jr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nv_?t-y_:t<-v_?t+y_:t,n]}function ni(t,n,e){return(t%=y_)?n||e?V_(ri(t),ii(n,e)):ri(t):n||e?ii(n,e):ti}function ei(t){return function(n,e){return n+=t,[n>v_?n-y_:n<-v_?n+y_:n,e]}}function ri(t){var n=ei(t);return n.invert=ei(-t),n}function ii(t,n){function e(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*r+a*i;return[M_(c*o-f*u,a*r-s*i),gr(f*o+c*u)]}var r=T_(t),i=A_(t),o=T_(n),u=A_(n);return e.invert=function(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*o-c*u;return[M_(c*o+s*u,a*r+f*i),gr(f*r-a*i)]},e}function oi(t,n,e,r,i,o){if(e){var u=T_(n),a=A_(n),c=r*e;null==i?(i=n+r*y_,o=n-c/2):(i=ui(u,i),o=ui(u,o),(r>0?io)&&(i+=r*y_));for(var s,f=i;r>0?f>o:f0)do{s.point(0===f||3===f?t:e,f>1?r:n)}while((f=(f+a+4)%4)!==l);else s.point(o[0],o[1])}function u(r,i){return b_(r[0]-t)0?0:3:b_(r[0]-e)0?2:1:b_(r[1]-n)0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&S.point(t,n)}function s(){for(var n=0,e=0,i=_.length;er&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){S=N,v=[],_=[],k=!0}function l(){var t=s(),n=k&&t,e=(v=ff(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&sg(v,a,t,o,u),u.polygonEnd()),S=u,v=_=g=null}function h(){E.point=d,_&&_.push(g=[]),T=!0,M=!1,b=w=NaN}function p(){v&&(d(y,m),x&&M&&N.rejoin(),v.push(N.result())),E.point=c,M&&S.lineEnd()}function d(o,u){var a=i(o,u);if(_&&g.push([o,u]),T)y=o,m=u,x=a,T=!1,a&&(S.lineStart(),S.point(o,u));else if(a&&M)S.point(o,u);else{var c=[b=Math.max(lg,Math.min(fg,b)),w=Math.max(lg,Math.min(fg,w))],s=[o=Math.max(lg,Math.min(fg,o)),u=Math.max(lg,Math.min(fg,u))];ag(c,s,t,n,e,r)?(M||(S.lineStart(),S.point(c[0],c[1])),S.point(s[0],s[1]),a||S.lineEnd(),k=!1):a&&(S.lineStart(),S.point(o,u),k=!1)}b=o,w=u,M=a}var v,_,g,y,m,x,b,w,M,T,k,S=u,N=ug(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function fi(){_g.point=hi,_g.lineEnd=li}function li(){_g.point=_g.lineEnd=mr}function hi(t,n){t*=x_,n*=x_,$_=t,W_=A_(n),Z_=T_(n),_g.point=pi}function pi(t,n){t*=x_,n*=x_;var e=A_(n),r=T_(n),i=b_(t-$_),o=T_(i),u=A_(i),a=r*u,c=Z_*e-W_*r*o,s=W_*e+Z_*r*o;vg.add(M_(z_(a*a+c*c),s)),$_=t,W_=e,Z_=r}function di(t,n){return!(!t||!wg.hasOwnProperty(t.type))&&wg[t.type](t,n)}function vi(t,n){return 0===xg(t,n)}function _i(t,n){var e=xg(t[0],t[1]);return xg(t[0],n)+xg(n,t[1])<=e+d_}function gi(t,n){return!!dg(t.map(yi),mi(n))}function yi(t){return t=t.map(mi),t.pop(),t}function mi(t){return[t[0]*x_,t[1]*x_]}function xi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function bi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function wi(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return Gs(k_(o/_)*_,i,_).map(h).concat(Gs(k_(s/g)*g,c,g).map(p)).concat(Gs(k_(r/d)*d,e,d).filter(function(t){return b_(t%_)>d_}).map(f)).concat(Gs(k_(a/v)*v,u,v).filter(function(t){return b_(t%g)>d_}).map(l))}var e,r,i,o,u,a,c,s,f,l,h,p,d=10,v=d,_=90,g=360,y=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[h(o).concat(p(c).slice(1),h(i).reverse().slice(1),p(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(y)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(y)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(_=+n[0],g=+n[1],t):[_,g]},t.stepMinor=function(n){return arguments.length?(d=+n[0],v=+n[1],t):[d,v]},t.precision=function(n){return arguments.length?(y=+n,f=xi(a,u,90),l=bi(r,e,y),h=xi(s,c,90),p=bi(o,i,y),t):y},t.extentMajor([[-180,-90+d_],[180,90-d_]]).extentMinor([[-180,-80-d_],[180,80+d_]])}function Mi(){return wi()()}function Ti(){Eg.point=ki}function ki(t,n){Eg.point=Si,G_=Q_=t,J_=K_=n}function Si(t,n){Ng.add(K_*t-Q_*n),Q_=t,K_=n}function Ni(){Si(G_,J_)}function Ei(t,n){tzg&&(zg=t),nPg&&(Pg=n)}function Ai(t,n){Rg+=t,qg+=n,++Ug}function Ci(){jg.point=zi}function zi(t,n){jg.point=Pi,Ai(eg=t,rg=n)}function Pi(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,Ai(eg=t,rg=n)}function Li(){jg.point=Ai}function Ri(){jg.point=Ui}function qi(){Di(tg,ng)}function Ui(t,n){jg.point=Di,Ai(tg=eg=t,ng=rg=n)}function Di(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,i=rg*t-eg*n,Ig+=i*(eg+t),Yg+=i*(rg+n),Bg+=3*i,Ai(eg=t,rg=n)}function Oi(t){this._context=t}function Fi(t,n){Gg.point=Ii,Xg=$g=t,Vg=Wg=n}function Ii(t,n){$g-=t,Wg-=n,Zg.add(z_($g*$g+Wg*Wg)),$g=t,Wg=n}function Yi(){this._string=[]}function Bi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function ji(t){return t.length>1}function Hi(t,n){return((t=t.x)[0]<0?t[1]-__-d_:__-t[1])-((n=n.x)[0]<0?n[1]-__-d_:__-n[1])}function Xi(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?v_:-v_,c=b_(o-e);b_(c-v_)0?__:-__),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=v_&&(b_(e-i)d_?w_((A_(n)*(o=T_(r))*A_(e)-A_(r)*(i=T_(n))*A_(t))/(i*o*u)):(n+r)/2}function $i(t,n,e,r){var i;if(null==t)i=e*__,r.point(-v_,i),r.point(0,i),r.point(v_,i),r.point(v_,0),r.point(v_,-i),r.point(0,-i),r.point(-v_,-i),r.point(-v_,0),r.point(-v_,i);else if(b_(t[0]-n[0])>d_){var o=t[0]4*n&&v--){var x=u+h,b=a+p,w=c+d,M=z_(x*x+b*b+w*w),T=gr(w/=M),k=b_(b_(w)-1)n||b_((g*A+y*C)/m-.5)>.3||u*h+a*p+c*d2?t[2]%360*x_:0,i()):[b*m_,w*m_,M*m_]},n.precision=function(t){return arguments.length?(A=iy(r,E=t*t),o()):z_(E)},n.fitExtent=function(t,e){return Gi(n,t,e)},n.fitSize=function(t,e){return Ji(n,t,e)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function eo(t){var n=0,e=v_/3,r=no(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*x_,e=t[1]*x_):[n*m_,e*m_]},i}function ro(t){function n(t,n){return[t*e,A_(n)/e]}var e=T_(t);return n.invert=function(t,n){return[t/e,gr(n*e)]},n}function io(t,n){function e(t,n){var e=z_(o-2*i*A_(n))/i;return[e*A_(t*=i),u-e*T_(t)]}var r=A_(t),i=(r+A_(n))/2;if(b_(i)0?n<-__+d_&&(n=-__+d_):n>__-d_&&(n=__-d_);var e=o/E_(fo(n),i);return[e*A_(i*t),o-e*T_(i*t)]}var r=T_(t),i=t===n?A_(t):N_(r/T_(n))/N_(fo(n)/fo(t)),o=r*E_(fo(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=C_(i)*z_(t*t+e*e);return[M_(t,b_(e))/i*C_(e),2*w_(E_(o/r,1/i))-__]},e):co}function ho(t,n){return[t,n]}function po(t,n){function e(t,n){var e=o-n,r=i*t;return[e*A_(r),o-e*T_(r)]}var r=T_(t),i=t===n?A_(t):(r-T_(n))/(n-t),o=r/i+t;return b_(i)=0;)n+=e[r].value;else n=1;t.value=n}function Eo(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function Ao(t,n){var e,r,i,o,u,a=new Ro(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=zo);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new Ro(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(Lo)}function Co(){return Ao(this).eachBefore(Po)}function zo(t){return t.children}function Po(t){t.data=t.data.data}function Lo(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Ro(t){this.data=t,this.depth=this.height=0,this.parent=null}function qo(t){this._=t,this.next=null}function Uo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Do(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=Oo(n[0]);break;case 2:e=Fo(n[0],n[1]);break;case 3:e=Io(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&Uo(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Do(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function Oo(t){return{x:t.x,y:t.y,r:t.r}}function Fo(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function Io(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),g=2*(i-f),y=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*g,b=(p*m-g*v)/x-r,w=(g*d-p*y)/x,M=(_*v-h*m)/x-i,T=(h*y-_*d)/x,k=w*w+T*T-1,S=2*(b*w+M*T+o),N=b*b+M*M-o*o,E=(-S-Math.sqrt(S*S-4*k*N))/(2*k);return{x:b+w*E+r,y:M+T*E+i,r:E}}function Yo(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Bo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i-1e-6>e*e+r*r}function jo(t,n,e){var r=t._,i=t.next._,o=r.r+i.r,u=(r.x*i.r+i.x*r.r)/o-n,a=(r.y*i.r+i.y*r.r)/o-e;return u*u+a*a}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Yo(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,g=h*n.y+p*e.y+d*r.y;n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;a=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function uu(t,n,e){return t.a.parent===n.parent?t.a:e}function au(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function cu(t){for(var n,e,r,i,o,u=new au(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new au(r[i],i)),e.parent=n;return(u.parent=new au(null,0)).children=[u],u}function su(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,g=[],y=n.children,m=0,x=0,b=y.length,w=n.value;mh&&(h=a),_=f*f*v,(p=Math.max(h/_,_/l))>d){f-=a;break}d=p}g.push(u={value:f,dice:c1&&em(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function hu(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function pu(t){if(!t._start)try{du(t)}catch(n){if(t._tasks[t._ended+t._active-1])_u(t,n);else if(!t._data)throw n}}function du(t){for(;t._start=t._waiting&&t._active=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,gu(t)}function gu(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function yu(t){return new hu(arguments.length?+t:1/0)}function mu(t){return function(n,e){t(null==n?e:null)}}function xu(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function bu(t,n){return function(e){return t(e.responseText,n)}}function wu(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==Sm)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=Ye(),r=[],i=Sm;return t=null==t?[]:km.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=Ye();for(var i,o,u=-1,a=t.length;++u=e?1:r(t)}}}function Eu(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function Au(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o2?Cu:Au,o=u=null,r}function r(n){return(o||(o=i(a,c,f?Nu(t):t,s)))(+n)}var i,o,u,a=Am,c=Am,s=Th,f=!1;return r.invert=function(t){return(u||(u=i(c,a,Su,f?Eu(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=Tm.call(t,Em),e()):a.slice()},r.range=function(t){return arguments.length?(c=km.call(t),e()):c.slice()},r.rangeRound=function(t){return c=km.call(t),s=kh,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function Lu(t){var n=t.domain;return t.ticks=function(t){var e=n();return tf(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Cm(n(),t,e)},t.nice=function(e){var i=n(),o=i.length-1,u=null==e?10:e,a=i[0],c=i[o],s=r(a,c,u);return s&&(s=r(Math.floor(a/s)*s,Math.ceil(c/s)*s,u),i[0]=Math.floor(a/s)*s,i[o]=Math.ceil(c/s)*s,n(i)),t},t}function Ru(){var t=Pu(Su,mh);return t.copy=function(){return zu(t,Ru())},Lu(t)}function qu(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Tm.call(e,Em),t):n.slice()},t.copy=function(){return qu().domain(n)},Lu(t)}function Uu(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:Nm(n)}function Du(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function Ou(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Fu(t){return 10===t?Ou:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Iu(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Yu(t){return function(n){return-t(-n)}}function Bu(){function n(){return o=Iu(i),u=Fu(i),r()[0]<0&&(o=Yu(o),u=Yu(u)),e}var e=Pu(Uu,Du).domain([1,10]),r=e.domain,i=10,o=Iu(10),u=Fu(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c0){for(;hc)break;v.push(l)}}else for(;h=1;--f)if(!((l=s*f)c)break;v.push(l)}}else v=tf(h,p,Math.min(p-h,d)).map(u);return n?v.reverse():v},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i0?i[n-1]:e[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return $u().domain([e,r]).range(u)},Lu(t)}function Wu(){function t(t){if(t<=t)return e[Us(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=km.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=km.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Wu().domain(n).range(e)},t}function Zu(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pm.setTime(+n),Lm.setTime(+r),t(Pm),t(Lm),Math.floor(e(Pm,Lm))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Gu(t){return Zu(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/Dm})}function Ju(t){return Zu(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Dm})}function Qu(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Ku(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ta(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function na(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date(+e));++a=c)return-1;if(37===(i=n.charCodeAt(u++))){if(i=n.charAt(u++),!(o=B[i in qx?n.charAt(u++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=R.exec(n.slice(e));return r?(t.w=q[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=L[r[0].toLowerCase()],e+r[0].length):-1}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return N[t.getDay()]}function p(t){return S[t.getDay()]}function d(t){return A[t.getMonth()]}function v(t){return E[t.getMonth()]}function _(t){return k[+(t.getHours()>=12)]}function g(t){return N[t.getUTCDay()]}function y(t){return S[t.getUTCDay()]}function m(t){return A[t.getUTCMonth()]}function x(t){return E[t.getUTCMonth()]}function b(t){return k[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,k=t.periods,S=t.days,N=t.shortDays,E=t.months,A=t.shortMonths,C=ia(k),z=oa(k),P=ia(S),L=oa(S),R=ia(N),q=oa(N),U=ia(E),D=oa(E),O=ia(A),F=oa(A),I={a:h,A:p,b:d,B:v,c:null,d:xa,e:xa,H:ba,I:wa,j:Ma,L:Ta,m:ka,M:Sa,p:_,S:Na,U:Ea,w:Aa,W:Ca,x:null,X:null,y:za,Y:Pa,Z:La,"%":Wa},Y={a:g,A:y,b:m,B:x,c:null,d:Ra,e:Ra,H:qa,I:Ua,j:Da,L:Oa,m:Fa,M:Ia,p:b,S:Ya,U:Ba,w:ja,W:Ha,x:null,X:null,y:Xa,Y:Va,Z:$a,"%":Wa},B={a:o,A:u,b:a,B:c,c:s,d:pa,e:pa,H:va,I:va,j:da,L:ya,m:ha,M:_a,p:i,S:ga,U:aa,w:ua,W:ca,x:f,X:l,y:fa,Y:sa,Z:la,"%":ma};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Qu);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Ku);return n.toString=function(){return t},n}}}function ea(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function la(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function ha(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function pa(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function da(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function va(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function _a(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function ga(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function ya(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function ma(t,n,e){var r=Dx.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function xa(t,n){return ea(t.getDate(),n,2)}function ba(t,n){return ea(t.getHours(),n,2)}function wa(t,n){return ea(t.getHours()%12||12,n,2)}function Ma(t,n){return ea(1+Hm.count(cx(t),t),n,3)}function Ta(t,n){return ea(t.getMilliseconds(),n,3)}function ka(t,n){return ea(t.getMonth()+1,n,2)}function Sa(t,n){return ea(t.getMinutes(),n,2)}function Na(t,n){return ea(t.getSeconds(),n,2)}function Ea(t,n){return ea(Vm.count(cx(t),t),n,2)}function Aa(t){return t.getDay()}function Ca(t,n){return ea($m.count(cx(t),t),n,2)}function za(t,n){return ea(t.getFullYear()%100,n,2)}function Pa(t,n){return ea(t.getFullYear()%1e4,n,4)}function La(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+ea(n/60|0,"0",2)+ea(n%60,"0",2)}function Ra(t,n){return ea(t.getUTCDate(),n,2)}function qa(t,n){return ea(t.getUTCHours(),n,2)}function Ua(t,n){return ea(t.getUTCHours()%12||12,n,2)}function Da(t,n){return ea(1+dx.count(Px(t),t),n,3)}function Oa(t,n){return ea(t.getUTCMilliseconds(),n,3)}function Fa(t,n){return ea(t.getUTCMonth()+1,n,2)}function Ia(t,n){return ea(t.getUTCMinutes(),n,2)}function Ya(t,n){return ea(t.getUTCSeconds(),n,2)}function Ba(t,n){return ea(_x.count(Px(t),t),n,2)}function ja(t){return t.getUTCDay()}function Ha(t,n){return ea(gx.count(Px(t),t),n,2)}function Xa(t,n){return ea(t.getUTCFullYear()%100,n,2)}function Va(t,n){return ea(t.getUTCFullYear()%1e4,n,4)}function $a(){return"+0000"}function Wa(){return"%"}function Za(n){return Lx=na(n),t.timeFormat=Lx.format,t.timeParse=Lx.parse,t.utcFormat=Lx.utcFormat,t.utcParse=Lx.utcParse,Lx}function Ga(t){return t.toISOString()}function Ja(t){var n=new Date(t);return isNaN(n)?null:n}function Qa(t){return new Date(t)}function Ka(t){return t instanceof Date?+t:+new Date(+t)}function tc(t,n,e,i,o,u,a,c,s){function f(r){return(a(r)1?0:t<-1?mb:Math.acos(t)}function ic(t){return t>=1?xb:t<=-1?-xb:Math.asin(t)}function oc(t){return t.innerRadius}function uc(t){return t.outerRadius}function ac(t){return t.startAngle}function cc(t){return t.endAngle}function sc(t){return t&&t.padAngle}function fc(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function lc(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/gb(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,g=(p+v)/2,y=d-h,m=v-p,x=y*y+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*gb(db(0,b*b*x-w*w)),T=(w*m-y*M)/x,k=(-w*y-m*M)/x,S=(w*m+y*M)/x,N=(-w*y+m*M)/x,E=T-_,A=k-g,C=S-_,z=N-g;return E*E+A*A>C*C+z*z&&(T=S,k=N),{cx:T,cy:k,x01:-f,y01:-l,x11:T*(i/b-1),y11:k*(i/b-1)}}function hc(t){this._context=t}function pc(t){return t[0]}function dc(t){return t[1]}function vc(t){this._curve=t}function _c(t){function n(n){return new vc(t(n))}return n._curve=t,n}function gc(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t}function yc(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function mc(t){this._context=t}function xc(t){this._context=t}function bc(t){this._context=t}function wc(t,n){this._basis=new mc(t),this._beta=n}function Mc(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Tc(t,n){this._context=t,this._k=(1-n)/6}function kc(t,n){this._context=t,this._k=(1-n)/6}function Sc(t,n){this._context=t,this._k=(1-n)/6}function Nc(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>yb){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>yb){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function Ec(t,n){this._context=t,this._alpha=n}function Ac(t,n){this._context=t,this._alpha=n}function Cc(t,n){this._context=t,this._alpha=n}function zc(t){this._context=t}function Pc(t){return t<0?-1:1}function Lc(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Pc(o)+Pc(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Rc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function qc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function Uc(t){this._context=t}function Dc(t){this._context=new Oc(t)}function Oc(t){this._context=t}function Fc(t){return new Uc(t)}function Ic(t){return new Dc(t)}function Yc(t){this._context=t}function Bc(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n0)){if(o/=d,d<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o0)){if(o/=v,v<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function os(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]Cw||Math.abs(i[0][1]-i[1][1])>Cw)||delete Nw[o]}function as(t){return kw[t.index]={site:t,halfedges:[]}}function cs(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function ss(t,n){return n[+(n.left!==t.site)]}function fs(t,n){return n[+(n.left===t.site)]}function ls(){for(var t,n,e,r,i=0,o=kw.length;iCw||Math.abs(v-h)>Cw)&&(c.splice(a,0,Nw.push(es(u,p,Math.abs(d-t)Cw?[t,Math.abs(l-t)Cw?[Math.abs(h-r)Cw?[e,Math.abs(l-e)Cw?[Math.abs(h-n)=-zw)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,g=Ew.pop()||new ps;g.arc=t,g.site=i,g.x=v+u,g.y=(g.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=g;for(var y=null,m=Sw._;m;)if(g.yCw)a=a.L;else{if(!((i=o-ws(a,u))>Cw)){r>-Cw?(n=a.P,e=a):i>-Cw?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}as(t);var c=gs(t);if(Tw.insert(n,c),n||e){if(n===e)return vs(n),e=gs(n.site),Tw.insert(c,e),c.edge=e.edge=ns(n.site,c.site),ds(n),void ds(e);if(!e)return void(c.edge=ns(n.site,c.site));vs(n),vs(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,g=2*(h*_-p*v),y=h*h+p*p,m=v*v+_*_,x=[(_*y-p*m)/g+f,(h*m-v*y)/g+l];rs(e.edge,s,d,x),c.edge=ns(s,t,null,x),e.edge=ns(t,d,null,x),ds(n),ds(e)}}function bs(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function ws(t,n){var e=t.N;if(e)return bs(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function Ms(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function Ts(t,n){return n[1]-t[1]||n[0]-t[0]}function ks(t,n){var e,r,i,o=t.sort(Ts).pop();for(Nw=[],kw=new Array(t.length),Tw=new Gc,Sw=new Gc;;)if(i=Mw,o&&(!i||o[1]n?1:t>=n?0:NaN},Rs=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}},qs=Rs(Ls),Us=qs.right,Ds=qs.left,Os=function(t,n){null==n&&(n=e);for(var r=0,i=t.length-1,o=t[0],u=new Array(i<0?0:i);rt?1:n>=t?0:NaN},Ys=function(t){return null===t?NaN:+t},Bs=function(t,n){var e,r,i=t.length,o=0,u=0,a=-1,c=0;if(null==n)for(;++a1)return u/(c-1)},js=function(t,n){var e=Bs(t,n);return e?Math.sqrt(e):e},Hs=function(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o=r){e=i=r;break}for(;++or&&(e=r),i=r){e=i=r;break}for(;++or&&(e=r),i=f;)l.pop(),--h;var p,d=new Array(h+1);for(i=0;i<=h;++i)p=d[i]=[],p.x0=i>0?l[i-1]:s,p.x1=i=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),u=+e(t[o],o,t);return u+(+e(t[o+1],o+1,t)-u)*(i-o)}},of=function(t,n,e){return t=$s.call(t,Ys).sort(Ls),Math.ceil((e-n)/(2*(rf(t,.75)-rf(t,.25))*Math.pow(t.length,-1/3)))},uf=function(t,n,e){return Math.ceil((e-n)/(3.5*js(t)*Math.pow(t.length,-1/3)))},af=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},cf=function(t,n){var e,r=0,i=t.length,o=-1,u=i;if(null==n)for(;++o=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e},lf=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},hf=function(t,n){for(var e=n.length,r=new Array(e);e--;)r[e]=t[n[e]];return r},pf=function(t,n){if(e=t.length){var e,r,i=0,o=0,u=t[o];for(n||(n=Ls);++i0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Nf.hasOwnProperty(n)?{space:Nf[n],local:t}:t},Af=function(t){var n=Ef(t);return(n.local?x:m)(n)},Cf=0;w.prototype=b.prototype={constructor:w,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var zf=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Pf=document.documentElement;if(!Pf.matches){var Lf=Pf.webkitMatchesSelector||Pf.msMatchesSelector||Pf.mozMatchesSelector||Pf.oMatchesSelector;zf=function(t){return function(){return Lf.call(this,t)}}}}var Rf=zf,qf={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(qf={mouseenter:"mouseover",mouseleave:"mouseout"})}var Uf=function(t,n,e){var r,i,o=k(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?N:S,null==e&&(e=!1),r=0;r=x&&(x=m+1);!(y=_[x])&&++x=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Kf=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=R);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?Y:"function"==typeof n?j:B)(t,n,null==e?"":e)):al(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},sl=function(t,n){return arguments.length>1?this.each((null==n?H:"function"==typeof n?V:X)(t,n)):this.node()[t]};Z.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var fl=function(t,n){var e=$(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=e.length;++i=240?t-240:t+120,i,r),Rt(t,i,r),Rt(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Bl=Math.PI/180,jl=180/Math.PI,Hl=.95047,Xl=1,Vl=1.08883,$l=4/29,Wl=6/29,Zl=3*Wl*Wl,Gl=Wl*Wl*Wl;Al(Dt,Ut,wt(Mt,{brighter:function(t){return new Dt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Dt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Xl*Ft(t),n=Hl*Ft(n),e=Vl*Ft(e),new At(It(3.2404542*n-1.5371385*t-.4985314*e),It(-.969266*n+1.8760108*t+.041556*e),It(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),Al(Ht,jt,wt(Mt,{brighter:function(t){return new Ht(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Ht(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return qt(this).rgb()}}));var Jl=-.14861,Ql=1.78277,Kl=-.29227,th=-.90649,nh=1.97294,eh=nh*th,rh=nh*Ql,ih=Ql*Kl-th*Jl;Al($t,Vt,wt(Mt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Bl,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new At(255*(n+e*(Jl*r+Ql*i)),255*(n+e*(Kl*r+th*i)),255*(n+e*(nh*r)),this.opacity)}}));var oh,uh,ah,ch,sh,fh,lh=function(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:mh(e,r)})),o=wh.lastIndex;return onp&&e.statetp&&e.name===n)return new Kn([[t]],td,n,+r)}return null},ed=function(t){return function(){return t}},rd=function(t,n,e){this.target=t,this.type=n,this.selection=e},id=function(){t.event.preventDefault(),t.event.stopImmediatePropagation()},od={name:"drag"},ud={name:"space"},ad={name:"handle"},cd={name:"center"},sd={name:"x",handles:["e","w"].map(Me),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fd={name:"y",handles:["n","s"].map(Me),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},ld={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(Me),input:function(t){return t},output:function(t){return t}},hd={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},pd={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},dd={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},vd={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},_d={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},gd=function(){return ze(ld)},yd=Math.cos,md=Math.sin,xd=Math.PI,bd=xd/2,wd=2*xd,Md=Math.max,Td=function(){function t(t){var o,u,a,c,s,f,l=t.length,h=[],p=Gs(l),d=[],v=[],_=v.groups=new Array(l),g=new Array(l*l);for(o=0,s=-1;++s1e-6)if(Math.abs(f*a-c*s)>1e-6&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),g=Math.sqrt(l),y=i*Math.tan((Nd-Math.acos((d+l-v)/(2*_*g)))/2),m=y/g,x=y/_;Math.abs(m-1)>1e-6&&(this._+="L"+(t+m*s)+","+(n+m*f)),this._+="A"+i+","+i+",0,0,"+ +(f*h>s*p)+","+(this._x1=t+x*a)+","+(this._y1=n+x*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+s:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-s)>1e-6)&&(this._+="L"+c+","+s),e&&(l<0&&(l=l%Ed+Ed),l>Ad?this._+="A"+e+","+e+",0,1,"+f+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+f+","+(this._x1=c)+","+(this._y1=s):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Nd)+","+f+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var Cd=function(){function t(){var t,a=kd.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,a)),l=i.apply(this,a)-bd,h=o.apply(this,a)-bd,p=f*yd(l),d=f*md(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-bd,g=o.apply(this,a)-bd;if(u||(u=t=Re()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===g||(u.quadraticCurveTo(0,0,v*yd(_),v*md(_)),u.arc(0,0,v,_,g)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=qe,e=Ue,r=De,i=Oe,o=Fe,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:Sd(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Sd(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Sd(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t};Ie.prototype=Ye.prototype={constructor:Ie,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var zd=function(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=Ye(),v=u();++lo.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,Be,je)},map:function(n){return t(n,0,He,Xe)},entries:function(e){return n(t(e,0,He,Xe),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},Pd=Ye.prototype;Ve.prototype=$e.prototype={constructor:Ve,has:Pd.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Pd.remove,clear:Pd.clear,values:Pd.keys,size:Pd.size,empty:Pd.empty,each:Pd.each};var Ld=function(t){var n=[];for(var e in t)n.push(e);return n},Rd=function(t){var n=[];for(var e in t)n.push(t[e]);return n},qd=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},Ud=function(t){function n(t,n){var r,i,o=e(t,function(t,e){if(r)return r(t,e-1);i=t,r=n?Ze(t,n):We(t)});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},Qd=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},Kd=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},tv=function(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i},nv=function(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new tv(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)=g)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var y=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=y*y+m*m;if(x=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+g)/2))?v=c:g=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)},rv=function(){return this._root},iv=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},ov=function(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new tv(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new tv(e,s,f,o,u)),(e=c[2])&&a.push(new tv(e,r,f,s,u)),(e=c[1])&&a.push(new tv(e,s,i,o,f)),(e=c[0])&&a.push(new tv(e,r,i,s,f))}return this},uv=function(t){var n,e=[],r=[];for(this._root&&e.push(new tv(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new tv(o,u,a,f,l)),(o=i[1])&&e.push(new tv(o,f,a,c,l)),(o=i[2])&&e.push(new tv(o,u,l,f,s)),(o=i[3])&&e.push(new tv(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},av=function(t){return arguments.length?(this._x=t,this):this._x},cv=function(t){return arguments.length?(this._y=t,this):this._y},sv=er.prototype=rr.prototype;sv.copy=function(){var t,n,e=new rr(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ir(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ir(n));return e},sv.add=Gd,sv.addAll=Qe,sv.cover=Jd,sv.data=Qd,sv.extent=Kd,sv.find=nv,sv.remove=ev,sv.removeAll=Ke,sv.root=rv,sv.size=iv,sv.visit=ov,sv.visitAfter=uv,sv.x=av,sv.y=cv;var fv,lv=function(t){function n(){function t(t,n,e,r,i){var o=t.data,a=t.r,p=l+a;{if(!o)return n>s+p||rf+p||ic.index){var d=s-o.x-o.vx,v=f-o.y-o.vy,_=d*d+v*v;_t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s1?(p.on(t,n),o):p.on(t)}}},_v=function(){function t(t){var n,a=i.length,c=er(i,sr,fr).visitAfter(e);for(u=t,n=0;n=f)){(t.data!==o||t.next)&&(0===i&&(i=Zd(),p+=i*i),0===c&&(c=Zd(),p+=c*c),p1?r[0]+r.slice(2):r,+t.slice(e+1)]},xv=function(t){return t=mv(Math.abs(t)),t?t[1]:NaN},bv=function(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},wv=function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}},Mv=function(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Tv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fv=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+mv(t,Math.max(0,n+o-1))[0]},kv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")},Sv={"":Mv,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return kv(100*t,n)},r:kv,s:Tv,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Nv=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;lr.prototype=hr.prototype,hr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Ev,Av=function(t){return t},Cv=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],zv=function(t){function n(t){function n(t){var n,i,s,m=v,x=_;if("c"===d)x=g(t)+x,t="";else{t=+t;var b=t<0;if(t=g(Math.abs(t),p),b&&0==+t&&(b=!1),m=(b?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=x+("s"===d?Cv[8+fv/3]:"")+(b&&"("===c?")":""),y)for(n=-1,i=t.length;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}h&&!f&&(t=r(t,1/0));var w=m.length+t.length+x.length,M=w>1)+m+t+x+M.slice(w);break;default:t=M+m+t+x}return u(t)}t=lr(t);var e=t.fill,a=t.align,c=t.sign,s=t.symbol,f=t.zero,l=t.width,h=t.comma,p=t.precision,d=t.type,v="$"===s?i[0]:"#"===s&&/[boxX]/.test(d)?"0"+d.toLowerCase():"",_="$"===s?i[1]:/[%p]/.test(d)?"%":"",g=Sv[d],y=!d||/[defgprs%]/.test(d);return p=null==p?d?6:12:/[gprs]/.test(d)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=lr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(xv(e)/3))),o=Math.pow(10,-i),u=Cv[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?bv(t.grouping,t.thousands):Av,i=t.currency,o=t.decimal,u=t.numerals?wv(t.numerals):Av;return{format:n,formatPrefix:e}};pr({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Pv=function(t){return Math.max(0,-xv(Math.abs(t)))},Lv=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(xv(n)/3)))-xv(Math.abs(t)))},Rv=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,xv(n)-xv(t))+1},qv=function(){return new dr};dr.prototype={constructor:dr,reset:function(){this.s=this.t=0},add:function(t){vr(p_,t,this.t),vr(this,p_.s,this.s),this.s?this.t+=p_.t:this.s=p_.t},valueOf:function(){return this.s}};var Uv,Dv,Ov,Fv,Iv,Yv,Bv,jv,Hv,Xv,Vv,$v,Wv,Zv,Gv,Jv,Qv,Kv,t_,n_,e_,r_,i_,o_,u_,a_,c_,s_,f_,l_,h_,p_=new dr,d_=1e-6,v_=Math.PI,__=v_/2,g_=v_/4,y_=2*v_,m_=180/v_,x_=v_/180,b_=Math.abs,w_=Math.atan,M_=Math.atan2,T_=Math.cos,k_=Math.ceil,S_=Math.exp,N_=Math.log,E_=Math.pow,A_=Math.sin,C_=Math.sign||function(t){return t>0?1:t<0?-1:0},z_=Math.sqrt,P_=Math.tan,L_={Feature:function(t,n){xr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rd_?Hv=90:I_<-d_&&(Bv=-90),Gv[0]=Yv,Gv[1]=jv}},B_=function(t){var n,e,r,i,o,u,a;if(Hv=jv=-(Yv=Bv=1/0),Zv=[],q_(t,Y_),e=Zv.length){for(Zv.sort(Br),n=1,r=Zv[0],o=[r];nYr(r[0],r[1])&&(r[1]=i[1]),Yr(i[0],r[1])>Yr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Yr(r[1],i[0]))>u&&(u=a,Yv=i[0],jv=r[1])}return Zv=Gv=null,Yv===1/0||Bv===1/0?[[NaN,NaN],[NaN,NaN]]:[[Yv,Bv],[jv,Hv]]},j_={sphere:mr,point:Hr,lineStart:Vr,lineEnd:Zr,polygonStart:function(){j_.lineStart=Gr,j_.lineEnd=Jr},polygonEnd:function(){j_.lineStart=Vr,j_.lineEnd=Zr}},H_=function(t){Jv=Qv=Kv=t_=n_=e_=r_=i_=o_=u_=a_=0,q_(t,j_);var n=o_,e=u_,r=a_,i=n*n+e*e+r*r;return i<1e-12&&(n=e_,e=r_,r=i_,Qv2?t[2]*x_:0),n.invert=function(n){return n=t.invert(n[0]*x_,n[1]*x_),n[0]*=m_,n[1]*=m_,n},n},og=function(){function t(t,n){e.push(t=r(t,n)),t[0]*=m_,t[1]*=m_}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*x_,c=u.apply(this,arguments)*x_;return e=[],r=ni(-t[0]*x_,-t[1]*x_,0).invert,oi(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=X_([0,0]),o=X_(90),u=X_(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:X_([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:X_(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:X_(+t),n):u},n},ug=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:mr,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},ag=function(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u0)){if(u/=d,d<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}},cg=function(t,n){return b_(t[0]-n[0])=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}},fg=1e9,lg=-fg,hg=function(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=si(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}},pg=qv(),dg=function(t,n){var e=n[0],r=n[1],i=[A_(e),-T_(e),0],o=0,u=0;pg.reset();for(var a=0,c=t.length;a=0?1:-1,T=M*w,k=T>v_,S=d*x;if(pg.add(M_(S*M*A_(T),v*b+S*T_(T))),o+=k?w+M*y_:w,k^h>=e^y>=e){var N=Cr(Er(l),Er(g));Lr(N);var E=Cr(i,N);Lr(E);var A=(k^w>=0?-1:1)*gr(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(u+=k^w>=0?1:-1)}}return(o<-d_||o0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(ji))}var p,d,v,_=n(o),g=i.invert(r[0],r[1]),y=ug(),m=n(y),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=ff(d);var t=dg(p,g);d.length?(x||(o.polygonStart(),x=!0),sg(d,Hi,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}},Kg=Qg(function(){return!0},Xi,$i,[-v_,-__]),ty=function(t,n){function e(e,r,i,o){oi(o,t,n,i,e,r)}function r(t,n){return T_(t)*T_(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?v_:-v_),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(cg(n,p)||cg(d,p))&&(d[0]+=d_,d[1]+=d_,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var g;_&e||!(g=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(g[0][0],g[0][1]),t.point(g[1][0],g[1][1]),t.lineEnd()):(t.point(g[1][0],g[1][1]),t.lineEnd(),t.lineStart(),t.point(g[0][0],g[0][1])))}!v||n&&cg(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=Er(t),i=Er(n),o=[1,0,0],u=Cr(r,i),c=Ar(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=Cr(o,u),d=Pr(o,l);zr(d,Pr(u,h));var v=p,_=Ar(d,v),g=Ar(v,v),y=_*_-g*(Ar(d,d)-1);if(!(y<0)){var m=z_(y),x=Pr(v,(-_-m)/g);if(zr(x,d),x=Nr(x),!e)return x;var b,w=t[0],M=n[0],T=t[1],k=n[1];M0^x[1]<(b_(x[0]-w)v_^(w<=x[0]&&x[0]<=M)){var A=Pr(v,(-_+m)/g);return zr(A,d),[x,Nr(A)]}}}function u(n,e){var r=c?t:v_-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=T_(t),c=a>0,s=b_(a)>d_;return Qg(r,i,e,c?[0,-t]:[-v_,t-v_])},ny=function(t){return{stream:Wi(t)}};Zi.prototype={constructor:Zi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var ey=16,ry=T_(30*x_),iy=function(t,n){return+n?Ki(t,n):Qi(t)},oy=Wi({point:function(t,n){this.stream.point(t*x_,n*x_)}}),uy=function(){return eo(io).scale(155.424).center([0,33.6442])},ay=function(){return uy().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])},cy=function(){function t(t){var n=t[0],e=t[1];return a=null,i.point(n,e),a||(o.point(n,e),a)||(u.point(n,e),a)}function n(){return e=r=null,t}var e,r,i,o,u,a,c=ay(),s=uy().rotate([154,0]).center([-2,58.5]).parallels([55,65]),f=uy().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,n){a=[t,n]}};return t.invert=function(t){var n=c.scale(),e=c.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?f:c).invert(t)},t.stream=function(t){return e&&r===t?e:e=oo([c.stream(r=t),s.stream(t),f.stream(t)])},t.precision=function(t){return arguments.length?(c.precision(t),s.precision(t),f.precision(t),n()):c.precision()},t.scale=function(n){return arguments.length?(c.scale(n),s.scale(.35*n),f.scale(n),t.translate(c.translate())):c.scale()},t.translate=function(t){if(!arguments.length)return c.translate();var e=c.scale(),r=+t[0],a=+t[1];return i=c.translate(t).clipExtent([[r-.455*e,a-.238*e],[r+.455*e,a+.238*e]]).stream(l),o=s.translate([r-.307*e,a+.201*e]).clipExtent([[r-.425*e+d_,a+.12*e+d_],[r-.214*e-d_,a+.234*e-d_]]).stream(l),u=f.translate([r-.205*e,a+.212*e]).clipExtent([[r-.214*e+d_,a+.166*e+d_],[r-.115*e-d_,a+.234*e-d_]]).stream(l),n()},t.fitExtent=function(n,e){return Gi(t,n,e)},t.fitSize=function(n,e){return Ji(t,n,e)},t.scale(1070)},sy=uo(function(t){return z_(2/(1+t))});sy.invert=ao(function(t){return 2*gr(t/2)});var fy=function(){return to(sy).scale(124.75).clipAngle(179.999)},ly=uo(function(t){return(t=_r(t))&&t/A_(t)});ly.invert=ao(function(t){return t});var hy=function(){return to(ly).scale(79.4188).clipAngle(179.999)};co.invert=function(t,n){return[t,2*w_(S_(n))-__]};var py=function(){return so(co).scale(961/y_)},dy=function(){return eo(lo).scale(109.5).parallels([30,30])};ho.invert=ho;var vy=function(){return to(ho).scale(152.63)},_y=function(){return eo(po).scale(131.154).center([0,13.9389])};vo.invert=ao(w_);var gy=function(){return to(vo).scale(144.049).clipAngle(60)},yy=function(){function t(){return i=o=null,u}var n,e,r,i,o,u,a=1,c=0,s=0,f=1,l=1,h=kg,p=null,d=kg;return u={stream:function(t){return i&&o===t?i:i=h(d(o=t))},clipExtent:function(i){return arguments.length?(d=null==i?(p=n=e=r=null,kg):si(p=+i[0][0],n=+i[0][1],e=+i[1][0],r=+i[1][1]),t()):null==p?null:[[p,n],[e,r]]},scale:function(n){return arguments.length?(h=_o((a=+n)*f,a*l,c,s),t()):a},translate:function(n){return arguments.length?(h=_o(a*f,a*l,c=+n[0],s=+n[1]),t()):[c,s]},reflectX:function(n){return arguments.length?(h=_o(a*(f=n?-1:1),a*l,c,s),t()):f<0},reflectY:function(n){return arguments.length?(h=_o(a*f,a*(l=n?-1:1),c,s),t()):l<0},fitExtent:function(t,n){return Gi(u,t,n)},fitSize:function(t,n){return Ji(u,t,n)}}};go.invert=ao(gr);var my=function(){return to(go).scale(249.5).clipAngle(90+d_)};yo.invert=ao(function(t){return 2*w_(t)});var xy=function(){return to(yo).scale(250).clipAngle(142)};mo.invert=function(t,n){return[-n,2*w_(S_(t))-__]};var by=function(){var t=so(mo),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},wy=function(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=bo(e),t.y=Mo(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=ko(t),c=So(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=xo,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},My=function(){return this.eachAfter(No)},Ty=function(t){var n,e,r,i,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},Sy=function(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e=0;)e+=r[i].value;n.value=e})},Ey=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Ay=function(t){for(var n=this,e=Eo(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},Cy=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},zy=function(){var t=[];return this.each(function(n){t.push(n)}),t},Py=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Ly=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ro.prototype=Ao.prototype={constructor:Ro,count:My,each:Ty,eachAfter:Sy,eachBefore:ky,sum:Ny,sort:Ey,path:Ay,ancestors:Cy,descendants:zy,leaves:Py,links:Ly,copy:Co};var Ry=function(t){for(var n=(t=t.slice()).length,e=null,r=e;n;){var i=new qo(t[n-1]);r=r?r.next=i:e=i,t[void 0]=t[--n]}return{head:e,tail:r}},qy=function(t){return Do(Ry(t),[])},Uy=function(t){return Xo(t),t},Dy=function(t){return function(){return t}},Oy=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Go(n)).eachAfter(Jo(i,.5)).eachBefore(Qo(1)):t.eachBefore(Go(Zo)).eachAfter(Jo(Wo,1)).eachAfter(Jo(i,t.r/Math.min(e,r))).eachBefore(Qo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Wo;return t.radius=function(e){return arguments.length?(n=Vo(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:Dy(+n),t):i},t},Fy=function(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)},Iy=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a0)throw new Error("cycle");return o}var n=Ko,e=tu;return t.id=function(e){return arguments.length?(n=$o(e),t):n},t.parentId=function(n){return arguments.length?(e=$o(n),t):e},t};au.prototype=Object.create(Ro.prototype);var Vy=function(){function t(t){var r=cu(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.xf.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){ou(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=ru(a),i=eu(i),a&&i;)c=eu(c),u=ru(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(iu(uu(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!ru(u)&&(u.t=a,u.m+=l-f),i&&!eu(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=nu,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t},$y=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a1?n:1)},e}(Wy),Gy=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Fy),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p>>1;f[v]a-i){var y=(r*g+u*_)/e;o(t,p,_,r,i,y,a),o(p,n,g,y,i,u,a)}else{var m=(i*g+a*_)/e;o(t,p,_,r,i,u,m),o(p,n,g,r,m,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u1?n:1)},e +}(Wy),tm=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;na!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f},om=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}},fm=function(){var t=sm.apply(this,arguments);return function(){return Math.exp(t())}},lm=function(t){return function(){for(var n=0,e=0;e=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(t){return void a.call("error",r,t)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=d("beforesend","progress","load","error"),c=Ye(),s=new XMLHttpRequest,f=null,l=null,h=0;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=mu(o)),null!=o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return r.get(n)}return r},vm=function(t,n){return function(e,r){var i=dm(e).mimeType(t).response(n);if(null!=r){if("function"!=typeof r)throw new Error("invalid callback: "+r);return i.get(r)}return i}},_m=vm("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),gm=vm("application/json",function(t){return JSON.parse(t.responseText)}),ym=vm("text/plain",function(t){return t.responseText}),mm=vm("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),xm=function(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=dm(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(bu(n,r=t)):r},o.row(r),i?o.get(i):o}},bm=xm("text/csv",Od),wm=xm("text/tab-separated-values",jd),Mm=Array.prototype,Tm=Mm.map,km=Mm.slice,Sm={name:"implicit"},Nm=function(t){return function(){return t}},Em=function(t){return+t},Am=[0,1],Cm=function(n,e,i){var o,u=n[0],a=n[n.length-1],c=r(u,a,null==e?10:e);switch(i=lr(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(u),Math.abs(a));return null!=i.precision||isNaN(o=Lv(c,s))||(i.precision=o),t.formatPrefix(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Rv(c,Math.max(Math.abs(u),Math.abs(a))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Pv(c))||(i.precision=o-2*("%"===i.type))}return t.format(i)},zm=function(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u0?t>1?Zu(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rm:null};var qm=Rm.range,Um=6e4,Dm=6048e5,Om=Zu(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Fm=Om.range,Im=Zu(function(t){t.setTime(Math.floor(t/Um)*Um)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getMinutes()}),Ym=Im.range,Bm=Zu(function(t){var n=t.getTimezoneOffset()*Um%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),jm=Bm.range,Hm=Zu(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/864e5},function(t){return t.getDate()-1}),Xm=Hm.range,Vm=Gu(0),$m=Gu(1),Wm=Gu(2),Zm=Gu(3),Gm=Gu(4),Jm=Gu(5),Qm=Gu(6),Km=Vm.range,tx=$m.range,nx=Wm.range,ex=Zm.range,rx=Gm.range,ix=Jm.range,ox=Qm.range,ux=Zu(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ax=ux.range,cx=Zu(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});cx.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var sx=cx.range,fx=Zu(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getUTCMinutes()}),lx=fx.range,hx=Zu(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),px=hx.range,dx=Zu(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),vx=dx.range,_x=Ju(0),gx=Ju(1),yx=Ju(2),mx=Ju(3),xx=Ju(4),bx=Ju(5),wx=Ju(6),Mx=_x.range,Tx=gx.range,kx=yx.range,Sx=mx.range,Nx=xx.range,Ex=bx.range,Ax=wx.range,Cx=Zu(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),zx=Cx.range,Px=Zu(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Px.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Lx,Rx=Px.range,qx={"-":"",_:" ",0:"0"},Ux=/^\s*\d+/,Dx=/^%/,Ox=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Za({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Fx=Date.prototype.toISOString?Ga:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ"),Ix=+new Date("2000-01-01T00:00:00.000Z")?Ja:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),Yx=1e3,Bx=60*Yx,jx=60*Bx,Hx=24*jx,Xx=7*Hx,Vx=30*Hx,$x=365*Hx,Wx=function(){return tc(cx,ux,Vm,Hm,Bm,Im,Om,Rm,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])},Zx=function(){return tc(Px,Cx,_x,dx,hx,fx,Om,Rm,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])},Gx=function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})},Jx=Gx("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Qx=Gx("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Kx=Gx("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),tb=Gx("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),nb=Oh(Vt(300,.5,0),Vt(-240,.5,1)),eb=Oh(Vt(-100,.75,.35),Vt(80,1.5,.8)),rb=Oh(Vt(260,.75,.35),Vt(80,1.5,.8)),ib=Vt(),ob=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return ib.h=360*t-100,ib.s=1.5-1.5*n,ib.l=.8-.9*n,ib+""},ub=nc(Gx("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),ab=nc(Gx("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),cb=nc(Gx("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),sb=nc(Gx("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),fb=function(t){return function(){return t}},lb=Math.abs,hb=Math.atan2,pb=Math.cos,db=Math.max,vb=Math.min,_b=Math.sin,gb=Math.sqrt,yb=1e-12,mb=Math.PI,xb=mb/2,bb=2*mb,wb=function(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-xb,p=u.apply(this,arguments)-xb,d=lb(p-h),v=p>h;if(c||(c=t=Re()),lyb)if(d>bb-yb)c.moveTo(l*pb(h),l*_b(h)),c.arc(0,0,l,h,p,!v),f>yb&&(c.moveTo(f*pb(p),f*_b(p)),c.arc(0,0,f,p,h,v));else{var _,g,y=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,k=T>yb&&(i?+i.apply(this,arguments):gb(f*f+l*l)),S=vb(lb(l-f)/2,+r.apply(this,arguments)),N=S,E=S;if(k>yb){var A=ic(k/f*_b(T)),C=ic(k/l*_b(T));(w-=2*A)>yb?(A*=v?1:-1,x+=A,b-=A):(w=0,x=b=(h+p)/2),(M-=2*C)>yb?(C*=v?1:-1,y+=C,m-=C):(M=0,y=m=(h+p)/2)}var z=l*pb(y),P=l*_b(y),L=f*pb(b),R=f*_b(b);if(S>yb){var q=l*pb(m),U=l*_b(m),D=f*pb(x),O=f*_b(x);if(dyb?fc(z,P,D,O,q,U,L,R):[L,R],I=z-F[0],Y=P-F[1],B=q-F[0],j=U-F[1],H=1/_b(rc((I*B+Y*j)/(gb(I*I+Y*Y)*gb(B*B+j*j)))/2),X=gb(F[0]*F[0]+F[1]*F[1]);N=vb(S,(f-X)/(H-1)),E=vb(S,(l-X)/(H+1))}}M>yb?E>yb?(_=lc(D,O,z,P,l,E,v),g=lc(q,U,L,R,l,E,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),Eyb&&w>yb?N>yb?(_=lc(L,R,q,U,f,-N,v),g=lc(z,P,D,O,f,-N,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),N=f;--l)s.point(_[l],g[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),g[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):g[n]))}if(p)return s=null,p+""||null}function n(){return Tb().defined(u).curve(c).context(a)}var e=pc,r=null,i=fb(0),o=dc,u=fb(!0),a=null,c=Mb,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:fb(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:fb(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:fb(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t},Sb=function(t,n){return nt?1:n>=t?0:NaN},Nb=function(t){return t},Eb=function(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),g=Math.min(bb,Math.max(-bb,o.apply(this,arguments)-_)),y=Math.min(Math.abs(g)/h,u.apply(this,arguments)),m=y*(g<0?-1:1);for(a=0;a0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(g-h*m)/p:0;a0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:y};return v}var n=Nb,e=Sb,r=null,i=fb(0),o=fb(bb),u=fb(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:fb(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:fb(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:fb(+n),t):u},t},Ab=_c(Mb);vc.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Cb=function(){return gc(Tb().curve(Ab))},zb=function(){var t=kb().curve(Ab),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return gc(e())},delete t.lineX0,t.lineEndAngle=function(){return gc(r())},delete t.lineX1,t.lineInnerRadius=function(){return gc(i())},delete t.lineY0,t.lineOuterRadius=function(){return gc(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t},Pb={draw:function(t,n){var e=Math.sqrt(n/mb);t.moveTo(e,0),t.arc(0,0,e,0,bb)}},Lb={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Rb=Math.sqrt(1/3),qb=2*Rb,Ub={draw:function(t,n){var e=Math.sqrt(n/qb),r=e*Rb;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Db=Math.sin(mb/10)/Math.sin(7*mb/10),Ob=Math.sin(bb/10)*Db,Fb=-Math.cos(bb/10)*Db,Ib={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Ob*e,i=Fb*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=bb*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},Yb={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Bb=Math.sqrt(3),jb={draw:function(t,n){var e=-Math.sqrt(n/(3*Bb));t.moveTo(0,2*e),t.lineTo(-Bb*e,-e),t.lineTo(Bb*e,-e),t.closePath()}},Hb=-.5,Xb=Math.sqrt(3)/2,Vb=1/Math.sqrt(12),$b=3*(Vb/2+1),Wb={draw:function(t,n){var e=Math.sqrt(n/$b),r=e/2,i=e*Vb,o=r,u=e*Vb+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Hb*r-Xb*i,Xb*r+Hb*i),t.lineTo(Hb*o-Xb*u,Xb*o+Hb*u),t.lineTo(Hb*a-Xb*c,Xb*a+Hb*c),t.lineTo(Hb*r+Xb*i,Hb*i-Xb*r),t.lineTo(Hb*o+Xb*u,Hb*u-Xb*o),t.lineTo(Hb*a+Xb*c,Hb*c-Xb*a),t.closePath()}},Zb=[Pb,Lb,Ub,Yb,Ib,jb,Wb],Gb=function(){function t(){var t;if(r||(r=t=Re()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=fb(Pb),e=fb(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:fb(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},Jb=function(){};mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:yc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Qb=function(t){return new mc(t)};xc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Kb=function(t){return new xc(t)};bc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var tw=function(t){return new bc(t)};wc.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var nw=function t(n){function e(t){return 1===n?new mc(t):new wc(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Tc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Mc(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ew=function t(n){function e(t){return new Tc(t,n)}return e.tension=function(n){return t(+n)},e}(0);kc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var rw=function t(n){function e(t){return new kc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var iw=function t(n){function e(t){return new Sc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0: +this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ow=function t(n){function e(t){return n?new Ec(t,n):new Tc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Ac.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var uw=function t(n){function e(t){return n?new Ac(t,n):new kc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Cc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var aw=function t(n){function e(t){return n?new Cc(t,n):new Sc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);zc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};var cw=function(t){return new zc(t)};Uc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:qc(this,this._t0,Rc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,qc(this,Rc(this,e=Lc(this,t,n)),e);break;default:qc(this,this._t0,e=Lc(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Dc.prototype=Object.create(Uc.prototype)).point=function(t,n){Uc.prototype.point.call(this,n,t)},Oc.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Yc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=Bc(t),i=Bc(n),o=0,u=1;u=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var fw=function(t){return new jc(t,.5)},lw=Array.prototype.slice,hw=function(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i=0;)e[n]=n;return e},dw=function(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o0){for(var e,r,i,o=0,u=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u=a)return null;var c=t-i.site[0],s=n-i.site[1],f=c*c+s*s;do{i=o.cells[r=u],u=null,i.halfedges.forEach(function(e){var r=o.edges[e],a=r.left;if(a!==i.site&&a||(a=r.right)){var c=t-a[0],s=n-a[1],l=c*c+s*s;le?(e+r)/2:Math.min(0,e)||Math.max(0,r),o>i?(i+o)/2:Math.min(0,i)||Math.max(0,o))}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=m.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=N(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new Ns(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=A.length;r0?bl(this).transition().duration(S).call(u,f,a):bl(this).call(n.transform,f)}}function h(){if(y.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,c=u.length;for(As(),e=0;e + + + var edges = []; + + links.forEach(function(e) { + var sourceNode = nodes.filter(function(n) { + return n.name === e.source; + })[0], + targetNode = nodes.filter(function(n) { + return n.name === e.target; + })[0]; + + edges.push({ + source: sourceNode, + target: targetNode, + value: 1}); + + }); + + edges.forEach(function(e) { + + if (!e.source["linkcount"]) e.source["linkcount"] = 0; + if (!e.target["linkcount"]) e.target["linkcount"] = 0; + + e.source["linkcount"]++; + e.target["linkcount"]++; + }); + + //var width = 960, height = 600; + var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; + var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; + var svg = d3.select("#vis").append("svg") + .attr("width", width) + .attr("height", height); + + // updated for d3 v4. + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody().strength(strength)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide(35)); + + // Node charge strength. Repel strength greater for less links. + //function strength(d) { return -50/d["linkcount"] ; } + function strength(d) { return -25 ; } + + // Link distance. Distance increases with number of links at source and target + function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } + + // Link strength. Strength is less for highly connected nodes (move towards target dist) + function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } + + simulation + .nodes(nodes) + .on("tick", tick); + + simulation.force("link") + .links(edges) + .distance(distance) + .strength(strengthl); + + // build the arrow. + svg.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .enter().append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 25) // Moves the arrow head out, allow for radius + .attr("refY", 0) // -1.5 + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + var link = svg.selectAll('.link') + .data(edges) + .enter().append('line') + .attr("class", "link") + .attr("marker-end", "url(#end)"); + + var node = svg.selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", function(d) { return "node " + d.type;}) + .attr('transform', function(d) { + return "translate(" + d.x + "," + d.y + ")"}) + .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); + + node.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + // add the nodes + node.append('circle') + .attr('r', 16) + ; + + // add text + node.append("text") + .attr("x", 12) + .attr("dy", "-1.1em") + .text(function(d) {return d.name;}); + + node.on("mouseover", function(d) { + + var g = d3.select(this); // the node (table) + + // tooltip + + var fields = d.fields; + var fieldformat = ""; + fields.forEach(function(d) { + fieldformat += ""; + }); + fieldformat += "
      "+ d.name+""+ d.type+""+ d.disp+"
      "; + var tiplength = d.fields.length; + + // Define 'div' for tooltips + var div = d3.select("body").append("div") // declare the tooltip div + .attr("class", "tooltip") // apply the 'tooltip' class + .style("opacity", 0) + .html('
      ' + d.name + '
      ' + fieldformat) + .style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px") + .style("top", tooltop(tiplength))// or ... + .transition() + .duration(800) + .style("opacity", 0.9); + }); + + function tooltop(tiplength) { + //aim to ensure tooltip is fully visible whenver possible + return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px" + } + + node.on("mouseout", function(d) { + d3.select("body").select('div.tooltip').remove(); + }); + + // instead of waiting for force to end with : force.on('end', function() + // use .on("tick", instead. Here is the tick function + function tick() { + node.attr('transform', function(d) { + d.x = Math.max(30, Math.min(width - 16, d.x)); + d.y = Math.max(30, Math.min(height - 16, d.y)); + return "translate(" + d.x + "," + d.y + ")"; }); + + link.attr('x1', function(d) {return d.source.x;}) + .attr('y1', function(d) {return d.source.y;}) + .attr('x2', function(d) {return d.target.x;}) + .attr('y2', function(d) {return d.target.y;}); + }; + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }; + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + }; + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }; + +}; \ No newline at end of file diff --git a/applications/examples/static/css/d3_graph.css b/applications/examples/static/css/d3_graph.css new file mode 100644 index 00000000..fd0ab8fb --- /dev/null +++ b/applications/examples/static/css/d3_graph.css @@ -0,0 +1,33 @@ + .node {fill: steelblue; + stroke: #636363; + stroke-width: 1px;} + + .auth {fill: lightgrey;} + + .table {r: 10;} + + .link {stroke: #bbbbbb; + stroke-width: 2px;} + td {padding: 4px;} + + div.tooltip { + position: absolute; + text-align: left; + /* width: 140px; */ + /* height: 28px;*/ + padding: 0px 5px 0px 5px; + padding-top: 0px; + font: 12px sans-serif; + background: #fff7bc; + border: solid 1px #aaa; + border-radius: 6px; + pointer-events: none;} + + h5 { font: 14px sans-serif; + background : #ec7014; + color: #ffffe5; + padding: 5px 2px 5px 2px; + margin-top: 1px;} + path { + fill: #aaaaaa;} + diff --git a/applications/examples/static/js/d3.min.js b/applications/examples/static/js/d3.min.js new file mode 100644 index 00000000..6ba6b41e --- /dev/null +++ b/applications/examples/static/js/d3.min.js @@ -0,0 +1,8 @@ +// https://d3js.org Version 4.7.4. Copyright 2017 Mike Bostock. +(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t){return function(n,e){return Ls(t(n),e)}}function e(t,n){return[t,n]}function r(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Js?i*=10:o>=Qs?i*=5:o>=Ks&&(i*=2),n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function g(t,n){for(var e,r=0,i=t.length;r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function S(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function q(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function D(t,n){return function(){this.setAttribute(t,n)}}function O(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function F(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function Y(t){return function(){this.style.removeProperty(t)}}function B(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t){return function(){delete this[t]}}function X(t,n){return function(){this[t]=n}}function V(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function $(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new Z(t)}function Z(t){this._node=t,this._names=$(t.getAttribute("class")||"")}function G(t,n){for(var e=W(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Rl.exec(t))?kt(parseInt(n[1],16)):(n=ql.exec(t))?new At(n[1],n[2],n[3],1):(n=Ul.exec(t))?new At(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Dl.exec(t))?St(n[1],n[2],n[3],n[4]):(n=Ol.exec(t))?St(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Fl.exec(t))?Ct(n[1],n[2]/100,n[3]/100,1):(n=Il.exec(t))?Ct(n[1],n[2]/100,n[3]/100,n[4]):Yl.hasOwnProperty(t)?kt(Yl[t]):"transparent"===t?new At(NaN,NaN,NaN,0):null}function kt(t){return new At(t>>16&255,t>>8&255,255&t,1)}function St(t,n,e,r){return r<=0&&(t=n=e=NaN),new At(t,n,e,r)}function Nt(t){return t instanceof Mt||(t=Tt(t)),t?(t=t.rgb(),new At(t.r,t.g,t.b,t.opacity)):new At}function Et(t,n,e,r){return 1===arguments.length?Nt(t):new At(t,n,e,null==r?1:r)}function At(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ct(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Lt(t,n,e,r)}function zt(t){if(t instanceof Lt)return new Lt(t.h,t.s,t.l,t.opacity);if(t instanceof Mt||(t=Tt(t)),!t)return new Lt;if(t instanceof Lt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&c<1?0:u,new Lt(u,a,c,t.opacity)}function Pt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function qt(t){if(t instanceof Dt)return new Dt(t.l,t.a,t.b,t.opacity);if(t instanceof Ht){var n=t.h*Bl;return new Dt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof At||(t=Nt(t));var e=Yt(t.r),r=Yt(t.g),i=Yt(t.b),o=Ot((.4124564*e+.3575761*r+.1804375*i)/Hl),u=Ot((.2126729*e+.7151522*r+.072175*i)/Xl);return new Dt(116*u-16,500*(o-u),200*(u-Ot((.0193339*e+.119192*r+.9503041*i)/Vl)),t.opacity)}function Ut(t,n,e,r){return 1===arguments.length?qt(t):new Dt(t,n,e,null==r?1:r)}function Dt(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Ot(t){return t>Gl?Math.pow(t,1/3):t/Zl+$l}function Ft(t){return t>Wl?t*t*t:Zl*(t-$l)}function It(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Yt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bt(t){if(t instanceof Ht)return new Ht(t.h,t.c,t.l,t.opacity);t instanceof Dt||(t=qt(t));var n=Math.atan2(t.b,t.a)*jl;return new Ht(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function jt(t,n,e,r){return 1===arguments.length?Bt(t):new Ht(t,n,e,null==r?1:r)}function Ht(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Xt(t){if(t instanceof $t)return new $t(t.h,t.s,t.l,t.opacity);t instanceof At||(t=Nt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(ih*r+eh*n-rh*e)/(ih+eh-rh),o=r-i,u=(nh*(e-i)-Kl*o)/th,a=Math.sqrt(u*u+o*o)/(nh*i*(1-i)),c=a?Math.atan2(u,o)*jl-120:NaN;return new $t(c<0?c+360:c,a,i,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Xt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function Zt(t,n){return function(e){return t+e*n}}function Gt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Jt(t,n){var e=n-t;return e?Zt(t,e>180||e<-180?e-360*Math.round(e/360):e):ph(isNaN(t)?n:t)}function Qt(t){return 1==(t=+t)?Kt:function(n,e){return e-n?Gt(n,e,t):ph(isNaN(n)?e:n)}}function Kt(t,n){var e=n-t;return e?Zt(t,e):ph(isNaN(t)?n:t)}function tn(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:mh(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:mh(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:mh(t,e)},{i:a-2,x:mh(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e=0&&n._call.call(null,t),n=n._next;--Ih}function mn(){Xh=(Hh=$h.now())+Vh,Ih=Yh=0;try{yn()}finally{Ih=0,bn(),Xh=0}}function xn(){var t=$h.now(),n=t-Hh;n>jh&&(Vh-=n,Hh=t)}function bn(){for(var t,n,e=sh,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:sh=n);fh=t,wn(r)}function wn(t){if(!Ih){Yh&&(Yh=clearTimeout(Yh));var n=t-Xh;n>24?(t<1/0&&(Yh=setTimeout(mn,n)),Bh&&(Bh=clearInterval(Bh))):(Bh||(Hh=Xh,Bh=setInterval(xn,jh)),Ih=1,Wh(mn))}}function Mn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Kh)throw new Error("too late");return e}function Tn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>np)throw new Error("too late");return e}function kn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function Sn(t,n,e){function r(t){e.state=tp,e.timer.restart(i,e.delay,e.time),e.delay<=t&&i(t-e.delay)}function i(r){var s,f,l,h;if(e.state!==tp)return u();for(s in c)if(h=c[s],h.name===e.name){if(h.state===ep)return Zh(i);h.state===rp?(h.state=op,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete c[s]):+s=0&&(t=t.slice(0,n)),!t||"start"===t})}function Hn(t,n,e){var r,i,o=jn(n)?Mn:Tn;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function Xn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Vn(t,n){var e,r,i;return function(){var o=al(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function $n(t){return function(){this.style.removeProperty(t)}}function Wn(t,n,e){var r,i;return function(){var o=al(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function Zn(t,n,e){var r,i,o;return function(){var u=al(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Gn(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function Jn(t){return function(){this.textContent=t}}function Qn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Kn(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function te(t){return vt().transition(t)}function ne(){return++Ep}function ee(t){return+t}function re(t){return t*t}function ie(t){return t*(2-t)}function oe(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function ue(t){return t*t*t}function ae(t){return--t*t*t+1}function ce(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function se(t){return 1-Math.cos(t*Rp)}function fe(t){return Math.sin(t*Rp)}function le(t){return(1-Math.cos(Lp*t))/2}function he(t){return Math.pow(2,10*t-10)}function pe(t){return 1-Math.pow(2,-10*t)}function de(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function ve(t){return 1-Math.sqrt(1-t*t)}function _e(t){return Math.sqrt(1- --t*t)}function ge(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ye(t){return 1-me(1-t)}function me(t){return(t=+t)Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,id(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],S){case ud:case od:N&&(m=Math.max(P-l,Math.min(R-v,m)),h=l+m,_=v+m),E&&(x=Math.max(L-p,Math.min(q-g,x)),d=p+x,y=g+x);break;case ad:N<0?(m=Math.max(P-l,Math.min(R-l,m)),h=l+m,_=v):N>0&&(m=Math.max(P-v,Math.min(R-v,m)),h=l,_=v+m),E<0?(x=Math.max(L-p,Math.min(q-p,x)),d=p+x,y=g):E>0&&(x=Math.max(L-g,Math.min(q-g,x)),d=p,y=g+x);break;case cd:N&&(h=Math.max(P,Math.min(R,l-m*N)),_=Math.max(P,Math.min(R,v+m*N))),E&&(d=Math.max(L,Math.min(q,p-x*E)),y=Math.max(L,Math.min(q,g+x*E)))}_0&&(l=h-m),E<0?g=y-x:E>0&&(p=d-x),S=ud,Y.attr("cursor",hd.selection),o());break;default:return}id()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:S===cd&&(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad,o());break;case 32:S===ud&&(t.event.altKey?(N&&(v=_-m*N,l=h+m*N),E&&(g=y-x*E,p=d+x*E),S=cd):(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad),Y.attr("cursor",hd[k]),o());break;default:return}id()}if(t.event.touches){if(t.event.changedTouches.length=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u}while((l=f<<1|s)==(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function Qe(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;ef&&(f=r),il&&(l=i));for(f",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Sv[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function pr(n){ +return Ev=zv(n),t.format=Ev.format,t.formatPrefix=Ev.formatPrefix,Ev}function dr(){this.reset()}function vr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function _r(t){return t>1?0:t<-1?v_:Math.acos(t)}function gr(t){return t>1?__:t<-1?-__:Math.asin(t)}function yr(t){return(t=A_(t/2))*t}function mr(){}function xr(t,n){t&&R_.hasOwnProperty(t.type)&&R_[t.type](t,n)}function br(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=T_(n),u=A_(n),a=Iv*u,c=Fv*o+a*T_(i),s=a*r*A_(i);U_.add(M_(s,c)),Ov=t,Fv=o,Iv=u}function Nr(t){return[M_(t[1],t[0]),gr(t[2])]}function Er(t){var n=t[0],e=t[1],r=T_(e);return[r*T_(n),r*A_(n),A_(e)]}function Ar(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Cr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function zr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Pr(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Lr(t){var n=z_(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Rr(t,n){Zv.push(Gv=[Yv=t,jv=t]),nHv&&(Hv=n)}function qr(t,n){var e=Er([t*x_,n*x_]);if(Wv){var r=Cr(Wv,e),i=[r[1],-r[0],0],o=Cr(i,r);Lr(o),o=Nr(o);var u,a=t-Xv,c=a>0?1:-1,s=o[0]*m_*c,f=b_(a)>180;f^(c*XvHv&&(Hv=u):(s=(s+360)%360-180,f^(c*XvHv&&(Hv=n))),f?tYr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t):jv>=Yv?(tjv&&(jv=t)):t>Xv?Yr(Yv,t)>Yr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t)}else Zv.push(Gv=[Yv=t,jv=t]);nHv&&(Hv=n),Wv=e,Xv=t}function Ur(){Y_.point=qr}function Dr(){Gv[0]=Yv,Gv[1]=jv,Y_.point=Rr,Wv=null}function Or(t,n){if(Wv){var e=t-Xv;I_.add(b_(e)>180?e+(e>0?360:-360):e)}else Vv=t,$v=n;O_.point(t,n),qr(t,n)}function Fr(){O_.lineStart()}function Ir(){Or(Vv,$v),O_.lineEnd(),b_(I_)>d_&&(Yv=-(jv=180)),Gv[0]=Yv,Gv[1]=jv,Wv=null}function Yr(t,n){return(n-=t)<0?n+360:n}function Br(t,n){return t[0]-n[0]}function jr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nv_?t-y_:t<-v_?t+y_:t,n]}function ni(t,n,e){return(t%=y_)?n||e?V_(ri(t),ii(n,e)):ri(t):n||e?ii(n,e):ti}function ei(t){return function(n,e){return n+=t,[n>v_?n-y_:n<-v_?n+y_:n,e]}}function ri(t){var n=ei(t);return n.invert=ei(-t),n}function ii(t,n){function e(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*r+a*i;return[M_(c*o-f*u,a*r-s*i),gr(f*o+c*u)]}var r=T_(t),i=A_(t),o=T_(n),u=A_(n);return e.invert=function(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*o-c*u;return[M_(c*o+s*u,a*r+f*i),gr(f*r-a*i)]},e}function oi(t,n,e,r,i,o){if(e){var u=T_(n),a=A_(n),c=r*e;null==i?(i=n+r*y_,o=n-c/2):(i=ui(u,i),o=ui(u,o),(r>0?io)&&(i+=r*y_));for(var s,f=i;r>0?f>o:f0)do{s.point(0===f||3===f?t:e,f>1?r:n)}while((f=(f+a+4)%4)!==l);else s.point(o[0],o[1])}function u(r,i){return b_(r[0]-t)0?0:3:b_(r[0]-e)0?2:1:b_(r[1]-n)0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&S.point(t,n)}function s(){for(var n=0,e=0,i=_.length;er&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){S=N,v=[],_=[],k=!0}function l(){var t=s(),n=k&&t,e=(v=ff(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&sg(v,a,t,o,u),u.polygonEnd()),S=u,v=_=g=null}function h(){E.point=d,_&&_.push(g=[]),T=!0,M=!1,b=w=NaN}function p(){v&&(d(y,m),x&&M&&N.rejoin(),v.push(N.result())),E.point=c,M&&S.lineEnd()}function d(o,u){var a=i(o,u);if(_&&g.push([o,u]),T)y=o,m=u,x=a,T=!1,a&&(S.lineStart(),S.point(o,u));else if(a&&M)S.point(o,u);else{var c=[b=Math.max(lg,Math.min(fg,b)),w=Math.max(lg,Math.min(fg,w))],s=[o=Math.max(lg,Math.min(fg,o)),u=Math.max(lg,Math.min(fg,u))];ag(c,s,t,n,e,r)?(M||(S.lineStart(),S.point(c[0],c[1])),S.point(s[0],s[1]),a||S.lineEnd(),k=!1):a&&(S.lineStart(),S.point(o,u),k=!1)}b=o,w=u,M=a}var v,_,g,y,m,x,b,w,M,T,k,S=u,N=ug(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function fi(){_g.point=hi,_g.lineEnd=li}function li(){_g.point=_g.lineEnd=mr}function hi(t,n){t*=x_,n*=x_,$_=t,W_=A_(n),Z_=T_(n),_g.point=pi}function pi(t,n){t*=x_,n*=x_;var e=A_(n),r=T_(n),i=b_(t-$_),o=T_(i),u=A_(i),a=r*u,c=Z_*e-W_*r*o,s=W_*e+Z_*r*o;vg.add(M_(z_(a*a+c*c),s)),$_=t,W_=e,Z_=r}function di(t,n){return!(!t||!wg.hasOwnProperty(t.type))&&wg[t.type](t,n)}function vi(t,n){return 0===xg(t,n)}function _i(t,n){var e=xg(t[0],t[1]);return xg(t[0],n)+xg(n,t[1])<=e+d_}function gi(t,n){return!!dg(t.map(yi),mi(n))}function yi(t){return t=t.map(mi),t.pop(),t}function mi(t){return[t[0]*x_,t[1]*x_]}function xi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function bi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function wi(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return Gs(k_(o/_)*_,i,_).map(h).concat(Gs(k_(s/g)*g,c,g).map(p)).concat(Gs(k_(r/d)*d,e,d).filter(function(t){return b_(t%_)>d_}).map(f)).concat(Gs(k_(a/v)*v,u,v).filter(function(t){return b_(t%g)>d_}).map(l))}var e,r,i,o,u,a,c,s,f,l,h,p,d=10,v=d,_=90,g=360,y=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[h(o).concat(p(c).slice(1),h(i).reverse().slice(1),p(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(y)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(y)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(_=+n[0],g=+n[1],t):[_,g]},t.stepMinor=function(n){return arguments.length?(d=+n[0],v=+n[1],t):[d,v]},t.precision=function(n){return arguments.length?(y=+n,f=xi(a,u,90),l=bi(r,e,y),h=xi(s,c,90),p=bi(o,i,y),t):y},t.extentMajor([[-180,-90+d_],[180,90-d_]]).extentMinor([[-180,-80-d_],[180,80+d_]])}function Mi(){return wi()()}function Ti(){Eg.point=ki}function ki(t,n){Eg.point=Si,G_=Q_=t,J_=K_=n}function Si(t,n){Ng.add(K_*t-Q_*n),Q_=t,K_=n}function Ni(){Si(G_,J_)}function Ei(t,n){tzg&&(zg=t),nPg&&(Pg=n)}function Ai(t,n){Rg+=t,qg+=n,++Ug}function Ci(){jg.point=zi}function zi(t,n){jg.point=Pi,Ai(eg=t,rg=n)}function Pi(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,Ai(eg=t,rg=n)}function Li(){jg.point=Ai}function Ri(){jg.point=Ui}function qi(){Di(tg,ng)}function Ui(t,n){jg.point=Di,Ai(tg=eg=t,ng=rg=n)}function Di(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,i=rg*t-eg*n,Ig+=i*(eg+t),Yg+=i*(rg+n),Bg+=3*i,Ai(eg=t,rg=n)}function Oi(t){this._context=t}function Fi(t,n){Gg.point=Ii,Xg=$g=t,Vg=Wg=n}function Ii(t,n){$g-=t,Wg-=n,Zg.add(z_($g*$g+Wg*Wg)),$g=t,Wg=n}function Yi(){this._string=[]}function Bi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function ji(t){return t.length>1}function Hi(t,n){return((t=t.x)[0]<0?t[1]-__-d_:__-t[1])-((n=n.x)[0]<0?n[1]-__-d_:__-n[1])}function Xi(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?v_:-v_,c=b_(o-e);b_(c-v_)0?__:-__),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=v_&&(b_(e-i)d_?w_((A_(n)*(o=T_(r))*A_(e)-A_(r)*(i=T_(n))*A_(t))/(i*o*u)):(n+r)/2}function $i(t,n,e,r){var i;if(null==t)i=e*__,r.point(-v_,i),r.point(0,i),r.point(v_,i),r.point(v_,0),r.point(v_,-i),r.point(0,-i),r.point(-v_,-i),r.point(-v_,0),r.point(-v_,i);else if(b_(t[0]-n[0])>d_){var o=t[0]4*n&&v--){var x=u+h,b=a+p,w=c+d,M=z_(x*x+b*b+w*w),T=gr(w/=M),k=b_(b_(w)-1)n||b_((g*A+y*C)/m-.5)>.3||u*h+a*p+c*d2?t[2]%360*x_:0,i()):[b*m_,w*m_,M*m_]},n.precision=function(t){return arguments.length?(A=iy(r,E=t*t),o()):z_(E)},n.fitExtent=function(t,e){return Gi(n,t,e)},n.fitSize=function(t,e){return Ji(n,t,e)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function eo(t){var n=0,e=v_/3,r=no(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*x_,e=t[1]*x_):[n*m_,e*m_]},i}function ro(t){function n(t,n){return[t*e,A_(n)/e]}var e=T_(t);return n.invert=function(t,n){return[t/e,gr(n*e)]},n}function io(t,n){function e(t,n){var e=z_(o-2*i*A_(n))/i;return[e*A_(t*=i),u-e*T_(t)]}var r=A_(t),i=(r+A_(n))/2;if(b_(i)0?n<-__+d_&&(n=-__+d_):n>__-d_&&(n=__-d_);var e=o/E_(fo(n),i);return[e*A_(i*t),o-e*T_(i*t)]}var r=T_(t),i=t===n?A_(t):N_(r/T_(n))/N_(fo(n)/fo(t)),o=r*E_(fo(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=C_(i)*z_(t*t+e*e);return[M_(t,b_(e))/i*C_(e),2*w_(E_(o/r,1/i))-__]},e):co}function ho(t,n){return[t,n]}function po(t,n){function e(t,n){var e=o-n,r=i*t;return[e*A_(r),o-e*T_(r)]}var r=T_(t),i=t===n?A_(t):(r-T_(n))/(n-t),o=r/i+t;return b_(i)=0;)n+=e[r].value;else n=1;t.value=n}function Eo(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function Ao(t,n){var e,r,i,o,u,a=new Ro(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=zo);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new Ro(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(Lo)}function Co(){return Ao(this).eachBefore(Po)}function zo(t){return t.children}function Po(t){t.data=t.data.data}function Lo(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Ro(t){this.data=t,this.depth=this.height=0,this.parent=null}function qo(t){this._=t,this.next=null}function Uo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Do(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=Oo(n[0]);break;case 2:e=Fo(n[0],n[1]);break;case 3:e=Io(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&Uo(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Do(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function Oo(t){return{x:t.x,y:t.y,r:t.r}}function Fo(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function Io(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),g=2*(i-f),y=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*g,b=(p*m-g*v)/x-r,w=(g*d-p*y)/x,M=(_*v-h*m)/x-i,T=(h*y-_*d)/x,k=w*w+T*T-1,S=2*(b*w+M*T+o),N=b*b+M*M-o*o,E=(-S-Math.sqrt(S*S-4*k*N))/(2*k);return{x:b+w*E+r,y:M+T*E+i,r:E}}function Yo(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Bo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i-1e-6>e*e+r*r}function jo(t,n,e){var r=t._,i=t.next._,o=r.r+i.r,u=(r.x*i.r+i.x*r.r)/o-n,a=(r.y*i.r+i.y*r.r)/o-e;return u*u+a*a}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Yo(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,g=h*n.y+p*e.y+d*r.y;n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;a=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function uu(t,n,e){return t.a.parent===n.parent?t.a:e}function au(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function cu(t){for(var n,e,r,i,o,u=new au(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new au(r[i],i)),e.parent=n;return(u.parent=new au(null,0)).children=[u],u}function su(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,g=[],y=n.children,m=0,x=0,b=y.length,w=n.value;mh&&(h=a),_=f*f*v,(p=Math.max(h/_,_/l))>d){f-=a;break}d=p}g.push(u={value:f,dice:c1&&em(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function hu(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function pu(t){if(!t._start)try{du(t)}catch(n){if(t._tasks[t._ended+t._active-1])_u(t,n);else if(!t._data)throw n}}function du(t){for(;t._start=t._waiting&&t._active=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,gu(t)}function gu(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function yu(t){return new hu(arguments.length?+t:1/0)}function mu(t){return function(n,e){t(null==n?e:null)}}function xu(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function bu(t,n){return function(e){return t(e.responseText,n)}}function wu(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==Sm)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=Ye(),r=[],i=Sm;return t=null==t?[]:km.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=Ye();for(var i,o,u=-1,a=t.length;++u=e?1:r(t)}}}function Eu(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function Au(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o2?Cu:Au,o=u=null,r}function r(n){return(o||(o=i(a,c,f?Nu(t):t,s)))(+n)}var i,o,u,a=Am,c=Am,s=Th,f=!1;return r.invert=function(t){return(u||(u=i(c,a,Su,f?Eu(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=Tm.call(t,Em),e()):a.slice()},r.range=function(t){return arguments.length?(c=km.call(t),e()):c.slice()},r.rangeRound=function(t){return c=km.call(t),s=kh,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function Lu(t){var n=t.domain;return t.ticks=function(t){var e=n();return tf(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Cm(n(),t,e)},t.nice=function(e){var i=n(),o=i.length-1,u=null==e?10:e,a=i[0],c=i[o],s=r(a,c,u);return s&&(s=r(Math.floor(a/s)*s,Math.ceil(c/s)*s,u),i[0]=Math.floor(a/s)*s,i[o]=Math.ceil(c/s)*s,n(i)),t},t}function Ru(){var t=Pu(Su,mh);return t.copy=function(){return zu(t,Ru())},Lu(t)}function qu(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Tm.call(e,Em),t):n.slice()},t.copy=function(){return qu().domain(n)},Lu(t)}function Uu(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:Nm(n)}function Du(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function Ou(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Fu(t){return 10===t?Ou:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Iu(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Yu(t){return function(n){return-t(-n)}}function Bu(){function n(){return o=Iu(i),u=Fu(i),r()[0]<0&&(o=Yu(o),u=Yu(u)),e}var e=Pu(Uu,Du).domain([1,10]),r=e.domain,i=10,o=Iu(10),u=Fu(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c0){for(;hc)break;v.push(l)}}else for(;h=1;--f)if(!((l=s*f)c)break;v.push(l)}}else v=tf(h,p,Math.min(p-h,d)).map(u);return n?v.reverse():v},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i0?i[n-1]:e[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return $u().domain([e,r]).range(u)},Lu(t)}function Wu(){function t(t){if(t<=t)return e[Us(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=km.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=km.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Wu().domain(n).range(e)},t}function Zu(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pm.setTime(+n),Lm.setTime(+r),t(Pm),t(Lm),Math.floor(e(Pm,Lm))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Gu(t){return Zu(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/Dm})}function Ju(t){return Zu(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Dm})}function Qu(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Ku(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ta(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function na(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date(+e));++a=c)return-1;if(37===(i=n.charCodeAt(u++))){if(i=n.charAt(u++),!(o=B[i in qx?n.charAt(u++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=R.exec(n.slice(e));return r?(t.w=q[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=L[r[0].toLowerCase()],e+r[0].length):-1}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return N[t.getDay()]}function p(t){return S[t.getDay()]}function d(t){return A[t.getMonth()]}function v(t){return E[t.getMonth()]}function _(t){return k[+(t.getHours()>=12)]}function g(t){return N[t.getUTCDay()]}function y(t){return S[t.getUTCDay()]}function m(t){return A[t.getUTCMonth()]}function x(t){return E[t.getUTCMonth()]}function b(t){return k[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,k=t.periods,S=t.days,N=t.shortDays,E=t.months,A=t.shortMonths,C=ia(k),z=oa(k),P=ia(S),L=oa(S),R=ia(N),q=oa(N),U=ia(E),D=oa(E),O=ia(A),F=oa(A),I={a:h,A:p,b:d,B:v,c:null,d:xa,e:xa,H:ba,I:wa,j:Ma,L:Ta,m:ka,M:Sa,p:_,S:Na,U:Ea,w:Aa,W:Ca,x:null,X:null,y:za,Y:Pa,Z:La,"%":Wa},Y={a:g,A:y,b:m,B:x,c:null,d:Ra,e:Ra,H:qa,I:Ua,j:Da,L:Oa,m:Fa,M:Ia,p:b,S:Ya,U:Ba,w:ja,W:Ha,x:null,X:null,y:Xa,Y:Va,Z:$a,"%":Wa},B={a:o,A:u,b:a,B:c,c:s,d:pa,e:pa,H:va,I:va,j:da,L:ya,m:ha,M:_a,p:i,S:ga,U:aa,w:ua,W:ca,x:f,X:l,y:fa,Y:sa,Z:la,"%":ma};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Qu);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Ku);return n.toString=function(){return t},n}}}function ea(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function la(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function ha(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function pa(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function da(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function va(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function _a(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function ga(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function ya(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function ma(t,n,e){var r=Dx.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function xa(t,n){return ea(t.getDate(),n,2)}function ba(t,n){return ea(t.getHours(),n,2)}function wa(t,n){return ea(t.getHours()%12||12,n,2)}function Ma(t,n){return ea(1+Hm.count(cx(t),t),n,3)}function Ta(t,n){return ea(t.getMilliseconds(),n,3)}function ka(t,n){return ea(t.getMonth()+1,n,2)}function Sa(t,n){return ea(t.getMinutes(),n,2)}function Na(t,n){return ea(t.getSeconds(),n,2)}function Ea(t,n){return ea(Vm.count(cx(t),t),n,2)}function Aa(t){return t.getDay()}function Ca(t,n){return ea($m.count(cx(t),t),n,2)}function za(t,n){return ea(t.getFullYear()%100,n,2)}function Pa(t,n){return ea(t.getFullYear()%1e4,n,4)}function La(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+ea(n/60|0,"0",2)+ea(n%60,"0",2)}function Ra(t,n){return ea(t.getUTCDate(),n,2)}function qa(t,n){return ea(t.getUTCHours(),n,2)}function Ua(t,n){return ea(t.getUTCHours()%12||12,n,2)}function Da(t,n){return ea(1+dx.count(Px(t),t),n,3)}function Oa(t,n){return ea(t.getUTCMilliseconds(),n,3)}function Fa(t,n){return ea(t.getUTCMonth()+1,n,2)}function Ia(t,n){return ea(t.getUTCMinutes(),n,2)}function Ya(t,n){return ea(t.getUTCSeconds(),n,2)}function Ba(t,n){return ea(_x.count(Px(t),t),n,2)}function ja(t){return t.getUTCDay()}function Ha(t,n){return ea(gx.count(Px(t),t),n,2)}function Xa(t,n){return ea(t.getUTCFullYear()%100,n,2)}function Va(t,n){return ea(t.getUTCFullYear()%1e4,n,4)}function $a(){return"+0000"}function Wa(){return"%"}function Za(n){return Lx=na(n),t.timeFormat=Lx.format,t.timeParse=Lx.parse,t.utcFormat=Lx.utcFormat,t.utcParse=Lx.utcParse,Lx}function Ga(t){return t.toISOString()}function Ja(t){var n=new Date(t);return isNaN(n)?null:n}function Qa(t){return new Date(t)}function Ka(t){return t instanceof Date?+t:+new Date(+t)}function tc(t,n,e,i,o,u,a,c,s){function f(r){return(a(r)1?0:t<-1?mb:Math.acos(t)}function ic(t){return t>=1?xb:t<=-1?-xb:Math.asin(t)}function oc(t){return t.innerRadius}function uc(t){return t.outerRadius}function ac(t){return t.startAngle}function cc(t){return t.endAngle}function sc(t){return t&&t.padAngle}function fc(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function lc(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/gb(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,g=(p+v)/2,y=d-h,m=v-p,x=y*y+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*gb(db(0,b*b*x-w*w)),T=(w*m-y*M)/x,k=(-w*y-m*M)/x,S=(w*m+y*M)/x,N=(-w*y+m*M)/x,E=T-_,A=k-g,C=S-_,z=N-g;return E*E+A*A>C*C+z*z&&(T=S,k=N),{cx:T,cy:k,x01:-f,y01:-l,x11:T*(i/b-1),y11:k*(i/b-1)}}function hc(t){this._context=t}function pc(t){return t[0]}function dc(t){return t[1]}function vc(t){this._curve=t}function _c(t){function n(n){return new vc(t(n))}return n._curve=t,n}function gc(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t}function yc(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function mc(t){this._context=t}function xc(t){this._context=t}function bc(t){this._context=t}function wc(t,n){this._basis=new mc(t),this._beta=n}function Mc(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Tc(t,n){this._context=t,this._k=(1-n)/6}function kc(t,n){this._context=t,this._k=(1-n)/6}function Sc(t,n){this._context=t,this._k=(1-n)/6}function Nc(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>yb){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>yb){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function Ec(t,n){this._context=t,this._alpha=n}function Ac(t,n){this._context=t,this._alpha=n}function Cc(t,n){this._context=t,this._alpha=n}function zc(t){this._context=t}function Pc(t){return t<0?-1:1}function Lc(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Pc(o)+Pc(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Rc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function qc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function Uc(t){this._context=t}function Dc(t){this._context=new Oc(t)}function Oc(t){this._context=t}function Fc(t){return new Uc(t)}function Ic(t){return new Dc(t)}function Yc(t){this._context=t}function Bc(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n0)){if(o/=d,d<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o0)){if(o/=v,v<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function os(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]Cw||Math.abs(i[0][1]-i[1][1])>Cw)||delete Nw[o]}function as(t){return kw[t.index]={site:t,halfedges:[]}}function cs(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function ss(t,n){return n[+(n.left!==t.site)]}function fs(t,n){return n[+(n.left===t.site)]}function ls(){for(var t,n,e,r,i=0,o=kw.length;iCw||Math.abs(v-h)>Cw)&&(c.splice(a,0,Nw.push(es(u,p,Math.abs(d-t)Cw?[t,Math.abs(l-t)Cw?[Math.abs(h-r)Cw?[e,Math.abs(l-e)Cw?[Math.abs(h-n)=-zw)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,g=Ew.pop()||new ps;g.arc=t,g.site=i,g.x=v+u,g.y=(g.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=g;for(var y=null,m=Sw._;m;)if(g.yCw)a=a.L;else{if(!((i=o-ws(a,u))>Cw)){r>-Cw?(n=a.P,e=a):i>-Cw?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}as(t);var c=gs(t);if(Tw.insert(n,c),n||e){if(n===e)return vs(n),e=gs(n.site),Tw.insert(c,e),c.edge=e.edge=ns(n.site,c.site),ds(n),void ds(e);if(!e)return void(c.edge=ns(n.site,c.site));vs(n),vs(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,g=2*(h*_-p*v),y=h*h+p*p,m=v*v+_*_,x=[(_*y-p*m)/g+f,(h*m-v*y)/g+l];rs(e.edge,s,d,x),c.edge=ns(s,t,null,x),e.edge=ns(t,d,null,x),ds(n),ds(e)}}function bs(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function ws(t,n){var e=t.N;if(e)return bs(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function Ms(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function Ts(t,n){return n[1]-t[1]||n[0]-t[0]}function ks(t,n){var e,r,i,o=t.sort(Ts).pop();for(Nw=[],kw=new Array(t.length),Tw=new Gc,Sw=new Gc;;)if(i=Mw,o&&(!i||o[1]n?1:t>=n?0:NaN},Rs=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}},qs=Rs(Ls),Us=qs.right,Ds=qs.left,Os=function(t,n){null==n&&(n=e);for(var r=0,i=t.length-1,o=t[0],u=new Array(i<0?0:i);rt?1:n>=t?0:NaN},Ys=function(t){return null===t?NaN:+t},Bs=function(t,n){var e,r,i=t.length,o=0,u=0,a=-1,c=0;if(null==n)for(;++a1)return u/(c-1)},js=function(t,n){var e=Bs(t,n);return e?Math.sqrt(e):e},Hs=function(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o=r){e=i=r;break}for(;++or&&(e=r),i=r){e=i=r;break}for(;++or&&(e=r),i=f;)l.pop(),--h;var p,d=new Array(h+1);for(i=0;i<=h;++i)p=d[i]=[],p.x0=i>0?l[i-1]:s,p.x1=i=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),u=+e(t[o],o,t);return u+(+e(t[o+1],o+1,t)-u)*(i-o)}},of=function(t,n,e){return t=$s.call(t,Ys).sort(Ls),Math.ceil((e-n)/(2*(rf(t,.75)-rf(t,.25))*Math.pow(t.length,-1/3)))},uf=function(t,n,e){return Math.ceil((e-n)/(3.5*js(t)*Math.pow(t.length,-1/3)))},af=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},cf=function(t,n){var e,r=0,i=t.length,o=-1,u=i;if(null==n)for(;++o=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e},lf=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},hf=function(t,n){for(var e=n.length,r=new Array(e);e--;)r[e]=t[n[e]];return r},pf=function(t,n){if(e=t.length){var e,r,i=0,o=0,u=t[o];for(n||(n=Ls);++i0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Nf.hasOwnProperty(n)?{space:Nf[n],local:t}:t},Af=function(t){var n=Ef(t);return(n.local?x:m)(n)},Cf=0;w.prototype=b.prototype={constructor:w,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var zf=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Pf=document.documentElement;if(!Pf.matches){var Lf=Pf.webkitMatchesSelector||Pf.msMatchesSelector||Pf.mozMatchesSelector||Pf.oMatchesSelector;zf=function(t){return function(){return Lf.call(this,t)}}}}var Rf=zf,qf={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(qf={mouseenter:"mouseover",mouseleave:"mouseout"})}var Uf=function(t,n,e){var r,i,o=k(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?N:S,null==e&&(e=!1),r=0;r=x&&(x=m+1);!(y=_[x])&&++x=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Kf=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=R);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?Y:"function"==typeof n?j:B)(t,n,null==e?"":e)):al(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},sl=function(t,n){return arguments.length>1?this.each((null==n?H:"function"==typeof n?V:X)(t,n)):this.node()[t]};Z.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var fl=function(t,n){var e=$(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=e.length;++i=240?t-240:t+120,i,r),Rt(t,i,r),Rt(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Bl=Math.PI/180,jl=180/Math.PI,Hl=.95047,Xl=1,Vl=1.08883,$l=4/29,Wl=6/29,Zl=3*Wl*Wl,Gl=Wl*Wl*Wl;Al(Dt,Ut,wt(Mt,{brighter:function(t){return new Dt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Dt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Xl*Ft(t),n=Hl*Ft(n),e=Vl*Ft(e),new At(It(3.2404542*n-1.5371385*t-.4985314*e),It(-.969266*n+1.8760108*t+.041556*e),It(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),Al(Ht,jt,wt(Mt,{brighter:function(t){return new Ht(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Ht(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return qt(this).rgb()}}));var Jl=-.14861,Ql=1.78277,Kl=-.29227,th=-.90649,nh=1.97294,eh=nh*th,rh=nh*Ql,ih=Ql*Kl-th*Jl;Al($t,Vt,wt(Mt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Bl,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new At(255*(n+e*(Jl*r+Ql*i)),255*(n+e*(Kl*r+th*i)),255*(n+e*(nh*r)),this.opacity)}}));var oh,uh,ah,ch,sh,fh,lh=function(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:mh(e,r)})),o=wh.lastIndex;return onp&&e.statetp&&e.name===n)return new Kn([[t]],td,n,+r)}return null},ed=function(t){return function(){return t}},rd=function(t,n,e){this.target=t,this.type=n,this.selection=e},id=function(){t.event.preventDefault(),t.event.stopImmediatePropagation()},od={name:"drag"},ud={name:"space"},ad={name:"handle"},cd={name:"center"},sd={name:"x",handles:["e","w"].map(Me),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fd={name:"y",handles:["n","s"].map(Me),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},ld={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(Me),input:function(t){return t},output:function(t){return t}},hd={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},pd={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},dd={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},vd={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},_d={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},gd=function(){return ze(ld)},yd=Math.cos,md=Math.sin,xd=Math.PI,bd=xd/2,wd=2*xd,Md=Math.max,Td=function(){function t(t){var o,u,a,c,s,f,l=t.length,h=[],p=Gs(l),d=[],v=[],_=v.groups=new Array(l),g=new Array(l*l);for(o=0,s=-1;++s1e-6)if(Math.abs(f*a-c*s)>1e-6&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),g=Math.sqrt(l),y=i*Math.tan((Nd-Math.acos((d+l-v)/(2*_*g)))/2),m=y/g,x=y/_;Math.abs(m-1)>1e-6&&(this._+="L"+(t+m*s)+","+(n+m*f)),this._+="A"+i+","+i+",0,0,"+ +(f*h>s*p)+","+(this._x1=t+x*a)+","+(this._y1=n+x*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+s:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-s)>1e-6)&&(this._+="L"+c+","+s),e&&(l<0&&(l=l%Ed+Ed),l>Ad?this._+="A"+e+","+e+",0,1,"+f+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+f+","+(this._x1=c)+","+(this._y1=s):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Nd)+","+f+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var Cd=function(){function t(){var t,a=kd.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,a)),l=i.apply(this,a)-bd,h=o.apply(this,a)-bd,p=f*yd(l),d=f*md(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-bd,g=o.apply(this,a)-bd;if(u||(u=t=Re()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===g||(u.quadraticCurveTo(0,0,v*yd(_),v*md(_)),u.arc(0,0,v,_,g)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=qe,e=Ue,r=De,i=Oe,o=Fe,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:Sd(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Sd(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Sd(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t};Ie.prototype=Ye.prototype={constructor:Ie,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var zd=function(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=Ye(),v=u();++lo.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,Be,je)},map:function(n){return t(n,0,He,Xe)},entries:function(e){return n(t(e,0,He,Xe),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},Pd=Ye.prototype;Ve.prototype=$e.prototype={constructor:Ve,has:Pd.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Pd.remove,clear:Pd.clear,values:Pd.keys,size:Pd.size,empty:Pd.empty,each:Pd.each};var Ld=function(t){var n=[];for(var e in t)n.push(e);return n},Rd=function(t){var n=[];for(var e in t)n.push(t[e]);return n},qd=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},Ud=function(t){function n(t,n){var r,i,o=e(t,function(t,e){if(r)return r(t,e-1);i=t,r=n?Ze(t,n):We(t)});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},Qd=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},Kd=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},tv=function(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i},nv=function(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new tv(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)=g)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var y=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=y*y+m*m;if(x=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+g)/2))?v=c:g=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)},rv=function(){return this._root},iv=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},ov=function(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new tv(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new tv(e,s,f,o,u)),(e=c[2])&&a.push(new tv(e,r,f,s,u)),(e=c[1])&&a.push(new tv(e,s,i,o,f)),(e=c[0])&&a.push(new tv(e,r,i,s,f))}return this},uv=function(t){var n,e=[],r=[];for(this._root&&e.push(new tv(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new tv(o,u,a,f,l)),(o=i[1])&&e.push(new tv(o,f,a,c,l)),(o=i[2])&&e.push(new tv(o,u,l,f,s)),(o=i[3])&&e.push(new tv(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},av=function(t){return arguments.length?(this._x=t,this):this._x},cv=function(t){return arguments.length?(this._y=t,this):this._y},sv=er.prototype=rr.prototype;sv.copy=function(){var t,n,e=new rr(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ir(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ir(n));return e},sv.add=Gd,sv.addAll=Qe,sv.cover=Jd,sv.data=Qd,sv.extent=Kd,sv.find=nv,sv.remove=ev,sv.removeAll=Ke,sv.root=rv,sv.size=iv,sv.visit=ov,sv.visitAfter=uv,sv.x=av,sv.y=cv;var fv,lv=function(t){function n(){function t(t,n,e,r,i){var o=t.data,a=t.r,p=l+a;{if(!o)return n>s+p||rf+p||ic.index){var d=s-o.x-o.vx,v=f-o.y-o.vy,_=d*d+v*v;_t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s1?(p.on(t,n),o):p.on(t)}}},_v=function(){function t(t){var n,a=i.length,c=er(i,sr,fr).visitAfter(e);for(u=t,n=0;n=f)){(t.data!==o||t.next)&&(0===i&&(i=Zd(),p+=i*i),0===c&&(c=Zd(),p+=c*c),p1?r[0]+r.slice(2):r,+t.slice(e+1)]},xv=function(t){return t=mv(Math.abs(t)),t?t[1]:NaN},bv=function(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},wv=function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}},Mv=function(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Tv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fv=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+mv(t,Math.max(0,n+o-1))[0]},kv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")},Sv={"":Mv,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return kv(100*t,n)},r:kv,s:Tv,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Nv=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;lr.prototype=hr.prototype,hr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Ev,Av=function(t){return t},Cv=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],zv=function(t){function n(t){function n(t){var n,i,s,m=v,x=_;if("c"===d)x=g(t)+x,t="";else{t=+t;var b=t<0;if(t=g(Math.abs(t),p),b&&0==+t&&(b=!1),m=(b?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=x+("s"===d?Cv[8+fv/3]:"")+(b&&"("===c?")":""),y)for(n=-1,i=t.length;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}h&&!f&&(t=r(t,1/0));var w=m.length+t.length+x.length,M=w>1)+m+t+x+M.slice(w);break;default:t=M+m+t+x}return u(t)}t=lr(t);var e=t.fill,a=t.align,c=t.sign,s=t.symbol,f=t.zero,l=t.width,h=t.comma,p=t.precision,d=t.type,v="$"===s?i[0]:"#"===s&&/[boxX]/.test(d)?"0"+d.toLowerCase():"",_="$"===s?i[1]:/[%p]/.test(d)?"%":"",g=Sv[d],y=!d||/[defgprs%]/.test(d);return p=null==p?d?6:12:/[gprs]/.test(d)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=lr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(xv(e)/3))),o=Math.pow(10,-i),u=Cv[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?bv(t.grouping,t.thousands):Av,i=t.currency,o=t.decimal,u=t.numerals?wv(t.numerals):Av;return{format:n,formatPrefix:e}};pr({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Pv=function(t){return Math.max(0,-xv(Math.abs(t)))},Lv=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(xv(n)/3)))-xv(Math.abs(t)))},Rv=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,xv(n)-xv(t))+1},qv=function(){return new dr};dr.prototype={constructor:dr,reset:function(){this.s=this.t=0},add:function(t){vr(p_,t,this.t),vr(this,p_.s,this.s),this.s?this.t+=p_.t:this.s=p_.t},valueOf:function(){return this.s}};var Uv,Dv,Ov,Fv,Iv,Yv,Bv,jv,Hv,Xv,Vv,$v,Wv,Zv,Gv,Jv,Qv,Kv,t_,n_,e_,r_,i_,o_,u_,a_,c_,s_,f_,l_,h_,p_=new dr,d_=1e-6,v_=Math.PI,__=v_/2,g_=v_/4,y_=2*v_,m_=180/v_,x_=v_/180,b_=Math.abs,w_=Math.atan,M_=Math.atan2,T_=Math.cos,k_=Math.ceil,S_=Math.exp,N_=Math.log,E_=Math.pow,A_=Math.sin,C_=Math.sign||function(t){return t>0?1:t<0?-1:0},z_=Math.sqrt,P_=Math.tan,L_={Feature:function(t,n){xr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rd_?Hv=90:I_<-d_&&(Bv=-90),Gv[0]=Yv,Gv[1]=jv}},B_=function(t){var n,e,r,i,o,u,a;if(Hv=jv=-(Yv=Bv=1/0),Zv=[],q_(t,Y_),e=Zv.length){for(Zv.sort(Br),n=1,r=Zv[0],o=[r];nYr(r[0],r[1])&&(r[1]=i[1]),Yr(i[0],r[1])>Yr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Yr(r[1],i[0]))>u&&(u=a,Yv=i[0],jv=r[1])}return Zv=Gv=null,Yv===1/0||Bv===1/0?[[NaN,NaN],[NaN,NaN]]:[[Yv,Bv],[jv,Hv]]},j_={sphere:mr,point:Hr,lineStart:Vr,lineEnd:Zr,polygonStart:function(){j_.lineStart=Gr,j_.lineEnd=Jr},polygonEnd:function(){j_.lineStart=Vr,j_.lineEnd=Zr}},H_=function(t){Jv=Qv=Kv=t_=n_=e_=r_=i_=o_=u_=a_=0,q_(t,j_);var n=o_,e=u_,r=a_,i=n*n+e*e+r*r;return i<1e-12&&(n=e_,e=r_,r=i_,Qv2?t[2]*x_:0),n.invert=function(n){return n=t.invert(n[0]*x_,n[1]*x_),n[0]*=m_,n[1]*=m_,n},n},og=function(){function t(t,n){e.push(t=r(t,n)),t[0]*=m_,t[1]*=m_}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*x_,c=u.apply(this,arguments)*x_;return e=[],r=ni(-t[0]*x_,-t[1]*x_,0).invert,oi(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=X_([0,0]),o=X_(90),u=X_(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:X_([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:X_(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:X_(+t),n):u},n},ug=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:mr,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},ag=function(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u0)){if(u/=d,d<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}},cg=function(t,n){return b_(t[0]-n[0])=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}},fg=1e9,lg=-fg,hg=function(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=si(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}},pg=qv(),dg=function(t,n){var e=n[0],r=n[1],i=[A_(e),-T_(e),0],o=0,u=0;pg.reset();for(var a=0,c=t.length;a=0?1:-1,T=M*w,k=T>v_,S=d*x;if(pg.add(M_(S*M*A_(T),v*b+S*T_(T))),o+=k?w+M*y_:w,k^h>=e^y>=e){var N=Cr(Er(l),Er(g));Lr(N);var E=Cr(i,N);Lr(E);var A=(k^w>=0?-1:1)*gr(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(u+=k^w>=0?1:-1)}}return(o<-d_||o0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(ji))}var p,d,v,_=n(o),g=i.invert(r[0],r[1]),y=ug(),m=n(y),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=ff(d);var t=dg(p,g);d.length?(x||(o.polygonStart(),x=!0),sg(d,Hi,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}},Kg=Qg(function(){return!0},Xi,$i,[-v_,-__]),ty=function(t,n){function e(e,r,i,o){oi(o,t,n,i,e,r)}function r(t,n){return T_(t)*T_(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?v_:-v_),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(cg(n,p)||cg(d,p))&&(d[0]+=d_,d[1]+=d_,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var g;_&e||!(g=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(g[0][0],g[0][1]),t.point(g[1][0],g[1][1]),t.lineEnd()):(t.point(g[1][0],g[1][1]),t.lineEnd(),t.lineStart(),t.point(g[0][0],g[0][1])))}!v||n&&cg(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=Er(t),i=Er(n),o=[1,0,0],u=Cr(r,i),c=Ar(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=Cr(o,u),d=Pr(o,l);zr(d,Pr(u,h));var v=p,_=Ar(d,v),g=Ar(v,v),y=_*_-g*(Ar(d,d)-1);if(!(y<0)){var m=z_(y),x=Pr(v,(-_-m)/g);if(zr(x,d),x=Nr(x),!e)return x;var b,w=t[0],M=n[0],T=t[1],k=n[1];M0^x[1]<(b_(x[0]-w)v_^(w<=x[0]&&x[0]<=M)){var A=Pr(v,(-_+m)/g);return zr(A,d),[x,Nr(A)]}}}function u(n,e){var r=c?t:v_-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=T_(t),c=a>0,s=b_(a)>d_;return Qg(r,i,e,c?[0,-t]:[-v_,t-v_])},ny=function(t){return{stream:Wi(t)}};Zi.prototype={constructor:Zi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var ey=16,ry=T_(30*x_),iy=function(t,n){return+n?Ki(t,n):Qi(t)},oy=Wi({point:function(t,n){this.stream.point(t*x_,n*x_)}}),uy=function(){return eo(io).scale(155.424).center([0,33.6442])},ay=function(){return uy().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])},cy=function(){function t(t){var n=t[0],e=t[1];return a=null,i.point(n,e),a||(o.point(n,e),a)||(u.point(n,e),a)}function n(){return e=r=null,t}var e,r,i,o,u,a,c=ay(),s=uy().rotate([154,0]).center([-2,58.5]).parallels([55,65]),f=uy().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,n){a=[t,n]}};return t.invert=function(t){var n=c.scale(),e=c.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?f:c).invert(t)},t.stream=function(t){return e&&r===t?e:e=oo([c.stream(r=t),s.stream(t),f.stream(t)])},t.precision=function(t){return arguments.length?(c.precision(t),s.precision(t),f.precision(t),n()):c.precision()},t.scale=function(n){return arguments.length?(c.scale(n),s.scale(.35*n),f.scale(n),t.translate(c.translate())):c.scale()},t.translate=function(t){if(!arguments.length)return c.translate();var e=c.scale(),r=+t[0],a=+t[1];return i=c.translate(t).clipExtent([[r-.455*e,a-.238*e],[r+.455*e,a+.238*e]]).stream(l),o=s.translate([r-.307*e,a+.201*e]).clipExtent([[r-.425*e+d_,a+.12*e+d_],[r-.214*e-d_,a+.234*e-d_]]).stream(l),u=f.translate([r-.205*e,a+.212*e]).clipExtent([[r-.214*e+d_,a+.166*e+d_],[r-.115*e-d_,a+.234*e-d_]]).stream(l),n()},t.fitExtent=function(n,e){return Gi(t,n,e)},t.fitSize=function(n,e){return Ji(t,n,e)},t.scale(1070)},sy=uo(function(t){return z_(2/(1+t))});sy.invert=ao(function(t){return 2*gr(t/2)});var fy=function(){return to(sy).scale(124.75).clipAngle(179.999)},ly=uo(function(t){return(t=_r(t))&&t/A_(t)});ly.invert=ao(function(t){return t});var hy=function(){return to(ly).scale(79.4188).clipAngle(179.999)};co.invert=function(t,n){return[t,2*w_(S_(n))-__]};var py=function(){return so(co).scale(961/y_)},dy=function(){return eo(lo).scale(109.5).parallels([30,30])};ho.invert=ho;var vy=function(){return to(ho).scale(152.63)},_y=function(){return eo(po).scale(131.154).center([0,13.9389])};vo.invert=ao(w_);var gy=function(){return to(vo).scale(144.049).clipAngle(60)},yy=function(){function t(){return i=o=null,u}var n,e,r,i,o,u,a=1,c=0,s=0,f=1,l=1,h=kg,p=null,d=kg;return u={stream:function(t){return i&&o===t?i:i=h(d(o=t))},clipExtent:function(i){return arguments.length?(d=null==i?(p=n=e=r=null,kg):si(p=+i[0][0],n=+i[0][1],e=+i[1][0],r=+i[1][1]),t()):null==p?null:[[p,n],[e,r]]},scale:function(n){return arguments.length?(h=_o((a=+n)*f,a*l,c,s),t()):a},translate:function(n){return arguments.length?(h=_o(a*f,a*l,c=+n[0],s=+n[1]),t()):[c,s]},reflectX:function(n){return arguments.length?(h=_o(a*(f=n?-1:1),a*l,c,s),t()):f<0},reflectY:function(n){return arguments.length?(h=_o(a*f,a*(l=n?-1:1),c,s),t()):l<0},fitExtent:function(t,n){return Gi(u,t,n)},fitSize:function(t,n){return Ji(u,t,n)}}};go.invert=ao(gr);var my=function(){return to(go).scale(249.5).clipAngle(90+d_)};yo.invert=ao(function(t){return 2*w_(t)});var xy=function(){return to(yo).scale(250).clipAngle(142)};mo.invert=function(t,n){return[-n,2*w_(S_(t))-__]};var by=function(){var t=so(mo),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},wy=function(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=bo(e),t.y=Mo(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=ko(t),c=So(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=xo,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},My=function(){return this.eachAfter(No)},Ty=function(t){var n,e,r,i,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},Sy=function(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e=0;)e+=r[i].value;n.value=e})},Ey=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Ay=function(t){for(var n=this,e=Eo(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},Cy=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},zy=function(){var t=[];return this.each(function(n){t.push(n)}),t},Py=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Ly=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ro.prototype=Ao.prototype={constructor:Ro,count:My,each:Ty,eachAfter:Sy,eachBefore:ky,sum:Ny,sort:Ey,path:Ay,ancestors:Cy,descendants:zy,leaves:Py,links:Ly,copy:Co};var Ry=function(t){for(var n=(t=t.slice()).length,e=null,r=e;n;){var i=new qo(t[n-1]);r=r?r.next=i:e=i,t[void 0]=t[--n]}return{head:e,tail:r}},qy=function(t){return Do(Ry(t),[])},Uy=function(t){return Xo(t),t},Dy=function(t){return function(){return t}},Oy=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Go(n)).eachAfter(Jo(i,.5)).eachBefore(Qo(1)):t.eachBefore(Go(Zo)).eachAfter(Jo(Wo,1)).eachAfter(Jo(i,t.r/Math.min(e,r))).eachBefore(Qo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Wo;return t.radius=function(e){return arguments.length?(n=Vo(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:Dy(+n),t):i},t},Fy=function(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)},Iy=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a0)throw new Error("cycle");return o}var n=Ko,e=tu;return t.id=function(e){return arguments.length?(n=$o(e),t):n},t.parentId=function(n){return arguments.length?(e=$o(n),t):e},t};au.prototype=Object.create(Ro.prototype);var Vy=function(){function t(t){var r=cu(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.xf.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){ou(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=ru(a),i=eu(i),a&&i;)c=eu(c),u=ru(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(iu(uu(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!ru(u)&&(u.t=a,u.m+=l-f),i&&!eu(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=nu,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t},$y=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a1?n:1)},e}(Wy),Gy=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Fy),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p>>1;f[v]a-i){var y=(r*g+u*_)/e;o(t,p,_,r,i,y,a),o(p,n,g,y,i,u,a)}else{var m=(i*g+a*_)/e;o(t,p,_,r,i,u,m),o(p,n,g,r,m,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u1?n:1)},e +}(Wy),tm=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;na!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f},om=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}},fm=function(){var t=sm.apply(this,arguments);return function(){return Math.exp(t())}},lm=function(t){return function(){for(var n=0,e=0;e=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(t){return void a.call("error",r,t)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=d("beforesend","progress","load","error"),c=Ye(),s=new XMLHttpRequest,f=null,l=null,h=0;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=mu(o)),null!=o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return r.get(n)}return r},vm=function(t,n){return function(e,r){var i=dm(e).mimeType(t).response(n);if(null!=r){if("function"!=typeof r)throw new Error("invalid callback: "+r);return i.get(r)}return i}},_m=vm("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),gm=vm("application/json",function(t){return JSON.parse(t.responseText)}),ym=vm("text/plain",function(t){return t.responseText}),mm=vm("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),xm=function(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=dm(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(bu(n,r=t)):r},o.row(r),i?o.get(i):o}},bm=xm("text/csv",Od),wm=xm("text/tab-separated-values",jd),Mm=Array.prototype,Tm=Mm.map,km=Mm.slice,Sm={name:"implicit"},Nm=function(t){return function(){return t}},Em=function(t){return+t},Am=[0,1],Cm=function(n,e,i){var o,u=n[0],a=n[n.length-1],c=r(u,a,null==e?10:e);switch(i=lr(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(u),Math.abs(a));return null!=i.precision||isNaN(o=Lv(c,s))||(i.precision=o),t.formatPrefix(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Rv(c,Math.max(Math.abs(u),Math.abs(a))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Pv(c))||(i.precision=o-2*("%"===i.type))}return t.format(i)},zm=function(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u0?t>1?Zu(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rm:null};var qm=Rm.range,Um=6e4,Dm=6048e5,Om=Zu(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Fm=Om.range,Im=Zu(function(t){t.setTime(Math.floor(t/Um)*Um)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getMinutes()}),Ym=Im.range,Bm=Zu(function(t){var n=t.getTimezoneOffset()*Um%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),jm=Bm.range,Hm=Zu(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/864e5},function(t){return t.getDate()-1}),Xm=Hm.range,Vm=Gu(0),$m=Gu(1),Wm=Gu(2),Zm=Gu(3),Gm=Gu(4),Jm=Gu(5),Qm=Gu(6),Km=Vm.range,tx=$m.range,nx=Wm.range,ex=Zm.range,rx=Gm.range,ix=Jm.range,ox=Qm.range,ux=Zu(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ax=ux.range,cx=Zu(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});cx.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var sx=cx.range,fx=Zu(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getUTCMinutes()}),lx=fx.range,hx=Zu(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),px=hx.range,dx=Zu(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),vx=dx.range,_x=Ju(0),gx=Ju(1),yx=Ju(2),mx=Ju(3),xx=Ju(4),bx=Ju(5),wx=Ju(6),Mx=_x.range,Tx=gx.range,kx=yx.range,Sx=mx.range,Nx=xx.range,Ex=bx.range,Ax=wx.range,Cx=Zu(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),zx=Cx.range,Px=Zu(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Px.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Lx,Rx=Px.range,qx={"-":"",_:" ",0:"0"},Ux=/^\s*\d+/,Dx=/^%/,Ox=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Za({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Fx=Date.prototype.toISOString?Ga:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ"),Ix=+new Date("2000-01-01T00:00:00.000Z")?Ja:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),Yx=1e3,Bx=60*Yx,jx=60*Bx,Hx=24*jx,Xx=7*Hx,Vx=30*Hx,$x=365*Hx,Wx=function(){return tc(cx,ux,Vm,Hm,Bm,Im,Om,Rm,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])},Zx=function(){return tc(Px,Cx,_x,dx,hx,fx,Om,Rm,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])},Gx=function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})},Jx=Gx("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Qx=Gx("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Kx=Gx("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),tb=Gx("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),nb=Oh(Vt(300,.5,0),Vt(-240,.5,1)),eb=Oh(Vt(-100,.75,.35),Vt(80,1.5,.8)),rb=Oh(Vt(260,.75,.35),Vt(80,1.5,.8)),ib=Vt(),ob=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return ib.h=360*t-100,ib.s=1.5-1.5*n,ib.l=.8-.9*n,ib+""},ub=nc(Gx("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),ab=nc(Gx("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),cb=nc(Gx("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),sb=nc(Gx("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),fb=function(t){return function(){return t}},lb=Math.abs,hb=Math.atan2,pb=Math.cos,db=Math.max,vb=Math.min,_b=Math.sin,gb=Math.sqrt,yb=1e-12,mb=Math.PI,xb=mb/2,bb=2*mb,wb=function(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-xb,p=u.apply(this,arguments)-xb,d=lb(p-h),v=p>h;if(c||(c=t=Re()),lyb)if(d>bb-yb)c.moveTo(l*pb(h),l*_b(h)),c.arc(0,0,l,h,p,!v),f>yb&&(c.moveTo(f*pb(p),f*_b(p)),c.arc(0,0,f,p,h,v));else{var _,g,y=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,k=T>yb&&(i?+i.apply(this,arguments):gb(f*f+l*l)),S=vb(lb(l-f)/2,+r.apply(this,arguments)),N=S,E=S;if(k>yb){var A=ic(k/f*_b(T)),C=ic(k/l*_b(T));(w-=2*A)>yb?(A*=v?1:-1,x+=A,b-=A):(w=0,x=b=(h+p)/2),(M-=2*C)>yb?(C*=v?1:-1,y+=C,m-=C):(M=0,y=m=(h+p)/2)}var z=l*pb(y),P=l*_b(y),L=f*pb(b),R=f*_b(b);if(S>yb){var q=l*pb(m),U=l*_b(m),D=f*pb(x),O=f*_b(x);if(dyb?fc(z,P,D,O,q,U,L,R):[L,R],I=z-F[0],Y=P-F[1],B=q-F[0],j=U-F[1],H=1/_b(rc((I*B+Y*j)/(gb(I*I+Y*Y)*gb(B*B+j*j)))/2),X=gb(F[0]*F[0]+F[1]*F[1]);N=vb(S,(f-X)/(H-1)),E=vb(S,(l-X)/(H+1))}}M>yb?E>yb?(_=lc(D,O,z,P,l,E,v),g=lc(q,U,L,R,l,E,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),Eyb&&w>yb?N>yb?(_=lc(L,R,q,U,f,-N,v),g=lc(z,P,D,O,f,-N,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),N=f;--l)s.point(_[l],g[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),g[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):g[n]))}if(p)return s=null,p+""||null}function n(){return Tb().defined(u).curve(c).context(a)}var e=pc,r=null,i=fb(0),o=dc,u=fb(!0),a=null,c=Mb,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:fb(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:fb(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:fb(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t},Sb=function(t,n){return nt?1:n>=t?0:NaN},Nb=function(t){return t},Eb=function(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),g=Math.min(bb,Math.max(-bb,o.apply(this,arguments)-_)),y=Math.min(Math.abs(g)/h,u.apply(this,arguments)),m=y*(g<0?-1:1);for(a=0;a0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(g-h*m)/p:0;a0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:y};return v}var n=Nb,e=Sb,r=null,i=fb(0),o=fb(bb),u=fb(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:fb(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:fb(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:fb(+n),t):u},t},Ab=_c(Mb);vc.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Cb=function(){return gc(Tb().curve(Ab))},zb=function(){var t=kb().curve(Ab),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return gc(e())},delete t.lineX0,t.lineEndAngle=function(){return gc(r())},delete t.lineX1,t.lineInnerRadius=function(){return gc(i())},delete t.lineY0,t.lineOuterRadius=function(){return gc(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t},Pb={draw:function(t,n){var e=Math.sqrt(n/mb);t.moveTo(e,0),t.arc(0,0,e,0,bb)}},Lb={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Rb=Math.sqrt(1/3),qb=2*Rb,Ub={draw:function(t,n){var e=Math.sqrt(n/qb),r=e*Rb;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Db=Math.sin(mb/10)/Math.sin(7*mb/10),Ob=Math.sin(bb/10)*Db,Fb=-Math.cos(bb/10)*Db,Ib={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Ob*e,i=Fb*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=bb*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},Yb={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Bb=Math.sqrt(3),jb={draw:function(t,n){var e=-Math.sqrt(n/(3*Bb));t.moveTo(0,2*e),t.lineTo(-Bb*e,-e),t.lineTo(Bb*e,-e),t.closePath()}},Hb=-.5,Xb=Math.sqrt(3)/2,Vb=1/Math.sqrt(12),$b=3*(Vb/2+1),Wb={draw:function(t,n){var e=Math.sqrt(n/$b),r=e/2,i=e*Vb,o=r,u=e*Vb+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Hb*r-Xb*i,Xb*r+Hb*i),t.lineTo(Hb*o-Xb*u,Xb*o+Hb*u),t.lineTo(Hb*a-Xb*c,Xb*a+Hb*c),t.lineTo(Hb*r+Xb*i,Hb*i-Xb*r),t.lineTo(Hb*o+Xb*u,Hb*u-Xb*o),t.lineTo(Hb*a+Xb*c,Hb*c-Xb*a),t.closePath()}},Zb=[Pb,Lb,Ub,Yb,Ib,jb,Wb],Gb=function(){function t(){var t;if(r||(r=t=Re()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=fb(Pb),e=fb(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:fb(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},Jb=function(){};mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:yc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Qb=function(t){return new mc(t)};xc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Kb=function(t){return new xc(t)};bc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var tw=function(t){return new bc(t)};wc.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var nw=function t(n){function e(t){return 1===n?new mc(t):new wc(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Tc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Mc(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ew=function t(n){function e(t){return new Tc(t,n)}return e.tension=function(n){return t(+n)},e}(0);kc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var rw=function t(n){function e(t){return new kc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var iw=function t(n){function e(t){return new Sc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0: +this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ow=function t(n){function e(t){return n?new Ec(t,n):new Tc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Ac.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var uw=function t(n){function e(t){return n?new Ac(t,n):new kc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Cc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var aw=function t(n){function e(t){return n?new Cc(t,n):new Sc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);zc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};var cw=function(t){return new zc(t)};Uc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:qc(this,this._t0,Rc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,qc(this,Rc(this,e=Lc(this,t,n)),e);break;default:qc(this,this._t0,e=Lc(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Dc.prototype=Object.create(Uc.prototype)).point=function(t,n){Uc.prototype.point.call(this,n,t)},Oc.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Yc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=Bc(t),i=Bc(n),o=0,u=1;u=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var fw=function(t){return new jc(t,.5)},lw=Array.prototype.slice,hw=function(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i=0;)e[n]=n;return e},dw=function(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o0){for(var e,r,i,o=0,u=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u=a)return null;var c=t-i.site[0],s=n-i.site[1],f=c*c+s*s;do{i=o.cells[r=u],u=null,i.halfedges.forEach(function(e){var r=o.edges[e],a=r.left;if(a!==i.site&&a||(a=r.right)){var c=t-a[0],s=n-a[1],l=c*c+s*s;le?(e+r)/2:Math.min(0,e)||Math.max(0,r),o>i?(i+o)/2:Math.min(0,i)||Math.max(0,o))}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=m.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=N(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new Ns(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=A.length;r0?bl(this).transition().duration(S).call(u,f,a):bl(this).call(n.transform,f)}}function h(){if(y.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,c=u.length;for(As(),e=0;e + + + var edges = []; + + links.forEach(function(e) { + var sourceNode = nodes.filter(function(n) { + return n.name === e.source; + })[0], + targetNode = nodes.filter(function(n) { + return n.name === e.target; + })[0]; + + edges.push({ + source: sourceNode, + target: targetNode, + value: 1}); + + }); + + edges.forEach(function(e) { + + if (!e.source["linkcount"]) e.source["linkcount"] = 0; + if (!e.target["linkcount"]) e.target["linkcount"] = 0; + + e.source["linkcount"]++; + e.target["linkcount"]++; + }); + + //var width = 960, height = 600; + var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; + var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; + var svg = d3.select("#vis").append("svg") + .attr("width", width) + .attr("height", height); + + // updated for d3 v4. + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody().strength(strength)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide(35)); + + // Node charge strength. Repel strength greater for less links. + //function strength(d) { return -50/d["linkcount"] ; } + function strength(d) { return -25 ; } + + // Link distance. Distance increases with number of links at source and target + function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } + + // Link strength. Strength is less for highly connected nodes (move towards target dist) + function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } + + simulation + .nodes(nodes) + .on("tick", tick); + + simulation.force("link") + .links(edges) + .distance(distance) + .strength(strengthl); + + // build the arrow. + svg.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .enter().append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 25) // Moves the arrow head out, allow for radius + .attr("refY", 0) // -1.5 + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + var link = svg.selectAll('.link') + .data(edges) + .enter().append('line') + .attr("class", "link") + .attr("marker-end", "url(#end)"); + + var node = svg.selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", function(d) { return "node " + d.type;}) + .attr('transform', function(d) { + return "translate(" + d.x + "," + d.y + ")"}) + .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); + + node.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + // add the nodes + node.append('circle') + .attr('r', 16) + ; + + // add text + node.append("text") + .attr("x", 12) + .attr("dy", "-1.1em") + .text(function(d) {return d.name;}); + + node.on("mouseover", function(d) { + + var g = d3.select(this); // the node (table) + + // tooltip + + var fields = d.fields; + var fieldformat = ""; + fields.forEach(function(d) { + fieldformat += ""; + }); + fieldformat += "
      "+ d.name+""+ d.type+""+ d.disp+"
      "; + var tiplength = d.fields.length; + + // Define 'div' for tooltips + var div = d3.select("body").append("div") // declare the tooltip div + .attr("class", "tooltip") // apply the 'tooltip' class + .style("opacity", 0) + .html('
      ' + d.name + '
      ' + fieldformat) + .style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px") + .style("top", tooltop(tiplength))// or ... + .transition() + .duration(800) + .style("opacity", 0.9); + }); + + function tooltop(tiplength) { + //aim to ensure tooltip is fully visible whenver possible + return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px" + } + + node.on("mouseout", function(d) { + d3.select("body").select('div.tooltip').remove(); + }); + + // instead of waiting for force to end with : force.on('end', function() + // use .on("tick", instead. Here is the tick function + function tick() { + node.attr('transform', function(d) { + d.x = Math.max(30, Math.min(width - 16, d.x)); + d.y = Math.max(30, Math.min(height - 16, d.y)); + return "translate(" + d.x + "," + d.y + ")"; }); + + link.attr('x1', function(d) {return d.source.x;}) + .attr('y1', function(d) {return d.source.y;}) + .attr('x2', function(d) {return d.target.x;}) + .attr('y2', function(d) {return d.target.y;}); + }; + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }; + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + }; + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }; + +}; \ No newline at end of file From 1c747357b06a474a48d0772195b902d15f0ff7ae Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Tue, 16 May 2017 06:47:30 +1200 Subject: [PATCH 230/436] Repointed appadmin to admin/static for d3 files --- applications/examples/controllers/appadmin.py | 6 +- applications/examples/static/css/d3_graph.css | 33 ---- applications/examples/static/js/d3.min.js | 8 - applications/examples/static/js/d3_graph.js | 181 ------------------ applications/welcome/controllers/appadmin.py | 6 +- applications/welcome/static/css/d3_graph.css | 33 ---- applications/welcome/static/js/d3.min.js | 8 - applications/welcome/static/js/d3_graph.js | 181 ------------------ 8 files changed, 6 insertions(+), 450 deletions(-) delete mode 100644 applications/examples/static/css/d3_graph.css delete mode 100644 applications/examples/static/js/d3.min.js delete mode 100644 applications/examples/static/js/d3_graph.js delete mode 100644 applications/welcome/static/css/d3_graph.css delete mode 100644 applications/welcome/static/js/d3.min.js delete mode 100644 applications/welcome/static/js/d3_graph.js diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 2935959d..139ad1aa 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -690,6 +690,6 @@ def d3_graph_model(): nodes.append(dict(name=tablename, type="table", fields = fields)) # d3 v4 allows individual modules to be specified. The complete d3 library is included below. - response.files.append(URL('static','js/d3.min.js')) - response.files.append(URL('static','js/d3_graph.js')) - return dict(databases=databases, nodes=nodes, links=links) \ No newline at end of file + response.files.append(URL('admin','static','js/d3.min.js')) + response.files.append(URL('admin','static','js/d3_graph.js')) + return dict(databases=databases, nodes=nodes, links=links) diff --git a/applications/examples/static/css/d3_graph.css b/applications/examples/static/css/d3_graph.css deleted file mode 100644 index fd0ab8fb..00000000 --- a/applications/examples/static/css/d3_graph.css +++ /dev/null @@ -1,33 +0,0 @@ - .node {fill: steelblue; - stroke: #636363; - stroke-width: 1px;} - - .auth {fill: lightgrey;} - - .table {r: 10;} - - .link {stroke: #bbbbbb; - stroke-width: 2px;} - td {padding: 4px;} - - div.tooltip { - position: absolute; - text-align: left; - /* width: 140px; */ - /* height: 28px;*/ - padding: 0px 5px 0px 5px; - padding-top: 0px; - font: 12px sans-serif; - background: #fff7bc; - border: solid 1px #aaa; - border-radius: 6px; - pointer-events: none;} - - h5 { font: 14px sans-serif; - background : #ec7014; - color: #ffffe5; - padding: 5px 2px 5px 2px; - margin-top: 1px;} - path { - fill: #aaaaaa;} - diff --git a/applications/examples/static/js/d3.min.js b/applications/examples/static/js/d3.min.js deleted file mode 100644 index 6ba6b41e..00000000 --- a/applications/examples/static/js/d3.min.js +++ /dev/null @@ -1,8 +0,0 @@ -// https://d3js.org Version 4.7.4. Copyright 2017 Mike Bostock. -(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t){return function(n,e){return Ls(t(n),e)}}function e(t,n){return[t,n]}function r(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Js?i*=10:o>=Qs?i*=5:o>=Ks&&(i*=2),n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function g(t,n){for(var e,r=0,i=t.length;r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function S(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function q(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function D(t,n){return function(){this.setAttribute(t,n)}}function O(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function F(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function Y(t){return function(){this.style.removeProperty(t)}}function B(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t){return function(){delete this[t]}}function X(t,n){return function(){this[t]=n}}function V(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function $(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new Z(t)}function Z(t){this._node=t,this._names=$(t.getAttribute("class")||"")}function G(t,n){for(var e=W(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Rl.exec(t))?kt(parseInt(n[1],16)):(n=ql.exec(t))?new At(n[1],n[2],n[3],1):(n=Ul.exec(t))?new At(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Dl.exec(t))?St(n[1],n[2],n[3],n[4]):(n=Ol.exec(t))?St(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Fl.exec(t))?Ct(n[1],n[2]/100,n[3]/100,1):(n=Il.exec(t))?Ct(n[1],n[2]/100,n[3]/100,n[4]):Yl.hasOwnProperty(t)?kt(Yl[t]):"transparent"===t?new At(NaN,NaN,NaN,0):null}function kt(t){return new At(t>>16&255,t>>8&255,255&t,1)}function St(t,n,e,r){return r<=0&&(t=n=e=NaN),new At(t,n,e,r)}function Nt(t){return t instanceof Mt||(t=Tt(t)),t?(t=t.rgb(),new At(t.r,t.g,t.b,t.opacity)):new At}function Et(t,n,e,r){return 1===arguments.length?Nt(t):new At(t,n,e,null==r?1:r)}function At(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ct(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Lt(t,n,e,r)}function zt(t){if(t instanceof Lt)return new Lt(t.h,t.s,t.l,t.opacity);if(t instanceof Mt||(t=Tt(t)),!t)return new Lt;if(t instanceof Lt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&c<1?0:u,new Lt(u,a,c,t.opacity)}function Pt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function qt(t){if(t instanceof Dt)return new Dt(t.l,t.a,t.b,t.opacity);if(t instanceof Ht){var n=t.h*Bl;return new Dt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof At||(t=Nt(t));var e=Yt(t.r),r=Yt(t.g),i=Yt(t.b),o=Ot((.4124564*e+.3575761*r+.1804375*i)/Hl),u=Ot((.2126729*e+.7151522*r+.072175*i)/Xl);return new Dt(116*u-16,500*(o-u),200*(u-Ot((.0193339*e+.119192*r+.9503041*i)/Vl)),t.opacity)}function Ut(t,n,e,r){return 1===arguments.length?qt(t):new Dt(t,n,e,null==r?1:r)}function Dt(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Ot(t){return t>Gl?Math.pow(t,1/3):t/Zl+$l}function Ft(t){return t>Wl?t*t*t:Zl*(t-$l)}function It(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Yt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bt(t){if(t instanceof Ht)return new Ht(t.h,t.c,t.l,t.opacity);t instanceof Dt||(t=qt(t));var n=Math.atan2(t.b,t.a)*jl;return new Ht(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function jt(t,n,e,r){return 1===arguments.length?Bt(t):new Ht(t,n,e,null==r?1:r)}function Ht(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Xt(t){if(t instanceof $t)return new $t(t.h,t.s,t.l,t.opacity);t instanceof At||(t=Nt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(ih*r+eh*n-rh*e)/(ih+eh-rh),o=r-i,u=(nh*(e-i)-Kl*o)/th,a=Math.sqrt(u*u+o*o)/(nh*i*(1-i)),c=a?Math.atan2(u,o)*jl-120:NaN;return new $t(c<0?c+360:c,a,i,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Xt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function Zt(t,n){return function(e){return t+e*n}}function Gt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Jt(t,n){var e=n-t;return e?Zt(t,e>180||e<-180?e-360*Math.round(e/360):e):ph(isNaN(t)?n:t)}function Qt(t){return 1==(t=+t)?Kt:function(n,e){return e-n?Gt(n,e,t):ph(isNaN(n)?e:n)}}function Kt(t,n){var e=n-t;return e?Zt(t,e):ph(isNaN(t)?n:t)}function tn(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:mh(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:mh(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:mh(t,e)},{i:a-2,x:mh(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e=0&&n._call.call(null,t),n=n._next;--Ih}function mn(){Xh=(Hh=$h.now())+Vh,Ih=Yh=0;try{yn()}finally{Ih=0,bn(),Xh=0}}function xn(){var t=$h.now(),n=t-Hh;n>jh&&(Vh-=n,Hh=t)}function bn(){for(var t,n,e=sh,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:sh=n);fh=t,wn(r)}function wn(t){if(!Ih){Yh&&(Yh=clearTimeout(Yh));var n=t-Xh;n>24?(t<1/0&&(Yh=setTimeout(mn,n)),Bh&&(Bh=clearInterval(Bh))):(Bh||(Hh=Xh,Bh=setInterval(xn,jh)),Ih=1,Wh(mn))}}function Mn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Kh)throw new Error("too late");return e}function Tn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>np)throw new Error("too late");return e}function kn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function Sn(t,n,e){function r(t){e.state=tp,e.timer.restart(i,e.delay,e.time),e.delay<=t&&i(t-e.delay)}function i(r){var s,f,l,h;if(e.state!==tp)return u();for(s in c)if(h=c[s],h.name===e.name){if(h.state===ep)return Zh(i);h.state===rp?(h.state=op,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete c[s]):+s=0&&(t=t.slice(0,n)),!t||"start"===t})}function Hn(t,n,e){var r,i,o=jn(n)?Mn:Tn;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function Xn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Vn(t,n){var e,r,i;return function(){var o=al(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function $n(t){return function(){this.style.removeProperty(t)}}function Wn(t,n,e){var r,i;return function(){var o=al(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function Zn(t,n,e){var r,i,o;return function(){var u=al(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Gn(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function Jn(t){return function(){this.textContent=t}}function Qn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Kn(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function te(t){return vt().transition(t)}function ne(){return++Ep}function ee(t){return+t}function re(t){return t*t}function ie(t){return t*(2-t)}function oe(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function ue(t){return t*t*t}function ae(t){return--t*t*t+1}function ce(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function se(t){return 1-Math.cos(t*Rp)}function fe(t){return Math.sin(t*Rp)}function le(t){return(1-Math.cos(Lp*t))/2}function he(t){return Math.pow(2,10*t-10)}function pe(t){return 1-Math.pow(2,-10*t)}function de(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function ve(t){return 1-Math.sqrt(1-t*t)}function _e(t){return Math.sqrt(1- --t*t)}function ge(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ye(t){return 1-me(1-t)}function me(t){return(t=+t)Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,id(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],S){case ud:case od:N&&(m=Math.max(P-l,Math.min(R-v,m)),h=l+m,_=v+m),E&&(x=Math.max(L-p,Math.min(q-g,x)),d=p+x,y=g+x);break;case ad:N<0?(m=Math.max(P-l,Math.min(R-l,m)),h=l+m,_=v):N>0&&(m=Math.max(P-v,Math.min(R-v,m)),h=l,_=v+m),E<0?(x=Math.max(L-p,Math.min(q-p,x)),d=p+x,y=g):E>0&&(x=Math.max(L-g,Math.min(q-g,x)),d=p,y=g+x);break;case cd:N&&(h=Math.max(P,Math.min(R,l-m*N)),_=Math.max(P,Math.min(R,v+m*N))),E&&(d=Math.max(L,Math.min(q,p-x*E)),y=Math.max(L,Math.min(q,g+x*E)))}_0&&(l=h-m),E<0?g=y-x:E>0&&(p=d-x),S=ud,Y.attr("cursor",hd.selection),o());break;default:return}id()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:S===cd&&(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad,o());break;case 32:S===ud&&(t.event.altKey?(N&&(v=_-m*N,l=h+m*N),E&&(g=y-x*E,p=d+x*E),S=cd):(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad),Y.attr("cursor",hd[k]),o());break;default:return}id()}if(t.event.touches){if(t.event.changedTouches.length=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u}while((l=f<<1|s)==(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function Qe(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;ef&&(f=r),il&&(l=i));for(f",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Sv[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function pr(n){ -return Ev=zv(n),t.format=Ev.format,t.formatPrefix=Ev.formatPrefix,Ev}function dr(){this.reset()}function vr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function _r(t){return t>1?0:t<-1?v_:Math.acos(t)}function gr(t){return t>1?__:t<-1?-__:Math.asin(t)}function yr(t){return(t=A_(t/2))*t}function mr(){}function xr(t,n){t&&R_.hasOwnProperty(t.type)&&R_[t.type](t,n)}function br(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=T_(n),u=A_(n),a=Iv*u,c=Fv*o+a*T_(i),s=a*r*A_(i);U_.add(M_(s,c)),Ov=t,Fv=o,Iv=u}function Nr(t){return[M_(t[1],t[0]),gr(t[2])]}function Er(t){var n=t[0],e=t[1],r=T_(e);return[r*T_(n),r*A_(n),A_(e)]}function Ar(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Cr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function zr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Pr(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Lr(t){var n=z_(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Rr(t,n){Zv.push(Gv=[Yv=t,jv=t]),nHv&&(Hv=n)}function qr(t,n){var e=Er([t*x_,n*x_]);if(Wv){var r=Cr(Wv,e),i=[r[1],-r[0],0],o=Cr(i,r);Lr(o),o=Nr(o);var u,a=t-Xv,c=a>0?1:-1,s=o[0]*m_*c,f=b_(a)>180;f^(c*XvHv&&(Hv=u):(s=(s+360)%360-180,f^(c*XvHv&&(Hv=n))),f?tYr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t):jv>=Yv?(tjv&&(jv=t)):t>Xv?Yr(Yv,t)>Yr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t)}else Zv.push(Gv=[Yv=t,jv=t]);nHv&&(Hv=n),Wv=e,Xv=t}function Ur(){Y_.point=qr}function Dr(){Gv[0]=Yv,Gv[1]=jv,Y_.point=Rr,Wv=null}function Or(t,n){if(Wv){var e=t-Xv;I_.add(b_(e)>180?e+(e>0?360:-360):e)}else Vv=t,$v=n;O_.point(t,n),qr(t,n)}function Fr(){O_.lineStart()}function Ir(){Or(Vv,$v),O_.lineEnd(),b_(I_)>d_&&(Yv=-(jv=180)),Gv[0]=Yv,Gv[1]=jv,Wv=null}function Yr(t,n){return(n-=t)<0?n+360:n}function Br(t,n){return t[0]-n[0]}function jr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nv_?t-y_:t<-v_?t+y_:t,n]}function ni(t,n,e){return(t%=y_)?n||e?V_(ri(t),ii(n,e)):ri(t):n||e?ii(n,e):ti}function ei(t){return function(n,e){return n+=t,[n>v_?n-y_:n<-v_?n+y_:n,e]}}function ri(t){var n=ei(t);return n.invert=ei(-t),n}function ii(t,n){function e(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*r+a*i;return[M_(c*o-f*u,a*r-s*i),gr(f*o+c*u)]}var r=T_(t),i=A_(t),o=T_(n),u=A_(n);return e.invert=function(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*o-c*u;return[M_(c*o+s*u,a*r+f*i),gr(f*r-a*i)]},e}function oi(t,n,e,r,i,o){if(e){var u=T_(n),a=A_(n),c=r*e;null==i?(i=n+r*y_,o=n-c/2):(i=ui(u,i),o=ui(u,o),(r>0?io)&&(i+=r*y_));for(var s,f=i;r>0?f>o:f0)do{s.point(0===f||3===f?t:e,f>1?r:n)}while((f=(f+a+4)%4)!==l);else s.point(o[0],o[1])}function u(r,i){return b_(r[0]-t)0?0:3:b_(r[0]-e)0?2:1:b_(r[1]-n)0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&S.point(t,n)}function s(){for(var n=0,e=0,i=_.length;er&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){S=N,v=[],_=[],k=!0}function l(){var t=s(),n=k&&t,e=(v=ff(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&sg(v,a,t,o,u),u.polygonEnd()),S=u,v=_=g=null}function h(){E.point=d,_&&_.push(g=[]),T=!0,M=!1,b=w=NaN}function p(){v&&(d(y,m),x&&M&&N.rejoin(),v.push(N.result())),E.point=c,M&&S.lineEnd()}function d(o,u){var a=i(o,u);if(_&&g.push([o,u]),T)y=o,m=u,x=a,T=!1,a&&(S.lineStart(),S.point(o,u));else if(a&&M)S.point(o,u);else{var c=[b=Math.max(lg,Math.min(fg,b)),w=Math.max(lg,Math.min(fg,w))],s=[o=Math.max(lg,Math.min(fg,o)),u=Math.max(lg,Math.min(fg,u))];ag(c,s,t,n,e,r)?(M||(S.lineStart(),S.point(c[0],c[1])),S.point(s[0],s[1]),a||S.lineEnd(),k=!1):a&&(S.lineStart(),S.point(o,u),k=!1)}b=o,w=u,M=a}var v,_,g,y,m,x,b,w,M,T,k,S=u,N=ug(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function fi(){_g.point=hi,_g.lineEnd=li}function li(){_g.point=_g.lineEnd=mr}function hi(t,n){t*=x_,n*=x_,$_=t,W_=A_(n),Z_=T_(n),_g.point=pi}function pi(t,n){t*=x_,n*=x_;var e=A_(n),r=T_(n),i=b_(t-$_),o=T_(i),u=A_(i),a=r*u,c=Z_*e-W_*r*o,s=W_*e+Z_*r*o;vg.add(M_(z_(a*a+c*c),s)),$_=t,W_=e,Z_=r}function di(t,n){return!(!t||!wg.hasOwnProperty(t.type))&&wg[t.type](t,n)}function vi(t,n){return 0===xg(t,n)}function _i(t,n){var e=xg(t[0],t[1]);return xg(t[0],n)+xg(n,t[1])<=e+d_}function gi(t,n){return!!dg(t.map(yi),mi(n))}function yi(t){return t=t.map(mi),t.pop(),t}function mi(t){return[t[0]*x_,t[1]*x_]}function xi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function bi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function wi(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return Gs(k_(o/_)*_,i,_).map(h).concat(Gs(k_(s/g)*g,c,g).map(p)).concat(Gs(k_(r/d)*d,e,d).filter(function(t){return b_(t%_)>d_}).map(f)).concat(Gs(k_(a/v)*v,u,v).filter(function(t){return b_(t%g)>d_}).map(l))}var e,r,i,o,u,a,c,s,f,l,h,p,d=10,v=d,_=90,g=360,y=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[h(o).concat(p(c).slice(1),h(i).reverse().slice(1),p(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(y)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(y)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(_=+n[0],g=+n[1],t):[_,g]},t.stepMinor=function(n){return arguments.length?(d=+n[0],v=+n[1],t):[d,v]},t.precision=function(n){return arguments.length?(y=+n,f=xi(a,u,90),l=bi(r,e,y),h=xi(s,c,90),p=bi(o,i,y),t):y},t.extentMajor([[-180,-90+d_],[180,90-d_]]).extentMinor([[-180,-80-d_],[180,80+d_]])}function Mi(){return wi()()}function Ti(){Eg.point=ki}function ki(t,n){Eg.point=Si,G_=Q_=t,J_=K_=n}function Si(t,n){Ng.add(K_*t-Q_*n),Q_=t,K_=n}function Ni(){Si(G_,J_)}function Ei(t,n){tzg&&(zg=t),nPg&&(Pg=n)}function Ai(t,n){Rg+=t,qg+=n,++Ug}function Ci(){jg.point=zi}function zi(t,n){jg.point=Pi,Ai(eg=t,rg=n)}function Pi(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,Ai(eg=t,rg=n)}function Li(){jg.point=Ai}function Ri(){jg.point=Ui}function qi(){Di(tg,ng)}function Ui(t,n){jg.point=Di,Ai(tg=eg=t,ng=rg=n)}function Di(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,i=rg*t-eg*n,Ig+=i*(eg+t),Yg+=i*(rg+n),Bg+=3*i,Ai(eg=t,rg=n)}function Oi(t){this._context=t}function Fi(t,n){Gg.point=Ii,Xg=$g=t,Vg=Wg=n}function Ii(t,n){$g-=t,Wg-=n,Zg.add(z_($g*$g+Wg*Wg)),$g=t,Wg=n}function Yi(){this._string=[]}function Bi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function ji(t){return t.length>1}function Hi(t,n){return((t=t.x)[0]<0?t[1]-__-d_:__-t[1])-((n=n.x)[0]<0?n[1]-__-d_:__-n[1])}function Xi(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?v_:-v_,c=b_(o-e);b_(c-v_)0?__:-__),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=v_&&(b_(e-i)d_?w_((A_(n)*(o=T_(r))*A_(e)-A_(r)*(i=T_(n))*A_(t))/(i*o*u)):(n+r)/2}function $i(t,n,e,r){var i;if(null==t)i=e*__,r.point(-v_,i),r.point(0,i),r.point(v_,i),r.point(v_,0),r.point(v_,-i),r.point(0,-i),r.point(-v_,-i),r.point(-v_,0),r.point(-v_,i);else if(b_(t[0]-n[0])>d_){var o=t[0]4*n&&v--){var x=u+h,b=a+p,w=c+d,M=z_(x*x+b*b+w*w),T=gr(w/=M),k=b_(b_(w)-1)n||b_((g*A+y*C)/m-.5)>.3||u*h+a*p+c*d2?t[2]%360*x_:0,i()):[b*m_,w*m_,M*m_]},n.precision=function(t){return arguments.length?(A=iy(r,E=t*t),o()):z_(E)},n.fitExtent=function(t,e){return Gi(n,t,e)},n.fitSize=function(t,e){return Ji(n,t,e)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function eo(t){var n=0,e=v_/3,r=no(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*x_,e=t[1]*x_):[n*m_,e*m_]},i}function ro(t){function n(t,n){return[t*e,A_(n)/e]}var e=T_(t);return n.invert=function(t,n){return[t/e,gr(n*e)]},n}function io(t,n){function e(t,n){var e=z_(o-2*i*A_(n))/i;return[e*A_(t*=i),u-e*T_(t)]}var r=A_(t),i=(r+A_(n))/2;if(b_(i)0?n<-__+d_&&(n=-__+d_):n>__-d_&&(n=__-d_);var e=o/E_(fo(n),i);return[e*A_(i*t),o-e*T_(i*t)]}var r=T_(t),i=t===n?A_(t):N_(r/T_(n))/N_(fo(n)/fo(t)),o=r*E_(fo(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=C_(i)*z_(t*t+e*e);return[M_(t,b_(e))/i*C_(e),2*w_(E_(o/r,1/i))-__]},e):co}function ho(t,n){return[t,n]}function po(t,n){function e(t,n){var e=o-n,r=i*t;return[e*A_(r),o-e*T_(r)]}var r=T_(t),i=t===n?A_(t):(r-T_(n))/(n-t),o=r/i+t;return b_(i)=0;)n+=e[r].value;else n=1;t.value=n}function Eo(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function Ao(t,n){var e,r,i,o,u,a=new Ro(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=zo);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new Ro(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(Lo)}function Co(){return Ao(this).eachBefore(Po)}function zo(t){return t.children}function Po(t){t.data=t.data.data}function Lo(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Ro(t){this.data=t,this.depth=this.height=0,this.parent=null}function qo(t){this._=t,this.next=null}function Uo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Do(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=Oo(n[0]);break;case 2:e=Fo(n[0],n[1]);break;case 3:e=Io(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&Uo(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Do(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function Oo(t){return{x:t.x,y:t.y,r:t.r}}function Fo(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function Io(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),g=2*(i-f),y=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*g,b=(p*m-g*v)/x-r,w=(g*d-p*y)/x,M=(_*v-h*m)/x-i,T=(h*y-_*d)/x,k=w*w+T*T-1,S=2*(b*w+M*T+o),N=b*b+M*M-o*o,E=(-S-Math.sqrt(S*S-4*k*N))/(2*k);return{x:b+w*E+r,y:M+T*E+i,r:E}}function Yo(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Bo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i-1e-6>e*e+r*r}function jo(t,n,e){var r=t._,i=t.next._,o=r.r+i.r,u=(r.x*i.r+i.x*r.r)/o-n,a=(r.y*i.r+i.y*r.r)/o-e;return u*u+a*a}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Yo(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,g=h*n.y+p*e.y+d*r.y;n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;a=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function uu(t,n,e){return t.a.parent===n.parent?t.a:e}function au(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function cu(t){for(var n,e,r,i,o,u=new au(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new au(r[i],i)),e.parent=n;return(u.parent=new au(null,0)).children=[u],u}function su(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,g=[],y=n.children,m=0,x=0,b=y.length,w=n.value;mh&&(h=a),_=f*f*v,(p=Math.max(h/_,_/l))>d){f-=a;break}d=p}g.push(u={value:f,dice:c1&&em(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function hu(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function pu(t){if(!t._start)try{du(t)}catch(n){if(t._tasks[t._ended+t._active-1])_u(t,n);else if(!t._data)throw n}}function du(t){for(;t._start=t._waiting&&t._active=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,gu(t)}function gu(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function yu(t){return new hu(arguments.length?+t:1/0)}function mu(t){return function(n,e){t(null==n?e:null)}}function xu(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function bu(t,n){return function(e){return t(e.responseText,n)}}function wu(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==Sm)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=Ye(),r=[],i=Sm;return t=null==t?[]:km.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=Ye();for(var i,o,u=-1,a=t.length;++u=e?1:r(t)}}}function Eu(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function Au(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o2?Cu:Au,o=u=null,r}function r(n){return(o||(o=i(a,c,f?Nu(t):t,s)))(+n)}var i,o,u,a=Am,c=Am,s=Th,f=!1;return r.invert=function(t){return(u||(u=i(c,a,Su,f?Eu(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=Tm.call(t,Em),e()):a.slice()},r.range=function(t){return arguments.length?(c=km.call(t),e()):c.slice()},r.rangeRound=function(t){return c=km.call(t),s=kh,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function Lu(t){var n=t.domain;return t.ticks=function(t){var e=n();return tf(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Cm(n(),t,e)},t.nice=function(e){var i=n(),o=i.length-1,u=null==e?10:e,a=i[0],c=i[o],s=r(a,c,u);return s&&(s=r(Math.floor(a/s)*s,Math.ceil(c/s)*s,u),i[0]=Math.floor(a/s)*s,i[o]=Math.ceil(c/s)*s,n(i)),t},t}function Ru(){var t=Pu(Su,mh);return t.copy=function(){return zu(t,Ru())},Lu(t)}function qu(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Tm.call(e,Em),t):n.slice()},t.copy=function(){return qu().domain(n)},Lu(t)}function Uu(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:Nm(n)}function Du(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function Ou(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Fu(t){return 10===t?Ou:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Iu(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Yu(t){return function(n){return-t(-n)}}function Bu(){function n(){return o=Iu(i),u=Fu(i),r()[0]<0&&(o=Yu(o),u=Yu(u)),e}var e=Pu(Uu,Du).domain([1,10]),r=e.domain,i=10,o=Iu(10),u=Fu(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c0){for(;hc)break;v.push(l)}}else for(;h=1;--f)if(!((l=s*f)c)break;v.push(l)}}else v=tf(h,p,Math.min(p-h,d)).map(u);return n?v.reverse():v},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i0?i[n-1]:e[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return $u().domain([e,r]).range(u)},Lu(t)}function Wu(){function t(t){if(t<=t)return e[Us(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=km.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=km.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Wu().domain(n).range(e)},t}function Zu(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pm.setTime(+n),Lm.setTime(+r),t(Pm),t(Lm),Math.floor(e(Pm,Lm))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Gu(t){return Zu(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/Dm})}function Ju(t){return Zu(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Dm})}function Qu(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Ku(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ta(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function na(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date(+e));++a=c)return-1;if(37===(i=n.charCodeAt(u++))){if(i=n.charAt(u++),!(o=B[i in qx?n.charAt(u++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=R.exec(n.slice(e));return r?(t.w=q[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=L[r[0].toLowerCase()],e+r[0].length):-1}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return N[t.getDay()]}function p(t){return S[t.getDay()]}function d(t){return A[t.getMonth()]}function v(t){return E[t.getMonth()]}function _(t){return k[+(t.getHours()>=12)]}function g(t){return N[t.getUTCDay()]}function y(t){return S[t.getUTCDay()]}function m(t){return A[t.getUTCMonth()]}function x(t){return E[t.getUTCMonth()]}function b(t){return k[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,k=t.periods,S=t.days,N=t.shortDays,E=t.months,A=t.shortMonths,C=ia(k),z=oa(k),P=ia(S),L=oa(S),R=ia(N),q=oa(N),U=ia(E),D=oa(E),O=ia(A),F=oa(A),I={a:h,A:p,b:d,B:v,c:null,d:xa,e:xa,H:ba,I:wa,j:Ma,L:Ta,m:ka,M:Sa,p:_,S:Na,U:Ea,w:Aa,W:Ca,x:null,X:null,y:za,Y:Pa,Z:La,"%":Wa},Y={a:g,A:y,b:m,B:x,c:null,d:Ra,e:Ra,H:qa,I:Ua,j:Da,L:Oa,m:Fa,M:Ia,p:b,S:Ya,U:Ba,w:ja,W:Ha,x:null,X:null,y:Xa,Y:Va,Z:$a,"%":Wa},B={a:o,A:u,b:a,B:c,c:s,d:pa,e:pa,H:va,I:va,j:da,L:ya,m:ha,M:_a,p:i,S:ga,U:aa,w:ua,W:ca,x:f,X:l,y:fa,Y:sa,Z:la,"%":ma};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Qu);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Ku);return n.toString=function(){return t},n}}}function ea(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function la(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function ha(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function pa(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function da(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function va(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function _a(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function ga(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function ya(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function ma(t,n,e){var r=Dx.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function xa(t,n){return ea(t.getDate(),n,2)}function ba(t,n){return ea(t.getHours(),n,2)}function wa(t,n){return ea(t.getHours()%12||12,n,2)}function Ma(t,n){return ea(1+Hm.count(cx(t),t),n,3)}function Ta(t,n){return ea(t.getMilliseconds(),n,3)}function ka(t,n){return ea(t.getMonth()+1,n,2)}function Sa(t,n){return ea(t.getMinutes(),n,2)}function Na(t,n){return ea(t.getSeconds(),n,2)}function Ea(t,n){return ea(Vm.count(cx(t),t),n,2)}function Aa(t){return t.getDay()}function Ca(t,n){return ea($m.count(cx(t),t),n,2)}function za(t,n){return ea(t.getFullYear()%100,n,2)}function Pa(t,n){return ea(t.getFullYear()%1e4,n,4)}function La(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+ea(n/60|0,"0",2)+ea(n%60,"0",2)}function Ra(t,n){return ea(t.getUTCDate(),n,2)}function qa(t,n){return ea(t.getUTCHours(),n,2)}function Ua(t,n){return ea(t.getUTCHours()%12||12,n,2)}function Da(t,n){return ea(1+dx.count(Px(t),t),n,3)}function Oa(t,n){return ea(t.getUTCMilliseconds(),n,3)}function Fa(t,n){return ea(t.getUTCMonth()+1,n,2)}function Ia(t,n){return ea(t.getUTCMinutes(),n,2)}function Ya(t,n){return ea(t.getUTCSeconds(),n,2)}function Ba(t,n){return ea(_x.count(Px(t),t),n,2)}function ja(t){return t.getUTCDay()}function Ha(t,n){return ea(gx.count(Px(t),t),n,2)}function Xa(t,n){return ea(t.getUTCFullYear()%100,n,2)}function Va(t,n){return ea(t.getUTCFullYear()%1e4,n,4)}function $a(){return"+0000"}function Wa(){return"%"}function Za(n){return Lx=na(n),t.timeFormat=Lx.format,t.timeParse=Lx.parse,t.utcFormat=Lx.utcFormat,t.utcParse=Lx.utcParse,Lx}function Ga(t){return t.toISOString()}function Ja(t){var n=new Date(t);return isNaN(n)?null:n}function Qa(t){return new Date(t)}function Ka(t){return t instanceof Date?+t:+new Date(+t)}function tc(t,n,e,i,o,u,a,c,s){function f(r){return(a(r)1?0:t<-1?mb:Math.acos(t)}function ic(t){return t>=1?xb:t<=-1?-xb:Math.asin(t)}function oc(t){return t.innerRadius}function uc(t){return t.outerRadius}function ac(t){return t.startAngle}function cc(t){return t.endAngle}function sc(t){return t&&t.padAngle}function fc(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function lc(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/gb(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,g=(p+v)/2,y=d-h,m=v-p,x=y*y+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*gb(db(0,b*b*x-w*w)),T=(w*m-y*M)/x,k=(-w*y-m*M)/x,S=(w*m+y*M)/x,N=(-w*y+m*M)/x,E=T-_,A=k-g,C=S-_,z=N-g;return E*E+A*A>C*C+z*z&&(T=S,k=N),{cx:T,cy:k,x01:-f,y01:-l,x11:T*(i/b-1),y11:k*(i/b-1)}}function hc(t){this._context=t}function pc(t){return t[0]}function dc(t){return t[1]}function vc(t){this._curve=t}function _c(t){function n(n){return new vc(t(n))}return n._curve=t,n}function gc(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t}function yc(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function mc(t){this._context=t}function xc(t){this._context=t}function bc(t){this._context=t}function wc(t,n){this._basis=new mc(t),this._beta=n}function Mc(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Tc(t,n){this._context=t,this._k=(1-n)/6}function kc(t,n){this._context=t,this._k=(1-n)/6}function Sc(t,n){this._context=t,this._k=(1-n)/6}function Nc(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>yb){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>yb){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function Ec(t,n){this._context=t,this._alpha=n}function Ac(t,n){this._context=t,this._alpha=n}function Cc(t,n){this._context=t,this._alpha=n}function zc(t){this._context=t}function Pc(t){return t<0?-1:1}function Lc(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Pc(o)+Pc(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Rc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function qc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function Uc(t){this._context=t}function Dc(t){this._context=new Oc(t)}function Oc(t){this._context=t}function Fc(t){return new Uc(t)}function Ic(t){return new Dc(t)}function Yc(t){this._context=t}function Bc(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n0)){if(o/=d,d<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o0)){if(o/=v,v<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function os(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]Cw||Math.abs(i[0][1]-i[1][1])>Cw)||delete Nw[o]}function as(t){return kw[t.index]={site:t,halfedges:[]}}function cs(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function ss(t,n){return n[+(n.left!==t.site)]}function fs(t,n){return n[+(n.left===t.site)]}function ls(){for(var t,n,e,r,i=0,o=kw.length;iCw||Math.abs(v-h)>Cw)&&(c.splice(a,0,Nw.push(es(u,p,Math.abs(d-t)Cw?[t,Math.abs(l-t)Cw?[Math.abs(h-r)Cw?[e,Math.abs(l-e)Cw?[Math.abs(h-n)=-zw)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,g=Ew.pop()||new ps;g.arc=t,g.site=i,g.x=v+u,g.y=(g.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=g;for(var y=null,m=Sw._;m;)if(g.yCw)a=a.L;else{if(!((i=o-ws(a,u))>Cw)){r>-Cw?(n=a.P,e=a):i>-Cw?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}as(t);var c=gs(t);if(Tw.insert(n,c),n||e){if(n===e)return vs(n),e=gs(n.site),Tw.insert(c,e),c.edge=e.edge=ns(n.site,c.site),ds(n),void ds(e);if(!e)return void(c.edge=ns(n.site,c.site));vs(n),vs(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,g=2*(h*_-p*v),y=h*h+p*p,m=v*v+_*_,x=[(_*y-p*m)/g+f,(h*m-v*y)/g+l];rs(e.edge,s,d,x),c.edge=ns(s,t,null,x),e.edge=ns(t,d,null,x),ds(n),ds(e)}}function bs(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function ws(t,n){var e=t.N;if(e)return bs(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function Ms(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function Ts(t,n){return n[1]-t[1]||n[0]-t[0]}function ks(t,n){var e,r,i,o=t.sort(Ts).pop();for(Nw=[],kw=new Array(t.length),Tw=new Gc,Sw=new Gc;;)if(i=Mw,o&&(!i||o[1]n?1:t>=n?0:NaN},Rs=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}},qs=Rs(Ls),Us=qs.right,Ds=qs.left,Os=function(t,n){null==n&&(n=e);for(var r=0,i=t.length-1,o=t[0],u=new Array(i<0?0:i);rt?1:n>=t?0:NaN},Ys=function(t){return null===t?NaN:+t},Bs=function(t,n){var e,r,i=t.length,o=0,u=0,a=-1,c=0;if(null==n)for(;++a1)return u/(c-1)},js=function(t,n){var e=Bs(t,n);return e?Math.sqrt(e):e},Hs=function(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o=r){e=i=r;break}for(;++or&&(e=r),i=r){e=i=r;break}for(;++or&&(e=r),i=f;)l.pop(),--h;var p,d=new Array(h+1);for(i=0;i<=h;++i)p=d[i]=[],p.x0=i>0?l[i-1]:s,p.x1=i=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),u=+e(t[o],o,t);return u+(+e(t[o+1],o+1,t)-u)*(i-o)}},of=function(t,n,e){return t=$s.call(t,Ys).sort(Ls),Math.ceil((e-n)/(2*(rf(t,.75)-rf(t,.25))*Math.pow(t.length,-1/3)))},uf=function(t,n,e){return Math.ceil((e-n)/(3.5*js(t)*Math.pow(t.length,-1/3)))},af=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},cf=function(t,n){var e,r=0,i=t.length,o=-1,u=i;if(null==n)for(;++o=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e},lf=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},hf=function(t,n){for(var e=n.length,r=new Array(e);e--;)r[e]=t[n[e]];return r},pf=function(t,n){if(e=t.length){var e,r,i=0,o=0,u=t[o];for(n||(n=Ls);++i0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Nf.hasOwnProperty(n)?{space:Nf[n],local:t}:t},Af=function(t){var n=Ef(t);return(n.local?x:m)(n)},Cf=0;w.prototype=b.prototype={constructor:w,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var zf=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Pf=document.documentElement;if(!Pf.matches){var Lf=Pf.webkitMatchesSelector||Pf.msMatchesSelector||Pf.mozMatchesSelector||Pf.oMatchesSelector;zf=function(t){return function(){return Lf.call(this,t)}}}}var Rf=zf,qf={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(qf={mouseenter:"mouseover",mouseleave:"mouseout"})}var Uf=function(t,n,e){var r,i,o=k(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?N:S,null==e&&(e=!1),r=0;r=x&&(x=m+1);!(y=_[x])&&++x=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Kf=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=R);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?Y:"function"==typeof n?j:B)(t,n,null==e?"":e)):al(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},sl=function(t,n){return arguments.length>1?this.each((null==n?H:"function"==typeof n?V:X)(t,n)):this.node()[t]};Z.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var fl=function(t,n){var e=$(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=e.length;++i=240?t-240:t+120,i,r),Rt(t,i,r),Rt(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Bl=Math.PI/180,jl=180/Math.PI,Hl=.95047,Xl=1,Vl=1.08883,$l=4/29,Wl=6/29,Zl=3*Wl*Wl,Gl=Wl*Wl*Wl;Al(Dt,Ut,wt(Mt,{brighter:function(t){return new Dt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Dt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Xl*Ft(t),n=Hl*Ft(n),e=Vl*Ft(e),new At(It(3.2404542*n-1.5371385*t-.4985314*e),It(-.969266*n+1.8760108*t+.041556*e),It(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),Al(Ht,jt,wt(Mt,{brighter:function(t){return new Ht(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Ht(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return qt(this).rgb()}}));var Jl=-.14861,Ql=1.78277,Kl=-.29227,th=-.90649,nh=1.97294,eh=nh*th,rh=nh*Ql,ih=Ql*Kl-th*Jl;Al($t,Vt,wt(Mt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Bl,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new At(255*(n+e*(Jl*r+Ql*i)),255*(n+e*(Kl*r+th*i)),255*(n+e*(nh*r)),this.opacity)}}));var oh,uh,ah,ch,sh,fh,lh=function(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:mh(e,r)})),o=wh.lastIndex;return onp&&e.statetp&&e.name===n)return new Kn([[t]],td,n,+r)}return null},ed=function(t){return function(){return t}},rd=function(t,n,e){this.target=t,this.type=n,this.selection=e},id=function(){t.event.preventDefault(),t.event.stopImmediatePropagation()},od={name:"drag"},ud={name:"space"},ad={name:"handle"},cd={name:"center"},sd={name:"x",handles:["e","w"].map(Me),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fd={name:"y",handles:["n","s"].map(Me),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},ld={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(Me),input:function(t){return t},output:function(t){return t}},hd={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},pd={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},dd={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},vd={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},_d={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},gd=function(){return ze(ld)},yd=Math.cos,md=Math.sin,xd=Math.PI,bd=xd/2,wd=2*xd,Md=Math.max,Td=function(){function t(t){var o,u,a,c,s,f,l=t.length,h=[],p=Gs(l),d=[],v=[],_=v.groups=new Array(l),g=new Array(l*l);for(o=0,s=-1;++s1e-6)if(Math.abs(f*a-c*s)>1e-6&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),g=Math.sqrt(l),y=i*Math.tan((Nd-Math.acos((d+l-v)/(2*_*g)))/2),m=y/g,x=y/_;Math.abs(m-1)>1e-6&&(this._+="L"+(t+m*s)+","+(n+m*f)),this._+="A"+i+","+i+",0,0,"+ +(f*h>s*p)+","+(this._x1=t+x*a)+","+(this._y1=n+x*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+s:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-s)>1e-6)&&(this._+="L"+c+","+s),e&&(l<0&&(l=l%Ed+Ed),l>Ad?this._+="A"+e+","+e+",0,1,"+f+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+f+","+(this._x1=c)+","+(this._y1=s):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Nd)+","+f+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var Cd=function(){function t(){var t,a=kd.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,a)),l=i.apply(this,a)-bd,h=o.apply(this,a)-bd,p=f*yd(l),d=f*md(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-bd,g=o.apply(this,a)-bd;if(u||(u=t=Re()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===g||(u.quadraticCurveTo(0,0,v*yd(_),v*md(_)),u.arc(0,0,v,_,g)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=qe,e=Ue,r=De,i=Oe,o=Fe,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:Sd(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Sd(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Sd(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t};Ie.prototype=Ye.prototype={constructor:Ie,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var zd=function(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=Ye(),v=u();++lo.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,Be,je)},map:function(n){return t(n,0,He,Xe)},entries:function(e){return n(t(e,0,He,Xe),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},Pd=Ye.prototype;Ve.prototype=$e.prototype={constructor:Ve,has:Pd.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Pd.remove,clear:Pd.clear,values:Pd.keys,size:Pd.size,empty:Pd.empty,each:Pd.each};var Ld=function(t){var n=[];for(var e in t)n.push(e);return n},Rd=function(t){var n=[];for(var e in t)n.push(t[e]);return n},qd=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},Ud=function(t){function n(t,n){var r,i,o=e(t,function(t,e){if(r)return r(t,e-1);i=t,r=n?Ze(t,n):We(t)});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},Qd=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},Kd=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},tv=function(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i},nv=function(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new tv(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)=g)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var y=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=y*y+m*m;if(x=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+g)/2))?v=c:g=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)},rv=function(){return this._root},iv=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},ov=function(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new tv(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new tv(e,s,f,o,u)),(e=c[2])&&a.push(new tv(e,r,f,s,u)),(e=c[1])&&a.push(new tv(e,s,i,o,f)),(e=c[0])&&a.push(new tv(e,r,i,s,f))}return this},uv=function(t){var n,e=[],r=[];for(this._root&&e.push(new tv(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new tv(o,u,a,f,l)),(o=i[1])&&e.push(new tv(o,f,a,c,l)),(o=i[2])&&e.push(new tv(o,u,l,f,s)),(o=i[3])&&e.push(new tv(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},av=function(t){return arguments.length?(this._x=t,this):this._x},cv=function(t){return arguments.length?(this._y=t,this):this._y},sv=er.prototype=rr.prototype;sv.copy=function(){var t,n,e=new rr(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ir(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ir(n));return e},sv.add=Gd,sv.addAll=Qe,sv.cover=Jd,sv.data=Qd,sv.extent=Kd,sv.find=nv,sv.remove=ev,sv.removeAll=Ke,sv.root=rv,sv.size=iv,sv.visit=ov,sv.visitAfter=uv,sv.x=av,sv.y=cv;var fv,lv=function(t){function n(){function t(t,n,e,r,i){var o=t.data,a=t.r,p=l+a;{if(!o)return n>s+p||rf+p||ic.index){var d=s-o.x-o.vx,v=f-o.y-o.vy,_=d*d+v*v;_t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s1?(p.on(t,n),o):p.on(t)}}},_v=function(){function t(t){var n,a=i.length,c=er(i,sr,fr).visitAfter(e);for(u=t,n=0;n=f)){(t.data!==o||t.next)&&(0===i&&(i=Zd(),p+=i*i),0===c&&(c=Zd(),p+=c*c),p1?r[0]+r.slice(2):r,+t.slice(e+1)]},xv=function(t){return t=mv(Math.abs(t)),t?t[1]:NaN},bv=function(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},wv=function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}},Mv=function(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Tv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fv=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+mv(t,Math.max(0,n+o-1))[0]},kv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")},Sv={"":Mv,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return kv(100*t,n)},r:kv,s:Tv,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Nv=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;lr.prototype=hr.prototype,hr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Ev,Av=function(t){return t},Cv=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],zv=function(t){function n(t){function n(t){var n,i,s,m=v,x=_;if("c"===d)x=g(t)+x,t="";else{t=+t;var b=t<0;if(t=g(Math.abs(t),p),b&&0==+t&&(b=!1),m=(b?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=x+("s"===d?Cv[8+fv/3]:"")+(b&&"("===c?")":""),y)for(n=-1,i=t.length;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}h&&!f&&(t=r(t,1/0));var w=m.length+t.length+x.length,M=w>1)+m+t+x+M.slice(w);break;default:t=M+m+t+x}return u(t)}t=lr(t);var e=t.fill,a=t.align,c=t.sign,s=t.symbol,f=t.zero,l=t.width,h=t.comma,p=t.precision,d=t.type,v="$"===s?i[0]:"#"===s&&/[boxX]/.test(d)?"0"+d.toLowerCase():"",_="$"===s?i[1]:/[%p]/.test(d)?"%":"",g=Sv[d],y=!d||/[defgprs%]/.test(d);return p=null==p?d?6:12:/[gprs]/.test(d)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=lr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(xv(e)/3))),o=Math.pow(10,-i),u=Cv[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?bv(t.grouping,t.thousands):Av,i=t.currency,o=t.decimal,u=t.numerals?wv(t.numerals):Av;return{format:n,formatPrefix:e}};pr({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Pv=function(t){return Math.max(0,-xv(Math.abs(t)))},Lv=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(xv(n)/3)))-xv(Math.abs(t)))},Rv=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,xv(n)-xv(t))+1},qv=function(){return new dr};dr.prototype={constructor:dr,reset:function(){this.s=this.t=0},add:function(t){vr(p_,t,this.t),vr(this,p_.s,this.s),this.s?this.t+=p_.t:this.s=p_.t},valueOf:function(){return this.s}};var Uv,Dv,Ov,Fv,Iv,Yv,Bv,jv,Hv,Xv,Vv,$v,Wv,Zv,Gv,Jv,Qv,Kv,t_,n_,e_,r_,i_,o_,u_,a_,c_,s_,f_,l_,h_,p_=new dr,d_=1e-6,v_=Math.PI,__=v_/2,g_=v_/4,y_=2*v_,m_=180/v_,x_=v_/180,b_=Math.abs,w_=Math.atan,M_=Math.atan2,T_=Math.cos,k_=Math.ceil,S_=Math.exp,N_=Math.log,E_=Math.pow,A_=Math.sin,C_=Math.sign||function(t){return t>0?1:t<0?-1:0},z_=Math.sqrt,P_=Math.tan,L_={Feature:function(t,n){xr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rd_?Hv=90:I_<-d_&&(Bv=-90),Gv[0]=Yv,Gv[1]=jv}},B_=function(t){var n,e,r,i,o,u,a;if(Hv=jv=-(Yv=Bv=1/0),Zv=[],q_(t,Y_),e=Zv.length){for(Zv.sort(Br),n=1,r=Zv[0],o=[r];nYr(r[0],r[1])&&(r[1]=i[1]),Yr(i[0],r[1])>Yr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Yr(r[1],i[0]))>u&&(u=a,Yv=i[0],jv=r[1])}return Zv=Gv=null,Yv===1/0||Bv===1/0?[[NaN,NaN],[NaN,NaN]]:[[Yv,Bv],[jv,Hv]]},j_={sphere:mr,point:Hr,lineStart:Vr,lineEnd:Zr,polygonStart:function(){j_.lineStart=Gr,j_.lineEnd=Jr},polygonEnd:function(){j_.lineStart=Vr,j_.lineEnd=Zr}},H_=function(t){Jv=Qv=Kv=t_=n_=e_=r_=i_=o_=u_=a_=0,q_(t,j_);var n=o_,e=u_,r=a_,i=n*n+e*e+r*r;return i<1e-12&&(n=e_,e=r_,r=i_,Qv2?t[2]*x_:0),n.invert=function(n){return n=t.invert(n[0]*x_,n[1]*x_),n[0]*=m_,n[1]*=m_,n},n},og=function(){function t(t,n){e.push(t=r(t,n)),t[0]*=m_,t[1]*=m_}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*x_,c=u.apply(this,arguments)*x_;return e=[],r=ni(-t[0]*x_,-t[1]*x_,0).invert,oi(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=X_([0,0]),o=X_(90),u=X_(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:X_([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:X_(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:X_(+t),n):u},n},ug=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:mr,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},ag=function(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u0)){if(u/=d,d<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}},cg=function(t,n){return b_(t[0]-n[0])=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}},fg=1e9,lg=-fg,hg=function(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=si(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}},pg=qv(),dg=function(t,n){var e=n[0],r=n[1],i=[A_(e),-T_(e),0],o=0,u=0;pg.reset();for(var a=0,c=t.length;a=0?1:-1,T=M*w,k=T>v_,S=d*x;if(pg.add(M_(S*M*A_(T),v*b+S*T_(T))),o+=k?w+M*y_:w,k^h>=e^y>=e){var N=Cr(Er(l),Er(g));Lr(N);var E=Cr(i,N);Lr(E);var A=(k^w>=0?-1:1)*gr(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(u+=k^w>=0?1:-1)}}return(o<-d_||o0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(ji))}var p,d,v,_=n(o),g=i.invert(r[0],r[1]),y=ug(),m=n(y),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=ff(d);var t=dg(p,g);d.length?(x||(o.polygonStart(),x=!0),sg(d,Hi,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}},Kg=Qg(function(){return!0},Xi,$i,[-v_,-__]),ty=function(t,n){function e(e,r,i,o){oi(o,t,n,i,e,r)}function r(t,n){return T_(t)*T_(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?v_:-v_),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(cg(n,p)||cg(d,p))&&(d[0]+=d_,d[1]+=d_,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var g;_&e||!(g=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(g[0][0],g[0][1]),t.point(g[1][0],g[1][1]),t.lineEnd()):(t.point(g[1][0],g[1][1]),t.lineEnd(),t.lineStart(),t.point(g[0][0],g[0][1])))}!v||n&&cg(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=Er(t),i=Er(n),o=[1,0,0],u=Cr(r,i),c=Ar(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=Cr(o,u),d=Pr(o,l);zr(d,Pr(u,h));var v=p,_=Ar(d,v),g=Ar(v,v),y=_*_-g*(Ar(d,d)-1);if(!(y<0)){var m=z_(y),x=Pr(v,(-_-m)/g);if(zr(x,d),x=Nr(x),!e)return x;var b,w=t[0],M=n[0],T=t[1],k=n[1];M0^x[1]<(b_(x[0]-w)v_^(w<=x[0]&&x[0]<=M)){var A=Pr(v,(-_+m)/g);return zr(A,d),[x,Nr(A)]}}}function u(n,e){var r=c?t:v_-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=T_(t),c=a>0,s=b_(a)>d_;return Qg(r,i,e,c?[0,-t]:[-v_,t-v_])},ny=function(t){return{stream:Wi(t)}};Zi.prototype={constructor:Zi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var ey=16,ry=T_(30*x_),iy=function(t,n){return+n?Ki(t,n):Qi(t)},oy=Wi({point:function(t,n){this.stream.point(t*x_,n*x_)}}),uy=function(){return eo(io).scale(155.424).center([0,33.6442])},ay=function(){return uy().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])},cy=function(){function t(t){var n=t[0],e=t[1];return a=null,i.point(n,e),a||(o.point(n,e),a)||(u.point(n,e),a)}function n(){return e=r=null,t}var e,r,i,o,u,a,c=ay(),s=uy().rotate([154,0]).center([-2,58.5]).parallels([55,65]),f=uy().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,n){a=[t,n]}};return t.invert=function(t){var n=c.scale(),e=c.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?f:c).invert(t)},t.stream=function(t){return e&&r===t?e:e=oo([c.stream(r=t),s.stream(t),f.stream(t)])},t.precision=function(t){return arguments.length?(c.precision(t),s.precision(t),f.precision(t),n()):c.precision()},t.scale=function(n){return arguments.length?(c.scale(n),s.scale(.35*n),f.scale(n),t.translate(c.translate())):c.scale()},t.translate=function(t){if(!arguments.length)return c.translate();var e=c.scale(),r=+t[0],a=+t[1];return i=c.translate(t).clipExtent([[r-.455*e,a-.238*e],[r+.455*e,a+.238*e]]).stream(l),o=s.translate([r-.307*e,a+.201*e]).clipExtent([[r-.425*e+d_,a+.12*e+d_],[r-.214*e-d_,a+.234*e-d_]]).stream(l),u=f.translate([r-.205*e,a+.212*e]).clipExtent([[r-.214*e+d_,a+.166*e+d_],[r-.115*e-d_,a+.234*e-d_]]).stream(l),n()},t.fitExtent=function(n,e){return Gi(t,n,e)},t.fitSize=function(n,e){return Ji(t,n,e)},t.scale(1070)},sy=uo(function(t){return z_(2/(1+t))});sy.invert=ao(function(t){return 2*gr(t/2)});var fy=function(){return to(sy).scale(124.75).clipAngle(179.999)},ly=uo(function(t){return(t=_r(t))&&t/A_(t)});ly.invert=ao(function(t){return t});var hy=function(){return to(ly).scale(79.4188).clipAngle(179.999)};co.invert=function(t,n){return[t,2*w_(S_(n))-__]};var py=function(){return so(co).scale(961/y_)},dy=function(){return eo(lo).scale(109.5).parallels([30,30])};ho.invert=ho;var vy=function(){return to(ho).scale(152.63)},_y=function(){return eo(po).scale(131.154).center([0,13.9389])};vo.invert=ao(w_);var gy=function(){return to(vo).scale(144.049).clipAngle(60)},yy=function(){function t(){return i=o=null,u}var n,e,r,i,o,u,a=1,c=0,s=0,f=1,l=1,h=kg,p=null,d=kg;return u={stream:function(t){return i&&o===t?i:i=h(d(o=t))},clipExtent:function(i){return arguments.length?(d=null==i?(p=n=e=r=null,kg):si(p=+i[0][0],n=+i[0][1],e=+i[1][0],r=+i[1][1]),t()):null==p?null:[[p,n],[e,r]]},scale:function(n){return arguments.length?(h=_o((a=+n)*f,a*l,c,s),t()):a},translate:function(n){return arguments.length?(h=_o(a*f,a*l,c=+n[0],s=+n[1]),t()):[c,s]},reflectX:function(n){return arguments.length?(h=_o(a*(f=n?-1:1),a*l,c,s),t()):f<0},reflectY:function(n){return arguments.length?(h=_o(a*f,a*(l=n?-1:1),c,s),t()):l<0},fitExtent:function(t,n){return Gi(u,t,n)},fitSize:function(t,n){return Ji(u,t,n)}}};go.invert=ao(gr);var my=function(){return to(go).scale(249.5).clipAngle(90+d_)};yo.invert=ao(function(t){return 2*w_(t)});var xy=function(){return to(yo).scale(250).clipAngle(142)};mo.invert=function(t,n){return[-n,2*w_(S_(t))-__]};var by=function(){var t=so(mo),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},wy=function(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=bo(e),t.y=Mo(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=ko(t),c=So(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=xo,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},My=function(){return this.eachAfter(No)},Ty=function(t){var n,e,r,i,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},Sy=function(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e=0;)e+=r[i].value;n.value=e})},Ey=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Ay=function(t){for(var n=this,e=Eo(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},Cy=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},zy=function(){var t=[];return this.each(function(n){t.push(n)}),t},Py=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Ly=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ro.prototype=Ao.prototype={constructor:Ro,count:My,each:Ty,eachAfter:Sy,eachBefore:ky,sum:Ny,sort:Ey,path:Ay,ancestors:Cy,descendants:zy,leaves:Py,links:Ly,copy:Co};var Ry=function(t){for(var n=(t=t.slice()).length,e=null,r=e;n;){var i=new qo(t[n-1]);r=r?r.next=i:e=i,t[void 0]=t[--n]}return{head:e,tail:r}},qy=function(t){return Do(Ry(t),[])},Uy=function(t){return Xo(t),t},Dy=function(t){return function(){return t}},Oy=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Go(n)).eachAfter(Jo(i,.5)).eachBefore(Qo(1)):t.eachBefore(Go(Zo)).eachAfter(Jo(Wo,1)).eachAfter(Jo(i,t.r/Math.min(e,r))).eachBefore(Qo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Wo;return t.radius=function(e){return arguments.length?(n=Vo(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:Dy(+n),t):i},t},Fy=function(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)},Iy=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a0)throw new Error("cycle");return o}var n=Ko,e=tu;return t.id=function(e){return arguments.length?(n=$o(e),t):n},t.parentId=function(n){return arguments.length?(e=$o(n),t):e},t};au.prototype=Object.create(Ro.prototype);var Vy=function(){function t(t){var r=cu(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.xf.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){ou(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=ru(a),i=eu(i),a&&i;)c=eu(c),u=ru(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(iu(uu(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!ru(u)&&(u.t=a,u.m+=l-f),i&&!eu(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=nu,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t},$y=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a1?n:1)},e}(Wy),Gy=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Fy),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p>>1;f[v]a-i){var y=(r*g+u*_)/e;o(t,p,_,r,i,y,a),o(p,n,g,y,i,u,a)}else{var m=(i*g+a*_)/e;o(t,p,_,r,i,u,m),o(p,n,g,r,m,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u1?n:1)},e -}(Wy),tm=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;na!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f},om=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}},fm=function(){var t=sm.apply(this,arguments);return function(){return Math.exp(t())}},lm=function(t){return function(){for(var n=0,e=0;e=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(t){return void a.call("error",r,t)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=d("beforesend","progress","load","error"),c=Ye(),s=new XMLHttpRequest,f=null,l=null,h=0;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=mu(o)),null!=o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return r.get(n)}return r},vm=function(t,n){return function(e,r){var i=dm(e).mimeType(t).response(n);if(null!=r){if("function"!=typeof r)throw new Error("invalid callback: "+r);return i.get(r)}return i}},_m=vm("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),gm=vm("application/json",function(t){return JSON.parse(t.responseText)}),ym=vm("text/plain",function(t){return t.responseText}),mm=vm("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),xm=function(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=dm(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(bu(n,r=t)):r},o.row(r),i?o.get(i):o}},bm=xm("text/csv",Od),wm=xm("text/tab-separated-values",jd),Mm=Array.prototype,Tm=Mm.map,km=Mm.slice,Sm={name:"implicit"},Nm=function(t){return function(){return t}},Em=function(t){return+t},Am=[0,1],Cm=function(n,e,i){var o,u=n[0],a=n[n.length-1],c=r(u,a,null==e?10:e);switch(i=lr(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(u),Math.abs(a));return null!=i.precision||isNaN(o=Lv(c,s))||(i.precision=o),t.formatPrefix(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Rv(c,Math.max(Math.abs(u),Math.abs(a))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Pv(c))||(i.precision=o-2*("%"===i.type))}return t.format(i)},zm=function(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u0?t>1?Zu(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rm:null};var qm=Rm.range,Um=6e4,Dm=6048e5,Om=Zu(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Fm=Om.range,Im=Zu(function(t){t.setTime(Math.floor(t/Um)*Um)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getMinutes()}),Ym=Im.range,Bm=Zu(function(t){var n=t.getTimezoneOffset()*Um%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),jm=Bm.range,Hm=Zu(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/864e5},function(t){return t.getDate()-1}),Xm=Hm.range,Vm=Gu(0),$m=Gu(1),Wm=Gu(2),Zm=Gu(3),Gm=Gu(4),Jm=Gu(5),Qm=Gu(6),Km=Vm.range,tx=$m.range,nx=Wm.range,ex=Zm.range,rx=Gm.range,ix=Jm.range,ox=Qm.range,ux=Zu(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ax=ux.range,cx=Zu(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});cx.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var sx=cx.range,fx=Zu(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getUTCMinutes()}),lx=fx.range,hx=Zu(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),px=hx.range,dx=Zu(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),vx=dx.range,_x=Ju(0),gx=Ju(1),yx=Ju(2),mx=Ju(3),xx=Ju(4),bx=Ju(5),wx=Ju(6),Mx=_x.range,Tx=gx.range,kx=yx.range,Sx=mx.range,Nx=xx.range,Ex=bx.range,Ax=wx.range,Cx=Zu(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),zx=Cx.range,Px=Zu(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Px.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Lx,Rx=Px.range,qx={"-":"",_:" ",0:"0"},Ux=/^\s*\d+/,Dx=/^%/,Ox=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Za({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Fx=Date.prototype.toISOString?Ga:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ"),Ix=+new Date("2000-01-01T00:00:00.000Z")?Ja:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),Yx=1e3,Bx=60*Yx,jx=60*Bx,Hx=24*jx,Xx=7*Hx,Vx=30*Hx,$x=365*Hx,Wx=function(){return tc(cx,ux,Vm,Hm,Bm,Im,Om,Rm,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])},Zx=function(){return tc(Px,Cx,_x,dx,hx,fx,Om,Rm,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])},Gx=function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})},Jx=Gx("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Qx=Gx("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Kx=Gx("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),tb=Gx("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),nb=Oh(Vt(300,.5,0),Vt(-240,.5,1)),eb=Oh(Vt(-100,.75,.35),Vt(80,1.5,.8)),rb=Oh(Vt(260,.75,.35),Vt(80,1.5,.8)),ib=Vt(),ob=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return ib.h=360*t-100,ib.s=1.5-1.5*n,ib.l=.8-.9*n,ib+""},ub=nc(Gx("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),ab=nc(Gx("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),cb=nc(Gx("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),sb=nc(Gx("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),fb=function(t){return function(){return t}},lb=Math.abs,hb=Math.atan2,pb=Math.cos,db=Math.max,vb=Math.min,_b=Math.sin,gb=Math.sqrt,yb=1e-12,mb=Math.PI,xb=mb/2,bb=2*mb,wb=function(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-xb,p=u.apply(this,arguments)-xb,d=lb(p-h),v=p>h;if(c||(c=t=Re()),lyb)if(d>bb-yb)c.moveTo(l*pb(h),l*_b(h)),c.arc(0,0,l,h,p,!v),f>yb&&(c.moveTo(f*pb(p),f*_b(p)),c.arc(0,0,f,p,h,v));else{var _,g,y=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,k=T>yb&&(i?+i.apply(this,arguments):gb(f*f+l*l)),S=vb(lb(l-f)/2,+r.apply(this,arguments)),N=S,E=S;if(k>yb){var A=ic(k/f*_b(T)),C=ic(k/l*_b(T));(w-=2*A)>yb?(A*=v?1:-1,x+=A,b-=A):(w=0,x=b=(h+p)/2),(M-=2*C)>yb?(C*=v?1:-1,y+=C,m-=C):(M=0,y=m=(h+p)/2)}var z=l*pb(y),P=l*_b(y),L=f*pb(b),R=f*_b(b);if(S>yb){var q=l*pb(m),U=l*_b(m),D=f*pb(x),O=f*_b(x);if(dyb?fc(z,P,D,O,q,U,L,R):[L,R],I=z-F[0],Y=P-F[1],B=q-F[0],j=U-F[1],H=1/_b(rc((I*B+Y*j)/(gb(I*I+Y*Y)*gb(B*B+j*j)))/2),X=gb(F[0]*F[0]+F[1]*F[1]);N=vb(S,(f-X)/(H-1)),E=vb(S,(l-X)/(H+1))}}M>yb?E>yb?(_=lc(D,O,z,P,l,E,v),g=lc(q,U,L,R,l,E,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),Eyb&&w>yb?N>yb?(_=lc(L,R,q,U,f,-N,v),g=lc(z,P,D,O,f,-N,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),N=f;--l)s.point(_[l],g[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),g[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):g[n]))}if(p)return s=null,p+""||null}function n(){return Tb().defined(u).curve(c).context(a)}var e=pc,r=null,i=fb(0),o=dc,u=fb(!0),a=null,c=Mb,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:fb(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:fb(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:fb(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t},Sb=function(t,n){return nt?1:n>=t?0:NaN},Nb=function(t){return t},Eb=function(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),g=Math.min(bb,Math.max(-bb,o.apply(this,arguments)-_)),y=Math.min(Math.abs(g)/h,u.apply(this,arguments)),m=y*(g<0?-1:1);for(a=0;a0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(g-h*m)/p:0;a0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:y};return v}var n=Nb,e=Sb,r=null,i=fb(0),o=fb(bb),u=fb(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:fb(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:fb(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:fb(+n),t):u},t},Ab=_c(Mb);vc.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Cb=function(){return gc(Tb().curve(Ab))},zb=function(){var t=kb().curve(Ab),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return gc(e())},delete t.lineX0,t.lineEndAngle=function(){return gc(r())},delete t.lineX1,t.lineInnerRadius=function(){return gc(i())},delete t.lineY0,t.lineOuterRadius=function(){return gc(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t},Pb={draw:function(t,n){var e=Math.sqrt(n/mb);t.moveTo(e,0),t.arc(0,0,e,0,bb)}},Lb={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Rb=Math.sqrt(1/3),qb=2*Rb,Ub={draw:function(t,n){var e=Math.sqrt(n/qb),r=e*Rb;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Db=Math.sin(mb/10)/Math.sin(7*mb/10),Ob=Math.sin(bb/10)*Db,Fb=-Math.cos(bb/10)*Db,Ib={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Ob*e,i=Fb*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=bb*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},Yb={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Bb=Math.sqrt(3),jb={draw:function(t,n){var e=-Math.sqrt(n/(3*Bb));t.moveTo(0,2*e),t.lineTo(-Bb*e,-e),t.lineTo(Bb*e,-e),t.closePath()}},Hb=-.5,Xb=Math.sqrt(3)/2,Vb=1/Math.sqrt(12),$b=3*(Vb/2+1),Wb={draw:function(t,n){var e=Math.sqrt(n/$b),r=e/2,i=e*Vb,o=r,u=e*Vb+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Hb*r-Xb*i,Xb*r+Hb*i),t.lineTo(Hb*o-Xb*u,Xb*o+Hb*u),t.lineTo(Hb*a-Xb*c,Xb*a+Hb*c),t.lineTo(Hb*r+Xb*i,Hb*i-Xb*r),t.lineTo(Hb*o+Xb*u,Hb*u-Xb*o),t.lineTo(Hb*a+Xb*c,Hb*c-Xb*a),t.closePath()}},Zb=[Pb,Lb,Ub,Yb,Ib,jb,Wb],Gb=function(){function t(){var t;if(r||(r=t=Re()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=fb(Pb),e=fb(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:fb(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},Jb=function(){};mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:yc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Qb=function(t){return new mc(t)};xc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Kb=function(t){return new xc(t)};bc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var tw=function(t){return new bc(t)};wc.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var nw=function t(n){function e(t){return 1===n?new mc(t):new wc(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Tc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Mc(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ew=function t(n){function e(t){return new Tc(t,n)}return e.tension=function(n){return t(+n)},e}(0);kc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var rw=function t(n){function e(t){return new kc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var iw=function t(n){function e(t){return new Sc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0: -this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ow=function t(n){function e(t){return n?new Ec(t,n):new Tc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Ac.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var uw=function t(n){function e(t){return n?new Ac(t,n):new kc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Cc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var aw=function t(n){function e(t){return n?new Cc(t,n):new Sc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);zc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};var cw=function(t){return new zc(t)};Uc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:qc(this,this._t0,Rc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,qc(this,Rc(this,e=Lc(this,t,n)),e);break;default:qc(this,this._t0,e=Lc(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Dc.prototype=Object.create(Uc.prototype)).point=function(t,n){Uc.prototype.point.call(this,n,t)},Oc.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Yc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=Bc(t),i=Bc(n),o=0,u=1;u=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var fw=function(t){return new jc(t,.5)},lw=Array.prototype.slice,hw=function(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i=0;)e[n]=n;return e},dw=function(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o0){for(var e,r,i,o=0,u=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u=a)return null;var c=t-i.site[0],s=n-i.site[1],f=c*c+s*s;do{i=o.cells[r=u],u=null,i.halfedges.forEach(function(e){var r=o.edges[e],a=r.left;if(a!==i.site&&a||(a=r.right)){var c=t-a[0],s=n-a[1],l=c*c+s*s;le?(e+r)/2:Math.min(0,e)||Math.max(0,r),o>i?(i+o)/2:Math.min(0,i)||Math.max(0,o))}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=m.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=N(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new Ns(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=A.length;r0?bl(this).transition().duration(S).call(u,f,a):bl(this).call(n.transform,f)}}function h(){if(y.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,c=u.length;for(As(),e=0;e - - - var edges = []; - - links.forEach(function(e) { - var sourceNode = nodes.filter(function(n) { - return n.name === e.source; - })[0], - targetNode = nodes.filter(function(n) { - return n.name === e.target; - })[0]; - - edges.push({ - source: sourceNode, - target: targetNode, - value: 1}); - - }); - - edges.forEach(function(e) { - - if (!e.source["linkcount"]) e.source["linkcount"] = 0; - if (!e.target["linkcount"]) e.target["linkcount"] = 0; - - e.source["linkcount"]++; - e.target["linkcount"]++; - }); - - //var width = 960, height = 600; - var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; - var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; - var svg = d3.select("#vis").append("svg") - .attr("width", width) - .attr("height", height); - - // updated for d3 v4. - var simulation = d3.forceSimulation() - .force("link", d3.forceLink().id(function(d) { return d.id; })) - .force("charge", d3.forceManyBody().strength(strength)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide(35)); - - // Node charge strength. Repel strength greater for less links. - //function strength(d) { return -50/d["linkcount"] ; } - function strength(d) { return -25 ; } - - // Link distance. Distance increases with number of links at source and target - function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } - - // Link strength. Strength is less for highly connected nodes (move towards target dist) - function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } - - simulation - .nodes(nodes) - .on("tick", tick); - - simulation.force("link") - .links(edges) - .distance(distance) - .strength(strengthl); - - // build the arrow. - svg.append("svg:defs").selectAll("marker") - .data(["end"]) // Different link/path types can be defined here - .enter().append("svg:marker") // This section adds in the arrows - .attr("id", String) - .attr("viewBox", "0 -5 10 10") - .attr("refX", 25) // Moves the arrow head out, allow for radius - .attr("refY", 0) // -1.5 - .attr("markerWidth", 6) - .attr("markerHeight", 6) - .attr("orient", "auto") - .append("svg:path") - .attr("d", "M0,-5L10,0L0,5"); - - var link = svg.selectAll('.link') - .data(edges) - .enter().append('line') - .attr("class", "link") - .attr("marker-end", "url(#end)"); - - var node = svg.selectAll(".node") - .data(nodes) - .enter().append("g") - .attr("class", function(d) { return "node " + d.type;}) - .attr('transform', function(d) { - return "translate(" + d.x + "," + d.y + ")"}) - .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); - - node.call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended)); - - // add the nodes - node.append('circle') - .attr('r', 16) - ; - - // add text - node.append("text") - .attr("x", 12) - .attr("dy", "-1.1em") - .text(function(d) {return d.name;}); - - node.on("mouseover", function(d) { - - var g = d3.select(this); // the node (table) - - // tooltip - - var fields = d.fields; - var fieldformat = ""; - fields.forEach(function(d) { - fieldformat += ""; - }); - fieldformat += "
      "+ d.name+""+ d.type+""+ d.disp+"
      "; - var tiplength = d.fields.length; - - // Define 'div' for tooltips - var div = d3.select("body").append("div") // declare the tooltip div - .attr("class", "tooltip") // apply the 'tooltip' class - .style("opacity", 0) - .html('
      ' + d.name + '
      ' + fieldformat) - .style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px") - .style("top", tooltop(tiplength))// or ... - .transition() - .duration(800) - .style("opacity", 0.9); - }); - - function tooltop(tiplength) { - //aim to ensure tooltip is fully visible whenver possible - return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px" - } - - node.on("mouseout", function(d) { - d3.select("body").select('div.tooltip').remove(); - }); - - // instead of waiting for force to end with : force.on('end', function() - // use .on("tick", instead. Here is the tick function - function tick() { - node.attr('transform', function(d) { - d.x = Math.max(30, Math.min(width - 16, d.x)); - d.y = Math.max(30, Math.min(height - 16, d.y)); - return "translate(" + d.x + "," + d.y + ")"; }); - - link.attr('x1', function(d) {return d.source.x;}) - .attr('y1', function(d) {return d.source.y;}) - .attr('x2', function(d) {return d.target.x;}) - .attr('y2', function(d) {return d.target.y;}); - }; - - function dragstarted(d) { - if (!d3.event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - }; - - function dragged(d) { - d.fx = d3.event.x; - d.fy = d3.event.y; - }; - - function dragended(d) { - if (!d3.event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - }; - -}; \ No newline at end of file diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 2935959d..139ad1aa 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -690,6 +690,6 @@ def d3_graph_model(): nodes.append(dict(name=tablename, type="table", fields = fields)) # d3 v4 allows individual modules to be specified. The complete d3 library is included below. - response.files.append(URL('static','js/d3.min.js')) - response.files.append(URL('static','js/d3_graph.js')) - return dict(databases=databases, nodes=nodes, links=links) \ No newline at end of file + response.files.append(URL('admin','static','js/d3.min.js')) + response.files.append(URL('admin','static','js/d3_graph.js')) + return dict(databases=databases, nodes=nodes, links=links) diff --git a/applications/welcome/static/css/d3_graph.css b/applications/welcome/static/css/d3_graph.css deleted file mode 100644 index fd0ab8fb..00000000 --- a/applications/welcome/static/css/d3_graph.css +++ /dev/null @@ -1,33 +0,0 @@ - .node {fill: steelblue; - stroke: #636363; - stroke-width: 1px;} - - .auth {fill: lightgrey;} - - .table {r: 10;} - - .link {stroke: #bbbbbb; - stroke-width: 2px;} - td {padding: 4px;} - - div.tooltip { - position: absolute; - text-align: left; - /* width: 140px; */ - /* height: 28px;*/ - padding: 0px 5px 0px 5px; - padding-top: 0px; - font: 12px sans-serif; - background: #fff7bc; - border: solid 1px #aaa; - border-radius: 6px; - pointer-events: none;} - - h5 { font: 14px sans-serif; - background : #ec7014; - color: #ffffe5; - padding: 5px 2px 5px 2px; - margin-top: 1px;} - path { - fill: #aaaaaa;} - diff --git a/applications/welcome/static/js/d3.min.js b/applications/welcome/static/js/d3.min.js deleted file mode 100644 index 6ba6b41e..00000000 --- a/applications/welcome/static/js/d3.min.js +++ /dev/null @@ -1,8 +0,0 @@ -// https://d3js.org Version 4.7.4. Copyright 2017 Mike Bostock. -(function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})})(this,function(t){"use strict";function n(t){return function(n,e){return Ls(t(n),e)}}function e(t,n){return[t,n]}function r(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Js?i*=10:o>=Qs?i*=5:o>=Ks&&(i*=2),n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function g(t,n){for(var e,r=0,i=t.length;r=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function S(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;rn?1:t>=n?0:NaN}function q(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function D(t,n){return function(){this.setAttribute(t,n)}}function O(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function F(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function Y(t){return function(){this.style.removeProperty(t)}}function B(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t){return function(){delete this[t]}}function X(t,n){return function(){this[t]=n}}function V(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function $(t){return t.trim().split(/^|\s+/)}function W(t){return t.classList||new Z(t)}function Z(t){this._node=t,this._names=$(t.getAttribute("class")||"")}function G(t,n){for(var e=W(t),r=-1,i=n.length;++r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Rl.exec(t))?kt(parseInt(n[1],16)):(n=ql.exec(t))?new At(n[1],n[2],n[3],1):(n=Ul.exec(t))?new At(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Dl.exec(t))?St(n[1],n[2],n[3],n[4]):(n=Ol.exec(t))?St(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Fl.exec(t))?Ct(n[1],n[2]/100,n[3]/100,1):(n=Il.exec(t))?Ct(n[1],n[2]/100,n[3]/100,n[4]):Yl.hasOwnProperty(t)?kt(Yl[t]):"transparent"===t?new At(NaN,NaN,NaN,0):null}function kt(t){return new At(t>>16&255,t>>8&255,255&t,1)}function St(t,n,e,r){return r<=0&&(t=n=e=NaN),new At(t,n,e,r)}function Nt(t){return t instanceof Mt||(t=Tt(t)),t?(t=t.rgb(),new At(t.r,t.g,t.b,t.opacity)):new At}function Et(t,n,e,r){return 1===arguments.length?Nt(t):new At(t,n,e,null==r?1:r)}function At(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Ct(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Lt(t,n,e,r)}function zt(t){if(t instanceof Lt)return new Lt(t.h,t.s,t.l,t.opacity);if(t instanceof Mt||(t=Tt(t)),!t)return new Lt;if(t instanceof Lt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e0&&c<1?0:u,new Lt(u,a,c,t.opacity)}function Pt(t,n,e,r){return 1===arguments.length?zt(t):new Lt(t,n,e,null==r?1:r)}function Lt(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Rt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function qt(t){if(t instanceof Dt)return new Dt(t.l,t.a,t.b,t.opacity);if(t instanceof Ht){var n=t.h*Bl;return new Dt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof At||(t=Nt(t));var e=Yt(t.r),r=Yt(t.g),i=Yt(t.b),o=Ot((.4124564*e+.3575761*r+.1804375*i)/Hl),u=Ot((.2126729*e+.7151522*r+.072175*i)/Xl);return new Dt(116*u-16,500*(o-u),200*(u-Ot((.0193339*e+.119192*r+.9503041*i)/Vl)),t.opacity)}function Ut(t,n,e,r){return 1===arguments.length?qt(t):new Dt(t,n,e,null==r?1:r)}function Dt(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Ot(t){return t>Gl?Math.pow(t,1/3):t/Zl+$l}function Ft(t){return t>Wl?t*t*t:Zl*(t-$l)}function It(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Yt(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bt(t){if(t instanceof Ht)return new Ht(t.h,t.c,t.l,t.opacity);t instanceof Dt||(t=qt(t));var n=Math.atan2(t.b,t.a)*jl;return new Ht(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function jt(t,n,e,r){return 1===arguments.length?Bt(t):new Ht(t,n,e,null==r?1:r)}function Ht(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Xt(t){if(t instanceof $t)return new $t(t.h,t.s,t.l,t.opacity);t instanceof At||(t=Nt(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(ih*r+eh*n-rh*e)/(ih+eh-rh),o=r-i,u=(nh*(e-i)-Kl*o)/th,a=Math.sqrt(u*u+o*o)/(nh*i*(1-i)),c=a?Math.atan2(u,o)*jl-120:NaN;return new $t(c<0?c+360:c,a,i,t.opacity)}function Vt(t,n,e,r){return 1===arguments.length?Xt(t):new $t(t,n,e,null==r?1:r)}function $t(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Wt(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function Zt(t,n){return function(e){return t+e*n}}function Gt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Jt(t,n){var e=n-t;return e?Zt(t,e>180||e<-180?e-360*Math.round(e/360):e):ph(isNaN(t)?n:t)}function Qt(t){return 1==(t=+t)?Kt:function(n,e){return e-n?Gt(n,e,t):ph(isNaN(n)?e:n)}}function Kt(t,n){var e=n-t;return e?Zt(t,e):ph(isNaN(t)?n:t)}function tn(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:mh(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:mh(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:mh(t,e)},{i:a-2,x:mh(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e=0&&n._call.call(null,t),n=n._next;--Ih}function mn(){Xh=(Hh=$h.now())+Vh,Ih=Yh=0;try{yn()}finally{Ih=0,bn(),Xh=0}}function xn(){var t=$h.now(),n=t-Hh;n>jh&&(Vh-=n,Hh=t)}function bn(){for(var t,n,e=sh,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:sh=n);fh=t,wn(r)}function wn(t){if(!Ih){Yh&&(Yh=clearTimeout(Yh));var n=t-Xh;n>24?(t<1/0&&(Yh=setTimeout(mn,n)),Bh&&(Bh=clearInterval(Bh))):(Bh||(Hh=Xh,Bh=setInterval(xn,jh)),Ih=1,Wh(mn))}}function Mn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Kh)throw new Error("too late");return e}function Tn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>np)throw new Error("too late");return e}function kn(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function Sn(t,n,e){function r(t){e.state=tp,e.timer.restart(i,e.delay,e.time),e.delay<=t&&i(t-e.delay)}function i(r){var s,f,l,h;if(e.state!==tp)return u();for(s in c)if(h=c[s],h.name===e.name){if(h.state===ep)return Zh(i);h.state===rp?(h.state=op,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete c[s]):+s=0&&(t=t.slice(0,n)),!t||"start"===t})}function Hn(t,n,e){var r,i,o=jn(n)?Mn:Tn;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function Xn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Vn(t,n){var e,r,i;return function(){var o=al(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function $n(t){return function(){this.style.removeProperty(t)}}function Wn(t,n,e){var r,i;return function(){var o=al(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function Zn(t,n,e){var r,i,o;return function(){var u=al(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Gn(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function Jn(t){return function(){this.textContent=t}}function Qn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Kn(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function te(t){return vt().transition(t)}function ne(){return++Ep}function ee(t){return+t}function re(t){return t*t}function ie(t){return t*(2-t)}function oe(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function ue(t){return t*t*t}function ae(t){return--t*t*t+1}function ce(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function se(t){return 1-Math.cos(t*Rp)}function fe(t){return Math.sin(t*Rp)}function le(t){return(1-Math.cos(Lp*t))/2}function he(t){return Math.pow(2,10*t-10)}function pe(t){return 1-Math.pow(2,-10*t)}function de(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function ve(t){return 1-Math.sqrt(1-t*t)}function _e(t){return Math.sqrt(1- --t*t)}function ge(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ye(t){return 1-me(1-t)}function me(t){return(t=+t)Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,id(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],S){case ud:case od:N&&(m=Math.max(P-l,Math.min(R-v,m)),h=l+m,_=v+m),E&&(x=Math.max(L-p,Math.min(q-g,x)),d=p+x,y=g+x);break;case ad:N<0?(m=Math.max(P-l,Math.min(R-l,m)),h=l+m,_=v):N>0&&(m=Math.max(P-v,Math.min(R-v,m)),h=l,_=v+m),E<0?(x=Math.max(L-p,Math.min(q-p,x)),d=p+x,y=g):E>0&&(x=Math.max(L-g,Math.min(q-g,x)),d=p,y=g+x);break;case cd:N&&(h=Math.max(P,Math.min(R,l-m*N)),_=Math.max(P,Math.min(R,v+m*N))),E&&(d=Math.max(L,Math.min(q,p-x*E)),y=Math.max(L,Math.min(q,g+x*E)))}_0&&(l=h-m),E<0?g=y-x:E>0&&(p=d-x),S=ud,Y.attr("cursor",hd.selection),o());break;default:return}id()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:S===cd&&(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad,o());break;case 32:S===ud&&(t.event.altKey?(N&&(v=_-m*N,l=h+m*N),E&&(g=y-x*E,p=d+x*E),S=cd):(N<0?v=_:N>0&&(l=h),E<0?g=y:E>0&&(p=d),S=ad),Y.attr("cursor",hd[k]),o());break;default:return}id()}if(t.event.touches){if(t.event.changedTouches.length=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+g)/2))?v=o:g=o,(f=e>=(u=(_+y)/2))?_=u:y=u}while((l=f<<1|s)==(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function Qe(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;ef&&(f=r),il&&(l=i));for(f",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Sv[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function pr(n){ -return Ev=zv(n),t.format=Ev.format,t.formatPrefix=Ev.formatPrefix,Ev}function dr(){this.reset()}function vr(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function _r(t){return t>1?0:t<-1?v_:Math.acos(t)}function gr(t){return t>1?__:t<-1?-__:Math.asin(t)}function yr(t){return(t=A_(t/2))*t}function mr(){}function xr(t,n){t&&R_.hasOwnProperty(t.type)&&R_[t.type](t,n)}function br(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i=0?1:-1,i=r*e,o=T_(n),u=A_(n),a=Iv*u,c=Fv*o+a*T_(i),s=a*r*A_(i);U_.add(M_(s,c)),Ov=t,Fv=o,Iv=u}function Nr(t){return[M_(t[1],t[0]),gr(t[2])]}function Er(t){var n=t[0],e=t[1],r=T_(e);return[r*T_(n),r*A_(n),A_(e)]}function Ar(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Cr(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function zr(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Pr(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Lr(t){var n=z_(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function Rr(t,n){Zv.push(Gv=[Yv=t,jv=t]),nHv&&(Hv=n)}function qr(t,n){var e=Er([t*x_,n*x_]);if(Wv){var r=Cr(Wv,e),i=[r[1],-r[0],0],o=Cr(i,r);Lr(o),o=Nr(o);var u,a=t-Xv,c=a>0?1:-1,s=o[0]*m_*c,f=b_(a)>180;f^(c*XvHv&&(Hv=u):(s=(s+360)%360-180,f^(c*XvHv&&(Hv=n))),f?tYr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t):jv>=Yv?(tjv&&(jv=t)):t>Xv?Yr(Yv,t)>Yr(Yv,jv)&&(jv=t):Yr(t,jv)>Yr(Yv,jv)&&(Yv=t)}else Zv.push(Gv=[Yv=t,jv=t]);nHv&&(Hv=n),Wv=e,Xv=t}function Ur(){Y_.point=qr}function Dr(){Gv[0]=Yv,Gv[1]=jv,Y_.point=Rr,Wv=null}function Or(t,n){if(Wv){var e=t-Xv;I_.add(b_(e)>180?e+(e>0?360:-360):e)}else Vv=t,$v=n;O_.point(t,n),qr(t,n)}function Fr(){O_.lineStart()}function Ir(){Or(Vv,$v),O_.lineEnd(),b_(I_)>d_&&(Yv=-(jv=180)),Gv[0]=Yv,Gv[1]=jv,Wv=null}function Yr(t,n){return(n-=t)<0?n+360:n}function Br(t,n){return t[0]-n[0]}function jr(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nv_?t-y_:t<-v_?t+y_:t,n]}function ni(t,n,e){return(t%=y_)?n||e?V_(ri(t),ii(n,e)):ri(t):n||e?ii(n,e):ti}function ei(t){return function(n,e){return n+=t,[n>v_?n-y_:n<-v_?n+y_:n,e]}}function ri(t){var n=ei(t);return n.invert=ei(-t),n}function ii(t,n){function e(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*r+a*i;return[M_(c*o-f*u,a*r-s*i),gr(f*o+c*u)]}var r=T_(t),i=A_(t),o=T_(n),u=A_(n);return e.invert=function(t,n){var e=T_(n),a=T_(t)*e,c=A_(t)*e,s=A_(n),f=s*o-c*u;return[M_(c*o+s*u,a*r+f*i),gr(f*r-a*i)]},e}function oi(t,n,e,r,i,o){if(e){var u=T_(n),a=A_(n),c=r*e;null==i?(i=n+r*y_,o=n-c/2):(i=ui(u,i),o=ui(u,o),(r>0?io)&&(i+=r*y_));for(var s,f=i;r>0?f>o:f0)do{s.point(0===f||3===f?t:e,f>1?r:n)}while((f=(f+a+4)%4)!==l);else s.point(o[0],o[1])}function u(r,i){return b_(r[0]-t)0?0:3:b_(r[0]-e)0?2:1:b_(r[1]-n)0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&S.point(t,n)}function s(){for(var n=0,e=0,i=_.length;er&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){S=N,v=[],_=[],k=!0}function l(){var t=s(),n=k&&t,e=(v=ff(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&sg(v,a,t,o,u),u.polygonEnd()),S=u,v=_=g=null}function h(){E.point=d,_&&_.push(g=[]),T=!0,M=!1,b=w=NaN}function p(){v&&(d(y,m),x&&M&&N.rejoin(),v.push(N.result())),E.point=c,M&&S.lineEnd()}function d(o,u){var a=i(o,u);if(_&&g.push([o,u]),T)y=o,m=u,x=a,T=!1,a&&(S.lineStart(),S.point(o,u));else if(a&&M)S.point(o,u);else{var c=[b=Math.max(lg,Math.min(fg,b)),w=Math.max(lg,Math.min(fg,w))],s=[o=Math.max(lg,Math.min(fg,o)),u=Math.max(lg,Math.min(fg,u))];ag(c,s,t,n,e,r)?(M||(S.lineStart(),S.point(c[0],c[1])),S.point(s[0],s[1]),a||S.lineEnd(),k=!1):a&&(S.lineStart(),S.point(o,u),k=!1)}b=o,w=u,M=a}var v,_,g,y,m,x,b,w,M,T,k,S=u,N=ug(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function fi(){_g.point=hi,_g.lineEnd=li}function li(){_g.point=_g.lineEnd=mr}function hi(t,n){t*=x_,n*=x_,$_=t,W_=A_(n),Z_=T_(n),_g.point=pi}function pi(t,n){t*=x_,n*=x_;var e=A_(n),r=T_(n),i=b_(t-$_),o=T_(i),u=A_(i),a=r*u,c=Z_*e-W_*r*o,s=W_*e+Z_*r*o;vg.add(M_(z_(a*a+c*c),s)),$_=t,W_=e,Z_=r}function di(t,n){return!(!t||!wg.hasOwnProperty(t.type))&&wg[t.type](t,n)}function vi(t,n){return 0===xg(t,n)}function _i(t,n){var e=xg(t[0],t[1]);return xg(t[0],n)+xg(n,t[1])<=e+d_}function gi(t,n){return!!dg(t.map(yi),mi(n))}function yi(t){return t=t.map(mi),t.pop(),t}function mi(t){return[t[0]*x_,t[1]*x_]}function xi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function bi(t,n,e){var r=Gs(t,n-d_,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function wi(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return Gs(k_(o/_)*_,i,_).map(h).concat(Gs(k_(s/g)*g,c,g).map(p)).concat(Gs(k_(r/d)*d,e,d).filter(function(t){return b_(t%_)>d_}).map(f)).concat(Gs(k_(a/v)*v,u,v).filter(function(t){return b_(t%g)>d_}).map(l))}var e,r,i,o,u,a,c,s,f,l,h,p,d=10,v=d,_=90,g=360,y=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[h(o).concat(p(c).slice(1),h(i).reverse().slice(1),p(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(y)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(y)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(_=+n[0],g=+n[1],t):[_,g]},t.stepMinor=function(n){return arguments.length?(d=+n[0],v=+n[1],t):[d,v]},t.precision=function(n){return arguments.length?(y=+n,f=xi(a,u,90),l=bi(r,e,y),h=xi(s,c,90),p=bi(o,i,y),t):y},t.extentMajor([[-180,-90+d_],[180,90-d_]]).extentMinor([[-180,-80-d_],[180,80+d_]])}function Mi(){return wi()()}function Ti(){Eg.point=ki}function ki(t,n){Eg.point=Si,G_=Q_=t,J_=K_=n}function Si(t,n){Ng.add(K_*t-Q_*n),Q_=t,K_=n}function Ni(){Si(G_,J_)}function Ei(t,n){tzg&&(zg=t),nPg&&(Pg=n)}function Ai(t,n){Rg+=t,qg+=n,++Ug}function Ci(){jg.point=zi}function zi(t,n){jg.point=Pi,Ai(eg=t,rg=n)}function Pi(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,Ai(eg=t,rg=n)}function Li(){jg.point=Ai}function Ri(){jg.point=Ui}function qi(){Di(tg,ng)}function Ui(t,n){jg.point=Di,Ai(tg=eg=t,ng=rg=n)}function Di(t,n){var e=t-eg,r=n-rg,i=z_(e*e+r*r);Dg+=i*(eg+t)/2,Og+=i*(rg+n)/2,Fg+=i,i=rg*t-eg*n,Ig+=i*(eg+t),Yg+=i*(rg+n),Bg+=3*i,Ai(eg=t,rg=n)}function Oi(t){this._context=t}function Fi(t,n){Gg.point=Ii,Xg=$g=t,Vg=Wg=n}function Ii(t,n){$g-=t,Wg-=n,Zg.add(z_($g*$g+Wg*Wg)),$g=t,Wg=n}function Yi(){this._string=[]}function Bi(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function ji(t){return t.length>1}function Hi(t,n){return((t=t.x)[0]<0?t[1]-__-d_:__-t[1])-((n=n.x)[0]<0?n[1]-__-d_:__-n[1])}function Xi(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?v_:-v_,c=b_(o-e);b_(c-v_)0?__:-__),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=v_&&(b_(e-i)d_?w_((A_(n)*(o=T_(r))*A_(e)-A_(r)*(i=T_(n))*A_(t))/(i*o*u)):(n+r)/2}function $i(t,n,e,r){var i;if(null==t)i=e*__,r.point(-v_,i),r.point(0,i),r.point(v_,i),r.point(v_,0),r.point(v_,-i),r.point(0,-i),r.point(-v_,-i),r.point(-v_,0),r.point(-v_,i);else if(b_(t[0]-n[0])>d_){var o=t[0]4*n&&v--){var x=u+h,b=a+p,w=c+d,M=z_(x*x+b*b+w*w),T=gr(w/=M),k=b_(b_(w)-1)n||b_((g*A+y*C)/m-.5)>.3||u*h+a*p+c*d2?t[2]%360*x_:0,i()):[b*m_,w*m_,M*m_]},n.precision=function(t){return arguments.length?(A=iy(r,E=t*t),o()):z_(E)},n.fitExtent=function(t,e){return Gi(n,t,e)},n.fitSize=function(t,e){return Ji(n,t,e)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function eo(t){var n=0,e=v_/3,r=no(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*x_,e=t[1]*x_):[n*m_,e*m_]},i}function ro(t){function n(t,n){return[t*e,A_(n)/e]}var e=T_(t);return n.invert=function(t,n){return[t/e,gr(n*e)]},n}function io(t,n){function e(t,n){var e=z_(o-2*i*A_(n))/i;return[e*A_(t*=i),u-e*T_(t)]}var r=A_(t),i=(r+A_(n))/2;if(b_(i)0?n<-__+d_&&(n=-__+d_):n>__-d_&&(n=__-d_);var e=o/E_(fo(n),i);return[e*A_(i*t),o-e*T_(i*t)]}var r=T_(t),i=t===n?A_(t):N_(r/T_(n))/N_(fo(n)/fo(t)),o=r*E_(fo(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=C_(i)*z_(t*t+e*e);return[M_(t,b_(e))/i*C_(e),2*w_(E_(o/r,1/i))-__]},e):co}function ho(t,n){return[t,n]}function po(t,n){function e(t,n){var e=o-n,r=i*t;return[e*A_(r),o-e*T_(r)]}var r=T_(t),i=t===n?A_(t):(r-T_(n))/(n-t),o=r/i+t;return b_(i)=0;)n+=e[r].value;else n=1;t.value=n}function Eo(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function Ao(t,n){var e,r,i,o,u,a=new Ro(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=zo);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new Ro(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(Lo)}function Co(){return Ao(this).eachBefore(Po)}function zo(t){return t.children}function Po(t){t.data=t.data.data}function Lo(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Ro(t){this.data=t,this.depth=this.height=0,this.parent=null}function qo(t){this._=t,this.next=null}function Uo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Do(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=Oo(n[0]);break;case 2:e=Fo(n[0],n[1]);break;case 3:e=Io(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&Uo(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Do(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function Oo(t){return{x:t.x,y:t.y,r:t.r}}function Fo(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function Io(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),g=2*(i-f),y=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*g,b=(p*m-g*v)/x-r,w=(g*d-p*y)/x,M=(_*v-h*m)/x-i,T=(h*y-_*d)/x,k=w*w+T*T-1,S=2*(b*w+M*T+o),N=b*b+M*M-o*o,E=(-S-Math.sqrt(S*S-4*k*N))/(2*k);return{x:b+w*E+r,y:M+T*E+i,r:E}}function Yo(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Bo(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i-1e-6>e*e+r*r}function jo(t,n,e){var r=t._,i=t.next._,o=r.r+i.r,u=(r.x*i.r+i.x*r.r)/o-n,a=(r.y*i.r+i.y*r.r)/o-e;return u*u+a*a}function Ho(t){this._=t,this.next=null,this.previous=null}function Xo(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Yo(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,g=h*n.y+p*e.y+d*r.y;n=new Ho(n),e=new Ho(e),r=new Ho(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;a=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function uu(t,n,e){return t.a.parent===n.parent?t.a:e}function au(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function cu(t){for(var n,e,r,i,o,u=new au(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new au(r[i],i)),e.parent=n;return(u.parent=new au(null,0)).children=[u],u}function su(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,g=[],y=n.children,m=0,x=0,b=y.length,w=n.value;mh&&(h=a),_=f*f*v,(p=Math.max(h/_,_/l))>d){f-=a;break}d=p}g.push(u={value:f,dice:c1&&em(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function hu(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function pu(t){if(!t._start)try{du(t)}catch(n){if(t._tasks[t._ended+t._active-1])_u(t,n);else if(!t._data)throw n}}function du(t){for(;t._start=t._waiting&&t._active=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,gu(t)}function gu(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function yu(t){return new hu(arguments.length?+t:1/0)}function mu(t){return function(n,e){t(null==n?e:null)}}function xu(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function bu(t,n){return function(e){return t(e.responseText,n)}}function wu(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==Sm)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=Ye(),r=[],i=Sm;return t=null==t?[]:km.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=Ye();for(var i,o,u=-1,a=t.length;++u=e?1:r(t)}}}function Eu(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function Au(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o2?Cu:Au,o=u=null,r}function r(n){return(o||(o=i(a,c,f?Nu(t):t,s)))(+n)}var i,o,u,a=Am,c=Am,s=Th,f=!1;return r.invert=function(t){return(u||(u=i(c,a,Su,f?Eu(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=Tm.call(t,Em),e()):a.slice()},r.range=function(t){return arguments.length?(c=km.call(t),e()):c.slice()},r.rangeRound=function(t){return c=km.call(t),s=kh,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function Lu(t){var n=t.domain;return t.ticks=function(t){var e=n();return tf(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Cm(n(),t,e)},t.nice=function(e){var i=n(),o=i.length-1,u=null==e?10:e,a=i[0],c=i[o],s=r(a,c,u);return s&&(s=r(Math.floor(a/s)*s,Math.ceil(c/s)*s,u),i[0]=Math.floor(a/s)*s,i[o]=Math.ceil(c/s)*s,n(i)),t},t}function Ru(){var t=Pu(Su,mh);return t.copy=function(){return zu(t,Ru())},Lu(t)}function qu(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Tm.call(e,Em),t):n.slice()},t.copy=function(){return qu().domain(n)},Lu(t)}function Uu(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:Nm(n)}function Du(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function Ou(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Fu(t){return 10===t?Ou:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Iu(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Yu(t){return function(n){return-t(-n)}}function Bu(){function n(){return o=Iu(i),u=Fu(i),r()[0]<0&&(o=Yu(o),u=Yu(u)),e}var e=Pu(Uu,Du).domain([1,10]),r=e.domain,i=10,o=Iu(10),u=Fu(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c0){for(;hc)break;v.push(l)}}else for(;h=1;--f)if(!((l=s*f)c)break;v.push(l)}}else v=tf(h,p,Math.min(p-h,d)).map(u);return n?v.reverse():v},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i0?i[n-1]:e[0],n=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return $u().domain([e,r]).range(u)},Lu(t)}function Wu(){function t(t){if(t<=t)return e[Us(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=km.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=km.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return Wu().domain(n).range(e)},t}function Zu(t,n,e,r){function i(n){return t(n=new Date(+n)),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Pm.setTime(+n),Lm.setTime(+r),t(Pm),t(Lm),Math.floor(e(Pm,Lm))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}function Gu(t){return Zu(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/Dm})}function Ju(t){return Zu(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Dm})}function Qu(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Ku(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ta(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function na(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date(+e));++a=c)return-1;if(37===(i=n.charCodeAt(u++))){if(i=n.charAt(u++),!(o=B[i in qx?n.charAt(u++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=R.exec(n.slice(e));return r?(t.w=q[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=L[r[0].toLowerCase()],e+r[0].length):-1}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return N[t.getDay()]}function p(t){return S[t.getDay()]}function d(t){return A[t.getMonth()]}function v(t){return E[t.getMonth()]}function _(t){return k[+(t.getHours()>=12)]}function g(t){return N[t.getUTCDay()]}function y(t){return S[t.getUTCDay()]}function m(t){return A[t.getUTCMonth()]}function x(t){return E[t.getUTCMonth()]}function b(t){return k[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,k=t.periods,S=t.days,N=t.shortDays,E=t.months,A=t.shortMonths,C=ia(k),z=oa(k),P=ia(S),L=oa(S),R=ia(N),q=oa(N),U=ia(E),D=oa(E),O=ia(A),F=oa(A),I={a:h,A:p,b:d,B:v,c:null,d:xa,e:xa,H:ba,I:wa,j:Ma,L:Ta,m:ka,M:Sa,p:_,S:Na,U:Ea,w:Aa,W:Ca,x:null,X:null,y:za,Y:Pa,Z:La,"%":Wa},Y={a:g,A:y,b:m,B:x,c:null,d:Ra,e:Ra,H:qa,I:Ua,j:Da,L:Oa,m:Fa,M:Ia,p:b,S:Ya,U:Ba,w:ja,W:Ha,x:null,X:null,y:Xa,Y:Va,Z:$a,"%":Wa},B={a:o,A:u,b:a,B:c,c:s,d:pa,e:pa,H:va,I:va,j:da,L:ya,m:ha,M:_a,p:i,S:ga,U:aa,w:ua,W:ca,x:f,X:l,y:fa,Y:sa,Z:la,"%":ma};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",Qu);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,Ku);return n.toString=function(){return t},n}}}function ea(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function la(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function ha(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function pa(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function da(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function va(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function _a(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function ga(t,n,e){var r=Ux.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function ya(t,n,e){var r=Ux.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function ma(t,n,e){var r=Dx.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function xa(t,n){return ea(t.getDate(),n,2)}function ba(t,n){return ea(t.getHours(),n,2)}function wa(t,n){return ea(t.getHours()%12||12,n,2)}function Ma(t,n){return ea(1+Hm.count(cx(t),t),n,3)}function Ta(t,n){return ea(t.getMilliseconds(),n,3)}function ka(t,n){return ea(t.getMonth()+1,n,2)}function Sa(t,n){return ea(t.getMinutes(),n,2)}function Na(t,n){return ea(t.getSeconds(),n,2)}function Ea(t,n){return ea(Vm.count(cx(t),t),n,2)}function Aa(t){return t.getDay()}function Ca(t,n){return ea($m.count(cx(t),t),n,2)}function za(t,n){return ea(t.getFullYear()%100,n,2)}function Pa(t,n){return ea(t.getFullYear()%1e4,n,4)}function La(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+ea(n/60|0,"0",2)+ea(n%60,"0",2)}function Ra(t,n){return ea(t.getUTCDate(),n,2)}function qa(t,n){return ea(t.getUTCHours(),n,2)}function Ua(t,n){return ea(t.getUTCHours()%12||12,n,2)}function Da(t,n){return ea(1+dx.count(Px(t),t),n,3)}function Oa(t,n){return ea(t.getUTCMilliseconds(),n,3)}function Fa(t,n){return ea(t.getUTCMonth()+1,n,2)}function Ia(t,n){return ea(t.getUTCMinutes(),n,2)}function Ya(t,n){return ea(t.getUTCSeconds(),n,2)}function Ba(t,n){return ea(_x.count(Px(t),t),n,2)}function ja(t){return t.getUTCDay()}function Ha(t,n){return ea(gx.count(Px(t),t),n,2)}function Xa(t,n){return ea(t.getUTCFullYear()%100,n,2)}function Va(t,n){return ea(t.getUTCFullYear()%1e4,n,4)}function $a(){return"+0000"}function Wa(){return"%"}function Za(n){return Lx=na(n),t.timeFormat=Lx.format,t.timeParse=Lx.parse,t.utcFormat=Lx.utcFormat,t.utcParse=Lx.utcParse,Lx}function Ga(t){return t.toISOString()}function Ja(t){var n=new Date(t);return isNaN(n)?null:n}function Qa(t){return new Date(t)}function Ka(t){return t instanceof Date?+t:+new Date(+t)}function tc(t,n,e,i,o,u,a,c,s){function f(r){return(a(r)1?0:t<-1?mb:Math.acos(t)}function ic(t){return t>=1?xb:t<=-1?-xb:Math.asin(t)}function oc(t){return t.innerRadius}function uc(t){return t.outerRadius}function ac(t){return t.startAngle}function cc(t){return t.endAngle}function sc(t){return t&&t.padAngle}function fc(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function lc(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/gb(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,g=(p+v)/2,y=d-h,m=v-p,x=y*y+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*gb(db(0,b*b*x-w*w)),T=(w*m-y*M)/x,k=(-w*y-m*M)/x,S=(w*m+y*M)/x,N=(-w*y+m*M)/x,E=T-_,A=k-g,C=S-_,z=N-g;return E*E+A*A>C*C+z*z&&(T=S,k=N),{cx:T,cy:k,x01:-f,y01:-l,x11:T*(i/b-1),y11:k*(i/b-1)}}function hc(t){this._context=t}function pc(t){return t[0]}function dc(t){return t[1]}function vc(t){this._curve=t}function _c(t){function n(n){return new vc(t(n))}return n._curve=t,n}function gc(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t}function yc(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function mc(t){this._context=t}function xc(t){this._context=t}function bc(t){this._context=t}function wc(t,n){this._basis=new mc(t),this._beta=n}function Mc(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Tc(t,n){this._context=t,this._k=(1-n)/6}function kc(t,n){this._context=t,this._k=(1-n)/6}function Sc(t,n){this._context=t,this._k=(1-n)/6}function Nc(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>yb){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>yb){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function Ec(t,n){this._context=t,this._alpha=n}function Ac(t,n){this._context=t,this._alpha=n}function Cc(t,n){this._context=t,this._alpha=n}function zc(t){this._context=t}function Pc(t){return t<0?-1:1}function Lc(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Pc(o)+Pc(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Rc(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function qc(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function Uc(t){this._context=t}function Dc(t){this._context=new Oc(t)}function Oc(t){this._context=t}function Fc(t){return new Uc(t)}function Ic(t){return new Dc(t)}function Yc(t){this._context=t}function Bc(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n0)){if(o/=d,d<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o0)){if(o/=v,v<0){if(o0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function os(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]Cw||Math.abs(i[0][1]-i[1][1])>Cw)||delete Nw[o]}function as(t){return kw[t.index]={site:t,halfedges:[]}}function cs(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function ss(t,n){return n[+(n.left!==t.site)]}function fs(t,n){return n[+(n.left===t.site)]}function ls(){for(var t,n,e,r,i=0,o=kw.length;iCw||Math.abs(v-h)>Cw)&&(c.splice(a,0,Nw.push(es(u,p,Math.abs(d-t)Cw?[t,Math.abs(l-t)Cw?[Math.abs(h-r)Cw?[e,Math.abs(l-e)Cw?[Math.abs(h-n)=-zw)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,g=Ew.pop()||new ps;g.arc=t,g.site=i,g.x=v+u,g.y=(g.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=g;for(var y=null,m=Sw._;m;)if(g.yCw)a=a.L;else{if(!((i=o-ws(a,u))>Cw)){r>-Cw?(n=a.P,e=a):i>-Cw?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}as(t);var c=gs(t);if(Tw.insert(n,c),n||e){if(n===e)return vs(n),e=gs(n.site),Tw.insert(c,e),c.edge=e.edge=ns(n.site,c.site),ds(n),void ds(e);if(!e)return void(c.edge=ns(n.site,c.site));vs(n),vs(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,g=2*(h*_-p*v),y=h*h+p*p,m=v*v+_*_,x=[(_*y-p*m)/g+f,(h*m-v*y)/g+l];rs(e.edge,s,d,x),c.edge=ns(s,t,null,x),e.edge=ns(t,d,null,x),ds(n),ds(e)}}function bs(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function ws(t,n){var e=t.N;if(e)return bs(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function Ms(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function Ts(t,n){return n[1]-t[1]||n[0]-t[0]}function ks(t,n){var e,r,i,o=t.sort(Ts).pop();for(Nw=[],kw=new Array(t.length),Tw=new Gc,Sw=new Gc;;)if(i=Mw,o&&(!i||o[1]n?1:t>=n?0:NaN},Rs=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}},qs=Rs(Ls),Us=qs.right,Ds=qs.left,Os=function(t,n){null==n&&(n=e);for(var r=0,i=t.length-1,o=t[0],u=new Array(i<0?0:i);rt?1:n>=t?0:NaN},Ys=function(t){return null===t?NaN:+t},Bs=function(t,n){var e,r,i=t.length,o=0,u=0,a=-1,c=0;if(null==n)for(;++a1)return u/(c-1)},js=function(t,n){var e=Bs(t,n);return e?Math.sqrt(e):e},Hs=function(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o=r){e=i=r;break}for(;++or&&(e=r),i=r){e=i=r;break}for(;++or&&(e=r),i=f;)l.pop(),--h;var p,d=new Array(h+1);for(i=0;i<=h;++i)p=d[i]=[],p.x0=i>0?l[i-1]:s,p.x1=i=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),u=+e(t[o],o,t);return u+(+e(t[o+1],o+1,t)-u)*(i-o)}},of=function(t,n,e){return t=$s.call(t,Ys).sort(Ls),Math.ceil((e-n)/(2*(rf(t,.75)-rf(t,.25))*Math.pow(t.length,-1/3)))},uf=function(t,n,e){return Math.ceil((e-n)/(3.5*js(t)*Math.pow(t.length,-1/3)))},af=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},cf=function(t,n){var e,r=0,i=t.length,o=-1,u=i;if(null==n)for(;++o=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e},lf=function(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},hf=function(t,n){for(var e=n.length,r=new Array(e);e--;)r[e]=t[n[e]];return r},pf=function(t,n){if(e=t.length){var e,r,i=0,o=0,u=t[o];for(n||(n=Ls);++i0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Nf.hasOwnProperty(n)?{space:Nf[n],local:t}:t},Af=function(t){var n=Ef(t);return(n.local?x:m)(n)},Cf=0;w.prototype=b.prototype={constructor:w,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var zf=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Pf=document.documentElement;if(!Pf.matches){var Lf=Pf.webkitMatchesSelector||Pf.msMatchesSelector||Pf.mozMatchesSelector||Pf.oMatchesSelector;zf=function(t){return function(){return Lf.call(this,t)}}}}var Rf=zf,qf={};if(t.event=null,"undefined"!=typeof document){"onmouseenter"in document.documentElement||(qf={mouseenter:"mouseover",mouseleave:"mouseout"})}var Uf=function(t,n,e){var r,i,o=k(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?N:S,null==e&&(e=!1),r=0;r=x&&(x=m+1);!(y=_[x])&&++x=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this},Kf=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=R);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?Y:"function"==typeof n?j:B)(t,n,null==e?"":e)):al(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)},sl=function(t,n){return arguments.length>1?this.each((null==n?H:"function"==typeof n?V:X)(t,n)):this.node()[t]};Z.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var fl=function(t,n){var e=$(t+"");if(arguments.length<2){for(var r=W(this.node()),i=-1,o=e.length;++i=240?t-240:t+120,i,r),Rt(t,i,r),Rt(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Bl=Math.PI/180,jl=180/Math.PI,Hl=.95047,Xl=1,Vl=1.08883,$l=4/29,Wl=6/29,Zl=3*Wl*Wl,Gl=Wl*Wl*Wl;Al(Dt,Ut,wt(Mt,{brighter:function(t){return new Dt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Dt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Xl*Ft(t),n=Hl*Ft(n),e=Vl*Ft(e),new At(It(3.2404542*n-1.5371385*t-.4985314*e),It(-.969266*n+1.8760108*t+.041556*e),It(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),Al(Ht,jt,wt(Mt,{brighter:function(t){return new Ht(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Ht(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return qt(this).rgb()}}));var Jl=-.14861,Ql=1.78277,Kl=-.29227,th=-.90649,nh=1.97294,eh=nh*th,rh=nh*Ql,ih=Ql*Kl-th*Jl;Al($t,Vt,wt(Mt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new $t(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Bl,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new At(255*(n+e*(Jl*r+Ql*i)),255*(n+e*(Kl*r+th*i)),255*(n+e*(nh*r)),this.opacity)}}));var oh,uh,ah,ch,sh,fh,lh=function(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=ro&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:mh(e,r)})),o=wh.lastIndex;return onp&&e.statetp&&e.name===n)return new Kn([[t]],td,n,+r)}return null},ed=function(t){return function(){return t}},rd=function(t,n,e){this.target=t,this.type=n,this.selection=e},id=function(){t.event.preventDefault(),t.event.stopImmediatePropagation()},od={name:"drag"},ud={name:"space"},ad={name:"handle"},cd={name:"center"},sd={name:"x",handles:["e","w"].map(Me),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fd={name:"y",handles:["n","s"].map(Me),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},ld={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(Me),input:function(t){return t},output:function(t){return t}},hd={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},pd={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},dd={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},vd={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},_d={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},gd=function(){return ze(ld)},yd=Math.cos,md=Math.sin,xd=Math.PI,bd=xd/2,wd=2*xd,Md=Math.max,Td=function(){function t(t){var o,u,a,c,s,f,l=t.length,h=[],p=Gs(l),d=[],v=[],_=v.groups=new Array(l),g=new Array(l*l);for(o=0,s=-1;++s1e-6)if(Math.abs(f*a-c*s)>1e-6&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),g=Math.sqrt(l),y=i*Math.tan((Nd-Math.acos((d+l-v)/(2*_*g)))/2),m=y/g,x=y/_;Math.abs(m-1)>1e-6&&(this._+="L"+(t+m*s)+","+(n+m*f)),this._+="A"+i+","+i+",0,0,"+ +(f*h>s*p)+","+(this._x1=t+x*a)+","+(this._y1=n+x*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+s:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-s)>1e-6)&&(this._+="L"+c+","+s),e&&(l<0&&(l=l%Ed+Ed),l>Ad?this._+="A"+e+","+e+",0,1,"+f+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+f+","+(this._x1=c)+","+(this._y1=s):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Nd)+","+f+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};var Cd=function(){function t(){var t,a=kd.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,a)),l=i.apply(this,a)-bd,h=o.apply(this,a)-bd,p=f*yd(l),d=f*md(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-bd,g=o.apply(this,a)-bd;if(u||(u=t=Re()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===g||(u.quadraticCurveTo(0,0,v*yd(_),v*md(_)),u.arc(0,0,v,_,g)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=qe,e=Ue,r=De,i=Oe,o=Fe,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:Sd(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Sd(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Sd(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t};Ie.prototype=Ye.prototype={constructor:Ie,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var zd=function(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=Ye(),v=u();++lo.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,Be,je)},map:function(n){return t(n,0,He,Xe)},entries:function(e){return n(t(e,0,He,Xe),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}},Pd=Ye.prototype;Ve.prototype=$e.prototype={constructor:Ve,has:Pd.has,add:function(t){return t+="",this["$"+t]=t,this},remove:Pd.remove,clear:Pd.clear,values:Pd.keys,size:Pd.size,empty:Pd.empty,each:Pd.each};var Ld=function(t){var n=[];for(var e in t)n.push(e);return n},Rd=function(t){var n=[];for(var e in t)n.push(t[e]);return n},qd=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},Ud=function(t){function n(t,n){var r,i,o=e(t,function(t,e){if(r)return r(t,e-1);i=t,r=n?Ze(t,n):We(t)});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=s,s=u}while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do{u=new Array(4),u[a]=s,s=u}while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this},Qd=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},Kd=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},tv=function(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i},nv=function(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new tv(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)=g)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var y=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=y*y+m*m;if(x=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+g)/2))?v=c:g=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)},rv=function(){return this._root},iv=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},ov=function(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new tv(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new tv(e,s,f,o,u)),(e=c[2])&&a.push(new tv(e,r,f,s,u)),(e=c[1])&&a.push(new tv(e,s,i,o,f)),(e=c[0])&&a.push(new tv(e,r,i,s,f))}return this},uv=function(t){var n,e=[],r=[];for(this._root&&e.push(new tv(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new tv(o,u,a,f,l)),(o=i[1])&&e.push(new tv(o,f,a,c,l)),(o=i[2])&&e.push(new tv(o,u,l,f,s)),(o=i[3])&&e.push(new tv(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},av=function(t){return arguments.length?(this._x=t,this):this._x},cv=function(t){return arguments.length?(this._y=t,this):this._y},sv=er.prototype=rr.prototype;sv.copy=function(){var t,n,e=new rr(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ir(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ir(n));return e},sv.add=Gd,sv.addAll=Qe,sv.cover=Jd,sv.data=Qd,sv.extent=Kd,sv.find=nv,sv.remove=ev,sv.removeAll=Ke,sv.root=rv,sv.size=iv,sv.visit=ov,sv.visitAfter=uv,sv.x=av,sv.y=cv;var fv,lv=function(t){function n(){function t(t,n,e,r,i){var o=t.data,a=t.r,p=l+a;{if(!o)return n>s+p||rf+p||ic.index){var d=s-o.x-o.vx,v=f-o.y-o.vy,_=d*d+v*v;_t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s1?(p.on(t,n),o):p.on(t)}}},_v=function(){function t(t){var n,a=i.length,c=er(i,sr,fr).visitAfter(e);for(u=t,n=0;n=f)){(t.data!==o||t.next)&&(0===i&&(i=Zd(),p+=i*i),0===c&&(c=Zd(),p+=c*c),p1?r[0]+r.slice(2):r,+t.slice(e+1)]},xv=function(t){return t=mv(Math.abs(t)),t?t[1]:NaN},bv=function(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},wv=function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}},Mv=function(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Tv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(fv=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+mv(t,Math.max(0,n+o-1))[0]},kv=function(t,n){var e=mv(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")},Sv={"":Mv,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return kv(100*t,n)},r:kv,s:Tv,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Nv=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;lr.prototype=hr.prototype,hr.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Ev,Av=function(t){return t},Cv=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],zv=function(t){function n(t){function n(t){var n,i,s,m=v,x=_;if("c"===d)x=g(t)+x,t="";else{t=+t;var b=t<0;if(t=g(Math.abs(t),p),b&&0==+t&&(b=!1),m=(b?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=x+("s"===d?Cv[8+fv/3]:"")+(b&&"("===c?")":""),y)for(n=-1,i=t.length;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}h&&!f&&(t=r(t,1/0));var w=m.length+t.length+x.length,M=w>1)+m+t+x+M.slice(w);break;default:t=M+m+t+x}return u(t)}t=lr(t);var e=t.fill,a=t.align,c=t.sign,s=t.symbol,f=t.zero,l=t.width,h=t.comma,p=t.precision,d=t.type,v="$"===s?i[0]:"#"===s&&/[boxX]/.test(d)?"0"+d.toLowerCase():"",_="$"===s?i[1]:/[%p]/.test(d)?"%":"",g=Sv[d],y=!d||/[defgprs%]/.test(d);return p=null==p?d?6:12:/[gprs]/.test(d)?Math.max(1,Math.min(21,p)):Math.max(0,Math.min(20,p)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=lr(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(xv(e)/3))),o=Math.pow(10,-i),u=Cv[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?bv(t.grouping,t.thousands):Av,i=t.currency,o=t.decimal,u=t.numerals?wv(t.numerals):Av;return{format:n,formatPrefix:e}};pr({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Pv=function(t){return Math.max(0,-xv(Math.abs(t)))},Lv=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(xv(n)/3)))-xv(Math.abs(t)))},Rv=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,xv(n)-xv(t))+1},qv=function(){return new dr};dr.prototype={constructor:dr,reset:function(){this.s=this.t=0},add:function(t){vr(p_,t,this.t),vr(this,p_.s,this.s),this.s?this.t+=p_.t:this.s=p_.t},valueOf:function(){return this.s}};var Uv,Dv,Ov,Fv,Iv,Yv,Bv,jv,Hv,Xv,Vv,$v,Wv,Zv,Gv,Jv,Qv,Kv,t_,n_,e_,r_,i_,o_,u_,a_,c_,s_,f_,l_,h_,p_=new dr,d_=1e-6,v_=Math.PI,__=v_/2,g_=v_/4,y_=2*v_,m_=180/v_,x_=v_/180,b_=Math.abs,w_=Math.atan,M_=Math.atan2,T_=Math.cos,k_=Math.ceil,S_=Math.exp,N_=Math.log,E_=Math.pow,A_=Math.sin,C_=Math.sign||function(t){return t>0?1:t<0?-1:0},z_=Math.sqrt,P_=Math.tan,L_={Feature:function(t,n){xr(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rd_?Hv=90:I_<-d_&&(Bv=-90),Gv[0]=Yv,Gv[1]=jv}},B_=function(t){var n,e,r,i,o,u,a;if(Hv=jv=-(Yv=Bv=1/0),Zv=[],q_(t,Y_),e=Zv.length){for(Zv.sort(Br),n=1,r=Zv[0],o=[r];nYr(r[0],r[1])&&(r[1]=i[1]),Yr(i[0],r[1])>Yr(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Yr(r[1],i[0]))>u&&(u=a,Yv=i[0],jv=r[1])}return Zv=Gv=null,Yv===1/0||Bv===1/0?[[NaN,NaN],[NaN,NaN]]:[[Yv,Bv],[jv,Hv]]},j_={sphere:mr,point:Hr,lineStart:Vr,lineEnd:Zr,polygonStart:function(){j_.lineStart=Gr,j_.lineEnd=Jr},polygonEnd:function(){j_.lineStart=Vr,j_.lineEnd=Zr}},H_=function(t){Jv=Qv=Kv=t_=n_=e_=r_=i_=o_=u_=a_=0,q_(t,j_);var n=o_,e=u_,r=a_,i=n*n+e*e+r*r;return i<1e-12&&(n=e_,e=r_,r=i_,Qv2?t[2]*x_:0),n.invert=function(n){return n=t.invert(n[0]*x_,n[1]*x_),n[0]*=m_,n[1]*=m_,n},n},og=function(){function t(t,n){e.push(t=r(t,n)),t[0]*=m_,t[1]*=m_}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*x_,c=u.apply(this,arguments)*x_;return e=[],r=ni(-t[0]*x_,-t[1]*x_,0).invert,oi(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=X_([0,0]),o=X_(90),u=X_(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:X_([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:X_(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:X_(+t),n):u},n},ug=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:mr,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},ag=function(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u0)){if(u/=d,d<0){if(u0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}},cg=function(t,n){return b_(t[0]-n[0])=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}},fg=1e9,lg=-fg,hg=function(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=si(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}},pg=qv(),dg=function(t,n){var e=n[0],r=n[1],i=[A_(e),-T_(e),0],o=0,u=0;pg.reset();for(var a=0,c=t.length;a=0?1:-1,T=M*w,k=T>v_,S=d*x;if(pg.add(M_(S*M*A_(T),v*b+S*T_(T))),o+=k?w+M*y_:w,k^h>=e^y>=e){var N=Cr(Er(l),Er(g));Lr(N);var E=Cr(i,N);Lr(E);var A=(k^w>=0?-1:1)*gr(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(u+=k^w>=0?1:-1)}}return(o<-d_||o0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(ji))}var p,d,v,_=n(o),g=i.invert(r[0],r[1]),y=ug(),m=n(y),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=ff(d);var t=dg(p,g);d.length?(x||(o.polygonStart(),x=!0),sg(d,Hi,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}},Kg=Qg(function(){return!0},Xi,$i,[-v_,-__]),ty=function(t,n){function e(e,r,i,o){oi(o,t,n,i,e,r)}function r(t,n){return T_(t)*T_(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?v_:-v_),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(cg(n,p)||cg(d,p))&&(d[0]+=d_,d[1]+=d_,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var g;_&e||!(g=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(g[0][0],g[0][1]),t.point(g[1][0],g[1][1]),t.lineEnd()):(t.point(g[1][0],g[1][1]),t.lineEnd(),t.lineStart(),t.point(g[0][0],g[0][1])))}!v||n&&cg(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=Er(t),i=Er(n),o=[1,0,0],u=Cr(r,i),c=Ar(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=Cr(o,u),d=Pr(o,l);zr(d,Pr(u,h));var v=p,_=Ar(d,v),g=Ar(v,v),y=_*_-g*(Ar(d,d)-1);if(!(y<0)){var m=z_(y),x=Pr(v,(-_-m)/g);if(zr(x,d),x=Nr(x),!e)return x;var b,w=t[0],M=n[0],T=t[1],k=n[1];M0^x[1]<(b_(x[0]-w)v_^(w<=x[0]&&x[0]<=M)){var A=Pr(v,(-_+m)/g);return zr(A,d),[x,Nr(A)]}}}function u(n,e){var r=c?t:v_-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=T_(t),c=a>0,s=b_(a)>d_;return Qg(r,i,e,c?[0,-t]:[-v_,t-v_])},ny=function(t){return{stream:Wi(t)}};Zi.prototype={constructor:Zi,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var ey=16,ry=T_(30*x_),iy=function(t,n){return+n?Ki(t,n):Qi(t)},oy=Wi({point:function(t,n){this.stream.point(t*x_,n*x_)}}),uy=function(){return eo(io).scale(155.424).center([0,33.6442])},ay=function(){return uy().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])},cy=function(){function t(t){var n=t[0],e=t[1];return a=null,i.point(n,e),a||(o.point(n,e),a)||(u.point(n,e),a)}function n(){return e=r=null,t}var e,r,i,o,u,a,c=ay(),s=uy().rotate([154,0]).center([-2,58.5]).parallels([55,65]),f=uy().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(t,n){a=[t,n]}};return t.invert=function(t){var n=c.scale(),e=c.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?f:c).invert(t)},t.stream=function(t){return e&&r===t?e:e=oo([c.stream(r=t),s.stream(t),f.stream(t)])},t.precision=function(t){return arguments.length?(c.precision(t),s.precision(t),f.precision(t),n()):c.precision()},t.scale=function(n){return arguments.length?(c.scale(n),s.scale(.35*n),f.scale(n),t.translate(c.translate())):c.scale()},t.translate=function(t){if(!arguments.length)return c.translate();var e=c.scale(),r=+t[0],a=+t[1];return i=c.translate(t).clipExtent([[r-.455*e,a-.238*e],[r+.455*e,a+.238*e]]).stream(l),o=s.translate([r-.307*e,a+.201*e]).clipExtent([[r-.425*e+d_,a+.12*e+d_],[r-.214*e-d_,a+.234*e-d_]]).stream(l),u=f.translate([r-.205*e,a+.212*e]).clipExtent([[r-.214*e+d_,a+.166*e+d_],[r-.115*e-d_,a+.234*e-d_]]).stream(l),n()},t.fitExtent=function(n,e){return Gi(t,n,e)},t.fitSize=function(n,e){return Ji(t,n,e)},t.scale(1070)},sy=uo(function(t){return z_(2/(1+t))});sy.invert=ao(function(t){return 2*gr(t/2)});var fy=function(){return to(sy).scale(124.75).clipAngle(179.999)},ly=uo(function(t){return(t=_r(t))&&t/A_(t)});ly.invert=ao(function(t){return t});var hy=function(){return to(ly).scale(79.4188).clipAngle(179.999)};co.invert=function(t,n){return[t,2*w_(S_(n))-__]};var py=function(){return so(co).scale(961/y_)},dy=function(){return eo(lo).scale(109.5).parallels([30,30])};ho.invert=ho;var vy=function(){return to(ho).scale(152.63)},_y=function(){return eo(po).scale(131.154).center([0,13.9389])};vo.invert=ao(w_);var gy=function(){return to(vo).scale(144.049).clipAngle(60)},yy=function(){function t(){return i=o=null,u}var n,e,r,i,o,u,a=1,c=0,s=0,f=1,l=1,h=kg,p=null,d=kg;return u={stream:function(t){return i&&o===t?i:i=h(d(o=t))},clipExtent:function(i){return arguments.length?(d=null==i?(p=n=e=r=null,kg):si(p=+i[0][0],n=+i[0][1],e=+i[1][0],r=+i[1][1]),t()):null==p?null:[[p,n],[e,r]]},scale:function(n){return arguments.length?(h=_o((a=+n)*f,a*l,c,s),t()):a},translate:function(n){return arguments.length?(h=_o(a*f,a*l,c=+n[0],s=+n[1]),t()):[c,s]},reflectX:function(n){return arguments.length?(h=_o(a*(f=n?-1:1),a*l,c,s),t()):f<0},reflectY:function(n){return arguments.length?(h=_o(a*f,a*(l=n?-1:1),c,s),t()):l<0},fitExtent:function(t,n){return Gi(u,t,n)},fitSize:function(t,n){return Ji(u,t,n)}}};go.invert=ao(gr);var my=function(){return to(go).scale(249.5).clipAngle(90+d_)};yo.invert=ao(function(t){return 2*w_(t)});var xy=function(){return to(yo).scale(250).clipAngle(142)};mo.invert=function(t,n){return[-n,2*w_(S_(t))-__]};var by=function(){var t=so(mo),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90]).scale(159.155)},wy=function(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=bo(e),t.y=Mo(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=ko(t),c=So(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=xo,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t},My=function(){return this.eachAfter(No)},Ty=function(t){var n,e,r,i,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},Sy=function(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e=0;)e+=r[i].value;n.value=e})},Ey=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Ay=function(t){for(var n=this,e=Eo(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},Cy=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},zy=function(){var t=[];return this.each(function(n){t.push(n)}),t},Py=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Ly=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ro.prototype=Ao.prototype={constructor:Ro,count:My,each:Ty,eachAfter:Sy,eachBefore:ky,sum:Ny,sort:Ey,path:Ay,ancestors:Cy,descendants:zy,leaves:Py,links:Ly,copy:Co};var Ry=function(t){for(var n=(t=t.slice()).length,e=null,r=e;n;){var i=new qo(t[n-1]);r=r?r.next=i:e=i,t[void 0]=t[--n]}return{head:e,tail:r}},qy=function(t){return Do(Ry(t),[])},Uy=function(t){return Xo(t),t},Dy=function(t){return function(){return t}},Oy=function(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(Go(n)).eachAfter(Jo(i,.5)).eachBefore(Qo(1)):t.eachBefore(Go(Zo)).eachAfter(Jo(Wo,1)).eachAfter(Jo(i,t.r/Math.min(e,r))).eachBefore(Qo(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Wo;return t.radius=function(e){return arguments.length?(n=Vo(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:Dy(+n),t):i},t},Fy=function(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)},Iy=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a0)throw new Error("cycle");return o}var n=Ko,e=tu;return t.id=function(e){return arguments.length?(n=$o(e),t):n},t.parentId=function(n){return arguments.length?(e=$o(n),t):e},t};au.prototype=Object.create(Ro.prototype);var Vy=function(){function t(t){var r=cu(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.xf.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){ou(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=ru(a),i=eu(i),a&&i;)c=eu(c),u=ru(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(iu(uu(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!ru(u)&&(u.t=a,u.m+=l-f),i&&!eu(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=nu,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t},$y=function(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a1?n:1)},e}(Wy),Gy=function(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Fy),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p>>1;f[v]a-i){var y=(r*g+u*_)/e;o(t,p,_,r,i,y,a),o(p,n,g,y,i,u,a)}else{var m=(i*g+a*_)/e;o(t,p,_,r,i,u,m),o(p,n,g,r,m,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u1?n:1)},e -}(Wy),tm=function(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;na!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f},om=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}},fm=function(){var t=sm.apply(this,arguments);return function(){return Math.exp(t())}},lm=function(t){return function(){for(var n=0,e=0;e=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(t){return void a.call("error",r,t)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=d("beforesend","progress","load","error"),c=Ye(),s=new XMLHttpRequest,f=null,l=null,h=0;if("undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=mu(o)),null!=o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return r.get(n)}return r},vm=function(t,n){return function(e,r){var i=dm(e).mimeType(t).response(n);if(null!=r){if("function"!=typeof r)throw new Error("invalid callback: "+r);return i.get(r)}return i}},_m=vm("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),gm=vm("application/json",function(t){return JSON.parse(t.responseText)}),ym=vm("text/plain",function(t){return t.responseText}),mm=vm("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),xm=function(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=dm(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(bu(n,r=t)):r},o.row(r),i?o.get(i):o}},bm=xm("text/csv",Od),wm=xm("text/tab-separated-values",jd),Mm=Array.prototype,Tm=Mm.map,km=Mm.slice,Sm={name:"implicit"},Nm=function(t){return function(){return t}},Em=function(t){return+t},Am=[0,1],Cm=function(n,e,i){var o,u=n[0],a=n[n.length-1],c=r(u,a,null==e?10:e);switch(i=lr(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(u),Math.abs(a));return null!=i.precision||isNaN(o=Lv(c,s))||(i.precision=o),t.formatPrefix(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Rv(c,Math.max(Math.abs(u),Math.abs(a))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Pv(c))||(i.precision=o-2*("%"===i.type))}return t.format(i)},zm=function(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u0?t>1?Zu(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Rm:null};var qm=Rm.range,Um=6e4,Dm=6048e5,Om=Zu(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),Fm=Om.range,Im=Zu(function(t){t.setTime(Math.floor(t/Um)*Um)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getMinutes()}),Ym=Im.range,Bm=Zu(function(t){var n=t.getTimezoneOffset()*Um%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),jm=Bm.range,Hm=Zu(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Um)/864e5},function(t){return t.getDate()-1}),Xm=Hm.range,Vm=Gu(0),$m=Gu(1),Wm=Gu(2),Zm=Gu(3),Gm=Gu(4),Jm=Gu(5),Qm=Gu(6),Km=Vm.range,tx=$m.range,nx=Wm.range,ex=Zm.range,rx=Gm.range,ix=Jm.range,ox=Qm.range,ux=Zu(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ax=ux.range,cx=Zu(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});cx.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var sx=cx.range,fx=Zu(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Um)},function(t,n){return(n-t)/Um},function(t){return t.getUTCMinutes()}),lx=fx.range,hx=Zu(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),px=hx.range,dx=Zu(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),vx=dx.range,_x=Ju(0),gx=Ju(1),yx=Ju(2),mx=Ju(3),xx=Ju(4),bx=Ju(5),wx=Ju(6),Mx=_x.range,Tx=gx.range,kx=yx.range,Sx=mx.range,Nx=xx.range,Ex=bx.range,Ax=wx.range,Cx=Zu(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),zx=Cx.range,Px=Zu(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Px.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zu(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Lx,Rx=Px.range,qx={"-":"",_:" ",0:"0"},Ux=/^\s*\d+/,Dx=/^%/,Ox=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Za({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Fx=Date.prototype.toISOString?Ga:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ"),Ix=+new Date("2000-01-01T00:00:00.000Z")?Ja:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),Yx=1e3,Bx=60*Yx,jx=60*Bx,Hx=24*jx,Xx=7*Hx,Vx=30*Hx,$x=365*Hx,Wx=function(){return tc(cx,ux,Vm,Hm,Bm,Im,Om,Rm,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])},Zx=function(){return tc(Px,Cx,_x,dx,hx,fx,Om,Rm,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])},Gx=function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})},Jx=Gx("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Qx=Gx("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Kx=Gx("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),tb=Gx("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),nb=Oh(Vt(300,.5,0),Vt(-240,.5,1)),eb=Oh(Vt(-100,.75,.35),Vt(80,1.5,.8)),rb=Oh(Vt(260,.75,.35),Vt(80,1.5,.8)),ib=Vt(),ob=function(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return ib.h=360*t-100,ib.s=1.5-1.5*n,ib.l=.8-.9*n,ib+""},ub=nc(Gx("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),ab=nc(Gx("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),cb=nc(Gx("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),sb=nc(Gx("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),fb=function(t){return function(){return t}},lb=Math.abs,hb=Math.atan2,pb=Math.cos,db=Math.max,vb=Math.min,_b=Math.sin,gb=Math.sqrt,yb=1e-12,mb=Math.PI,xb=mb/2,bb=2*mb,wb=function(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-xb,p=u.apply(this,arguments)-xb,d=lb(p-h),v=p>h;if(c||(c=t=Re()),lyb)if(d>bb-yb)c.moveTo(l*pb(h),l*_b(h)),c.arc(0,0,l,h,p,!v),f>yb&&(c.moveTo(f*pb(p),f*_b(p)),c.arc(0,0,f,p,h,v));else{var _,g,y=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,k=T>yb&&(i?+i.apply(this,arguments):gb(f*f+l*l)),S=vb(lb(l-f)/2,+r.apply(this,arguments)),N=S,E=S;if(k>yb){var A=ic(k/f*_b(T)),C=ic(k/l*_b(T));(w-=2*A)>yb?(A*=v?1:-1,x+=A,b-=A):(w=0,x=b=(h+p)/2),(M-=2*C)>yb?(C*=v?1:-1,y+=C,m-=C):(M=0,y=m=(h+p)/2)}var z=l*pb(y),P=l*_b(y),L=f*pb(b),R=f*_b(b);if(S>yb){var q=l*pb(m),U=l*_b(m),D=f*pb(x),O=f*_b(x);if(dyb?fc(z,P,D,O,q,U,L,R):[L,R],I=z-F[0],Y=P-F[1],B=q-F[0],j=U-F[1],H=1/_b(rc((I*B+Y*j)/(gb(I*I+Y*Y)*gb(B*B+j*j)))/2),X=gb(F[0]*F[0]+F[1]*F[1]);N=vb(S,(f-X)/(H-1)),E=vb(S,(l-X)/(H+1))}}M>yb?E>yb?(_=lc(D,O,z,P,l,E,v),g=lc(q,U,L,R,l,E,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),Eyb&&w>yb?N>yb?(_=lc(L,R,q,U,f,-N,v),g=lc(z,P,D,O,f,-N,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),N=f;--l)s.point(_[l],g[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),g[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):g[n]))}if(p)return s=null,p+""||null}function n(){return Tb().defined(u).curve(c).context(a)}var e=pc,r=null,i=fb(0),o=dc,u=fb(!0),a=null,c=Mb,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:fb(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:fb(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:fb(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t},Sb=function(t,n){return nt?1:n>=t?0:NaN},Nb=function(t){return t},Eb=function(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),g=Math.min(bb,Math.max(-bb,o.apply(this,arguments)-_)),y=Math.min(Math.abs(g)/h,u.apply(this,arguments)),m=y*(g<0?-1:1);for(a=0;a0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(g-h*m)/p:0;a0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:y};return v}var n=Nb,e=Sb,r=null,i=fb(0),o=fb(bb),u=fb(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:fb(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:fb(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:fb(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:fb(+n),t):u},t},Ab=_c(Mb);vc.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Cb=function(){return gc(Tb().curve(Ab))},zb=function(){var t=kb().curve(Ab),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return gc(e())},delete t.lineX0,t.lineEndAngle=function(){return gc(r())},delete t.lineX1,t.lineInnerRadius=function(){return gc(i())},delete t.lineY0,t.lineOuterRadius=function(){return gc(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(_c(t)):n()._curve},t},Pb={draw:function(t,n){var e=Math.sqrt(n/mb);t.moveTo(e,0),t.arc(0,0,e,0,bb)}},Lb={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Rb=Math.sqrt(1/3),qb=2*Rb,Ub={draw:function(t,n){var e=Math.sqrt(n/qb),r=e*Rb;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Db=Math.sin(mb/10)/Math.sin(7*mb/10),Ob=Math.sin(bb/10)*Db,Fb=-Math.cos(bb/10)*Db,Ib={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Ob*e,i=Fb*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=bb*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},Yb={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Bb=Math.sqrt(3),jb={draw:function(t,n){var e=-Math.sqrt(n/(3*Bb));t.moveTo(0,2*e),t.lineTo(-Bb*e,-e),t.lineTo(Bb*e,-e),t.closePath()}},Hb=-.5,Xb=Math.sqrt(3)/2,Vb=1/Math.sqrt(12),$b=3*(Vb/2+1),Wb={draw:function(t,n){var e=Math.sqrt(n/$b),r=e/2,i=e*Vb,o=r,u=e*Vb+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Hb*r-Xb*i,Xb*r+Hb*i),t.lineTo(Hb*o-Xb*u,Xb*o+Hb*u),t.lineTo(Hb*a-Xb*c,Xb*a+Hb*c),t.lineTo(Hb*r+Xb*i,Hb*i-Xb*r),t.lineTo(Hb*o+Xb*u,Hb*u-Xb*o),t.lineTo(Hb*a+Xb*c,Hb*c-Xb*a),t.closePath()}},Zb=[Pb,Lb,Ub,Yb,Ib,jb,Wb],Gb=function(){function t(){var t;if(r||(r=t=Re()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=fb(Pb),e=fb(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:fb(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:fb(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t},Jb=function(){};mc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:yc(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Qb=function(t){return new mc(t)};xc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var Kb=function(t){return new xc(t)};bc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:yc(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}};var tw=function(t){return new bc(t)};wc.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var nw=function t(n){function e(t){return 1===n?new mc(t):new wc(t,n)}return e.beta=function(n){return t(+n)},e}(.85);Tc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Mc(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ew=function t(n){function e(t){return new Tc(t,n)}return e.tension=function(n){return t(+n)},e}(0);kc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var rw=function t(n){function e(t){return new kc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Sc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Mc(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var iw=function t(n){function e(t){return new Sc(t,n)}return e.tension=function(n){return t(+n)},e}(0);Ec.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0: -this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var ow=function t(n){function e(t){return n?new Ec(t,n):new Tc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Ac.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var uw=function t(n){function e(t){return n?new Ac(t,n):new kc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);Cc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Nc(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var aw=function t(n){function e(t){return n?new Cc(t,n):new Sc(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);zc.prototype={areaStart:Jb,areaEnd:Jb,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}};var cw=function(t){return new zc(t)};Uc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:qc(this,this._t0,Rc(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,qc(this,Rc(this,e=Lc(this,t,n)),e);break;default:qc(this,this._t0,e=Lc(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Dc.prototype=Object.create(Uc.prototype)).point=function(t,n){Uc.prototype.point.call(this,n,t)},Oc.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Yc.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=Bc(t),i=Bc(n),o=0,u=1;u=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var fw=function(t){return new jc(t,.5)},lw=Array.prototype.slice,hw=function(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i=0;)e[n]=n;return e},dw=function(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o0){for(var e,r,i,o=0,u=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u=a)return null;var c=t-i.site[0],s=n-i.site[1],f=c*c+s*s;do{i=o.cells[r=u],u=null,i.halfedges.forEach(function(e){var r=o.edges[e],a=r.left;if(a!==i.site&&a||(a=r.right)){var c=t-a[0],s=n-a[1],l=c*c+s*s;le?(e+r)/2:Math.min(0,e)||Math.max(0,r),o>i?(i+o)/2:Math.min(0,i)||Math.max(0,o))}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=m.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=N(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new Ns(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=A.length;r0?bl(this).transition().duration(S).call(u,f,a):bl(this).call(n.transform,f)}}function h(){if(y.apply(this,arguments)){var n,e,r,i,o=a(this,arguments),u=t.event.changedTouches,c=u.length;for(As(),e=0;e - - - var edges = []; - - links.forEach(function(e) { - var sourceNode = nodes.filter(function(n) { - return n.name === e.source; - })[0], - targetNode = nodes.filter(function(n) { - return n.name === e.target; - })[0]; - - edges.push({ - source: sourceNode, - target: targetNode, - value: 1}); - - }); - - edges.forEach(function(e) { - - if (!e.source["linkcount"]) e.source["linkcount"] = 0; - if (!e.target["linkcount"]) e.target["linkcount"] = 0; - - e.source["linkcount"]++; - e.target["linkcount"]++; - }); - - //var width = 960, height = 600; - var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; - var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; - var svg = d3.select("#vis").append("svg") - .attr("width", width) - .attr("height", height); - - // updated for d3 v4. - var simulation = d3.forceSimulation() - .force("link", d3.forceLink().id(function(d) { return d.id; })) - .force("charge", d3.forceManyBody().strength(strength)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide(35)); - - // Node charge strength. Repel strength greater for less links. - //function strength(d) { return -50/d["linkcount"] ; } - function strength(d) { return -25 ; } - - // Link distance. Distance increases with number of links at source and target - function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } - - // Link strength. Strength is less for highly connected nodes (move towards target dist) - function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } - - simulation - .nodes(nodes) - .on("tick", tick); - - simulation.force("link") - .links(edges) - .distance(distance) - .strength(strengthl); - - // build the arrow. - svg.append("svg:defs").selectAll("marker") - .data(["end"]) // Different link/path types can be defined here - .enter().append("svg:marker") // This section adds in the arrows - .attr("id", String) - .attr("viewBox", "0 -5 10 10") - .attr("refX", 25) // Moves the arrow head out, allow for radius - .attr("refY", 0) // -1.5 - .attr("markerWidth", 6) - .attr("markerHeight", 6) - .attr("orient", "auto") - .append("svg:path") - .attr("d", "M0,-5L10,0L0,5"); - - var link = svg.selectAll('.link') - .data(edges) - .enter().append('line') - .attr("class", "link") - .attr("marker-end", "url(#end)"); - - var node = svg.selectAll(".node") - .data(nodes) - .enter().append("g") - .attr("class", function(d) { return "node " + d.type;}) - .attr('transform', function(d) { - return "translate(" + d.x + "," + d.y + ")"}) - .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); - - node.call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended)); - - // add the nodes - node.append('circle') - .attr('r', 16) - ; - - // add text - node.append("text") - .attr("x", 12) - .attr("dy", "-1.1em") - .text(function(d) {return d.name;}); - - node.on("mouseover", function(d) { - - var g = d3.select(this); // the node (table) - - // tooltip - - var fields = d.fields; - var fieldformat = ""; - fields.forEach(function(d) { - fieldformat += ""; - }); - fieldformat += "
      "+ d.name+""+ d.type+""+ d.disp+"
      "; - var tiplength = d.fields.length; - - // Define 'div' for tooltips - var div = d3.select("body").append("div") // declare the tooltip div - .attr("class", "tooltip") // apply the 'tooltip' class - .style("opacity", 0) - .html('
      ' + d.name + '
      ' + fieldformat) - .style("left", 20 + (d3.event.pageX) + "px")// or just (d.x + 50 + "px") - .style("top", tooltop(tiplength))// or ... - .transition() - .duration(800) - .style("opacity", 0.9); - }); - - function tooltop(tiplength) { - //aim to ensure tooltip is fully visible whenver possible - return (Math.max(d3.event.pageY - 20 - (tiplength * 14),0)) + "px" - } - - node.on("mouseout", function(d) { - d3.select("body").select('div.tooltip').remove(); - }); - - // instead of waiting for force to end with : force.on('end', function() - // use .on("tick", instead. Here is the tick function - function tick() { - node.attr('transform', function(d) { - d.x = Math.max(30, Math.min(width - 16, d.x)); - d.y = Math.max(30, Math.min(height - 16, d.y)); - return "translate(" + d.x + "," + d.y + ")"; }); - - link.attr('x1', function(d) {return d.source.x;}) - .attr('y1', function(d) {return d.source.y;}) - .attr('x2', function(d) {return d.target.x;}) - .attr('y2', function(d) {return d.target.y;}); - }; - - function dragstarted(d) { - if (!d3.event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - }; - - function dragged(d) { - d.fx = d3.event.x; - d.fy = d3.event.y; - }; - - function dragended(d) { - if (!d3.event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - }; - -}; \ No newline at end of file From 59405b9f184b8f296eb679c904ca637eaff12cb2 Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Tue, 16 May 2017 06:54:51 +1200 Subject: [PATCH 231/436] d3 js check was pointing to app, now checking in admin app. --- applications/admin/views/default/design.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index 5b81f83a..3166dc10 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -105,7 +105,7 @@ def deletefile(arglist, vars={}): {{if os.access(os.path.join(request.folder,'..',app,'databases','sql.log'),os.R_OK):}} {{=button(URL('peek/%s/databases/sql.log'%app), 'sql.log')}} {{pass}} - {{if os.access(os.path.join(request.folder,'..',app,'static','js','d3_graph.js'),os.R_OK):}} + {{if os.access(os.path.join(request.folder,'..','admin','static','js','d3_graph.js'),os.R_OK):}} {{=button(URL(a=app, c='appadmin',f='d3_graph_model'), T('graph model'))}} {{pass}}
    From f30c31a8a27b3b3c4b286ab4c6db77094292843a Mon Sep 17 00:00:00 2001 From: Tim Richardson Date: Thu, 1 Jun 2017 16:53:04 +1000 Subject: [PATCH 232/436] fix to #1636: sqlgrid left= (left outer join) fails because db._adapter.tables() now returns dict (previously a table) --- gluon/sqlhtml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index b37e6b93..b8cc55e5 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY from pydal.base import DEFAULT from pydal.objects import Table, Row, Expression, Field, Set, Rows from pydal.adapters.base import CALLABLETYPES -from pydal.helpers.methods import smart_query, bar_encode, _repr_ref +from pydal.helpers.methods import smart_query, bar_encode, _repr_ref, merge_tablemaps from pydal.helpers.classes import Reference, SQLCustomType from gluon.storage import Storage from gluon.utils import md5_hash @@ -2329,7 +2329,7 @@ class SQLFORM(FORM): if not isinstance(left, (list, tuple)): left = [left] for join in left: - tablenames += db._adapter.tables(join) + tablenames = merge_tablemaps(tablenames, db._adapter.tables(join)) tables = [db[tablename] for tablename in tablenames] if fields: # add missing tablename to virtual fields From 376c12a225467a97c87da894ca427934662053d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 5 Jun 2017 23:35:41 +0100 Subject: [PATCH 233/436] Fixes #1628 --- gluon/authapi.py | 4 ++++ gluon/tools.py | 38 +++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 1937972f..82ae86ed 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -244,11 +244,13 @@ class AuthAPI(object): migrate = db._migrate if fake_migrate is None: fake_migrate = db._fake_migrate + settings = self.settings if username is None: username = settings.use_username else: settings.use_username = username + if not self.signature: self.define_signature() if signature is True: @@ -259,6 +261,8 @@ class AuthAPI(object): signature_list = [signature] else: signature_list = signature + self._table_signature_list = signature_list + is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) is_crypted = CRYPT(key=settings.hmac_key, min_length=settings.password_min_length) diff --git a/gluon/tools.py b/gluon/tools.py index 11376126..9c4d7ef1 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2214,7 +2214,7 @@ class Auth(AuthAPI): fake_migrate = db._fake_migrate settings = self.settings settings.enable_tokens = enable_tokens - super(Auth, self).define_tables(username, signature, migrate, fake_migrate) + signature_list = super(Auth, self).define_tables(username, signature, migrate, fake_migrate)._table_signature_list now = current.request.now reference_table_user = 'reference %s' % settings.table_user_name @@ -3759,11 +3759,10 @@ class Auth(AuthAPI): extra_fields = self.settings.extra_fields.get(self.settings.table_user_name, []) if any(f.compute for f in extra_fields): user = table_user[self.user.id] - self._update_session_user(user) + self._update_session_user(user) else: self.user.update(table_user._filter_fields(form.vars)) - session.flash = self.messages.profile_updated self.log_event(log, self.user) callback(onaccept, form) @@ -4220,15 +4219,15 @@ class Auth(AuthAPI): class Crud(object): # pragma: no cover default_messages = dict( - submit_button = 'Submit', - delete_label = 'Check to delete', - record_created = 'Record Created', - record_updated = 'Record Updated', - record_deleted = 'Record Deleted', - update_log = 'Record %(id)s updated', - create_log = 'Record %(id)s created', - read_log = 'Record %(id)s read', - delete_log = 'Record %(id)s deleted', + submit_button='Submit', + delete_label='Check to delete', + record_created='Record Created', + record_updated='Record Updated', + record_deleted='Record Deleted', + update_log='Record %(id)s updated', + create_log='Record %(id)s created', + read_log='Record %(id)s read', + delete_log='Record %(id)s deleted', ) def url(self, f=None, args=None, vars=None): @@ -4809,7 +4808,7 @@ class Service(object): def __init__(self, environment=None, check_args=False): self.check_args = check_args - + self.run_procedures = {} self.csv_procedures = {} self.xml_procedures = {} @@ -5067,7 +5066,7 @@ class Service(object): args = request.args if args and args[0] in self.run_procedures: return str(self.call_service_function(self.run_procedures[args[0]], - *args[1:], **dict(request.vars))) + *args[1:], **dict(request.vars))) self.error() def serve_csv(self, args=None): @@ -5088,7 +5087,7 @@ class Service(object): if args and args[0] in self.csv_procedures: import types r = self.call_service_function(self.csv_procedures[args[0]], - *args[1:], **dict(request.vars)) + *args[1:], **dict(request.vars)) s = StringIO() if hasattr(r, 'export_to_csv_file'): r.export_to_csv_file(s) @@ -5115,7 +5114,7 @@ class Service(object): args = request.args if args and args[0] in self.xml_procedures: s = self.call_service_function(self.xml_procedures[args[0]], - *args[1:], **dict(request.vars)) + *args[1:], **dict(request.vars)) if hasattr(s, 'as_list'): s = s.as_list() return serializers.xml(s, quote=False) @@ -5128,7 +5127,7 @@ class Service(object): args = request.args if args and args[0] in self.rss_procedures: feed = self.call_service_function(self.rss_procedures[args[0]], - *args[1:], **dict(request.vars)) + *args[1:], **dict(request.vars)) else: self.error() response.headers['Content-Type'] = 'application/rss+xml' @@ -5476,7 +5475,7 @@ class Service(object): def error(self): raise HTTP(404, "Object does not exist") - + # we make this a method so that subclasses can override it if they want to do more specific argument-checking # but the default implmentation is the simplest: just pass the arguments we got, with no checking def call_service_function(self, f, *a, **b): @@ -5484,7 +5483,8 @@ class Service(object): return universal_caller(f, *a, **b) else: return f(*a, **b) - + + def completion(callback): """ Executes a task on completion of the called action. From 617ca4a98d8f6bd42c0b73001d92eb586548dc6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 5 Jun 2017 23:52:25 +0100 Subject: [PATCH 234/436] Fixes #1639 --- gluon/tests/test_validators.py | 5 +++++ gluon/validators.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 77a7e6b5..e534fbd8 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -235,6 +235,11 @@ class TestValidators(unittest.TestCase): self.assertEqual(sorted(rtn), [('%d' % george_id, 'george'), ('%d' % costanza_id, 'costanza')]) rtn = IS_IN_DB(db, db.person.id, db.person.name, error_message='oops', sort=True).options(zero=True) self.assertEqual(rtn, [('', ''), ('%d' % costanza_id, 'costanza'), ('%d' % george_id, 'george')]) + # Test None + rtn = IS_IN_DB(db, 'person.id', '%(name)s', error_message='oops')(None) + self.assertEqual(rtn, (None, 'oops')) + rtn = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops')(None) + self.assertEqual(rtn, (None, 'oops')) # Test using the set it made for options vldtr = IS_IN_DB(db, 'person.name', '%(name)s', error_message='oops') vldtr.options() diff --git a/gluon/validators.py b/gluon/validators.py index 7ace3c5c..90ee8302 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,7 +21,7 @@ import urllib import struct import decimal import unicodedata -from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native +from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native, string_types from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce @@ -659,7 +659,7 @@ class IS_IN_DB(Validator): return (values, None) else: if field.type in ('id', 'integer'): - if isinstance(value, (int, long)) or value.isdigit(): + if isinstance(value, (int, long)) or (isinstance(value, string_types) and value.isdigit()): value = int(value) elif self.auto_add: value = self.maybe_add(table, self.fieldnames[0], value) From ac67beb280d0b89e5d6a5138888ea042a0c708d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Tue, 6 Jun 2017 01:53:42 +0100 Subject: [PATCH 235/436] Fixes #1634 by adding a parameter "ignore_extension" to Request.restful which allows you to leave the args untouched if you set it to True. --- gluon/globals.py | 4 +- gluon/tests/test_globals.py | 96 +++++++++++++++++++++++++++---------- 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 29fb3f0d..9d17fae6 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -349,14 +349,14 @@ class Request(Storage): current.session.forget() redirect(URL(scheme='https', args=self.args, vars=self.vars)) - def restful(self): + def restful(self, ignore_extension=False): def wrapper(action, request=self): def f(_action=action, *a, **b): request.is_restful = True env = request.env is_json = env.content_type=='application/json' method = env.request_method - if len(request.args) and '.' in request.args[-1]: + if not ignore_extension and len(request.args) and '.' in request.args[-1]: request.args[-1], _, request.extension = request.args[-1].rpartition('.') current.response.headers['Content-Type'] = \ contenttype('.' + request.extension.lower()) diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index 1ad85001..390079ce 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -10,27 +10,76 @@ import re import unittest from gluon.globals import Request, Response, Session +from gluon.rewrite import regex_url_in from gluon import URL from gluon._compat import basestring + def setup_clean_session(): - request = Request(env={}) - request.application = 'a' - request.controller = 'c' - request.function = 'f' - request.folder = 'applications/admin' - response = Response() - session = Session() - session.connect(request, response) + request = Request(env={}) + request.application = 'a' + request.controller = 'c' + request.function = 'f' + request.folder = 'applications/admin' + response = Response() + session = Session() + session.connect(request, response) + from gluon.globals import current + current.request = request + current.response = response + current.session = session + return current + + +class testRequest(unittest.TestCase): + + def setUp(self): from gluon.globals import current - current.request = request - current.response = response - current.session = session - return current + current.response = Response() + + def test_restful_simple(self): + env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/1.pdf'} + r = Request(env) + regex_url_in(r, env) + + @r.restful() + def simple_rest(): + def GET(*args, **vars): + return args[0] + return locals() + + self.assertEqual(simple_rest(), '1') + + def test_restful_calls_post(self): + env = {'request_method': 'POST', 'PATH_INFO': '/welcome/default/index'} + r = Request(env) + regex_url_in(r, env) + + @r.restful() + def post_rest(): + def POST(*args, **vars): + return 'I posted' + return locals() + + self.assertEqual(post_rest(), 'I posted') + + def test_restful_ignore_extension(self): + env = {'request_method': 'GET', 'PATH_INFO': '/welcome/default/index/127.0.0.1'} + r = Request(env) + regex_url_in(r, env) + + @r.restful(ignore_extension=True) + def ignore_rest(): + def GET(*args, **vars): + return args[0] + return locals() + + self.assertEqual(ignore_rest(), '127.0.0.1') + class testResponse(unittest.TestCase): - #port from python 2.7, needed for 2.5 and 2.6 tests + # port from python 2.7, needed for 2.5 and 2.6 tests def assertRegexpMatches(self, text, expected_regexp, msg=None): """Fail the test unless the text matches the regular expression.""" if isinstance(expected_regexp, basestring): @@ -99,8 +148,8 @@ class testResponse(unittest.TestCase): response.files.append(URL('a', 'static', 'css/file.css')) content = return_includes(response) self.assertEqual(content, - '' + - '') + '' + + '') response = Response() response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) @@ -109,12 +158,11 @@ class testResponse(unittest.TestCase): response.files.append(URL('a', 'static', 'css/file.ts')) content = return_includes(response) self.assertEqual(content, - '' + - '' + - '' + - '' - ) - + '' + + '' + + '' + + '' + ) response = Response() response.files.append(URL('a', 'static', 'css/file.js')) @@ -122,13 +170,13 @@ class testResponse(unittest.TestCase): content = return_includes(response, extensions=['css']) self.assertEqual(content, '') - #regr test for #628 + # regr test for #628 response = Response() response.files.append('http://maps.google.com/maps/api/js?sensor=false') content = return_includes(response) self.assertEqual(content, '') - #regr test for #628 + # regr test for #628 response = Response() response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) content = return_includes(response) @@ -147,7 +195,7 @@ class testResponse(unittest.TestCase): def test_cookies(self): current = setup_clean_session() cookie = str(current.response.cookies) - session_key='%s=%s'%(current.response.session_id_name,current.response.session_id) + session_key = '%s=%s' % (current.response.session_id_name, current.response.session_id) self.assertRegexpMatches(cookie, r'^Set-Cookie: ') self.assertTrue(session_key in cookie) self.assertTrue('Path=/' in cookie) From 2a33c0faff153a167298dc35dce2a85bbf42eec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 7 Jun 2017 04:59:03 +0100 Subject: [PATCH 236/436] Changed URL validation to use urlparse instead of regex for spliting the URL Enabled test_is_url in Python 3 since it is now passing This might be one of the last fixes to #1353 Fixes #1598 --- gluon/tests/__init__.py | 2 +- gluon/tests/test_is_url.py | 78 ++++++++--------- gluon/validators.py | 170 ++++++++++++++++++------------------- 3 files changed, 121 insertions(+), 129 deletions(-) diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 0b9346cb..83379804 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -24,7 +24,7 @@ from .test_web import * from .test_sqlhtml import * from .test_scheduler import * from .test_cron import * +from .test_is_url import * if sys.version[:3] == '2.7': - from .test_is_url import * from .test_old_doctests import * diff --git a/gluon/tests/test_is_url.py b/gluon/tests/test_is_url.py index d4a3162b..51a204d1 100644 --- a/gluon/tests/test_is_url.py +++ b/gluon/tests/test_is_url.py @@ -586,81 +586,81 @@ class TestUnicode(unittest.TestCase): # disables prepending the scheme in the return value def testUnicodeToAsciiUrl(self): - self.assertEquals(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu') - self.assertEquals( + self.assertEqual(unicode_to_ascii_authority(u'www.Alliancefran\xe7aise.nu'), 'www.xn--alliancefranaise-npb.nu') + self.assertEqual( unicode_to_ascii_authority(u'www.benn.ca'), 'www.benn.ca') self.assertRaises(UnicodeError, unicode_to_ascii_authority, u'\u4e2d' * 1000) # label is too long def testValidUrls(self): - self.assertEquals(self.x(u'www.Alliancefrancaise.nu'), ( + self.assertEqual(self.x(u'www.Alliancefrancaise.nu'), ( 'http://www.Alliancefrancaise.nu', None)) - self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu'), ( + self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu'), ( 'http://www.xn--alliancefranaise-npb.nu', None)) - self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu:8080'), ( + self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu:8080'), ( 'http://www.xn--alliancefranaise-npb.nu:8080', None)) - self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu'), + self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu'), ('http://www.xn--alliancefranaise-npb.nu', None)) - self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None)) - self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None)) - self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) - self.assertEquals(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None)) - self.assertEquals(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) - self.assertEquals(self.x( + self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue', None)) + self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue#fragment', None)) + self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) + self.assertEqual(self.x(u'http://www.Alliancefran\xe7aise.nu:8080/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu:8080/parnaise/blue?query=value#fragment', None)) + self.assertEqual(self.x(u'www.Alliancefran\xe7aise.nu/parnaise/blue?query=value#fragment'), ('http://www.xn--alliancefranaise-npb.nu/parnaise/blue?query=value#fragment', None)) + self.assertEqual(self.x( u'http://\u4e2d\u4fd4.com'), ('http://xn--fiq13b.com', None)) - self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'), + self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86'), ('http://xn--fiq13b.com/%4e%86', None)) - self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None)) - self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None)) - self.assertEquals(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None)) - self.assertEquals( + self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86', None)) + self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com/\u4e86?query=\u4e86#fragment'), ('http://xn--fiq13b.com/%4e%86?query=%4e%86#fragment', None)) + self.assertEqual(self.x(u'http://\u4e2d\u4fd4.com?query=\u4e86#fragment'), ('http://xn--fiq13b.com?query=%4e%86#fragment', None)) + self.assertEqual( self.x(u'http://B\xfccher.ch'), ('http://xn--bcher-kva.ch', None)) - self.assertEquals(self.x(u'http://\xe4\xf6\xfc\xdf.com'), ( + self.assertEqual(self.x(u'http://\xe4\xf6\xfc\xdf.com'), ( 'http://xn--ss-uia6e4a.com', None)) - self.assertEquals(self.x( + self.assertEqual(self.x( u'http://visegr\xe1d.com'), ('http://xn--visegrd-mwa.com', None)) - self.assertEquals(self.x(u'http://h\xe1zipatika.com'), ( + self.assertEqual(self.x(u'http://h\xe1zipatika.com'), ( 'http://xn--hzipatika-01a.com', None)) - self.assertEquals(self.x(u'http://www.\xe7ukurova.com'), ( + self.assertEqual(self.x(u'http://www.\xe7ukurova.com'), ( 'http://www.xn--ukurova-txa.com', None)) - self.assertEquals(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None)) - self.assertEquals(self.x(u'google.ca.'), ('http://google.ca.', None)) + self.assertEqual(self.x(u'http://nixier\xf6hre.nixieclock-tube.com'), ('http://xn--nixierhre-57a.nixieclock-tube.com', None)) + self.assertEqual(self.x(u'google.ca.'), ('http://google.ca.', None)) - self.assertEquals( + self.assertEqual( self.y(u'https://google.ca'), ('https://google.ca', None)) - self.assertEquals(self.y( + self.assertEqual(self.y( u'https://\u4e2d\u4fd4.com'), ('https://xn--fiq13b.com', None)) - self.assertEquals(self.z(u'google.ca'), ('google.ca', None)) + self.assertEqual(self.z(u'google.ca'), ('google.ca', None)) def testInvalidUrls(self): - self.assertEquals( + self.assertEqual( self.x(u'://ABC.com'), (u'://ABC.com', 'Enter a valid URL')) - self.assertEquals(self.x(u'http://\u4e2d\u4fd4.dne'), ( + self.assertEqual(self.x(u'http://\u4e2d\u4fd4.dne'), ( u'http://\u4e2d\u4fd4.dne', 'Enter a valid URL')) - self.assertEquals(self.x(u'https://google.dne'), ( + self.assertEqual(self.x(u'https://google.dne'), ( u'https://google.dne', 'Enter a valid URL')) - self.assertEquals(self.x(u'https://google..ca'), ( + self.assertEqual(self.x(u'https://google..ca'), ( u'https://google..ca', 'Enter a valid URL')) - self.assertEquals( + self.assertEqual( self.x(u'google..ca'), (u'google..ca', 'Enter a valid URL')) - self.assertEquals(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), ( + self.assertEqual(self.x(u'http://' + u'\u4e2d' * 1000 + u'.com'), ( u'http://' + u'\u4e2d' * 1000 + u'.com', 'Enter a valid URL')) - self.assertEquals(self.x(u'http://google.com#fragment_\u4e86'), ( + self.assertEqual(self.x(u'http://google.com#fragment_\u4e86'), ( u'http://google.com#fragment_\u4e86', 'Enter a valid URL')) - self.assertEquals(self.x(u'http\u4e86://google.com'), ( + self.assertEqual(self.x(u'http\u4e86://google.com'), ( u'http\u4e86://google.com', 'Enter a valid URL')) - self.assertEquals(self.x(u'http\u4e86://google.com#fragment_\u4e86'), ( + self.assertEqual(self.x(u'http\u4e86://google.com#fragment_\u4e86'), ( u'http\u4e86://google.com#fragment_\u4e86', 'Enter a valid URL')) - self.assertEquals(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), ( + self.assertEqual(self.y(u'http://\u4e2d\u4fd4.com/\u4e86'), ( u'http://\u4e2d\u4fd4.com/\u4e86', 'Enter a valid URL')) - #self.assertEquals(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL')) + #self.assertEqual(self.y(u'google.ca'), (u'google.ca', 'Enter a valid URL')) - self.assertEquals(self.z(u'invalid.domain..com'), ( + self.assertEqual(self.z(u'invalid.domain..com'), ( u'invalid.domain..com', 'Enter a valid URL')) - self.assertEquals(self.z(u'invalid.\u4e2d\u4fd4.blargg'), ( + self.assertEqual(self.z(u'invalid.\u4e2d\u4fd4.blargg'), ( u'invalid.\u4e2d\u4fd4.blargg', 'Enter a valid URL')) # ############################################################################## diff --git a/gluon/validators.py b/gluon/validators.py index 7ace3c5c..3d04f215 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -10,7 +10,6 @@ Validators ----------- """ - import os import re import datetime @@ -21,7 +20,7 @@ import urllib import struct import decimal import unicodedata -from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native +from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native, urlparse from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce @@ -195,7 +194,7 @@ class IS_MATCH(Validator): self.is_unicode = is_unicode or (not(PY2)) def __call__(self, value): - if not(PY2): # PY3 convert bytes to unicode + if not(PY2): # PY3 convert bytes to unicode value = to_unicode(value) if self.is_unicode or not(PY2): @@ -270,7 +269,7 @@ class IS_EXPR(Validator): return (value, self.expression(value)) # for backward compatibility self.environment.update(value=value) - exec ('__ret__=' + self.expression, self.environment) + exec('__ret__=' + self.expression, self.environment) if self.environment['__ret__']: return (value, None) return (value, translate(self.error_message)) @@ -1185,7 +1184,6 @@ class IS_EMAIL(Validator): regex_proposed_but_failed = re.compile('^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$', re.VERBOSE | re.IGNORECASE) - def __init__(self, banned=None, forced=None, @@ -1200,7 +1198,7 @@ class IS_EMAIL(Validator): def __call__(self, value): if not(isinstance(value, (basestring, unicodeT))) or not value or '@' not in value: - return (value, translate(self.error_message)) + return (value, translate(self.error_message)) body, domain = value.rsplit('@', 1) @@ -1388,19 +1386,6 @@ unofficial_url_schemes = [ all_url_schemes = [None] + official_url_schemes + unofficial_url_schemes http_schemes = [None, 'http', 'https'] - -# This regex comes from RFC 2396, Appendix B. It's used to split a URL into -# its component parts -# Here are the regex groups that it extracts: -# scheme = group(2) -# authority = group(4) -# path = group(5) -# query = group(7) -# fragment = group(9) - -url_split_regex = \ - re.compile('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?') - # Defined in RFC 3490, Section 3.1, Requirement #1 # Use this regex to split the authority component of a unicode URL into # its component labels @@ -1470,18 +1455,15 @@ def unicode_to_ascii_authority(authority): # We use the ToASCII operation because we are about to put the authority # into an IDN-unaware slot asciiLabels = [] - try: - import encodings.idna - for label in labels: - if label: - asciiLabels.append(to_native(encodings.idna.ToASCII(label))) - else: - # encodings.idna.ToASCII does not accept an empty string, but - # it is necessary for us to allow for empty labels so that we - # don't modify the URL - asciiLabels.append('') - except: - asciiLabels = [str(label) for label in labels] + import encodings.idna + for label in labels: + if label: + asciiLabels.append(to_native(encodings.idna.ToASCII(label))) + else: + # encodings.idna.ToASCII does not accept an empty string, but + # it is necessary for us to allow for empty labels so that we + # don't modify the URL + asciiLabels.append('') # RFC 3490, Section 4, Step 5 return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels)) @@ -1520,33 +1502,35 @@ def unicode_to_ascii_url(url, prepend_scheme): """ # convert the authority component of the URL into an ASCII punycode string, # but encode the rest using the regular URI character encoding - - groups = url_split_regex.match(url).groups() + components = urlparse.urlparse(url) + prepended = False # If no authority was found - if not groups[3]: + if not components.netloc: # Try appending a scheme to see if that fixes the problem scheme_to_prepend = prepend_scheme or 'http' - groups = url_split_regex.match( - to_unicode(scheme_to_prepend) + u'://' + url).groups() + components = urlparse.urlparse(to_unicode(scheme_to_prepend) + u'://' + url) + prepended = True + # if we still can't find the authority - if not groups[3]: + if not components.netloc: raise Exception('No authority component found, ' + 'could not decode unicode to US-ASCII') # We're here if we found an authority, let's rebuild the URL - scheme = groups[1] - authority = groups[3] - path = groups[4] or '' - query = groups[5] or '' - fragment = groups[7] or '' + scheme = components.scheme + authority = components.netloc + path = components.path + query = components.query + fragment = components.fragment - if prepend_scheme: - scheme = str(scheme) + '://' - else: + if prepended: scheme = '' - return scheme + unicode_to_ascii_authority(authority) +\ - escape_unicode(path) + escape_unicode(query) + str(fragment) + unparsed = urlparse.urlunparse((scheme, unicode_to_ascii_authority(authority), escape_unicode(path), '', escape_unicode(query), str(fragment))) + if unparsed.startswith('//'): + unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning + return unparsed + class IS_GENERIC_URL(Validator): @@ -1607,7 +1591,8 @@ class IS_GENERIC_URL(Validator): % (self.prepend_scheme, self.allowed_schemes)) GENERIC_URL = re.compile(r"%[^0-9A-Fa-f]{2}|%[^0-9A-Fa-f][0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]|%$|%[0-9A-Fa-f]$|%[^0-9A-Fa-f]$") - GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%#]+$") + GENERIC_URL_VALID = re.compile(r"[A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$") + URL_FRAGMENT_VALID = re.compile(r"[|A-Za-z0-9;/?:@&=+$,\-_\.!~*'\(\)%]+$") def __call__(self, value): """ @@ -1619,41 +1604,49 @@ class IS_GENERIC_URL(Validator): prepended with prepend_scheme), and tuple[1] is either None (success!) or the string error_message """ - try: - # if the URL does not misuse the '%' character - if not self.GENERIC_URL.search(value): - # if the URL is only composed of valid characters - if self.GENERIC_URL_VALID.match(value): - # Then split up the URL into its components and check on - # the scheme - scheme = url_split_regex.match(value).group(2) - # Clean up the scheme before we check it - if not scheme is None: - scheme = urllib_unquote(scheme).lower() - # If the scheme really exists - if scheme in self.allowed_schemes: - # Then the URL is valid - return (value, None) - else: - # else, for the possible case of abbreviated URLs with - # ports, check to see if adding a valid scheme fixes - # the problem (but only do this if it doesn't have - # one already!) - if value.find('://') < 0 and None in self.allowed_schemes: - schemeToUse = self.prepend_scheme or 'http' - prependTest = self.__call__( - schemeToUse + '://' + value) - # if the prepend test succeeded - if prependTest[1] is None: - # if prepending in the output is enabled - if self.prepend_scheme: - return prependTest - else: - # else return the original, - # non-prepended value - return (value, None) - except: - pass + + # if we dont have anything or the URL misuses the '%' character + + if not value or self.GENERIC_URL.search(value): + return (value, translate(self.error_message)) + + if '#' in value: + url, fragment_part = value.split('#') + else: + url, fragment_part = value, '' + # if the URL is only composed of valid characters + if self.GENERIC_URL_VALID.match(url) and (not fragment_part or self.URL_FRAGMENT_VALID.match(fragment_part)): + # Then parse the URL into its components and check on + try: + components = urlparse.urlparse(urllib_unquote(value))._asdict() + except ValueError: + return (value, translate(self.error_message)) + + # Clean up the scheme before we check it + scheme = components['scheme'] + if len(scheme) == 0: + scheme = None + else: + scheme = components['scheme'].lower() + # If the scheme doesn't really exists + if scheme not in self.allowed_schemes or not scheme and ':' in components['path']: + # for the possible case of abbreviated URLs with + # ports, check to see if adding a valid scheme fixes + # the problem (but only do this if it doesn't have + # one already!) + if '://' not in value and None in self.allowed_schemes: + schemeToUse = self.prepend_scheme or 'http' + prependTest = self.__call__( + schemeToUse + '://' + value) + # if the prepend test succeeded + if prependTest[1] is None: + # if prepending in the output is enabled + if self.prepend_scheme: + return prependTest + else: + return (value, None) + else: + return (value, None) # else the URL is not valid return (value, translate(self.error_message)) @@ -1920,15 +1913,14 @@ class IS_HTTP_URL(Validator): (possible prepended with prepend_scheme), and tuple[1] is either None (success!) or the string error_message """ - try: # if the URL passes generic validation x = IS_GENERIC_URL(error_message=self.error_message, allowed_schemes=self.allowed_schemes, prepend_scheme=self.prepend_scheme) if x(value)[1] is None: - componentsMatch = url_split_regex.match(value) - authority = componentsMatch.group(4) + components = urlparse.urlparse(value) + authority = components.netloc # if there is an authority component if authority: # if authority is a valid IP address @@ -1948,7 +1940,7 @@ class IS_HTTP_URL(Validator): else: # else this is a relative/abbreviated URL, which will parse # into the URL's path component - path = componentsMatch.group(5) + path = components.path # relative case: if this is a valid path (if it starts with # a slash) if path.startswith('/'): @@ -1957,7 +1949,7 @@ class IS_HTTP_URL(Validator): else: # abbreviated case: if we haven't already, prepend a # scheme and see if it fixes the problem - if value.find('://') < 0: + if '://' not in value and None in self.allowed_schemes: schemeToUse = self.prepend_scheme or 'http' prependTest = self.__call__(schemeToUse + '://' + value) @@ -2124,7 +2116,6 @@ class IS_URL(Validator): # If we are not able to convert the unicode url into a # US-ASCII URL, then the URL is not valid return (value, translate(self.error_message)) - methodResult = subMethod(asciiValue) # if the validation of the US-ASCII version of the value failed if not methodResult[1] is None: @@ -2494,6 +2485,7 @@ class IS_LOWER(Validator): ('\\xc3\\xb1', None) """ + def __call__(self, value): cast_back = lambda x: x if isinstance(value, str): From 2ea59396405b3f8d03ab2c457238114c4e11f40d Mon Sep 17 00:00:00 2001 From: ilvalle Date: Wed, 14 Jun 2017 21:42:48 +0200 Subject: [PATCH 237/436] fix Pickler.dispatch_table --- gluon/globals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 29fb3f0d..84c2517c 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -89,10 +89,11 @@ class SortingPickler(Pickler): self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) if PY2: -#FIXME PY3 SortingPickler.dispatch = copy.copy(Pickler.dispatch) SortingPickler.dispatch[dict] = SortingPickler.save_dict - +else: + SortingPickler.dispatch_table = copyreg.dispatch_table.copy() + SortingPickler.dispatch_table[dict] = SortingPickler.save_dict def sorting_dumps(obj, protocol=None): file = StringIO() @@ -415,7 +416,6 @@ class Response(Storage): if not escape: self.body.write(str(data)) else: - # FIXME PY3: self.body.write(to_native(xmlescape(data))) def render(self, *a, **b): From 0c3662fb6d1aff6ef3d61cfbd1d912a45cff9a7c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 15 Jun 2017 03:59:51 -0500 Subject: [PATCH 238/436] scheduler should decode credentials --- gluon/packages/dal | 2 +- gluon/scheduler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 32cde26b..881a5337 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 32cde26be1913c67738f21f1a674f5dadfe72a8a +Subproject commit 881a533765bada3b1d63690f6549157f4e0508b9 diff --git a/gluon/scheduler.py b/gluon/scheduler.py index e758c379..710815db 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -1158,7 +1158,7 @@ class Scheduler(MetaScheduler): if not self.db_thread: logger.debug('thread building own DAL object') self.db_thread = DAL( - self.db._uri, folder=self.db._adapter.folder) + self.db._uri, folder=self.db._adapter.folder, decode_credentials=True) self.define_tables(self.db_thread, migrate=False) try: db = self.db_thread @@ -1698,7 +1698,7 @@ def main(): print('groups for this worker: ' + ', '.join(group_names)) print('connecting to database in folder: ' + options.db_folder or './') print('using URI: ' + options.db_uri) - db = DAL(options.db_uri, folder=options.db_folder) + db = DAL(options.db_uri, folder=options.db_folder, decode_credentials=True) print('instantiating scheduler...') scheduler = Scheduler(db=db, worker_name=options.worker_name, From 954cef48da03104f8622a7d9c2ce6b9176f9ab55 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 11:53:47 -0500 Subject: [PATCH 239/436] fixes #1648, thanks you tbear --- applications/admin/models/access.py | 2 +- gluon/packages/dal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/admin/models/access.py b/applications/admin/models/access.py index 7e24ac58..ded471c4 100644 --- a/applications/admin/models/access.py +++ b/applications/admin/models/access.py @@ -52,7 +52,7 @@ def verify_password(password): if DEMO_MODE: ret = True elif not _config.get('password'): - ret - False + ret = False elif _config['password'].startswith('pam_user:'): session.pam_user = _config['password'][9:].strip() import gluon.contrib.pam diff --git a/gluon/packages/dal b/gluon/packages/dal index 881a5337..c1167686 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 881a533765bada3b1d63690f6549157f4e0508b9 +Subproject commit c11676867cb3e472e4c37e9a71b6b735f57482d4 From 77a947b35c3382d84a2ebad89b151975bba071a6 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 12:02:40 -0500 Subject: [PATCH 240/436] fixed #1638, thanks Anthony --- gluon/authapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 82ae86ed..5ef26258 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -530,7 +530,7 @@ class AuthAPI(object): return record.id else: id = membership.insert(group_id=group_id, user_id=user_id) - if role: + if role and user_id == self.user_id: self.user_groups[group_id] = role else: self.update_groups() From 2174fc3becc08af685f4a06a7a476483834fac04 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 12:26:14 -0500 Subject: [PATCH 241/436] partially fixes #1621, thanks Nico. The strip issue is a python3 error in my opinion --- gluon/restricted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/restricted.py b/gluon/restricted.py index 84c46b36..95410f5c 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -137,7 +137,7 @@ class RestrictedError(Exception): self.environment = environment if layer: try: - self.traceback = traceback.format_exc() + self.traceback = traceback.format_exc(limit=1) except: self.traceback = 'no traceback because template parsing error' try: From 18b755b8da2172c4e0698fcdb58147a763923211 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 14:24:35 -0500 Subject: [PATCH 242/436] fixed #1583, thanks matclab --- gluon/packages/dal | 2 +- gluon/tools.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index c1167686..38066734 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit c11676867cb3e472e4c37e9a71b6b735f57482d4 +Subproject commit 38066734f71f2cfe8688493ac8e08c6e654e16bc diff --git a/gluon/tools.py b/gluon/tools.py index 9c4d7ef1..b5e896ee 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -776,16 +776,16 @@ class Mail(object): if attachments: result = mail.send_mail( sender=sender, to=origTo, - subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), html=html, + subject=to_unicode(subject, encoding), body=to_unicode(text or '', encoding), html=html, attachments=attachments, **xcc) elif html and (not raw): result = mail.send_mail( sender=sender, to=origTo, - subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), html=html, **xcc) + subject=to_unicode(subject, encoding), body=to_unicode(text or '', encoding), html=html, **xcc) else: result = mail.send_mail( sender=sender, to=origTo, - subject=to_unicode(subject, encoding), body=to_unicode(text, encoding), **xcc) + subject=to_unicode(subject, encoding), body=to_unicode(text or '', encoding), **xcc) else: smtp_args = self.settings.server.split(':') kwargs = dict(timeout=self.settings.timeout) From 067411112950c0cfff767f321fbe97f4d8a64d20 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 14:29:47 -0500 Subject: [PATCH 243/436] fixes #1579, thanks Nico --- gluon/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gluon/tools.py b/gluon/tools.py index b5e896ee..ce6db5dc 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -5786,6 +5786,9 @@ class Expose(object): @staticmethod def isprivate(f): + # remove '/private' prefix to deal with symbolic links on OSX + if f.startswith('/private/'): + f = f[8:] return 'private' in f or f.startswith('.') or f.endswith('~') @staticmethod From 16df6840edcae642e5cead850bccff35dd835996 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 14:47:59 -0500 Subject: [PATCH 244/436] fixes #1547, thanks yaminle --- gluon/contrib/markmin/markmin2html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 9492a6ca..af27404e 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -544,7 +544,7 @@ regex_code = re.compile( '(' + META + '|' + DISABLED_META + r'|````)|(``(?P.+?)``(?::(?P[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P

    [^\]]*)\])?)?)', re.S) regex_strong = re.compile(r'\*\*(?P[^\s*]+( +[^\s*]+)*)\*\*') -regex_del = re.compile(r'~~(?P[^\s*]+( +[^\s*]+)*)~~') +regex_del = re.compile(r'~~(?P[^\s~]+( +[^\s~]+)*)~~') regex_em = re.compile(r"''(?P([^\s']| |'(?!'))+)''") regex_num = re.compile(r"^\s*[+-]?((\d+(\.\d*)?)|\.\d+)([eE][+-]?[0-9]+)?\s*$") regex_list = re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$') From 159dd0d0220cf0ee3ece9d8992b794e2a2a46011 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 14:49:54 -0500 Subject: [PATCH 245/436] fixed #1542, thanks RekGRpth --- gluon/sqlhtml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index b8cc55e5..b52d22df 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2553,6 +2553,7 @@ class SQLFORM(FORM): if isinstance(field, Field.Virtual) and not str(field) in expcolumns: expcolumns.append(str(field)) + expcolumns = ['"%s"' % '"."'.join(f.split('.')) for f in expcolumns] if export_type in exportManager and exportManager[export_type]: if keywords: try: From 81fa787ec2c1c9b71434f0fd3e57c6ae2f3607da Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 20 Jun 2017 14:59:57 -0500 Subject: [PATCH 246/436] fixes #1514, thanks RekGRpth --- gluon/contrib/simplejsonrpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/simplejsonrpc.py b/gluon/contrib/simplejsonrpc.py index f60dc4c7..438b75b7 100644 --- a/gluon/contrib/simplejsonrpc.py +++ b/gluon/contrib/simplejsonrpc.py @@ -105,14 +105,14 @@ class ServerProxy(object): def __getattr__(self, attr): "pseudo method that can be called" - return lambda *args: self.call(attr, *args) + return lambda *args, **vars: self.call(attr, *args, **vars) - def call(self, method, *args): + def call(self, method, *args, **vars): "JSON RPC communication (method invocation)" # build data sent to the service request_id = random.randint(0, sys.maxsize) - data = {'id': request_id, 'method': method, 'params': args, } + data = {'id': request_id, 'method': method, 'params': args or vars, } if self.version: data['jsonrpc'] = self.version #mandatory key/value for jsonrpc2 validation else err -32600 request = json.dumps(data) From a78662e4cca7e3a5d44401271327583b894dd470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Tue, 20 Jun 2017 16:02:50 -0400 Subject: [PATCH 247/436] Enhance authapi.py PEP8 compliancy --- gluon/authapi.py | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 5ef26258..4bc7727f 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -8,7 +8,8 @@ from gluon._compat import long from gluon import current from gluon.storage import Messages, Settings, Storage from gluon.utils import web2py_uuid -from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, IS_NOT_IN_DB +from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, \ + IS_NOT_IN_DB from pydal.objects import Table, Field, Row import datetime from gluon.settings import global_settings @@ -153,7 +154,7 @@ class AuthAPI(object): if type(migrate).__name__ == 'str': return (migrate + tablename + '.table') - elif migrate == False: + elif not migrate: return False else: return True @@ -261,7 +262,7 @@ class AuthAPI(object): signature_list = [signature] else: signature_list = signature - self._table_signature_list = signature_list + self._table_signature_list = signature_list # Should it defined in __init__ first?? is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) is_crypted = CRYPT(key=settings.hmac_key, @@ -276,7 +277,8 @@ class AuthAPI(object): passfield = settings.password_field extra_fields = settings.extra_fields.get( settings.table_user_name, []) + signature_list - if username or settings.cas_provider: # cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables + # cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables + if username or settings.cas_provider: is_unique_username = \ [IS_MATCH('[\w\.\-]+', strict=True, error_message=self.messages.invalid_username), @@ -313,7 +315,7 @@ class AuthAPI(object): *extra_fields, **dict( migrate=self._get_migrate(settings.table_user_name, - migrate), + migrate), fake_migrate=fake_migrate, format='%(username)s')) else: @@ -343,7 +345,7 @@ class AuthAPI(object): *extra_fields, **dict( migrate=self._get_migrate(settings.table_user_name, - migrate), + migrate), fake_migrate=fake_migrate, format='%(first_name)s %(last_name)s (%(id)s)')) reference_table_user = 'reference %s' % settings.table_user_name @@ -771,7 +773,8 @@ class AuthAPI(object): user = table_user(**{userfield: validated}) if user is None: - return {'errors': {userfield: self.messages.invalid_user}, 'message': self.messages.invalid_login, 'user': None} + return {'errors': {userfield: self.messages.invalid_user}, + 'message': self.messages.invalid_login, 'user': None} if (user.registration_key or '').startswith('pending'): return {'errors': None, 'message': self.messages.registration_pending, 'user': None} @@ -792,10 +795,12 @@ class AuthAPI(object): settings.expiration session.auth.remember_me = kwargs.get('remember_me', False) self.log_event(log, user) - return {'errors': None, 'message': self.messages.logged_in, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + return {'errors': None, 'message': self.messages.logged_in, + 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} else: self.log_event(self.messages['login_failed_log'], kwargs) - return {'errors': {passfield: self.messages.invalid_password}, 'message': self.messages.invalid_login, 'user': None} + return {'errors': {passfield: self.messages.invalid_password}, + 'message': self.messages.invalid_login, 'user': None} def logout(self, log=DEFAULT, onlogout=DEFAULT, **kwargs): """ @@ -889,7 +894,7 @@ class AuthAPI(object): if settings.registration_requires_verification: d = {k: user[k] for k in table_user.fields if table_user[k].readable} - d['key']= key + d['key'] = key if settings.login_after_registration and not settings.registration_requires_approval: self.login_user(user) return {'errors': None, 'message': None, 'user': d} @@ -904,7 +909,8 @@ class AuthAPI(object): self.log_event(log, user) - return {'errors': None, 'message': message, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + return {'errors': None, 'message': message, + 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} def profile(self, log=DEFAULT, **kwargs): """ @@ -920,20 +926,23 @@ class AuthAPI(object): if not kwargs: user = table_user[self.user.id] - return {'errors': None, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + return {'errors': None, 'message': None, + 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} result = self.db(table_user.id == self.user.id).validate_and_update(**kwargs) user = table_user[self.user.id] if result.errors: - return {'errors': result.errors, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + return {'errors': result.errors, 'message': None, + 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} if log is DEFAULT: log = self.messages['profile_log'] self.log_event(log, user) self._update_session_user(user) - return {'errors': None, 'message': self.messages.profile_updated, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + return {'errors': None, 'message': self.messages.profile_updated, + 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} def change_password(self, log=DEFAULT, **kwargs): """ @@ -997,10 +1006,10 @@ class AuthAPI(object): return {'errors': None, 'message': messages.password_changed} def verify_key(self, - key=None, - ignore_approval=False, - log=DEFAULT, - ): + key=None, + ignore_approval=False, + log=DEFAULT, + ): """ Verify a given registration_key actually exists in the user table. Resets the key to empty string '' or 'pending' if @@ -1012,7 +1021,7 @@ class AuthAPI(object): table_user = self.table_user() user = table_user(registration_key=key) if (user is None) or (key is None): - return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key } + return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key} if self.settings.registration_requires_approval: user.update_record(registration_key='pending') From 9f79dccb05dcaae0ab9af3e7a5736422aad632e0 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Tue, 20 Jun 2017 22:12:43 +0200 Subject: [PATCH 248/436] fix minify, added tests --- gluon/contrib/minify/cssmin.py | 5 ++++- gluon/contrib/minify/minify.py | 26 ++++++++++++++++++-------- gluon/globals.py | 7 +++---- gluon/tests/test_appadmin.py | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/gluon/contrib/minify/cssmin.py b/gluon/contrib/minify/cssmin.py index f9ed89f9..15d97a0b 100644 --- a/gluon/contrib/minify/cssmin.py +++ b/gluon/contrib/minify/cssmin.py @@ -10,8 +10,11 @@ Original author: Zachary Voase Modified for inclusion into web2py by: Ross Peoples """ +try: + from StringIO import StringIO +except ImportError: + from io import StringIO -from StringIO import StringIO # The pure-Python StringIO supports unicode. import re diff --git a/gluon/contrib/minify/minify.py b/gluon/contrib/minify/minify.py index 8749acd7..134cc664 100644 --- a/gluon/contrib/minify/minify.py +++ b/gluon/contrib/minify/minify.py @@ -8,30 +8,40 @@ Created by: Ross Peoples Modified by: Massimo Di Pierro """ -import cssmin -import jsmin +from . import cssmin +from . import jsmin import os import hashlib import re +import sys +PY2 = sys.version_info[0] == 2 +if PY2: + hashlib_md5 = hashlib.md5 +else: + hashlib_md5 = lambda s: hashlib.md5(bytes(s, 'utf8')) + +def open_py23(filename, mode): + if PY2: + f = open(filename, mode + 'b') + else: + f = open(filename, mode, encoding="utf8") + return f def read_binary_file(filename): - f = open(filename, 'rb') + f = open_py23(filename, 'r') data = f.read() f.close() return data - def write_binary_file(filename, data): - f = open(filename, 'wb') + f = open_py23(filename, 'w') f.write(data) f.close() - def fix_links(css, static_path): return re.sub(r'url\((["\'])\.\./', 'url(\\1' + static_path, css) - def minify(files, path_info, folder, optimize_css, optimize_js, ignore_concat=[], ignore_minify=['/jquery.js', '/anytime.js']): @@ -109,7 +119,7 @@ def minify(files, path_info, folder, optimize_css, optimize_js, js.append(contents) else: js.append(filename) - dest_key = hashlib.md5(repr(processed)).hexdigest() + dest_key = hashlib_md5(repr(processed)).hexdigest() if css and concat_css: css = '\n\n'.join(contents for contents in css) if not inline_css: diff --git a/gluon/globals.py b/gluon/globals.py index 9d17fae6..5b963f0a 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,7 @@ Contains the classes for the global used variables: - Session """ -from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long +from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long, hashlib_md5 from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -457,7 +457,6 @@ class Response(Storage): self.write(s, escape=False) def include_files(self, extensions=None): - """ Includes files (usually in the head). Can minify and cache local files @@ -484,8 +483,7 @@ class Response(Storage): if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): # cache for 5 minutes by default - key = hashlib.md5(repr(files)).hexdigest() - + key = hashlib_md5(repr(files)).hexdigest() cache = self.cache_includes or (current.cache.ram, 60 * 5) def call_minify(files=files): @@ -523,6 +521,7 @@ class Response(Storage): tmpl = template_mapping.get(f) if tmpl: s.append(tmpl % item[1]) + self.write(''.join(s), escape=False) def stream(self, diff --git a/gluon/tests/test_appadmin.py b/gluon/tests/test_appadmin.py index 41613152..22b1d05a 100644 --- a/gluon/tests/test_appadmin.py +++ b/gluon/tests/test_appadmin.py @@ -17,6 +17,7 @@ from gluon import fileutils from gluon.dal import DAL, Field, Table from gluon.http import HTTP from gluon.fileutils import open_file +from gluon.cache import CacheInRam DEFAULT_URI = os.getenv('DB', 'sqlite:memory') @@ -104,6 +105,21 @@ class TestAppAdmin(unittest.TestCase): self._test_index() remove_compiled_application(appname_path) + def test_index_minify(self): + # test for gluon/contrib/minify + self.env['response'].optimize_css = 'concat|minify' + self.env['response'].optimize_js = 'concat|minify' + self.env['current'].cache = Storage({'ram':CacheInRam()}) + appname_path = os.path.join(os.getcwd(), 'applications', 'welcome') + self._test_index() + file_l = os.listdir(os.path.join(appname_path, 'static', 'temp')) + file_l.sort() + self.assertTrue(len(file_l) == 2) + self.assertEqual(file_l[0][0:10], 'compressed') + self.assertEqual(file_l[1][0:10], 'compressed') + self.assertEqual(file_l[0][-3:], 'css') + self.assertEqual(file_l[1][-2:], 'js') + def test_select(self): request = self.env['request'] request.args = List(['db']) From 7ada2cf89aae44fb24fdb144963494741d1f832a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 11:27:54 -0400 Subject: [PATCH 249/436] Enhance tools.py PEP8 compliancy --- gluon/tools.py | 88 ++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index ce6db5dc..7c9c260f 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1128,7 +1128,6 @@ def addrow(form, a, b, c, style, _id, position=-1): class AuthJWT(object): - """ Experimental! @@ -1538,7 +1537,8 @@ class Auth(AuthAPI): # ## these are messages that can be customized default_messages = dict(AuthAPI.default_messages, access_denied='Insufficient privileges', - bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', + bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete ' + 'the process', bulk_invite_subject='Invitation to join %(site)s', delete_label='Check to delete', email_sent='Email sent', @@ -1841,14 +1841,15 @@ class Auth(AuthAPI): # ## these are messages that can be customized messages = self.messages = Messages(current.T) messages.update(Auth.default_messages) - messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'), - 'Please ', - A('login', - _href=self.settings.login_url + - ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) - if current.request.env.http_web2py_component_location else ''), - ' to view this content.', - _class='not-authorized alert alert-block')) + messages.update(ajax_failed_authentication= + DIV(H4('NOT AUTHORIZED'), + 'Please ', + A('login', + _href=self.settings.login_url + + ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) + if current.request.env.http_web2py_component_location else ''), + ' to view this content.', + _class='not-authorized alert alert-block')) messages.lock_keys = True # for "remember me" option @@ -1877,7 +1878,7 @@ class Auth(AuthAPI): # _next variable in the request. if next: parts = next.split('/') - if not ':' in parts[0]: + if ':' not in parts[0]: return next elif len(parts) > 2 and parts[0].endswith(':') and parts[1:3] == ['', host]: return next @@ -2009,8 +2010,7 @@ class Auth(AuthAPI): items.append({'name': T('Lost password?'), 'href': href('request_reset_password'), 'icon': 'icon-lock'}) - if (self.settings.use_username and not - 'retrieve_username' in self.settings.actions_disabled): + if self.settings.use_username and 'retrieve_username' not in self.settings.actions_disabled: items.append({'name': T('Forgot username?'), 'href': href('retrieve_username'), 'icon': 'icon-edit'}) @@ -2182,9 +2182,7 @@ class Auth(AuthAPI): current_record.replace('_', ' ').title()) for table in tables: fieldnames = table.fields() - if ('id' in fieldnames and - 'modified_on' in fieldnames and - not current_record in fieldnames): + if 'id' in fieldnames and 'modified_on' in fieldnames and current_record not in fieldnames: table._enable_record_versioning(archive_db=archive_db, archive_name=archive_names, current_record=current_record, @@ -2214,7 +2212,8 @@ class Auth(AuthAPI): fake_migrate = db._fake_migrate settings = self.settings settings.enable_tokens = enable_tokens - signature_list = super(Auth, self).define_tables(username, signature, migrate, fake_migrate)._table_signature_list + signature_list = \ + super(Auth, self).define_tables(username, signature, migrate, fake_migrate)._table_signature_list now = current.request.now reference_table_user = 'reference %s' % settings.table_user_name @@ -2361,7 +2360,7 @@ class Auth(AuthAPI): if callable(basic_auth_realm): basic_auth_realm = basic_auth_realm() elif isinstance(basic_auth_realm, (unicode, str)): - basic_realm = unicode(basic_auth_realm) + basic_realm = unicode(basic_auth_realm) # Warning python 3.5 does not have method unicod elif basic_auth_realm is True: basic_realm = u'' + current.request.application http_401 = HTTP(401, u'Not Authorized', **{'WWW-Authenticate': u'Basic realm="' + basic_realm + '"'}) @@ -2463,7 +2462,7 @@ class Auth(AuthAPI): _href=service + query_sep + "ticket=" + ticket) else: redirect(service + query_sep + "ticket=" + ticket) - if self.is_logged_in() and not 'renew' in request.vars: + if self.is_logged_in() and 'renew' not in request.vars: return allow_access() elif not self.is_logged_in() and 'gateway' in request.vars: redirect(session._cas_service) @@ -2780,7 +2779,7 @@ class Auth(AuthAPI): # If auth.settings.auth_two_factor_enabled it will enable two factor # for all the app. Another way to anble two factor is that the user # must be part of a group that is called auth.settings.two_factor_authentication_group - if user and self.settings.auth_two_factor_enabled == True: + if user and self.settings.auth_two_factor_enabled is True: session.auth_two_factor_enabled = True elif user and self.settings.two_factor_authentication_group: role = self.settings.two_factor_authentication_group @@ -2810,7 +2809,7 @@ class Auth(AuthAPI): # Set the way we generate the code or we send the code. For example using SMS... two_factor_methods = self.settings.two_factor_methods - if two_factor_methods == []: + if not two_factor_methods: # TODO: Add some error checking to handle cases where email cannot be sent self.settings.mailer.send( to=user.email, @@ -2833,47 +2832,49 @@ class Auth(AuthAPI): hideerror=settings.hideerror): accepted_form = True - ''' + """ The lists is executed after form validation for each of the corresponding action. For example, in your model: In your models copy and paste: - #Before define tables, we add some extra field to auth_user + # Before define tables, we add some extra field to auth_user auth.settings.extra_fields['auth_user'] = [ Field('motp_secret', 'password', length=512, default='', label='MOTP Secret'), Field('motp_pin', 'string', length=128, default='', label='MOTP PIN')] - OFFSET = 60 #Be sure is the same in your OTP Client + OFFSET = 60 # Be sure is the same in your OTP Client - #Set session.auth_two_factor to None. Because the code is generated by external app. + # Set session.auth_two_factor to None. Because the code is generated by external app. # This will avoid to use the default setting and send a code by email. def _set_two_factor(user, auth_two_factor): return None def verify_otp(user, otp): - import time - from hashlib import md5 - epoch_time = int(time.time()) - time_start = int(str(epoch_time - OFFSET)[:-1]) - time_end = int(str(epoch_time + OFFSET)[:-1]) - for t in range(time_start - 1, time_end + 1): - to_hash = str(t) + user.motp_secret + user.motp_pin - hash = md5(to_hash).hexdigest()[:6] - if otp == hash: - return hash + import time + from hashlib import md5 + epoch_time = int(time.time()) + time_start = int(str(epoch_time - OFFSET)[:-1]) + time_end = int(str(epoch_time + OFFSET)[:-1]) + for t in range(time_start - 1, time_end + 1): + to_hash = str(t) + user.motp_secret + user.motp_pin + hash = md5(to_hash).hexdigest()[:6] + if otp == hash: + return hash auth.settings.auth_two_factor_enabled = True auth.messages.two_factor_comment = "Verify your OTP Client for the code." - auth.settings.two_factor_methods = [lambda user, auth_two_factor: _set_two_factor(user, auth_two_factor)] + auth.settings.two_factor_methods = [lambda user, + auth_two_factor: _set_two_factor(user, auth_two_factor)] auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)] - ''' - if self.settings.two_factor_onvalidation != []: + """ + if self.settings.two_factor_onvalidation: for two_factor_onvalidation in self.settings.two_factor_onvalidation: try: - session.auth_two_factor = two_factor_onvalidation(session.auth_two_factor_user, form.vars['authentication_code']) + session.auth_two_factor = \ + two_factor_onvalidation(session.auth_two_factor_user, form.vars['authentication_code']) except: pass else: @@ -4147,7 +4148,7 @@ class Auth(AuthAPI): archive_table = table._db[archive_table_name] new_record = {current_record: form.vars.id} for fieldname in archive_table.fields: - if not fieldname in ['id', current_record]: + if fieldname not in ['id', current_record]: if archive_current and fieldname in form.vars: new_record[fieldname] = form.vars[fieldname] elif form.record and fieldname in form.record: @@ -4957,7 +4958,10 @@ class Service(object): Then call it with: - wget --post-data '{"jsonrpc": "2.0", "id": 1, "method": "myfunction", "params": {"a": 1, "b": 2}}' http://..../app/default/call/jsonrpc2 + wget --post-data '{"jsonrpc": "2.0", + "id": 1, + "method": "myfunction", + "params": {"a": 1, "b": 2}}' http://..../app/default/call/jsonrpc2 """ self.jsonrpc2_procedures[f.__name__] = f @@ -5781,7 +5785,7 @@ class Expose(object): return os.path.realpath(f) def issymlink_out(self, f): - "True if f is a symlink and is pointing outside of self.base" + """True if f is a symlink and is pointing outside of self.base""" return os.path.islink(f) and not self.in_base(f) @staticmethod From 583d106104df6321fd11614c712968ac786a4926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 11:33:00 -0400 Subject: [PATCH 250/436] Fix docstring py3 compatibility issues print -> print() --- gluon/tools.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 7c9c260f..039d972b 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -5605,12 +5605,12 @@ class PluginManager(object): where the plugin is used:: - >>> print plugins.me.param1 + >>> print(plugins.me.param1) 3 - >>> print plugins.me.param2 + >>> print(plugins.me.param2) 6 >>> plugins.me.param3 = 8 - >>> print plugins.me.param3 + >>> print(plugins.me.param3) 8 Here are some tests:: @@ -5618,25 +5618,25 @@ class PluginManager(object): >>> a=PluginManager() >>> a.x=6 >>> b=PluginManager('check') - >>> print b.x + >>> print(b.x) 6 >>> b=PluginManager() # reset settings - >>> print b.x + >>> print(b.x) >>> b.x=7 - >>> print a.x + >>> print(a.x) 7 >>> a.y.z=8 - >>> print b.y.z + >>> print(b.y.z) 8 >>> test_thread_separation() 5 >>> plugins=PluginManager('me',db='mydb') - >>> print plugins.me.db + >>> print(plugins.me.db) mydb - >>> print 'me' in plugins + >>> print('me' in plugins) True - >>> print plugins.me.installed + >>> print(plugins.me.installed) True """ From 590de9c8900ac1d5da58758730ad9c805c7e8429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 11:57:31 -0400 Subject: [PATCH 251/436] Enhance validators.py PEP8 and fix docstring py3 compatibility --- gluon/validators.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/gluon/validators.py b/gluon/validators.py index d7fa6260..cff35918 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,7 +21,8 @@ import struct import decimal import unicodedata -from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native, string_types, urlparse +from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, \ + to_unicode, to_native, string_types, urlparse from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce @@ -455,7 +456,7 @@ class IS_IN_SET(Validator): items = [(k, self.labels[i]) for (i, k) in enumerate(self.theset)] if self.sort: items.sort(key=lambda o: str(o[1]).upper()) - if zero and not self.zero is None and not self.multiple: + if zero and self.zero is not None and not self.multiple: items.insert(0, ('', self.zero)) return items @@ -823,7 +824,7 @@ class IS_INT_IN_RANGE(Validator): def str2dec(number): s = str(number) - if not '.' in s: + if '.' not in s: s += '.00' else: s += '0' * (2 - len(s.split('.')[1])) @@ -1213,7 +1214,7 @@ class IS_EMAIL(Validator): domain_encoded = to_unicode(domain).encode('idna').decode('ascii') match_domain = self.domain_regex.match(domain_encoded) - match = (match_body != None) and (match_domain != None) + match = (match_body is not None) and (match_domain is not None) except (TypeError, UnicodeError): # Value may not be a string where we can look for matches. # Example: we're calling ANY_OF formatter and IS_EMAIL is asked to validate a date. @@ -1247,7 +1248,7 @@ class IS_LIST_OF_EMAILS(object): f = IS_EMAIL() for email in self.split_emails.findall(value): error = f(email)[1] - if error and not email in bad_emails: + if error and email not in bad_emails: bad_emails.append(email) if not bad_emails: return (value, None) @@ -1461,9 +1462,9 @@ def unicode_to_ascii_authority(authority): if label: asciiLabels.append(to_native(encodings.idna.ToASCII(label))) else: - # encodings.idna.ToASCII does not accept an empty string, but - # it is necessary for us to allow for empty labels so that we - # don't modify the URL + # encodings.idna.ToASCII does not accept an empty string, but + # it is necessary for us to allow for empty labels so that we + # don't modify the URL asciiLabels.append('') # RFC 3490, Section 4, Step 5 return str(reduce(lambda x, y: x + unichr(0x002E) + y, asciiLabels)) @@ -1527,13 +1528,17 @@ def unicode_to_ascii_url(url, prepend_scheme): if prepended: scheme = '' - unparsed = urlparse.urlunparse((scheme, unicode_to_ascii_authority(authority), escape_unicode(path), '', escape_unicode(query), str(fragment))) + unparsed = urlparse.urlunparse((scheme, + unicode_to_ascii_authority(authority), + escape_unicode(path), + '', + escape_unicode(query), + str(fragment))) if unparsed.startswith('//'): - unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning + unparsed = unparsed[2:] # Remove the // urlunparse puts in the beginning return unparsed - class IS_GENERIC_URL(Validator): """ Rejects a URL string if any of the following is true: @@ -2622,7 +2627,7 @@ class ANY_OF(Validator): def __call__(self, value): for validator in self.subs: value, error = validator(value) - if error == None: + if error is None: break return value, error @@ -2762,7 +2767,7 @@ class LazyCrypt(object): else: digest_alg, key = self.crypt.digest_alg, '' if self.crypt.salt: - if self.crypt.salt == True: + if self.crypt.salt: salt = str(web2py_uuid()).replace('-', '')[-16:] else: salt = self.crypt.salt @@ -2847,7 +2852,7 @@ class CRYPT(object): Supports standard algorithms >>> for alg in ('md5','sha1','sha256','sha384','sha512'): - ... print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) + ... print(str(CRYPT(digest_alg=alg,salt=True)('test')[0])) md5$...$... sha1$...$... sha256$...$... @@ -2859,13 +2864,13 @@ class CRYPT(object): Supports for pbkdf2 >>> alg = 'pbkdf2(1000,20,sha512)' - >>> print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) + >>> print(str(CRYPT(digest_alg=alg,salt=True)('test')[0])) pbkdf2(1000,20,sha512)$...$... An optional hmac_key can be specified and it is used as salt prefix >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0]) - >>> print a + >>> print(a) md5$...$... Even if the algorithm changes the hash can still be validated From 833cb03ee1bee582f27caeaf5ff9aa6bd0f7de47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 13:22:30 -0400 Subject: [PATCH 252/436] Enhance admin.py PEP8 --- gluon/admin.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/gluon/admin.py b/gluon/admin.py index 3d8d13c8..ad5e18d7 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -54,7 +54,8 @@ def app_pack(app, request, raise_ex=False, filenames=None): """ try: - if filenames is None: app_cleanup(app, request) + if filenames is None: + app_cleanup(app, request) filename = apath('../deposit/web2py.app.%s.w2p' % app, request) w2p_pack(filename, apath(app, request), filenames=filenames) return filename @@ -104,7 +105,8 @@ def app_cleanup(app, request): if os.path.exists(path): for f in os.listdir(path): try: - if f[:1] != '.': os.unlink(os.path.join(path, f)) + if f[:1] != '.': + os.unlink(os.path.join(path, f)) except IOError: r = False @@ -113,7 +115,8 @@ def app_cleanup(app, request): if os.path.exists(path): for f in os.listdir(path): try: - if f[:1] != '.': recursive_unlink(os.path.join(path, f)) + if f[:1] != '.': + recursive_unlink(os.path.join(path, f)) except (OSError, IOError): r = False @@ -123,7 +126,8 @@ def app_cleanup(app, request): CacheOnDisk(folder=path).clear() for f in os.listdir(path): try: - if f[:1] != '.': recursive_unlink(os.path.join(path, f)) + if f[:1] != '.': + recursive_unlink(os.path.join(path, f)) except (OSError, IOError): r = False return r @@ -175,10 +179,9 @@ def app_create(app, request, force=False, key=None, info=False): return False try: w2p_unpack('welcome.w2p', path) - for subfolder in [ - 'models', 'views', 'controllers', 'databases', - 'modules', 'cron', 'errors', 'sessions', 'cache', - 'languages', 'static', 'private', 'uploads']: + for subfolder in ['models', 'views', 'controllers', 'databases', + 'modules', 'cron', 'errors', 'sessions', 'cache', + 'languages', 'static', 'private', 'uploads']: subpath = os.path.join(path, subfolder) if not os.path.exists(subpath): os.mkdir(subpath) @@ -368,7 +371,7 @@ def unzip(filename, dir, subfolder=''): for name in sorted(zf.namelist()): if not name.startswith(subfolder): continue - #print name[n:] + # print name[n:] if name.endswith('/'): folder = os.path.join(dir, name[n:]) if not os.path.exists(folder): @@ -435,6 +438,7 @@ def add_path_first(path): if not global_settings.web2py_runtime_gae: site.addsitedir(path) + def try_mkdir(path): if not os.path.exists(path): try: @@ -444,11 +448,12 @@ def try_mkdir(path): else: os.mkdir(path) except OSError as e: - if e.strerror == 'File exists': # In case of race condition. + if e.strerror == 'File exists': # In case of race condition. pass else: raise e + def create_missing_folders(): if not global_settings.web2py_runtime_gae: for path in ('applications', 'deposit', 'site-packages', 'logs'): From 7f5fc798c5248e2211f91986588f8cec74771142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 13:30:02 -0400 Subject: [PATCH 253/436] Enhance compileapp.py PEP8 --- gluon/compileapp.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 3dc08bcb..a40328ca 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -41,11 +41,12 @@ import imp import logging import types from functools import reduce -logger = logging.getLogger("web2py") from gluon import rewrite from gluon.custom_import import custom_import_install import py_compile +logger = logging.getLogger("web2py") + is_pypy = settings.global_settings.is_pypy is_gae = settings.global_settings.web2py_runtime_gae is_jython = settings.global_settings.is_jython @@ -111,7 +112,7 @@ class mybuiltin(object): NOTE could simple use a dict and populate it, NOTE not sure if this changes things though if monkey patching import..... """ - #__builtins__ + # __builtins__ def __getitem__(self, key): try: return getattr(builtin, key) @@ -185,7 +186,7 @@ def LOAD(c=None, f='index', args=None, vars=None, else: statement = "$.web2py.component('%s','%s');" % (url, target) attr['_data-w2p_remote'] = url - if not target is None: + if target is not None: return DIV(content, **attr) else: @@ -211,7 +212,8 @@ def LOAD(c=None, f='index', args=None, vars=None, request.env.path_info other_request.cid = target other_request.env.http_web2py_component_element = target - other_request.restful = types.MethodType(request.restful.__func__, other_request) # A bit nasty but needed to use LOAD on action decorates with @request.restful() + other_request.restful = types.MethodType(request.restful.__func__, other_request) + # A bit nasty but needed to use LOAD on action decorates with @request.restful() other_response.view = '%s/%s.%s' % (c, f, other_request.extension) other_environment = copy.copy(current.globalenv) # NASTY @@ -405,7 +407,7 @@ def build_environment(request, response, session, store_current=True): """ Build the environment dictionary into which web2py files are executed. """ - #h,v = html,validators + # h,v = html,validators environment = dict(_base_environment_) if not request.env: @@ -418,7 +420,7 @@ def build_environment(request, response, session, store_current=True): r'^%s/%s/\w+\.py$' % (request.controller, request.function) ] - t = environment['T'] = translator(os.path.join(request.folder,'languages'), + t = environment['T'] = translator(os.path.join(request.folder, 'languages'), request.env.http_accept_language) c = environment['cache'] = Cache(request) @@ -506,10 +508,12 @@ def compile_models(folder): save_pyc(filename) os.unlink(filename) + def find_exposed_functions(data): - data = regex_longcomments.sub('',data) + data = regex_longcomments.sub('', data) return regex_expose.findall(data) + def compile_controllers(folder): """ Compiles all the controllers in the application specified by `folder` @@ -524,16 +528,19 @@ def compile_controllers(folder): command = data + "\nresponse._vars=response._caller(%s)\n" % \ function filename = pjoin(folder, 'compiled', - 'controllers.%s.%s.py' % (fname[:-3],function)) + 'controllers.%s.%s.py' % (fname[:-3], function)) write_file(filename, command) save_pyc(filename) os.unlink(filename) + def model_cmp(a, b, sep='.'): return cmp(a.count(sep), b.count(sep)) or cmp(a, b) + def model_cmp_sep(a, b, sep=os.path.sep): - return model_cmp(a,b,sep) + return model_cmp(a, b, sep) + def run_models_in(environment): """ @@ -544,7 +551,7 @@ def run_models_in(environment): request = current.request folder = request.folder c = request.controller - #f = environment['request'].function + # f = environment['request'].function response = current.response path = pjoin(folder, 'models') @@ -557,9 +564,11 @@ def run_models_in(environment): models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), model_cmp_sep) else: if compiled: - models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0), key=lambda f: '{0:03d}'.format(f.count('.')) + f) + models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0), + key=lambda f: '{0:03d}'.format(f.count('.')) + f) else: - models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), key=lambda f: '{0:03d}'.format(f.count(os.path.sep)) + f) + models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), + key=lambda f: '{0:03d}'.format(f.count(os.path.sep)) + f) models_to_run = None for model in models: @@ -570,10 +579,10 @@ def run_models_in(environment): if models_to_run: if compiled: n = len(cpath)+8 - fname = model[n:-4].replace('.','/')+'.py' + fname = model[n:-4].replace('.', '/')+'.py' else: n = len(path)+1 - fname = model[n:].replace(os.path.sep,'/') + fname = model[n:].replace(os.path.sep, '/') if not regex.search(fname) and c != 'appadmin': continue elif compiled: @@ -583,6 +592,7 @@ def run_models_in(environment): ccode = getcfs(model, model, f) restricted(ccode, environment, layer=model) + def run_controller_in(controller, function, environment): """ Runs the controller.function() (for the app specified by @@ -631,7 +641,7 @@ def run_controller_in(controller, function, environment): web2py_error=badc) code = getcfs(filename, filename, lambda: read_file(filename)) exposed = find_exposed_functions(code) - if not function in exposed: + if function not in exposed: raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) @@ -678,7 +688,7 @@ def run_view_in(environment): layer = 'file stream' else: filename = pjoin(folder, 'views', view) - if os.path.exists(cpath): # compiled views + if os.path.exists(cpath): # compiled views x = view.replace('/', '.') files = ['views.%s.pyc' % x] is_compiled = os.path.exists(pjoin(cpath, files[0])) @@ -715,6 +725,7 @@ def run_view_in(environment): # parse_template saves everything in response body return environment['response'].body.getvalue() + def remove_compiled_application(folder): """ Deletes the folder `compiled` containing the compiled application. From 2080e0460f2ee15a403049e85b723b44ca727a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 13:32:42 -0400 Subject: [PATCH 254/436] Enhance contenttype.py PEP8 --- gluon/contenttype.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 7dd28f35..e5f6dd3c 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -330,7 +330,7 @@ CONTENT_TYPE = { '.lha': 'application/x-lha', '.lhs': 'text/x-literate-haskell', '.lhz': 'application/x-lhz', - '.load' : 'text/html', + '.load': 'text/html', '.log': 'text/x-log', '.lrz': 'application/x-lrzip', '.ltx': 'text/x-tex', @@ -823,7 +823,7 @@ CONTENT_TYPE = { '.xsd': 'application/xml', '.xsl': 'application/xslt+xml', '.xslfo': 'text/x-xslfo', - '.xslm' : 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.xslm': 'application/vnd.ms-excel.sheet.macroEnabled.12', '.xslt': 'application/xslt+xml', '.xspf': 'application/xspf+xml', '.xul': 'application/vnd.mozilla.xul+xml', @@ -843,7 +843,7 @@ def contenttype(filename, default='text/plain'): """ Returns the Content-Type string matching extension of the given filename. """ - filename=to_native(filename) + filename = to_native(filename) i = filename.rfind('.') if i >= 0: default = CONTENT_TYPE.get(filename[i:].lower(), default) From 8d5464692f475f4871a7022e90bd899b3984946f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 13:41:10 -0400 Subject: [PATCH 255/436] Enhance dal.py PEP8 --- gluon/dal.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index 95476a9f..ceecf2e0 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -14,6 +14,11 @@ from pydal import DAL as DAL from pydal import Field from pydal.objects import Row, Rows, Table, Query, Set, Expression from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon +from gluon.serializers import custom_json, xml +from gluon.utils import web2py_uuid +from gluon import sqlhtml +from pydal.drivers import DRIVERS + def _default_validators(db, field): """ @@ -78,14 +83,10 @@ def _default_validators(db, field): if (field.notnull or field.unique) and field_type not in excluded_fields: requires.insert(0, validators.IS_NOT_EMPTY()) elif not field.notnull and not field.unique and requires: - requires[0] = validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) + requires[0] = \ + validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) return requires -from gluon.serializers import custom_json, xml -from gluon.utils import web2py_uuid -from gluon import sqlhtml - - DAL.serializers = {'json': custom_json, 'xml': xml} DAL.validators_method = _default_validators DAL.uuid = lambda x: web2py_uuid() @@ -96,8 +97,7 @@ DAL.representers = { DAL.Field = Field DAL.Table = Table -#: add web2py contrib drivers to pyDAL -from pydal.drivers import DRIVERS +# add web2py contrib drivers to pyDAL if not DRIVERS.get('pymysql'): try: from .contrib import pymysql From a8fb41333bbf496d2258f12725703208a96f195a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 14:38:52 -0400 Subject: [PATCH 256/436] Enhance decoder.py PEP8 --- gluon/decoder.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/gluon/decoder.py b/gluon/decoder.py index 4fc068dd..57044bc6 100644 --- a/gluon/decoder.py +++ b/gluon/decoder.py @@ -15,13 +15,13 @@ import codecs # None represents a potentially variable byte. "##" in the XML spec... autodetect_dict = { # bytepattern : ("name", (0x00, 0x00, 0xFE, 0xFF): ("ucs4_be"), - (0xFF, 0xFE, 0x00, 0x00): ("ucs4_le"), - (0xFE, 0xFF, None, None): ("utf_16_be"), - (0xFF, 0xFE, None, None): ("utf_16_le"), - (0x00, 0x3C, 0x00, 0x3F): ("utf_16_be"), - (0x3C, 0x00, 0x3F, 0x00): ("utf_16_le"), - (0x3C, 0x3F, 0x78, 0x6D): ("utf_8"), - (0x4C, 0x6F, 0xA7, 0x94): ("EBCDIC") + (0xFF, 0xFE, 0x00, 0x00): ("ucs4_le"), + (0xFE, 0xFF, None, None): ("utf_16_be"), + (0xFF, 0xFE, None, None): ("utf_16_le"), + (0x00, 0x3C, 0x00, 0x3F): ("utf_16_be"), + (0x3C, 0x00, 0x3F, 0x00): ("utf_16_le"), + (0x3C, 0x3F, 0x78, 0x6D): ("utf_8"), + (0x4C, 0x6F, 0xA7, 0x94): ("EBCDIC") } @@ -36,10 +36,10 @@ def autoDetectXMLEncoding(buffer): # buffer at once but otherwise we'd have to decode a character at # a time looking for the quote character...that's a pain - encoding = "utf_8" # according to the XML spec, this is the default - # this code successively tries to refine the default - # whenever it fails to refine, it falls back to - # the last place encoding was set. + encoding = "utf_8" + # according to the XML spec, this is the default this code successively tries to refine the default + # whenever it fails to refine, it falls back to the last place encoding was set. + if len(buffer) >= 4: bytes = (byte1, byte2, byte3, byte4) = tuple(map(ord, buffer[0:4])) enc_info = autodetect_dict.get(bytes, None) @@ -51,8 +51,7 @@ def autoDetectXMLEncoding(buffer): enc_info = None if enc_info: - encoding = enc_info # we've got a guess... these are - #the new defaults + encoding = enc_info # we've got a guess... these are the new defaults # try to find a more precise encoding using xml declaration secret_decoder_ring = codecs.lookup(encoding)[1] From 88790dcaee417719841a8443b745d57b2d269c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 14:49:10 -0400 Subject: [PATCH 257/436] Enhance globals.py PEP8 --- gluon/globals.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 4f835298..d515b16d 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -13,7 +13,8 @@ Contains the classes for the global used variables: - Session """ -from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, unicodeT, long +from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \ + unicodeT, long from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -30,7 +31,7 @@ from gluon.fileutils import copystream import hashlib from pydal.contrib import portalocker from pickle import Pickler, MARK, DICT, EMPTY_DICT -#from types import DictionaryType +# from types import DictionaryType import datetime import re import os @@ -48,7 +49,7 @@ PAST = 'Sat, 1-Jan-1971 00:00:00' FUTURE = 'Tue, 1-Dec-2999 23:59:59' try: - #FIXME PY3 + # FIXME PY3 from gluon.contrib.minify import minify have_minify = True except ImportError: @@ -79,6 +80,7 @@ template_mapping = { 'js:inline': js_inline } + # IMPORTANT: # this is required so that pickled dict(s) and class.__dict__ # are sorted and web2py can detect without ambiguity when a session changes @@ -95,6 +97,7 @@ else: SortingPickler.dispatch_table = copyreg.dispatch_table.copy() SortingPickler.dispatch_table[dict] = SortingPickler.save_dict + def sorting_dumps(obj, protocol=None): file = StringIO() SortingPickler(file, protocol).dump(obj) @@ -120,7 +123,7 @@ def copystream_progress(request, chunk_size=10 ** 5): dest = tempfile.NamedTemporaryFile() except NotImplementedError: # and GAE this dest = tempfile.TemporaryFile() - if not 'X-Progress-ID' in request.get_vars: + if 'X-Progress-ID' not in request.get_vars: copystream(source, dest, size, chunk_size) return dest cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID'] @@ -198,7 +201,8 @@ class Request(Storage): """Takes the QUERY_STRING and unpacks it to get_vars """ query_string = self.env.get('query_string', '') - dget = urlparse.parse_qs(query_string, keep_blank_values=1) # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs + dget = urlparse.parse_qs(query_string, keep_blank_values=1) + # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs get_vars = self._get_vars = Storage(dget) for (key, value) in iteritems(get_vars): if isinstance(value, list) and len(value) == 1: @@ -228,8 +232,7 @@ class Request(Storage): body.seek(0) # parse POST variables on POST, PUT, BOTH only in post_vars - if (body and not is_json - and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH')): + if body and not is_json and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH'): query_string = env.pop('QUERY_STRING', None) dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1) try: @@ -355,7 +358,7 @@ class Request(Storage): def f(_action=action, *a, **b): request.is_restful = True env = request.env - is_json = env.content_type=='application/json' + is_json = env.content_type == 'application/json' method = env.request_method if not ignore_extension and len(request.args) and '.' in request.args[-1]: request.args[-1], _, request.extension = request.args[-1].rpartition('.') @@ -451,7 +454,8 @@ class Response(Storage): for meta in iteritems((self.meta or {})): k, v = meta if isinstance(v, dict): - s += '\n' + s += '\n' else: s += '\n' % (k, to_native(xmlescape(v))) self.write(s, escape=False) @@ -578,9 +582,9 @@ class Response(Storage): if hasattr(stream, 'name'): filename = stream.name - if filename and not 'content-type' in keys: + if filename and 'content-type' not in keys: headers['Content-Type'] = contenttype(filename) - if filename and not 'content-length' in keys: + if filename and 'content-length' not in keys: try: headers['Content-Length'] = \ os.path.getsize(filename) @@ -1022,7 +1026,7 @@ class Session(Storage): if self._forget: del rcookies[response.session_id_name] return - if self.get('httponly_cookies',True): + if self.get('httponly_cookies', True): scookies['HttpOnly'] = True if self._secure: scookies['secure'] = True @@ -1193,7 +1197,7 @@ class Session(Storage): if (not response.session_id or not response.session_filename or self._forget - or self._unchanged(response)): + or self._unchanged(response)): # self.clear_session_cookies() return False else: From 58bedd4c1a324ded681b076ead664a39737fffcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 14:54:59 -0400 Subject: [PATCH 258/436] Enhance highlight.py PEP8 --- gluon/highlight.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gluon/highlight.py b/gluon/highlight.py index d6faff87..951dab81 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -182,8 +182,8 @@ class Highlighter(object): )), 'PYTHONMultilineString': (python_tokenizer, (('ENDMULTILINESTRING', - re.compile(r'.*?("""|\'\'\')', - re.DOTALL), 'color: darkred'), )), + re.compile(r'.*?("""|\'\'\')', + re.DOTALL), 'color: darkred'), )), 'HTML': (html_tokenizer, ( ('GOTOPYTHON', re.compile(r'\{\{'), 'color: red'), ('COMMENT', re.compile(r'|'), @@ -209,7 +209,7 @@ class Highlighter(object): mode = self.mode while i < len(data): for (token, o_re, style) in Highlighter.all_styles[mode][1]: - if not token in self.suppress_tokens: + if token not in self.suppress_tokens: match = o_re.match(data, i) if match: if style: @@ -221,7 +221,7 @@ class Highlighter(object): new_mode = \ Highlighter.all_styles[mode][0](self, token, match, style) - if not new_mode is None: + if new_mode is not None: mode = new_mode i += max(1, len(match.group())) break @@ -241,9 +241,9 @@ class Highlighter(object): style = self.styles[token] if self.span_style != style: if style != 'Keep': - if not self.span_style is None: + if self.span_style is not None: self.output.append('') - if not style is None: + if style is not None: self.output.append('' % style) self.span_style = style @@ -260,7 +260,7 @@ def highlight( ): styles = styles or {} attributes = attributes or {} - if not 'CODE' in styles: + if 'CODE' not in styles: code_style = """ font-size: 11px; font-family: Bitstream Vera Sans Mono,monospace; @@ -272,7 +272,7 @@ def highlight( white-space: pre !important;\n""" else: code_style = styles['CODE'] - if not 'LINENUMBERS' in styles: + if 'LINENUMBERS' not in styles: linenumbers_style = """ font-size: 11px; font-family: Bitstream Vera Sans Mono,monospace; @@ -283,7 +283,7 @@ def highlight( color: #A0A0A0;\n""" else: linenumbers_style = styles['LINENUMBERS'] - if not 'LINEHIGHLIGHT' in styles: + if 'LINEHIGHLIGHT' not in styles: linehighlight_style = "background-color: #EBDDE2;" else: linehighlight_style = styles['LINEHIGHLIGHT'] @@ -333,8 +333,9 @@ def highlight( == '_' and value]) if fa: fa = ' ' + fa - return '

    %s
    %s
    '\ - % (fa, linenumbers_style, numbers, code_style, code) + return '' \ + '
    %s
    ' \ + '
    %s
    ' % (fa, linenumbers_style, numbers, code_style, code) if __name__ == '__main__': @@ -342,5 +343,4 @@ if __name__ == '__main__': argfp = open(sys.argv[1]) data = argfp.read() argfp.close() - print('' + highlight(data, sys.argv[2])\ - + '') + print('' + highlight(data, sys.argv[2]) + '') From 472c0ff2fb87469d5b634df125c3646d9dee0844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 15:08:52 -0400 Subject: [PATCH 259/436] Enhance html.py PEP8 --- gluon/html.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/gluon/html.py b/gluon/html.py index 6446bd8c..f5383109 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -20,7 +20,8 @@ import urllib import base64 from gluon import sanitizer, decoder import itertools -from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type, long +from gluon._compat import reduce, pickle, copyreg, HTMLParser, name2codepoint, iteritems, unichr, unicodeT, \ + urllib_quote, to_bytes, to_native, to_unicode, basestring, urlencode, implements_bool, text_type, long from gluon.utils import local_html_escape import marshal @@ -109,6 +110,7 @@ __all__ = [ DEFAULT_PASSWORD_DISPLAY = '*' * 8 + def xmlescape(data, quote=True): """ Returns an escaped string of the provided data @@ -124,10 +126,9 @@ def xmlescape(data, quote=True): if not(isinstance(data, (text_type, bytes))): # i.e., integers - data=str(data) + data = str(data) data = to_bytes(data, 'utf8', 'xmlcharrefreplace') - # ... and do the escaping data = local_html_escape(data, quote) return data @@ -671,6 +672,7 @@ def XML_pickle(data): return XML_unpickle, (marshal.dumps(str(data)),) copyreg.pickle(XML, XML_pickle, XML_unpickle) + @implements_bool class DIV(XmlComponent): """ @@ -1309,8 +1311,10 @@ class HTML(DIV): tag = b'html' strict = b'\n' - transitional = b'\n' - frameset = b'\n' + transitional = \ + b'\n' + frameset = \ + b'\n' html5 = b'\n' def xml(self): @@ -1861,7 +1865,7 @@ class INPUT(DIV): except: import traceback print(traceback.format_exc()) - msg = "Validation error, field:%s %s" % (name,validator) + msg = "Validation error, field:%s %s" % (name, validator) raise Exception(msg) if errors is not None: self.vars[name] = value @@ -1911,7 +1915,7 @@ class INPUT(DIV): name = self.attributes.get('_name', None) if name and hasattr(self, 'errors') \ and self.errors.get(name, None) \ - and self['hideerror'] != True: + and self['hideerror'] is not True: self['_class'] = (self['_class'] and self['_class'] + ' ' or '') + 'invalidinput' return DIV.xml(self) + DIV( DIV( @@ -1979,7 +1983,6 @@ class OPTGROUP(DIV): class SELECT(INPUT): - """ Examples: @@ -2014,7 +2017,7 @@ class SELECT(INPUT): if value is not None: if not self['_multiple']: for c in options: # my patch - if ((value is not None) and (str(c['_value']) == str(value))): + if (value is not None) and (str(c['_value']) == str(value)): c['_selected'] = 'selected' else: c['_selected'] = None @@ -2024,7 +2027,7 @@ class SELECT(INPUT): else: values = [str(value)] for c in options: # my patch - if ((value is not None) and (str(c['_value']) in values)): + if (value is not None) and (str(c['_value']) in values): c['_selected'] = 'selected' else: c['_selected'] = None @@ -2390,7 +2393,6 @@ class FORM(DIV): class BEAUTIFY(DIV): - """ Turns any list, dictionary, etc into decent looking html. @@ -2547,7 +2549,7 @@ class MENU(DIV): li['_class'] = li['_class'] + ' ' + self['li_active'] else: li['_class'] = self['li_active'] - if len(item) <= 4 or item[4] == True: + if len(item) <= 4 or item[4] is True: ul.append(li) return ul @@ -2561,7 +2563,7 @@ class MENU(DIV): # ex: ('', False, A('title', _href=URL(...), _title="title")) # ex: (A('title', _href=URL(...), _title="title"), False, None) custom_items.append(item) - elif len(item) <= 4 or item[4] == True: + elif len(item) <= 4 or item[4] is True: select.append(OPTION(CAT(prefix, item[0]), _value=item[2], _selected=item[1])) if len(item) > 3 and len(item[3]): @@ -2703,7 +2705,8 @@ class web2pyHTMLParser(HTMLParser): self.parent = self.parent.parent except: raise RuntimeError("unable to balance tag %s" % tagname) - if parent_tagname[:len(tagname)] == tagname: break + if parent_tagname[:len(tagname)] == tagname: + break def markdown_serializer(text, tag=None, attr=None): From aa1b71e431b18118e998eaa1ac096c2dcb58b195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 15:14:41 -0400 Subject: [PATCH 260/436] Enhance http.py PEP8 --- gluon/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/http.py b/gluon/http.py index e34fb8f0..ac630cc6 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -116,7 +116,7 @@ class HTTP(Exception): for k, v in iteritems(headers): if isinstance(v, list): rheaders += [(k, str(item)) for item in v] - elif not v is None: + elif v is not None: rheaders.append((k, str(v))) responder(status, rheaders) if env.get('request_method', '') == 'HEAD': @@ -148,7 +148,7 @@ class HTTP(Exception): web2py_error=self.headers.get('web2py_error')) def __str__(self): - "stringify me" + """stringify me""" return self.message From b5b98d6e19ced074b768034c726758732a614b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Wed, 21 Jun 2017 15:32:28 -0400 Subject: [PATCH 261/436] Close #1664 remaining py2.6 specific code --- gluon/import_all.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gluon/import_all.py b/gluon/import_all.py index a8cbffc6..85f14864 100644 --- a/gluon/import_all.py +++ b/gluon/import_all.py @@ -78,13 +78,9 @@ alert_dependency = ['hashlib', 'uuid'] # Now we remove the blacklisted modules if we are using the stated # python version. # -# List of modules deprecated in Python 2.6 or 2.7 that are in the above set +# List of modules deprecated in Python 2.7 that are in the above list py27_deprecated = ['mhlib', 'multifile', 'mimify', 'sets', 'MimeWriter'] # And ['optparse'] but we need it for now -if python_version >= '2.6': - base_modules += ['json', 'multiprocessing'] - base_modules = list(set(base_modules).difference(set(py26_deprecated))) - if python_version >= '2.7': base_modules += ['argparse', 'json', 'multiprocessing'] base_modules = list(set(base_modules).difference(set(py27_deprecated))) From 78f3af6fc1be7d8b8c78aeb3b5daf9935c427b20 Mon Sep 17 00:00:00 2001 From: abastardi Date: Wed, 21 Jun 2017 16:29:19 -0400 Subject: [PATCH 262/436] Use FormData when supported to allow Ajax file uploads --- applications/welcome/static/js/web2py.js | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 313d00bc..1b52245e 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -12,6 +12,8 @@ $.error('web2py.js has already been loaded!'); } + var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + String.prototype.reverse = function () { return this.split('').reverse().join(''); }; @@ -320,7 +322,15 @@ form.submit(function (e) { web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.hide_flash(); - web2py.ajax_page('post', url, form.serialize(), target, form); + + var formData; + if (FORMDATA_IS_SUPPORTED) { + formData = new FormData(form[0]); // Allows file uploads. + } else { + formData = form.serialize(); // Fallback for older browsers. + } + web2py.ajax_page('post', url, formData, target, form); + e.preventDefault(); }); form.on('click', web2py.formInputClickSelector, function (e) { @@ -339,11 +349,18 @@ if (web2py.isUndefined(element)) element = $(document); /* if target is not there, fill it with something that there isn't in the page*/ if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; + + /* processData and contentType must be set to false when passing a FormData + object to jQuery.ajax. */ + var isFormData = Object.prototype.toString.call(data) === '[object FormData]'; + var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8'; if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ $.ajax({ 'type': method, 'url': action, 'data': data, + 'processData': !isFormData, + 'contentType': contentType, 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); @@ -699,8 +716,9 @@ }); }, /* Disables form elements: + - Does not disable elements with 'data-w2p_disable' attribute - Caches element value in 'w2p_enable_with' data store - - Replaces element text with value of 'data-disable-with' attribute + - Replaces element text with value of 'data-w2p_disable_with' attribute - Sets disabled property to true */ disableFormElements: function (form) { @@ -712,13 +730,15 @@ if (!web2py.isUndefined(disable)) { return false; } - if (web2py.isUndefined(disable_with)) { - element.data('w2p_disable_with', element[method]()); + if (!element.is(':file')) { // Altering file input values is not allowed. + if (web2py.isUndefined(disable_with)) { + element.data('w2p_disable_with', element[method]()); + } + if (web2py.isUndefined(element.data('w2p_enable_with'))) { + element.data('w2p_enable_with', element[method]()); + } + element[method](element.data('w2p_disable_with')); } - if (web2py.isUndefined(element.data('w2p_enable_with'))) { - element.data('w2p_enable_with', element[method]()); - } - element[method](element.data('w2p_disable_with')); element.prop('disabled', true); }); }, From 1a52b0ee3bd57879b5d017e830ba20fa42bb543e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Thu, 22 Jun 2017 09:46:55 -0400 Subject: [PATCH 263/436] Enhance main.py PEP8 --- gluon/main.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/gluon/main.py b/gluon/main.py index 24327a89..32103ea6 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -11,7 +11,8 @@ The gluon wsgi application """ from __future__ import print_function -if False: import import_all # DO NOT REMOVE PART OF FREEZE PROCESS +if False: + import import_all # DO NOT REMOVE PART OF FREEZE PROCESS import gc import os @@ -26,7 +27,7 @@ import random import string from gluon._compat import Cookie, urllib2 -#from thread import allocate_lock +# from thread import allocate_lock from gluon.fileutils import abspath, write_file from gluon.settings import global_settings @@ -67,14 +68,14 @@ import gluon.messageboxhandler logging.gluon = gluon # so we must restore it! Thanks ozancag import locale -locale.setlocale(locale.LC_CTYPE, "C") # IMPORTANT, web2py requires locale "C" +locale.setlocale(locale.LC_CTYPE, "C") # IMPORTANT, web2py requires locale "C" exists = os.path.exists pjoin = os.path.join try: logging.config.fileConfig(abspath("logging.conf")) -except: # fails on GAE or when logfile is missing +except: # fails on GAE or when logfile is missing logging.basicConfig() logger = logging.getLogger("web2py") @@ -254,6 +255,7 @@ class LazyWSGI(object): return [data] for item in middleware_apps: app = item(app) + def caller(app): return app(self.environ, self.start_response) return lambda caller=caller, app=app: caller(app) @@ -294,9 +296,9 @@ def wsgibase(environ, responder): response = Response() session = Session() env = request.env - #env.web2py_path = global_settings.applications_parent + # env.web2py_path = global_settings.applications_parent env.web2py_version = web2py_version - #env.update(global_settings) + # env.update(global_settings) static_file = False http_response = None try: @@ -325,7 +327,6 @@ def wsgibase(environ, responder): 'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT' response.stream(static_file, request=request) - # ################################################## # fill in request items # ################################################## @@ -356,17 +357,15 @@ def wsgibase(environ, responder): cmd_opts = global_settings.cmd_options request.update( - client = client, - folder = abspath('applications', app) + os.sep, - ajax = x_req_with == 'xmlhttprequest', - cid = env.http_web2py_component_element, - is_local = (env.remote_addr in local_hosts and - client == env.remote_addr), - is_shell = False, - is_scheduler = False, - is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \ - request.env.http_x_forwarded_proto in HTTPS_SCHEMES \ - or env.https == 'on' + client=client, + folder=abspath('applications', app) + os.sep, + ajax=x_req_with == 'xmlhttprequest', + cid=env.http_web2py_component_element, + is_local=(env.remote_addr in local_hosts and client == env.remote_addr), + is_shell=False, + is_scheduler=False, + is_https=env.wsgi_url_scheme in HTTPS_SCHEMES or + request.env.http_x_forwarded_proto in HTTPS_SCHEMES or env.https == 'on' ) request.url = environ['PATH_INFO'] @@ -390,7 +389,7 @@ def wsgibase(environ, responder): % 'invalid request', web2py_error='invalid application') elif not request.is_local and exists(disabled): - five0three = os.path.join(request.folder,'static','503.html') + five0three = os.path.join(request.folder, 'static', '503.html') if os.path.exists(five0three): raise HTTP(503, file(five0three, 'r').read()) else: @@ -406,7 +405,7 @@ def wsgibase(environ, responder): # get the GET and POST data # ################################################## - #parse_get_post_vars(request, environ) + # parse_get_post_vars(request, environ) # ################################################## # expose wsgi hooks for convenience @@ -625,7 +624,7 @@ def appfactory(wsgiapp=wsgibase, raise BaseException("Can't create dir %s" % profiler_dir) filepath = pjoin(profiler_dir, 'wtest') try: - filehandle = open( filepath, 'w' ) + filehandle = open(filepath, 'w') filehandle.close() os.unlink(filepath) except IOError: From 054320820cf9e8b96987c578eb9297b21b7b65ff Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 24 Jun 2017 07:03:10 +0200 Subject: [PATCH 264/436] fix performance regression with compiled views --- gluon/compileapp.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 3dc08bcb..1d01d302 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -596,13 +596,13 @@ def run_controller_in(controller, function, environment): badc = 'invalid controller (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function) if os.path.exists(cpath): - filename = pjoin(cpath, 'controllers.%s.%s.pyc' - % (controller, function)) - if not os.path.exists(filename): + filename = pjoin(cpath, 'controllers.%s.%s.pyc' % (controller, function)) + try: + ccode = getcfs(filename, filename, lambda: read_pyc(filename)) + except IOError: raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badf, web2py_error=badf) - ccode = getcfs(filename, filename, lambda: read_pyc(filename)) elif function == '_TEST': # TESTING: adjust the path to include site packages from gluon.settings import global_settings @@ -623,13 +623,13 @@ def run_controller_in(controller, function, environment): code += TEST_CODE ccode = compile2(code, filename) else: - filename = pjoin(folder, 'controllers/%s.py' - % controller) - if not os.path.exists(filename): + filename = pjoin(folder, 'controllers/%s.py' % controller) + try: + code = getcfs(filename, filename, lambda: read_file(filename)) + except IOError: raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badc, web2py_error=badc) - code = getcfs(filename, filename, lambda: read_file(filename)) exposed = find_exposed_functions(code) if not function in exposed: raise HTTP(404, @@ -705,12 +705,14 @@ def run_view_in(environment): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badv, web2py_error=badv) - layer = filename - # Compile the template - ccode = parse_template(view, - pjoin(folder, 'views'), - context=environment) + # if the view is not compiled + if not layer: + # Compile the template + ccode = parse_template(view, + pjoin(folder, 'views'), + context=environment) + layer = filename restricted(ccode, environment, layer=layer) # parse_template saves everything in response body return environment['response'].body.getvalue() From af7bfac1e2009226d2eeaace4ed83888b4b3b6a7 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 24 Jun 2017 08:07:15 +0200 Subject: [PATCH 265/436] Testing pypy3.5 in travis, switching to ubuntu trusty --- .travis.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 74198cea..e7c88e7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,18 @@ sudo: required cache: pip +dist: "trusty" + python: - '2.7' - - 'pypy' - '3.5' - '3.6' + - 'pypy-5.3.1' + - 'pypy3.5-5.7.1-beta' + +matrix: + allow_failures: + - python: 'pypy3.5-5.7.1-beta' install: - pip install -e . @@ -33,3 +40,6 @@ notifications: addons: postgresql: "9.4" + apt: + packages: + - postgresql-9.4-postgis-2.3 From 757ce4e76ef80d97174687f4a606b1cb37eeeafd Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 27 Jun 2017 16:43:42 -0500 Subject: [PATCH 266/436] fixes #1528 --- applications/admin/controllers/default.py | 5 ++++- gluon/packages/dal | 2 +- gluon/restricted.py | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 94fc91ce..ff4ac4a2 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -16,6 +16,7 @@ from gluon.tools import Config from gluon.compileapp import find_exposed_functions from glob import glob from gluon._compat import iteritems, PY2, pickle, xrange, urlopen, to_bytes, StringIO, to_native +import gluon.rewrite import shutil import platform @@ -249,6 +250,7 @@ def site(): db.app.insert(name=appname, owner=auth.user.id) log_progress(appname) session.flash = T('new application "%s" created', appname) + gluon.rewrite.load() redirect(URL('design', args=appname)) else: session.flash = \ @@ -266,6 +268,7 @@ def site(): new_repo = git.Repo.clone_from(form_update.vars.url, target) session.flash = T('new application "%s" imported', form_update.vars.name) + gluon.rewrite.load() except git.GitCommandError as err: session.flash = T('Invalid git repository specified.') redirect(URL(r=request)) @@ -302,6 +305,7 @@ def site(): log_progress(appname) session.flash = T(msg, dict(appname=appname, digest=md5_hash(installed))) + gluon.rewrite.load() else: msg = 'unable to install application "%(appname)s"' session.flash = T(msg, dict(appname=form_update.vars.name)) @@ -1861,7 +1865,6 @@ def user(): def reload_routes(): """ Reload routes.py """ - import gluon.rewrite gluon.rewrite.load() redirect(URL('site')) diff --git a/gluon/packages/dal b/gluon/packages/dal index 38066734..0a27f4b6 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 38066734f71f2cfe8688493ac8e08c6e654e16bc +Subproject commit 0a27f4b6c8331895a0040733637941ea6729ec41 diff --git a/gluon/restricted.py b/gluon/restricted.py index 95410f5c..3fe5bfb0 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -137,7 +137,10 @@ class RestrictedError(Exception): self.environment = environment if layer: try: - self.traceback = traceback.format_exc(limit=1) + try: + self.traceback = traceback.format_exc() + except: + self.traceback = traceback.format_exc(limit=1) except: self.traceback = 'no traceback because template parsing error' try: From 3d0b81cbe68564db9596484b91f073dfa5687a8b Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Jun 2017 08:22:26 -0500 Subject: [PATCH 267/436] reverted backward imcompatible change --- gluon/sqlhtml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index b52d22df..310e61b1 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2341,10 +2341,11 @@ class SQLFORM(FORM): else: fields = [] columns = [] - filter1 = lambda f: isinstance(f, Field) and f.readable and (f.type!='blob' or showblobs) + filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs) + filter2 = lambda f: isinstance(f, Field) and f.readable for table in tables: fields += filter(filter1, table) - columns += filter(filter1, table) + columns += filter(filter2, table) for k, f in iteritems(table): if not k.startswith('_'): if isinstance(f, Field.Virtual) and f.readable: From 2fd0c7c778f0e3ceca1734267a5df09e9672d87c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 30 Jun 2017 09:22:50 -0500 Subject: [PATCH 268/436] new dal --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 0a27f4b6..c7636a65 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 0a27f4b6c8331895a0040733637941ea6729ec41 +Subproject commit c7636a6587424c2b2d2f052c3379a809e246b85f From 1c0b498880ebb37e2dc705d5df84a86efcb3bc84 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 2 Jul 2017 01:32:25 -0500 Subject: [PATCH 269/436] fixed undefined variable --- gluon/packages/dal | 2 +- gluon/tools.py | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index c7636a65..6fd68e4d 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit c7636a6587424c2b2d2f052c3379a809e246b85f +Subproject commit 6fd68e4d4877e848c44192b990744b2d1a8e95eb diff --git a/gluon/tools.py b/gluon/tools.py index ce6db5dc..3f69e94b 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -266,7 +266,7 @@ class Mail(object): settings.sender = sender settings.login = login settings.tls = tls - settings.timeout = 60 # seconds + settings.timeout = 5 # seconds settings.hostname = None settings.ssl = False settings.cipher_type = None @@ -789,19 +789,18 @@ class Mail(object): else: smtp_args = self.settings.server.split(':') kwargs = dict(timeout=self.settings.timeout) - if self.settings.ssl: - server = smtplib.SMTP_SSL(*smtp_args, **kwargs) - else: - server = smtplib.SMTP(*smtp_args, **kwargs) - if self.settings.tls and not self.settings.ssl: - server.ehlo(self.settings.hostname) - server.starttls() - server.ehlo(self.settings.hostname) - if self.settings.login: - server.login(*self.settings.login.split(':', 1)) - result = server.sendmail( - sender, to, payload.as_string()) - server.quit() + func = smtplib.SMTP_SSL if self.settings.ssl else smtplib.SMTP + server = func(*smtp_args, **kwargs) + try: + if self.settings.tls and not self.settings.ssl: + server.ehlo(self.settings.hostname) + server.starttls() + server.ehlo(self.settings.hostname) + if self.settings.login: + server.login(*self.settings.login.split(':', 1)) + result = server.sendmail(sender, to, payload.as_string()) + finally: + server.quit() except Exception as e: logger.warning('Mail.send failure:%s' % e) self.result = result @@ -6425,7 +6424,7 @@ class Wiki(object): query = (db.wiki_page.id == db.wiki_tag.wiki_page) &\ (db.wiki_tag.name.belongs(tags)) query = query | db.wiki_page.title.contains(request.vars.q) - if self.settings.restrict_search and not self.manage(): + if self.settings.restrict_search and not self.can_manage(): query = query & (db.wiki_page.created_by == self.auth.user_id) pages = db(query).select(count, *fields, **dict(orderby=orderby or ~count, From f657b42f6544fb10b2ec3bbcd4af9bde151a90c3 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 2 Jul 2017 01:34:05 -0500 Subject: [PATCH 270/436] fixed undefined variable --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 3f69e94b..882ffffc 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -13,7 +13,7 @@ Auth, Mail, PluginManager and various utilities import base64 from functools import reduce from gluon._compat import pickle, thread, urllib2, Cookie, StringIO -from gluon._compat import configparser, MIMEBase, MIMEMultipart, MIMEText +from gluon._compat import configparser, MIMEBase, MIMEMultipart, MIMEText, Header from gluon._compat import Encoders, Charset, long, urllib_quote, iteritems from gluon._compat import to_bytes, to_native, add_charset from gluon._compat import charset_QP, basestring, unicodeT, to_unicode From 9ded289924686d5d33490309ed90153e76ce81d3 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 2 Jul 2017 01:55:51 -0500 Subject: [PATCH 271/436] fixed minor pylint -E errors --- gluon/newcron.py | 6 +++--- gluon/rocket.py | 9 ++++----- gluon/sqlhtml.py | 4 +++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gluon/newcron.py b/gluon/newcron.py index f7482fcb..d422f074 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -225,8 +225,8 @@ def parsecronline(line): params = line.strip().split(None, 6) if len(params) < 7: return None - daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, - 'fri': 5, 'sat': 6} + daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3, + 'thu': 4, 'fri': 5, 'sat': 6} for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']): if not s in [None, '*']: task[id] = [] @@ -239,7 +239,7 @@ def parsecronline(line): elif val.isdigit() or val == '-1': task[id].append(int(val)) elif id == 'dow' and val[:3].lower() in daysofweek: - task[id].append(daysofweek(val[:3].lower())) + task[id].append(daysofweek[val[:3].lower()]) task['user'] = params[5] task['cmd'] = params[6] return task diff --git a/gluon/rocket.py b/gluon/rocket.py index ba13faac..e3faf0bd 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -307,13 +307,13 @@ try: except ImportError: has_futures = False - class Future: + class Future(object): pass - class ThreadPoolExecutor: + class ThreadPoolExecutor(object): pass - class _WorkItem: + class _WorkItem(object): pass @@ -784,8 +784,7 @@ class Rocket(object): the application developer. Please update your \ applications to no longer call rocket.stop(True)" try: - import warnings - raise warnings.DeprecationWarning(msg) + raise DeprecationWarning(msg) except ImportError: raise RuntimeError(msg) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 310e61b1..59ebbeea 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -3152,7 +3152,9 @@ class SQLFORM(FORM): # if isinstance(linked_tables, dict): # linked_tables = linked_tables.get(table._tablename, []) if linked_tables is None or referee in linked_tables: - field.represent = lambda id, r=None, referee=referee, rep=field.represent: A(callable(rep) and rep(id) or id, cid=request.cid, _href=url(args=['view', referee, id])) + field.represent = (lambda id, r=None, referee=referee, rep=field.represent: + A(callable(rep) and rep(id) or id, + cid=request.cid, _href=url(args=['view', referee, id]))) except (KeyError, ValueError, TypeError): redirect(URL(args=table._tablename)) if nargs == len(args) + 1: From 5d8a25626c4e6608e99b84bd25d2b88535430537 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 5 Jul 2017 01:32:50 -0500 Subject: [PATCH 272/436] tracking PyDAL 17.07 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 6fd68e4d..476ebfda 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 6fd68e4d4877e848c44192b990744b2d1a8e95eb +Subproject commit 476ebfda678492ee8bf9f399fb60ccd4925b2206 From 5583e9cdc7d9c08250707895fa383863069e2bcf Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 5 Jul 2017 01:37:48 -0500 Subject: [PATCH 273/436] R-2.16.0b1 --- CHANGELOG | 33 +++++++++++++++++++++++ applications/admin/static/js/jquery.js | 9 +++---- applications/examples/static/js/jquery.js | 9 +++---- applications/welcome/static/js/jquery.js | 9 +++---- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 22406bdd..bae28cee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,36 @@ +## 2.16.0b1 +- experimental python 3 support +- experimental authapi for service login +- more tests +- d3.js model visulization +- improved scheduler +- is_email support for internationalized Domain Names +- improved used of cookies with CookieJar +- SQLFORM.grid(showblobs=True) +- import JS events (added w2p.componentBegin event) +- added support for CASv3 +- allow first_name and last_name placeholders in verify_email message +- added three-quote support in markmin +- updated pg8000 driver (but we still recommend psycopg2) +- compiled views use . separator not _ separator (must recompile code) +- better serbian, french, and catalan translations +- speed improvements (refactor of compileapp and pyc caching) +- removed web shell (never worked as intended) +- allow Expose(..., follow_symlink_out=False). +- Updated fpdf to latest version +- JWT support +- import fabfile for remote deployment +- jQuery 3.2.1 +- PyDAL 17.07 including: + allow jsonb support for postgres + correctly configure adapters that need connection for configuration + better caching + updated IMAP adapter methods to new API + experimental suport for joinable subselects + improved Teradata support + improved mongodb support + overall refactoring + experimental support for Google Cloud SQL v2 (TODO) ## 2.15.x - web2py does not support python 2.6 anymore diff --git a/applications/admin/static/js/jquery.js b/applications/admin/static/js/jquery.js index 5cb0acfa..644d35e2 100644 --- a/applications/admin/static/js/jquery.js +++ b/applications/admin/static/js/jquery.js @@ -1,5 +1,4 @@ -/*! jQuery v1.12.2 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.2",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; -}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
    a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:l.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("