Merge github.com:web2py/web2py

This commit is contained in:
Michele Comitini
2013-01-16 12:22:56 +01:00
11 changed files with 814 additions and 225 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.4.1-alpha.2+timestamp.2013.01.09.16.56.34 Version 2.4.1-alpha.2+timestamp.2013.01.15.22.22.06
+37 -26
View File
@@ -15,7 +15,13 @@ from glob import glob
import shutil import shutil
import platform import platform
try: try:
from git import * import git
GIT_ERRORS = (git.GitCommandError, git.InvalidGitRepositoryError,
git.NoSuchPathError)
if git.__version__ >= '0.3.1':
GIT_ERRORS += (git.CacheError, git.CheckoutError,
git.ODBError, git.ParseError,
git.UnmergedEntriesError)
have_git = True have_git = True
except ImportError: except ImportError:
have_git = False have_git = False
@@ -240,10 +246,10 @@ def site():
redirect(URL(r=request)) redirect(URL(r=request))
target = os.path.join(apath(r=request), form_update.vars.name) target = os.path.join(apath(r=request), form_update.vars.name)
try: try:
new_repo = Repo.clone_from(form_update.vars.url, target) new_repo = git.Repo.clone_from(form_update.vars.url, target)
session.flash = T('new application "%s" imported', session.flash = T('new application "%s" imported',
form_update.vars.name) form_update.vars.name)
except GitCommandError, err: except git.GitCommandError, err:
session.flash = T('Invalid git repository specified.') session.flash = T('Invalid git repository specified.')
redirect(URL(r=request)) redirect(URL(r=request))
@@ -1726,29 +1732,31 @@ def git_pull():
{T('Cancel'): URL('site')}) {T('Cancel'): URL('site')})
if dialog.accepted: if dialog.accepted:
try: try:
repo = Repo(os.path.join(apath(r=request), app)) repo = git.Repo(os.path.join(apath(r=request), app))
origin = repo.remotes.origin origin = repo.remotes.origin
origin.fetch() origin.fetch()
origin.pull() origin.pull()
session.flash = T("Application updated via git pull") session.flash = T("Application updated via git pull")
redirect(URL('site')) redirect(URL('site'))
except CheckoutError, message: except GIT_ERRORS, e:
session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.") error_type = type(e)
redirect(URL('site')) if 'CheckoutError' in error_type:
except UnmergedEntriesError: session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.")
session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.") redirect(URL('site'))
redirect(URL('site')) elif 'UnmergedEntriesError' in error_type:
session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
redirect(URL('site'))
elif 'GitCommandError' in error_type:
session.flash = T(
"Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
else:
session.flash = T(
"Git error: %s %s" % (error_type, str(e)))
redirect(URL('site'))
except AssertionError: except AssertionError:
session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.") session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.")
redirect(URL('site')) redirect(URL('site'))
except GitCommandError, status:
session.flash = T(
"Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
except Exception, e:
session.flash = T(
"Pull failed, git exited abnormally. See logs for details.")
redirect(URL('site'))
elif 'cancel' in request.vars: elif 'cancel' in request.vars:
redirect(URL('site')) redirect(URL('site'))
return dict(app=app, dialog=dialog) return dict(app=app, dialog=dialog)
@@ -1766,7 +1774,7 @@ def git_push():
form.process() form.process()
if form.accepted: if form.accepted:
try: try:
repo = Repo(os.path.join(apath(r=request), app)) repo = git.Repo(os.path.join(apath(r=request), app))
index = repo.index index = repo.index
index.add([apath(r=request) + app + '/*']) index.add([apath(r=request) + app + '/*'])
new_commit = index.commit(form.vars.changelog) new_commit = index.commit(form.vars.changelog)
@@ -1775,11 +1783,14 @@ def git_push():
session.flash = T( session.flash = T(
"Git repo updated with latest application changes.") "Git repo updated with latest application changes.")
redirect(URL('site')) redirect(URL('site'))
except UnmergedEntriesError: except GIT_ERRORS, e:
session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.") error_type = type(e)
redirect(URL('site')) if "UnmergedEntriesError" in error_type:
except Exception, e: session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.")
session.flash = T( redirect(URL('site'))
"Push failed, git exited abnormally. See logs for details.") else:
redirect(URL('site')) session.flash = T(
"Git error: %s %s" % (error_type, str(e)))
redirect(URL('site'))
return dict(app=app, form=form) return dict(app=app, form=form)
+2 -2
View File
@@ -92,7 +92,7 @@
'Database': 'База данных', 'Database': 'База данных',
'database': 'база данных', 'database': 'база данных',
'database %s select': 'Выбор базы данных %s ', 'database %s select': 'Выбор базы данных %s ',
'database administration': 'администраторирование базы данных', 'database administration': 'администрирование базы данных',
'Date and Time': 'Дата и время', 'Date and Time': 'Дата и время',
'db': 'бд', 'db': 'бд',
'DB Model': 'Модель БД', 'DB Model': 'Модель БД',
@@ -134,7 +134,7 @@
'Editing Language file': 'Правка языкового файла', 'Editing Language file': 'Правка языкового файла',
'Editing Plural Forms File': 'Editing Plural Forms File', 'Editing Plural Forms File': 'Editing Plural Forms File',
'Enterprise Web Framework': 'Enterprise Web Framework', 'Enterprise Web Framework': 'Enterprise Web Framework',
'Error logs for "%(app)s"': 'Журнал ошибок для "%(app)"', 'Error logs for "%(app)s"': 'Журнал ошибок для "%(app)s"',
'Error snapshot': 'Error snapshot', 'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket', 'Error ticket': 'Error ticket',
'Errors': 'Ошибка', 'Errors': 'Ошибка',
+212
View File
@@ -0,0 +1,212 @@
# coding: utf8
{
'!langcode!': 'sr-cr',
'!langname!': 'Српски (Ћирилица)',
'%Y-%m-%d': '%d-%m-%Y',
'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S',
'(requires internet access)': '(захтијева приступ интернету)',
'(something like "it-it")': '(нешто као "it-it")',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(датотека **gluon/contrib/plural_rules/%s.py** није пронађена)',
'About': 'Информације',
'About application': 'О апликацији',
'Additional code for your application': 'Додатни код за апликацију',
'admin disabled because unable to access password file': 'администрација онемогућена јер не могу приступити датотеци са лозинком',
'Admin language': 'Језик администратора',
'administrative interface': 'административни интерфејс',
'Administrator Password:': 'Лозинка администратора:',
'and rename it:': 'и преименуј у:',
'Application name:': 'Назив апликације:',
'are not used': 'није кориштено',
'are not used yet': 'није још кориштено',
'Are you sure you want to delete this object?': 'Да ли сте сигурни да желите обрисати?',
'arguments': 'arguments',
'at char %s': 'код слова %s',
'at line %s': 'на линији %s',
'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.',
'back': 'назад',
'Basics': 'Основе',
'Begin': 'Почетак',
'cache, errors and sessions cleaned': 'кеш, грешке и сесије су обрисани',
'can be a git repo': 'може бити git repo',
'cannot upload file "%(filename)s"': 'не мофу отпремити датотеку "%(filename)s"',
'Change admin password': 'Промијени лзинку администратора',
'check all': 'check all',
'Check for upgrades': 'Провјери могућност надоградње',
'Checking for upgrades...': 'Провјеравам могућност надоградње...',
'Clean': 'Прочисти',
'Click row to expand traceback': 'Click row to expand traceback',
'code': 'код',
'collapse/expand all': 'сакрити/приказати све',
'Compile': 'Компајлирај',
'Controllers': 'Контролери',
'controllers': 'контролери',
'Count': 'Count',
'Create': 'Креирај',
'create file with filename:': 'Креирај датотеку под називом:',
'Create rules': 'Креирај правила',
'created by': 'израдио',
'crontab': 'crontab',
'currently running': 'тренутно покренут',
'currently saved or': 'тренутно сачувано или',
'database administration': 'администрација базе података',
'Debug': 'Debug',
'defines tables': 'дефинише табеле',
'delete': 'обриши',
'Delete': 'Обриши',
'delete all checked': 'delete all checked',
'Delete this file (you will be asked to confirm deletion)': 'Обриши ову даатотеку (бићете упитани за потврду брисања)',
'Deploy': 'Постави',
'Deploy on Google App Engine': 'Постави на Google App Engine',
'Deploy to OpenShift': 'Постави на OpenShift',
'Detailed traceback description': 'Detailed traceback description',
'direction: ltr': 'direction: ltr',
'Disable': 'Искључи',
'docs': 'документација',
'download layouts': 'преузми layouts',
'download plugins': 'преузми plugins',
'Edit': 'Уређивање',
'edit all': 'уреди све',
'Edit application': 'Уреди апликацију',
'edit controller': 'уреди контролер',
'edit views:': 'уреди views:',
'Editing file "%s"': 'Уређивање датотеке "%s"',
'Editing Language file': 'Уређивање језичке датотеке',
'Error': 'Грешка',
'Error logs for "%(app)s"': 'Преглед грешака за "%(app)s"',
'Error snapshot': 'Error snapshot',
'Error ticket': 'Error ticket',
'Errors': 'Грешке',
'Exception instance attributes': 'Exception instance attributes',
'Expand Abbreviation': 'Expand Abbreviation',
'exposes': 'exposes',
'exposes:': 'exposes:',
'extends': 'проширује',
'failed to compile file because:': 'нисам могао да компајлирам због:',
'File': 'Датотека',
'file does not exist': 'датотека не постоји',
'file saved on %s': 'датотека сачувана на %s',
'filter': 'филтер',
'Find Next': 'Пронађи сљедећи',
'Find Previous': 'Пронађи претходни',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.',
'Generate': 'Generate',
'Get from URL:': 'Преузми са странице:',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Go to Matching Pair': 'Go to Matching Pair',
'go!': 'крени!',
'Help': 'Помоћ',
'Hide/Show Translated strings': 'Сакрити/Приказати преведене ријечи',
'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.',
'includes': 'укључује',
'inspect attributes': 'inspect attributes',
'Install': 'Инсталирај',
'Installed applications': 'Инсталиране апликације',
'invalid password.': 'погрешна лозинка.',
'Key bindings': 'Пречице',
'Key bindings for ZenCoding Plugin': 'Пречице за ZenCoding Plugin',
'Language files (static strings) updated': 'Језичке датотеке су ажуриране',
'languages': 'језици',
'Languages': 'Језици',
'Last saved on:': 'Посљедња измјена:',
'License for': 'Лиценца за',
'loading...': 'преузимам...',
'locals': 'locals',
'Login': 'Пријава',
'Login to the Administrative Interface': 'Пријава за административни интерфејс',
'Logout': 'Излаз',
'Match Pair': 'Match Pair',
'Merge Lines': 'Споји линије',
'models': 'models',
'Models': 'Models',
'Modules': 'Modules',
'modules': 'modules',
'New Application Wizard': 'Чаробњак за нове апликације',
'New application wizard': 'Чаробњак за нове апликације',
'New simple application': 'Нова једноставна апликација',
'Next Edit Point': 'Next Edit Point',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'online designer': 'онлајн дизајнер',
'Original/Translation': 'Оргинал/Превод',
'Overwrite installed app': 'Пребриши постојећу апликацију',
'Pack all': 'Запакуј све',
'Peeking at file': 'Peeking at file',
'Plugins': 'Plugins',
'plugins': 'plugins',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Омогућио',
'Previous Edit Point': 'Previous Edit Point',
'Private files': 'Private files',
'private files': 'private files',
'Project Progress': 'Напредак пројекта',
'Reload routes': 'Обнови преусмјерења',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Замијени',
'Replace All': 'Замијени све',
'request': 'request',
'response': 'response',
'restart': 'restart',
'restore': 'restore',
'revert': 'revert',
'rules are not defined': 'правила нису дефинисана',
'rules:': 'правила:',
"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 via Ajax': 'сачувај via Ajax',
'Saved file hash:': 'Сачувано као хаш:',
'session': 'сесија',
'session expired': 'сесија истекла',
'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s',
'shell': 'shell',
'Site': 'Сајт',
'skip to generate': 'skip to generate',
'Start a new app': 'Покрени нову апликацију',
'Start searching': 'Покрени претрагу',
'Start wizard': 'Покрени чаробњака',
'static': 'static',
'Static files': 'Static files',
'Step': 'Корак',
'Submit': 'Прихвати',
'successful': 'успјешан',
'test': 'тест',
'Testing application': 'Тестирање апликације',
'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 models': 'There are no models',
'There are no plugins': 'There are no plugins',
'There are no private files': 'There are no private files',
'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',
'Ticket ID': 'Ticket ID',
'Ticket Missing': 'Ticket nedostaje',
'to previous version.': 'на претходну верзију.',
'To create a plugin, name a file/folder plugin_[name]': 'To create a plugin, name a file/folder plugin_[name]',
'toggle breakpoint': 'toggle breakpoint',
'Toggle Fullscreen': 'Toggle Fullscreen',
'Traceback': 'Traceback',
'Translation strings for the application': 'Ријечи у апликацији које треба превести',
'Try the mobile interface': 'Пробај мобилни интерфејс',
'try view': 'try view',
'uncheck all': 'uncheck all',
'Uninstall': 'Деинсталирај',
'update': 'ажурирај',
'update all languages': 'ажурирај све језике',
'upload': 'Отпреми',
'Upload a package:': 'Преузми пакет:',
'Upload and install packed application': 'Преузми и инсталирај запаковану апликацију',
'upload file:': 'преузми датотеку:',
'upload plugin file:': 'преузми плагин датотеку:',
'variables': 'variables',
'Version': 'Верзија',
'Version %s.%s.%s (%s) %s': 'Верзија %s.%s.%s (%s) %s',
'Versioning': 'Versioning',
'views': 'views',
'Views': 'Views',
'Web Framework': 'Web Framework',
'web2py is up to date': 'web2py је ажуран',
'web2py Recent Tweets': 'web2py Recent Tweets',
'Wrap with Abbreviation': 'Wrap with Abbreviation',
}
+212
View File
@@ -0,0 +1,212 @@
# coding: utf8
{
'!langcode!': 'sr-lt',
'!langname!': 'Srpski (Latinica)',
'%Y-%m-%d': '%d-%m-%Y',
'%Y-%m-%d %H:%M:%S': '%d-%m-%Y %H:%M:%S',
'(requires internet access)': '(zahtijeva pristup internetu)',
'(something like "it-it")': '(nešto kao "it-it")',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(datoteka **gluon/contrib/plural_rules/%s.py** nije pronađena)',
'About': 'Informacije',
'About application': 'O aplikaciji',
'Additional code for your application': 'Dodatni kod za aplikaciju',
'admin disabled because unable to access password file': 'administracija onemogućena jer ne mogu pristupiti datoteci sa lozinkom',
'Admin language': 'Jezik administratora',
'administrative interface': 'administrativni interfejs',
'Administrator Password:': 'Lozinka administratora:',
'and rename it:': 'i preimenuj u:',
'Application name:': 'Naziv aplikacije:',
'are not used': 'nije korišteno',
'are not used yet': 'nije još korišteno',
'Are you sure you want to delete this object?': 'Da li ste sigurni da želite obrisati?',
'arguments': 'arguments',
'at char %s': 'kod slova %s',
'at line %s': 'na liniji %s',
'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.',
'back': 'nazad',
'Basics': 'Osnove',
'Begin': 'Početak',
'cache, errors and sessions cleaned': 'keš, greške i sesije su obrisani',
'can be a git repo': 'može biti git repo',
'cannot upload file "%(filename)s"': 'ne mogu otpremiti datoteku "%(filename)s"',
'Change admin password': 'Promijeni lozinku administratora',
'check all': 'check all',
'Check for upgrades': 'Provjeri mogućnost nadogradnje',
'Checking for upgrades...': 'Provjeravam mogućnost nadogradnje...',
'Clean': 'Pročisti',
'Click row to expand traceback': 'Click row to expand traceback',
'code': 'kod',
'collapse/expand all': 'sakriti/prikazati sve',
'Compile': 'Kompajliraj',
'Controllers': 'Kontroleri',
'controllers': 'kontroleri',
'Count': 'Count',
'Create': 'Kreiraj',
'create file with filename:': 'Kreiraj datoteku pod nazivom:',
'Create rules': 'Kreiraj pravila',
'created by': 'izradio',
'crontab': 'crontab',
'currently running': 'trenutno pokrenut',
'currently saved or': 'trenutno sačuvano ili',
'database administration': 'administracija baze podataka',
'Debug': 'Debug',
'defines tables': 'definiše tabele',
'delete': 'obriši',
'Delete': 'Obriši',
'delete all checked': 'delete all checked',
'Delete this file (you will be asked to confirm deletion)': 'Obriši ovu datoteku (bićete upitani za potvrdu brisanja)',
'Deploy': 'Postavi',
'Deploy on Google App Engine': 'Postavi na Google App Engine',
'Deploy to OpenShift': 'Postavi na OpenShift',
'Detailed traceback description': 'Detailed traceback description',
'direction: ltr': 'direction: ltr',
'Disable': 'Isključi',
'docs': 'dokumentacija',
'download layouts': 'preuzmi layouts',
'download plugins': 'preuzmi plugins',
'Edit': 'Uređivanje',
'edit all': 'uredi sve',
'Edit application': 'Uredi aplikaciju',
'edit controller': 'uredi controller',
'edit views:': 'uredi views:',
'Editing file "%s"': 'Uređivanje datoteke "%s"',
'Editing Language file': 'Uređivanje jezičke datoteke',
'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 instance attributes': 'Exception instance attributes',
'Expand Abbreviation': 'Expand Abbreviation',
'exposes': 'exposes',
'exposes:': 'exposes:',
'extends': 'proširuje',
'failed to compile file because:': 'nisam mogao da kompajliram zbog:',
'File': 'Datoteka',
'file does not exist': 'datoteka ne postoji',
'file saved on %s': 'datoteka sačuvana na %s',
'filter': 'filter',
'Find Next': 'Pronađi sljedeći',
'Find Previous': 'Pronađi prethodni',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Functions with no doctests will result in [passed] tests.',
'Generate': 'Generate',
'Get from URL:': 'Preuzmi sa stranice:',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Go to Matching Pair': 'Go to Matching Pair',
'go!': 'kreni!',
'Help': 'Pomoć',
'Hide/Show Translated strings': 'Sakriti/Prikazati prevedene riječi',
'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.',
'includes': 'uključuje',
'inspect attributes': 'inspect attributes',
'Install': 'Instaliraj',
'Installed applications': 'Instalirane aplikacije',
'invalid password.': 'pogrešna lozinka.',
'Key bindings': 'Prečice',
'Key bindings for ZenCoding Plugin': 'Prečice za for ZenCoding Plugin',
'Language files (static strings) updated': 'Jezičke datoteke su ažurirane',
'languages': 'jezici',
'Languages': 'Jezici',
'Last saved on:': 'Posljednja izmjena:',
'License for': 'Licenca za',
'loading...': 'preuzimam...',
'locals': 'locals',
'Login': 'Prijava',
'Login to the Administrative Interface': 'Prijava za administrativni interfejs',
'Logout': 'Izlaz',
'Match Pair': 'Match Pair',
'Merge Lines': 'Spoji linije',
'models': 'models',
'Models': 'Models',
'Modules': 'Modules',
'modules': 'modules',
'New Application Wizard': 'Čarobnjak za nove aplikacije',
'New application wizard': 'Čarobnjak za nove aplikacije',
'New simple application': 'Nova jednostavna aplikacija',
'Next Edit Point': 'Next Edit Point',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'online designer': 'onlajn dizajner',
'Original/Translation': 'Original/Prevod',
'Overwrite installed app': 'Prebriši postojeću aplikaciju',
'Pack all': 'Zapakuj sve',
'Peeking at file': 'Peeking at file',
'Plugins': 'Plugins',
'plugins': 'plugins',
'Plural-Forms:': 'Plural-Forms:',
'Powered by': 'Omogućio',
'Previous Edit Point': 'Previous Edit Point',
'Private files': 'Private files',
'private files': 'private files',
'Project Progress': 'Napredak projekta',
'Reload routes': 'Obnovi preusmjerenja',
'Removed Breakpoint on %s at line %s': 'Removed Breakpoint on %s at line %s',
'Replace': 'Zamijeni',
'Replace All': 'Zamijeni sve',
'request': 'request',
'response': 'response',
'restart': 'restart',
'restore': 'restore',
'revert': 'revert',
'rules are not defined': 'pravila nisu definisana',
'rules:': 'pravila:',
"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 via Ajax': 'Sačuvaj via Ajax',
'Saved file hash:': 'Sačuvano kao haš:',
'session': 'sesija',
'session expired': 'sesija istekla',
'Set Breakpoint on %s at line %s: %s': 'Set Breakpoint on %s at line %s: %s',
'shell': 'shell',
'Site': 'Sajt',
'skip to generate': 'skip to generate',
'Start a new app': 'Pokreni novu aplikaciju',
'Start searching': 'Pokreni pretragu',
'Start wizard': 'Pokreni čarobnjaka',
'static': 'static',
'Static files': 'Static files',
'Step': 'Korak',
'Submit': 'Prihvati',
'successful': 'uspješan',
'test': 'test',
'Testing application': 'Testing application',
'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 models': 'There are no models',
'There are no plugins': 'There are no plugins',
'There are no private files': 'There are no private files',
'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',
'Ticket ID': 'Ticket ID',
'Ticket Missing': 'Ticket nedostaje',
'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]',
'toggle breakpoint': 'toggle breakpoint',
'Toggle Fullscreen': 'Toggle Fullscreen',
'Traceback': 'Traceback',
'Translation strings for the application': 'Riječi u aplikaciji koje treba prevesti',
'Try the mobile interface': 'Probaj mobilni interfejs',
'try view': 'try view',
'uncheck all': 'uncheck all',
'Uninstall': 'Deinstaliraj',
'update': 'ažuriraj',
'update all languages': 'ažuriraj sve jezike',
'upload': 'Otpremi',
'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:',
'variables': 'variables',
'Version': 'Verzija',
'Version %s.%s.%s (%s) %s': 'Verzija %s.%s.%s (%s) %s',
'Versioning': 'Versioning',
'views': 'views',
'Views': 'Views',
'Web Framework': 'Web Framework',
'web2py is up to date': 'web2py je ažuran',
'web2py Recent Tweets': 'web2py Recent Tweets',
'Wrap with Abbreviation': 'Wrap with Abbreviation',
}
+264 -137
View File
@@ -447,7 +447,7 @@ def pluralize(singular, rules=PLURALIZE_RULES):
if plural: return plural if plural: return plural
def hide_password(uri): def hide_password(uri):
return REGEX_PASSWORD.sub('://******:',uri) return REGEX_NOPASSWD.sub('******',uri)
def OR(a,b): def OR(a,b):
return a|b return a|b
@@ -5565,7 +5565,8 @@ class IMAPAdapter(NoSQLAdapter):
subject string subject string
mime string The mime header declaration mime string The mime header declaration
email string The complete RFC822 message** email string The complete RFC822 message**
attachments list:string Each non text decoded part as string attachments <type list> Each non text part as dict
encoding string The main detected encoding
*At the application side it is measured as the length of the RFC822 *At the application side it is measured as the length of the RFC822
message string message string
@@ -5623,11 +5624,22 @@ class IMAPAdapter(NoSQLAdapter):
# mapped names (which native # mapped names (which native
# mailbox has what table name) # mailbox has what table name)
db.mailboxes <dict> # tablename, server native name pairs imapdb.mailboxes <dict> # tablename, server native name pairs
# To retrieve a table native mailbox name use: # To retrieve a table native mailbox name use:
db.<table>.mailbox imapdb.<table>.mailbox
### New features v2.4.1:
# Declare mailboxes statically with tablename, name pairs
# This avoids the extra server names retrieval
imapdb.define_tables({"inbox": "INBOX"})
# Selects without content/attachments/email columns will only
# fetch header and flags
imapdb(q).select(imapdb.INBOX.sender, imapdb.INBOX.subject)
""" """
types = { types = {
@@ -5670,6 +5682,7 @@ class IMAPAdapter(NoSQLAdapter):
self.driver_args = driver_args self.driver_args = driver_args
self.adapter_args = adapter_args self.adapter_args = adapter_args
self.mailbox_size = None self.mailbox_size = None
self.static_names = None
self.charset = sys.getfilesystemencoding() self.charset = sys.getfilesystemencoding()
# imap class # imap class
self.imap4 = None self.imap4 = None
@@ -5829,16 +5842,22 @@ class IMAPAdapter(NoSQLAdapter):
else: else:
return None return None
@staticmethod
def header_represent(f, r):
from email.header import decode_header
text, encoding = decode_header(f)[0]
return text
def encode_text(self, text, charset, errors="replace"): def encode_text(self, text, charset, errors="replace"):
""" convert text for mail to unicode""" """ convert text for mail to unicode"""
if text is None: if text is None:
text = "" text = ""
else: else:
if isinstance(text, str): if isinstance(text, str):
if charset is not None: if charset is None:
text = unicode(text, charset, errors)
else:
text = unicode(text, "utf-8", errors) text = unicode(text, "utf-8", errors)
else:
text = unicode(text, charset, errors)
else: else:
raise Exception("Unsupported mail text type %s" % type(text)) raise Exception("Unsupported mail text type %s" % type(text))
return text.encode("utf-8") return text.encode("utf-8")
@@ -5847,12 +5866,13 @@ class IMAPAdapter(NoSQLAdapter):
charset = message.get_content_charset() charset = message.get_content_charset()
return charset return charset
def reset_mailboxes(self):
self.connection.mailbox_names = None
self.get_mailboxes()
def get_mailboxes(self): def get_mailboxes(self):
""" Query the mail database for mailbox names """ """ Query the mail database for mailbox names """
if self.static_names:
# statically defined mailbox names
self.connection.mailbox_names = self.static_names
return self.static_names.keys()
mailboxes_list = self.connection.list() mailboxes_list = self.connection.list()
self.connection.mailbox_names = dict() self.connection.mailbox_names = dict()
mailboxes = list() mailboxes = list()
@@ -5864,7 +5884,8 @@ class IMAPAdapter(NoSQLAdapter):
sub_items = item.split("\"") sub_items = item.split("\"")
sub_items = [sub_item for sub_item in sub_items \ sub_items = [sub_item for sub_item in sub_items \
if len(sub_item.strip()) > 0] if len(sub_item.strip()) > 0]
mailbox = sub_items[len(sub_items) - 1] # mailbox = sub_items[len(sub_items) -1]
mailbox = sub_items[-1]
# remove unwanted characters and store original names # remove unwanted characters and store original names
# Don't allow leading non alphabetic characters # Don't allow leading non alphabetic characters
mailbox_name = re.sub('^[_0-9]*', '', re.sub('[^_\w]','',re.sub('[/ ]','_',mailbox))) mailbox_name = re.sub('^[_0-9]*', '', re.sub('[^_\w]','',re.sub('[/ ]','_',mailbox)))
@@ -5896,7 +5917,7 @@ class IMAPAdapter(NoSQLAdapter):
else: else:
return False return False
def define_tables(self): def define_tables(self, mailbox_names=None):
""" """
Auto create common IMAP fileds Auto create common IMAP fileds
@@ -5908,11 +5929,18 @@ class IMAPAdapter(NoSQLAdapter):
Returns a dictionary with tablename, server native mailbox name Returns a dictionary with tablename, server native mailbox name
pairs. pairs.
""" """
if mailbox_names:
# optional statically declared mailboxes
self.static_names = mailbox_names
else:
self.static_names = None
if not isinstance(self.connection.mailbox_names, dict): if not isinstance(self.connection.mailbox_names, dict):
self.get_mailboxes() self.get_mailboxes()
mailboxes = self.connection.mailbox_names.keys()
for mailbox_name in mailboxes: names = self.connection.mailbox_names.keys()
self.db.define_table("%s" % mailbox_name,
for name in names:
self.db.define_table("%s" % name,
Field("uid", "string", writable=False), Field("uid", "string", writable=False),
Field("answered", "boolean"), Field("answered", "boolean"),
Field("created", "datetime", writable=False), Field("created", "datetime", writable=False),
@@ -5930,13 +5958,19 @@ class IMAPAdapter(NoSQLAdapter):
Field("subject", "string", writable=False), Field("subject", "string", writable=False),
Field("mime", "string", writable=False), Field("mime", "string", writable=False),
Field("email", "string", writable=False, readable=False), Field("email", "string", writable=False, readable=False),
Field("attachments", "list:string", writable=False, readable=False), Field("attachments", list, writable=False, readable=False),
Field("encoding")
) )
# Set a special _mailbox attribute for storing # Set a special _mailbox attribute for storing
# native mailbox names # native mailbox names
self.db[mailbox_name].mailbox = \ self.db[name].mailbox = \
self.connection.mailbox_names[mailbox_name] self.connection.mailbox_names[name]
# decode quoted printable
self.db[name].to.represent = self.db[name].cc.represent = \
self.db[name].bcc.represent = self.db[name].sender.represent = \
self.db[name].subject.represent = self.header_represent
# Set the db instance mailbox collections # Set the db instance mailbox collections
self.db.mailboxes = self.connection.mailbox_names self.db.mailboxes = self.connection.mailbox_names
@@ -5952,7 +5986,7 @@ class IMAPAdapter(NoSQLAdapter):
query = self.common_filter(query, [self.get_query_mailbox(query),]) query = self.common_filter(query, [self.get_query_mailbox(query),])
return str(query) return str(query)
def select(self,query,fields,attributes): def select(self, query, fields, attributes):
""" Search and Fetch records and return web2py rows """ Search and Fetch records and return web2py rows
""" """
# move this statement elsewhere (upper-level) # move this statement elsewhere (upper-level)
@@ -5960,19 +5994,22 @@ class IMAPAdapter(NoSQLAdapter):
query = self.common_filter(query, [self.get_query_mailbox(query),]) query = self.common_filter(query, [self.get_query_mailbox(query),])
import email import email
import email.header
decode_header = email.header.decode_header
# get records from imap server with search + fetch # get records from imap server with search + fetch
# convert results to a dictionary # convert results to a dictionary
tablename = None tablename = None
fetch_results = list() fetch_results = list()
if isinstance(query, Query): if isinstance(query, Query):
tablename = self.get_table(query) tablename = self.get_table(query)
mailbox = self.connection.mailbox_names.get(tablename, None) mailbox = self.connection.mailbox_names.get(tablename, None)
if mailbox is not None: if mailbox is None:
raise ValueError("Mailbox name not found: %s" % mailbox)
else:
# select with readonly # select with readonly
selected = self.connection.select(mailbox, True) result, selected = self.connection.select(mailbox, True)
self.mailbox_size = int(selected[1][0]) if result != "OK":
raise Exception("IMAP error: %s" % selected)
self.mailbox_size = int(selected[0])
search_query = "(%s)" % str(query).strip() search_query = "(%s)" % str(query).strip()
search_result = self.connection.uid("search", None, search_query) search_result = self.connection.uid("search", None, search_query)
# Normal IMAP response OK is assumed (change this) # Normal IMAP response OK is assumed (change this)
@@ -5981,18 +6018,22 @@ class IMAPAdapter(NoSQLAdapter):
# ten records (change for non-experimental implementation) # ten records (change for non-experimental implementation)
# However, light responses are not guaranteed with this # However, light responses are not guaranteed with this
# approach, just fewer messages. # approach, just fewer messages.
# TODO: change limitby single to 2-tuple argument
limitby = attributes.get('limitby', None) limitby = attributes.get('limitby', None)
messages_set = search_result[1][0].split() messages_set = search_result[1][0].split()
# descending order # descending order
messages_set.reverse() messages_set.reverse()
if limitby is not None: if limitby is not None:
# TODO: asc/desc attributes # TODO: orderby, asc/desc, limitby from complete message set
messages_set = messages_set[int(limitby[0]):int(limitby[1])] messages_set = messages_set[int(limitby[0]):int(limitby[1])]
# Partial fetches are not used since the email
# library does not seem to support it (it converts # keep the requests small for header/flags
# partial messages to mangled message instances) if any([(field.name in ["content",
imap_fields = "(RFC822)" "attachments", "email"]) for
field in fields]):
imap_fields = "(RFC822 FLAGS)"
else:
imap_fields = "(RFC822.HEADER FLAGS)"
if len(messages_set) > 0: if len(messages_set) > 0:
# create fetch results object list # create fetch results object list
# fetch each remote message and store it in memmory # fetch each remote message and store it in memmory
@@ -6008,16 +6049,13 @@ class IMAPAdapter(NoSQLAdapter):
"raw_message": data[0][1]} "raw_message": data[0][1]}
fr["multipart"] = fr["email"].is_multipart() fr["multipart"] = fr["email"].is_multipart()
# fetch flags for the message # fetch flags for the message
ftyp, fdata = self.connection.uid("fetch", uid, "(FLAGS)") fr["flags"] = self.driver.ParseFlags(data[1])
if ftyp == "OK": fetch_results.append(fr)
fr["flags"] = self.driver.ParseFlags(fdata[0])
fetch_results.append(fr)
else:
# error retrieving the flags for this message
pass
else: else:
# error retrieving the message body # error retrieving the message body
pass raise Exception("IMAP error retrieving the body: %s" % data)
else:
raise Exception("IMAP search error: %s" % search_result[1])
elif isinstance(query, (Expression, basestring)): elif isinstance(query, (Expression, basestring)):
raise NotImplementedError() raise NotImplementedError()
else: else:
@@ -6033,11 +6071,11 @@ class IMAPAdapter(NoSQLAdapter):
else: else:
allfields = False allfields = False
if allfields: if allfields:
fieldnames = ["%s.%s" % (tablename, field) for field in self.search_fields.keys()] colnames = ["%s.%s" % (tablename, field) for field in self.search_fields.keys()]
else: else:
fieldnames = ["%s.%s" % (tablename, field.name) for field in fields] colnames = ["%s.%s" % (tablename, field.name) for field in fields]
for k in fieldnames: for k in colnames:
imapfields_dict[k] = k imapfields_dict[k] = k
imapqry_list = list() imapqry_list = list()
@@ -6061,62 +6099,58 @@ class IMAPAdapter(NoSQLAdapter):
# pending: search flags states trough the email message # pending: search flags states trough the email message
# instances for correct output # instances for correct output
if "%s.id" % tablename in fieldnames: # preserve subject encoding (ASCII/quoted printable)
if "%s.id" % tablename in colnames:
item_dict["%s.id" % tablename] = n item_dict["%s.id" % tablename] = n
if "%s.created" % tablename in fieldnames: if "%s.created" % tablename in colnames:
item_dict["%s.created" % tablename] = self.convert_date(message["Date"]) item_dict["%s.created" % tablename] = self.convert_date(message["Date"])
if "%s.uid" % tablename in fieldnames: if "%s.uid" % tablename in colnames:
item_dict["%s.uid" % tablename] = uid item_dict["%s.uid" % tablename] = uid
if "%s.sender" % tablename in fieldnames: if "%s.sender" % tablename in colnames:
# If there is no encoding found in the message header # If there is no encoding found in the message header
# force utf-8 replacing characters (change this to # force utf-8 replacing characters (change this to
# module's defaults). Applies to .sender, .to, .cc and .bcc fields # module's defaults). Applies to .sender, .to, .cc and .bcc fields
############################################################################# item_dict["%s.sender" % tablename] = message["From"]
# TODO: External function to manage encoding and decoding of message strings if "%s.to" % tablename in colnames:
############################################################################# item_dict["%s.to" % tablename] = message["To"]
item_dict["%s.sender" % tablename] = self.encode_text(message["From"], charset) if "%s.cc" % tablename in colnames:
if "%s.to" % tablename in fieldnames:
item_dict["%s.to" % tablename] = self.encode_text(message["To"], charset)
if "%s.cc" % tablename in fieldnames:
if "Cc" in message.keys(): if "Cc" in message.keys():
item_dict["%s.cc" % tablename] = self.encode_text(message["Cc"], charset) item_dict["%s.cc" % tablename] = message["Cc"]
else: else:
item_dict["%s.cc" % tablename] = "" item_dict["%s.cc" % tablename] = ""
if "%s.bcc" % tablename in fieldnames: if "%s.bcc" % tablename in colnames:
if "Bcc" in message.keys(): if "Bcc" in message.keys():
item_dict["%s.bcc" % tablename] = self.encode_text(message["Bcc"], charset) item_dict["%s.bcc" % tablename] = message["Bcc"]
else: else:
item_dict["%s.bcc" % tablename] = "" item_dict["%s.bcc" % tablename] = ""
if "%s.deleted" % tablename in fieldnames: if "%s.deleted" % tablename in colnames:
item_dict["%s.deleted" % tablename] = "\\Deleted" in flags item_dict["%s.deleted" % tablename] = "\\Deleted" in flags
if "%s.draft" % tablename in fieldnames: if "%s.draft" % tablename in colnames:
item_dict["%s.draft" % tablename] = "\\Draft" in flags item_dict["%s.draft" % tablename] = "\\Draft" in flags
if "%s.flagged" % tablename in fieldnames: if "%s.flagged" % tablename in colnames:
item_dict["%s.flagged" % tablename] = "\\Flagged" in flags item_dict["%s.flagged" % tablename] = "\\Flagged" in flags
if "%s.recent" % tablename in fieldnames: if "%s.recent" % tablename in colnames:
item_dict["%s.recent" % tablename] = "\\Recent" in flags item_dict["%s.recent" % tablename] = "\\Recent" in flags
if "%s.seen" % tablename in fieldnames: if "%s.seen" % tablename in colnames:
item_dict["%s.seen" % tablename] = "\\Seen" in flags item_dict["%s.seen" % tablename] = "\\Seen" in flags
if "%s.subject" % tablename in fieldnames: if "%s.subject" % tablename in colnames:
subject = message["Subject"] item_dict["%s.subject" % tablename] = message["Subject"]
decoded_subject = decode_header(subject) if "%s.answered" % tablename in colnames:
text = decoded_subject[0][0]
encoding = decoded_subject[0][1]
if encoding in (None, ""):
encoding = charset
item_dict["%s.subject" % tablename] = self.encode_text(text, encoding)
if "%s.answered" % tablename in fieldnames:
item_dict["%s.answered" % tablename] = "\\Answered" in flags item_dict["%s.answered" % tablename] = "\\Answered" in flags
if "%s.mime" % tablename in fieldnames: if "%s.mime" % tablename in colnames:
item_dict["%s.mime" % tablename] = message.get_content_type() item_dict["%s.mime" % tablename] = message.get_content_type()
if "%s.encoding" % tablename in colnames:
item_dict["%s.encoding" % tablename] = charset
# Here goes the whole RFC822 body as an email instance # Here goes the whole RFC822 body as an email instance
# for controller side custom processing # for controller side custom processing
# The message is stored as a raw string # The message is stored as a raw string
# >> email.message_from_string(raw string) # >> email.message_from_string(raw string)
# returns a Message object for enhanced object processing # returns a Message object for enhanced object processing
if "%s.email" % tablename in fieldnames: if "%s.email" % tablename in colnames:
item_dict["%s.email" % tablename] = self.encode_text(raw_message, charset) # WARNING: no encoding performed (raw message)
item_dict["%s.email" % tablename] = raw_message
# Size measure as suggested in a Velocity Reviews post # Size measure as suggested in a Velocity Reviews post
# by Tim Williams: "how to get size of email attachment" # by Tim Williams: "how to get size of email attachment"
@@ -6124,18 +6158,31 @@ class IMAPAdapter(NoSQLAdapter):
# To retrieve the server size for representation would add a new # To retrieve the server size for representation would add a new
# fetch transaction to the process # fetch transaction to the process
for part in message.walk(): for part in message.walk():
if "%s.attachments" % tablename in fieldnames: maintype = part.get_content_maintype()
if not "text" in part.get_content_maintype(): if ("%s.attachments" % tablename in colnames) or \
attachments.append(part.get_payload(decode=True)) ("%s.content" % tablename in colnames):
if "%s.content" % tablename in fieldnames: if "%s.attachments" % tablename in colnames:
if "text" in part.get_content_maintype(): if not ("text" in maintype):
payload = self.encode_text(part.get_payload(decode=True), charset) payload = part.get_payload(decode=True)
content.append(payload) if payload:
if "%s.size" % tablename in fieldnames: attachment = {
"payload": payload,
"filename": part.get_filename(),
"encoding": part.get_content_charset(),
"mime": part.get_content_type(),
"disposition": part["Content-Disposition"]}
attachments.append(attachment)
if "%s.content" % tablename in colnames:
payload = part.get_payload(decode=True)
part_charset = self.get_charset(part)
if "text" in maintype:
if payload:
content.append(self.encode_text(payload, part_charset))
if "%s.size" % tablename in colnames:
if part is not None: if part is not None:
size += len(str(part)) size += len(str(part))
item_dict["%s.content" % tablename] = bar_encode(content) item_dict["%s.content" % tablename] = bar_encode(content)
item_dict["%s.attachments" % tablename] = bar_encode(attachments) item_dict["%s.attachments" % tablename] = attachments
item_dict["%s.size" % tablename] = size item_dict["%s.size" % tablename] = size
imapqry_list.append(item_dict) imapqry_list.append(item_dict)
@@ -6143,12 +6190,12 @@ class IMAPAdapter(NoSQLAdapter):
# creation (sends an array or lists) # creation (sends an array or lists)
for item_dict in imapqry_list: for item_dict in imapqry_list:
imapqry_array_item = list() imapqry_array_item = list()
for fieldname in fieldnames: for fieldname in colnames:
imapqry_array_item.append(item_dict[fieldname]) imapqry_array_item.append(item_dict[fieldname])
imapqry_array.append(imapqry_array_item) imapqry_array.append(imapqry_array_item)
# parse result and return a rows object # parse result and return a rows object
colnames = fieldnames colnames = colnames
processor = attributes.get('processor',self.parse) processor = attributes.get('processor',self.parse)
return processor(imapqry_array, fields, colnames) return processor(imapqry_array, fields, colnames)
@@ -6193,6 +6240,8 @@ class IMAPAdapter(NoSQLAdapter):
result, data = self.connection.store(*command) result, data = self.connection.store(*command)
if result == "OK": if result == "OK":
rowcount += 1 rowcount += 1
else:
raise Exception("IMAP storing error: %s" % data)
return rowcount return rowcount
def _count(self, query, distinct=None): def _count(self, query, distinct=None):
@@ -6224,6 +6273,8 @@ class IMAPAdapter(NoSQLAdapter):
result, data = self.connection.store(number, "+FLAGS", "(\\Deleted)") result, data = self.connection.store(number, "+FLAGS", "(\\Deleted)")
if result == "OK": if result == "OK":
counter += 1 counter += 1
else:
raise Exception("IMAP store error: %s" % data)
if counter > 0: if counter > 0:
result, data = self.connection.expunge() result, data = self.connection.expunge()
return counter return counter
@@ -6662,6 +6713,89 @@ class Row(object):
del d[k] del d[k]
return d return d
def as_xml(self, row_name="row", colnames=None, indent=' '):
def f(row,field,indent=' '):
if isinstance(row,Row):
spc = indent+' \n'
items = [f(row[x],x,indent+' ') for x in row]
return '%s<%s>\n%s\n%s</%s>' % (
indent,
field,
spc.join(item for item in items if item),
indent,
field)
elif not callable(row):
if REGEX_ALPHANUMERIC.match(field):
return '%s<%s>%s</%s>' % (indent,field,row,field)
else:
return '%s<extra name="%s">%s</extra>' % \
(indent,field,row)
else:
return None
return f(self, row_name, indent=indent)
def as_json(self, mode="object", default=None, colnames=None,
serialize=True, **kwargs):
"""
serializes the table to a JSON list of objects
kwargs are passed to .as_dict method
only "object" mode supported for single row
serialize = False used by Rows.as_json
TODO: return array mode with query column order
"""
def inner_loop(record, col):
(t, f) = col.split('.')
res = None
if not REGEX_TABLE_DOT_FIELD.match(col):
key = col
res = record._extra[col]
else:
key = f
if isinstance(record.get(t, None), Row):
res = record[t][f]
else:
res = record[f]
if mode == 'object':
return (key, res)
else:
return res
multi = any([isinstance(v, self.__class__) for v in self.values()])
mode = mode.lower()
if not mode in ['object', 'array']:
raise SyntaxError('Invalid JSON serialization mode: %s' % mode)
if mode=='object' and colnames:
item = dict([inner_loop(self, col) for col in colnames])
elif colnames:
item = [inner_loop(self, col) for col in colnames]
else:
if not mode == 'object':
raise SyntaxError('Invalid JSON serialization mode: %s' % mode)
if multi:
item = dict()
[item.update(**v.as_dict(**kwargs)) for v in self.values()]
else:
item = self.as_dict(**kwargs)
if serialize:
if have_serializers:
return serializers.json(item,
default=default or
serializers.custom_json)
else:
try:
import json as simplejson
except ImportError:
import gluon.contrib.simplejson as simplejson
return simplejson.dumps(item)
else:
return item
################################################################################ ################################################################################
# Everything below should be independent of the specifics of the database # Everything below should be independent of the specifics of the database
# and should work for RDBMs and some NoSQL databases # and should work for RDBMs and some NoSQL databases
@@ -6857,7 +6991,7 @@ class DAL(object):
for db in db_group: for db in db_group:
if not db._uri: if not db._uri:
continue continue
k = REGEX_NOPASSWD.sub('******',db._uri) k = hide_password(db._uri)
infos[k] = dict(dbstats = [(row[0], row[1]) for row in db._timings], infos[k] = dict(dbstats = [(row[0], row[1]) for row in db._timings],
dbtables = {'defined': dbtables = {'defined':
sorted(list(set(db.tables) - sorted(list(set(db.tables) -
@@ -7163,6 +7297,8 @@ def index():
i = 0 i = 0
while i<len(patterns): while i<len(patterns):
pattern = patterns[i] pattern = patterns[i]
if not isinstance(pattern,str):
pattern = pattern[0]
tokens = pattern.split('/') tokens = pattern.split('/')
if tokens[-1].startswith(':auto') and re2.match(tokens[-1]): if tokens[-1].startswith(':auto') and re2.match(tokens[-1]):
new_patterns = auto_table(tokens[-1][tokens[-1].find('[')+1:-1], new_patterns = auto_table(tokens[-1][tokens[-1].find('[')+1:-1],
@@ -7239,15 +7375,21 @@ def index():
ref = tag[tag.find('[')+1:-1] ref = tag[tag.find('[')+1:-1]
if '.' in ref and otable: if '.' in ref and otable:
table,field = ref.split('.') table,field = ref.split('.')
# print table,field selfld = '_id'
if db[table][field].type.startswith('reference '):
refs = [ x.name for x in db[otable] if x.type == db[table][field].type ]
else:
refs = [ x.name for x in db[table]._referenced_by if x.tablename==otable ]
if refs:
selfld = refs[0]
if nested_select: if nested_select:
try: try:
dbset=db(db[table][field].belongs(dbset._select(db[otable]._id))) dbset=db(db[table][field].belongs(dbset._select(db[otable][selfld])))
except ValueError: except ValueError:
return Row({'status':400,'pattern':pattern, return Row({'status':400,'pattern':pattern,
'error':'invalid path','response':None}) 'error':'invalid path','response':None})
else: else:
items = [item.id for item in dbset.select(db[otable]._id)] items = [item.id for item in dbset.select(db[otable][selfld])]
dbset=db(db[table][field].belongs(items)) dbset=db(db[table][field].belongs(items))
else: else:
table = ref table = ref
@@ -8062,7 +8204,7 @@ class Table(object):
return fields return fields
def _insert(self, **fields): def _insert(self, **fields):
fields = self._default(fields) fields = self._defaults(fields)
return self._db._adapter._insert(self, self._listify(fields)) return self._db._adapter._insert(self, self._listify(fields))
def insert(self, **fields): def insert(self, **fields):
@@ -9461,6 +9603,22 @@ class Rows(object):
:param storage_to_dict: when True returns a dict, otherwise a list(default True) :param storage_to_dict: when True returns a dict, otherwise a list(default True)
:param datetime_to_str: convert datetime fields as strings (default True) :param datetime_to_str: convert datetime fields as strings (default True)
""" """
# test for multiple rows
multi = False
f = self.first()
if f:
multi = any([isinstance(v, f.__class__) for v in f.values()])
if (not "." in key) and multi:
# No key provided, default to int indices
def new_key():
i = 0
while True:
yield i
i += 1
key_generator = new_key()
key = lambda r: key_generator.next()
rows = self.as_list(compact, storage_to_dict, datetime_to_str, custom_types) rows = self.as_list(compact, storage_to_dict, datetime_to_str, custom_types)
if isinstance(key,str) and key.count('.')==1: if isinstance(key,str) and key.count('.')==1:
(table, field) = key.split('.') (table, field) = key.split('.')
@@ -9537,64 +9695,30 @@ class Rows(object):
""" """
serializes the table using sqlhtml.SQLTABLE (if present) serializes the table using sqlhtml.SQLTABLE (if present)
""" """
if strict: if strict:
ncols = len(self.colnames) ncols = len(self.colnames)
def f(row,field,indent=' '): return '<%s>\n%s\n</%s>' % (rows_name,
if isinstance(row,Row): '\n'.join(row.as_xml(row_name=row_name,
spc = indent+' \n' colnames=self.colnames) for
items = [f(row[x],x,indent+' ') for x in row] row in self), rows_name)
return '%s<%s>\n%s\n%s</%s>' % (
indent,
field,
spc.join(item for item in items if item),
indent,
field)
elif not callable(row):
if REGEX_ALPHANUMERIC.match(field):
return '%s<%s>%s</%s>' % (indent,field,row,field)
else:
return '%s<extra name="%s">%s</extra>' % \
(indent,field,row)
else:
return None
return '<%s>\n%s\n</%s>' % (
rows_name,
'\n'.join(f(row,row_name) for row in self),
rows_name)
import sqlhtml import sqlhtml
return sqlhtml.SQLTABLE(self).xml() return sqlhtml.SQLTABLE(self).xml()
def json(self, mode='object', default=None): def as_xml(self,row_name='row',rows_name='rows'):
return self.xml(strict=True, row_name=row_name, rows_name=rows_name)
def as_json(self, mode='object', default=None):
""" """
serializes the table to a JSON list of objects serializes the table to a JSON list of objects
""" """
mode = mode.lower()
if not mode in ['object', 'array']:
raise SyntaxError('Invalid JSON serialization mode: %s' % mode)
def inner_loop(record, col): items = [record.as_json(mode=mode, default=default,
(t, f) = col.split('.') serialize=False,
res = None colnames=self.colnames) for
if not REGEX_TABLE_DOT_FIELD.match(col): record in self]
key = col
res = record._extra[col]
else:
key = f
if isinstance(record.get(t, None), Row):
res = record[t][f]
else:
res = record[f]
if mode == 'object':
return (key, res)
else:
return res
if mode == 'object':
items = [dict([inner_loop(record, col) for col in
self.colnames]) for record in self]
else:
items = [[inner_loop(record, col) for col in self.colnames]
for record in self]
if have_serializers: if have_serializers:
return serializers.json(items, return serializers.json(items,
default=default or default=default or
@@ -9606,6 +9730,9 @@ class Rows(object):
import gluon.contrib.simplejson as simplejson import gluon.contrib.simplejson as simplejson
return simplejson.dumps(items) return simplejson.dumps(items)
# for consistent naming yet backwards compatible
as_csv = __str__
json = as_json
################################################################################ ################################################################################
# dummy function used to define some doctests # dummy function used to define some doctests
+4 -2
View File
@@ -375,7 +375,7 @@ class Response(Storage):
wrapped = streamer(stream, chunk_size=chunk_size) wrapped = streamer(stream, chunk_size=chunk_size)
return wrapped return wrapped
def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True): def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True, download_filename=None):
""" """
example of usage in controller:: example of usage in controller::
@@ -403,9 +403,11 @@ class Response(Storage):
raise HTTP(404) raise HTTP(404)
headers = self.headers headers = self.headers
headers['Content-Type'] = contenttype(name) headers['Content-Type'] = contenttype(name)
if download_filename == None:
download_filename = filename
if attachment: if attachment:
headers['Content-Disposition'] = \ headers['Content-Disposition'] = \
'attachment; filename="%s"' % filename.replace('"','\"') 'attachment; filename="%s"' % download_filename.replace('"','\"')
return self.stream(stream, chunk_size=chunk_size, request=request) return self.stream(stream, chunk_size=chunk_size, request=request)
def json(self, data, default=None): def json(self, data, default=None):
+3 -2
View File
@@ -556,7 +556,7 @@ class Scheduler(MetaScheduler):
self.die() self.die()
def wrapped_assign_tasks(self, db): def wrapped_assign_tasks(self, db):
db.commit() # ?don't know if it's useful, let's be completely sure db.commit() #db.commit() only for Mysql
x = 0 x = 0
while x < 10: while x < 10:
try: try:
@@ -571,6 +571,7 @@ class Scheduler(MetaScheduler):
def wrapped_pop_task(self): def wrapped_pop_task(self):
db = self.db db = self.db
db.commit() #another nifty db.commit() only for Mysql
x = 0 x = 0
while x < 10: while x < 10:
try: try:
@@ -579,7 +580,7 @@ class Scheduler(MetaScheduler):
break break
except: except:
db.rollback() db.rollback()
logger.error('TICKER(%s): error popping tasks', self.worker_name) logger.error('%s: error popping tasks', self.worker_name)
x += 1 x += 1
time.sleep(0.5) time.sleep(0.5)
+25 -7
View File
@@ -1623,7 +1623,7 @@ class SQLFORM(FORM):
'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 'integer': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
'double': ['=', '!=', '<', '>', '<=', '>='], 'double': ['=', '!=', '<', '>', '<=', '>='],
'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 'id': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'],
'reference': ['=', '!=', '<', '>', '<=', '>=', 'in', 'not in'], 'reference': ['=', '!='],
'boolean': ['=', '!=']} 'boolean': ['=', '!=']}
if fields[0]._db._adapter.dbengine == 'google:datastore': if fields[0]._db._adapter.dbengine == 'google:datastore':
search_options['string'] = ['=', '!=', '<', '>', '<=', '>='] search_options['string'] = ['=', '!=', '<', '>', '<=', '>=']
@@ -1641,15 +1641,33 @@ class SQLFORM(FORM):
field.label, str) and T(field.label) or field.label field.label, str) and T(field.label) or field.label
selectfields.append(OPTION(label, _value=str(field))) selectfields.append(OPTION(label, _value=str(field)))
operators = SELECT(*[OPTION(T(option), _value=option) for option in options]) operators = SELECT(*[OPTION(T(option), _value=option) for option in options])
_id = "%s_%s" % (value_id,name)
if field.type == 'boolean': if field.type == 'boolean':
value_input = SQLFORM.widgets.boolean.widget(field,field.default,_id=_id)
elif field.type == 'double':
value_input = SQLFORM.widgets.double.widget(field,field.default,_id=_id)
elif field.type == 'time':
value_input = SQLFORM.widgets.time.widget(field,field.default,_id=_id)
elif field.type == 'date':
value_input = SQLFORM.widgets.date.widget(field,field.default,_id=_id)
elif field.type == 'datetime':
value_input = SQLFORM.widgets.datetime.widget(field,field.default,_id=_id)
elif (field.type.startswith('reference ') or
field.type.startswith('list:reference ')) and \
hasattr(field.requires,'options'):
value_input = SELECT( value_input = SELECT(
OPTION(T("True"), _value="T"), *[OPTION(v, _value=k)
OPTION(T("False"), _value="F"), for k,v in field.requires.options()],
_id="%s_%s" % (value_id,name)) **dict(_id=_id))
elif field.type == 'integer' or \
field.type.startswith('reference ') or \
field.type.startswith('list:integer') or \
field.type.startswith('list:reference '):
value_input = SQLFORM.widgets.integer.widget(field,field.default,_id=_id)
else: else:
value_input = INPUT(_type='text', value_input = INPUT(
_id="%s_%s" % (value_id,name), _type='text', _id=_id, _class=field.type)
_class=field.type)
new_button = INPUT( new_button = INPUT(
_type="button", _value=T('New'), _class="btn", _type="button", _value=T('New'), _class="btn",
_onclick="%s_build_query('new','%s')" % (prefix,field)) _onclick="%s_build_query('new','%s')" % (prefix,field))
+53 -46
View File
@@ -1256,6 +1256,9 @@ class Auth(object):
def navbar(self, prefix='Welcome', action=None, def navbar(self, prefix='Welcome', action=None,
separators=(' [ ', ' | ', ' ] '), user_identifier=DEFAULT, separators=(' [ ', ' | ', ' ] '), user_identifier=DEFAULT,
referrer_actions=DEFAULT, mode='default'): referrer_actions=DEFAULT, mode='default'):
def Anr(*a,**b):
b['_rel']='nofollow'
return A(*a,**b)
referrer_actions = [] if not referrer_actions else referrer_actions referrer_actions = [] if not referrer_actions else referrer_actions
request = current.request request = current.request
asdropdown = (mode == 'dropdown') asdropdown = (mode == 'dropdown')
@@ -1286,19 +1289,19 @@ class Auth(object):
user_identifier = user_identifier % self.user user_identifier = user_identifier % self.user
if not user_identifier: if not user_identifier:
user_identifier = '' user_identifier = ''
logout = A(T('Logout'), _href='%s/logout?_next=%s' % logout = Anr(T('Logout'), _href='%s/logout?_next=%s' %
(action, urllib.quote(self.settings.logout_next))) (action, urllib.quote(self.settings.logout_next)))
profile = A(T('Profile'), _href=href('profile')) profile = Anr(T('Profile'), _href=href('profile'))
password = A(T('Password'), _href=href('change_password')) password = Anr(T('Password'), _href=href('change_password'))
bar = SPAN( bar = SPAN(
prefix, user_identifier, s1, logout, s3, _class='auth_navbar') prefix, user_identifier, s1, logout, s3, _class='auth_navbar')
if asdropdown: if asdropdown:
logout = LI(A(I(_class='icon-off'), ' ' + T('Logout'), _href='%s/logout?_next=%s' % logout = LI(Anr(I(_class='icon-off'), ' ' + T('Logout'), _href='%s/logout?_next=%s' %
(action, urllib.quote(self.settings.logout_next)))) # the space before T('Logout') is intentional. It creates a gap between icon and text (action, urllib.quote(self.settings.logout_next)))) # the space before T('Logout') is intentional. It creates a gap between icon and text
profile = LI(A(I(_class='icon-user'), ' ' + profile = LI(Anr(I(_class='icon-user'), ' ' +
T('Profile'), _href=href('profile'))) T('Profile'), _href=href('profile')))
password = LI(A(I(_class='icon-lock'), ' ' + password = LI(Anr(I(_class='icon-lock'), ' ' +
T('Password'), _href=href('change_password'))) T('Password'), _href=href('change_password')))
bar = UL(logout, _class='dropdown-menu') bar = UL(logout, _class='dropdown-menu')
# logout will be the last item in list # logout will be the last item in list
@@ -1312,21 +1315,21 @@ class Auth(object):
bar.insert(-1, s2) bar.insert(-1, s2)
bar.insert(-1, password) bar.insert(-1, password)
else: else:
login = A(T('Login'), _href=href('login')) login = Anr(T('Login'), _href=href('login'))
register = A(T('Register'), _href=href('register')) register = Anr(T('Register'), _href=href('register'))
retrieve_username = A( retrieve_username = Anr(
T('Forgot username?'), _href=href('retrieve_username')) T('Forgot username?'), _href=href('retrieve_username'))
lost_password = A( lost_password = Anr(
T('Lost password?'), _href=href('request_reset_password')) T('Lost password?'), _href=href('request_reset_password'))
bar = SPAN(s1, login, s3, _class='auth_navbar') bar = SPAN(s1, login, s3, _class='auth_navbar')
if asdropdown: if asdropdown:
login = LI(A(I(_class='icon-off'), ' ' + T('Login'), _href=href('login'))) # the space before T('Login') is intentional. It creates a gap between icon and text login = LI(Anr(I(_class='icon-off'), ' ' + T('Login'), _href=href('login'))) # the space before T('Login') is intentional. It creates a gap between icon and text
register = LI(A(I(_class='icon-user'), register = LI(Anr(I(_class='icon-user'),
' ' + T('Register'), _href=href('register'))) ' ' + T('Register'), _href=href('register')))
retrieve_username = LI(A(I(_class='icon-edit'), ' ' + T( retrieve_username = LI(Anr(I(_class='icon-edit'), ' ' + T(
'Forgot username?'), _href=href('retrieve_username'))) 'Forgot username?'), _href=href('retrieve_username')))
lost_password = LI(A(I(_class='icon-lock'), ' ' + T( lost_password = LI(Anr(I(_class='icon-lock'), ' ' + T(
'Lost password?'), _href=href('request_reset_password'))) 'Lost password?'), _href=href('request_reset_password')))
bar = UL(login, _class='dropdown-menu') bar = UL(login, _class='dropdown-menu')
# login will be the last item in list # login will be the last item in list
@@ -1349,10 +1352,10 @@ class Auth(object):
if asdropdown: if asdropdown:
bar.insert(-1, LI('', _class='divider')) bar.insert(-1, LI('', _class='divider'))
if self.user_id: if self.user_id:
bar = LI(A(prefix, user_identifier, _href='#'), bar = LI(Anr(prefix, user_identifier, _href='#'),
bar, _class='dropdown') bar, _class='dropdown')
else: else:
bar = LI(A(T('Login'), _href='#'), bar = LI(Anr(T('Login'), _href='#'),
bar, _class='dropdown') bar, _class='dropdown')
return bar return bar
@@ -4761,7 +4764,7 @@ class Wiki(object):
Field('slug', Field('slug',
requires=[IS_SLUG(), requires=[IS_SLUG(),
IS_NOT_IN_DB(db, 'wiki_page.slug')], IS_NOT_IN_DB(db, 'wiki_page.slug')],
readable=False, writable=False), writable=False),
Field('title', unique=True), Field('title', unique=True),
Field('body', 'text', notnull=True), Field('body', 'text', notnull=True),
Field('tags', 'list:string'), Field('tags', 'list:string'),
@@ -4970,7 +4973,7 @@ class Wiki(object):
slug.startswith(self.force_prefix)): slug.startswith(self.force_prefix)):
current.session.flash = 'slug must have "%s" prefix' \ current.session.flash = 'slug must have "%s" prefix' \
% self.force_prefix % self.force_prefix
redirect(URL(args=('_edit', self.force_prefix + slug))) redirect(URL(args=('_create')))
db.wiki_page.can_read.default = [Wiki.everybody] db.wiki_page.can_read.default = [Wiki.everybody]
db.wiki_page.can_edit.default = [auth.user_group_role()] db.wiki_page.can_edit.default = [auth.user_group_role()]
db.wiki_page.title.default = title_guess db.wiki_page.title.default = title_guess
@@ -4978,8 +4981,8 @@ class Wiki(object):
if slug == 'wiki-menu': if slug == 'wiki-menu':
db.wiki_page.body.default = \ db.wiki_page.body.default = \
'- Menu Item > @////index\n- - Submenu > http://web2py.com' '- Menu Item > @////index\n- - Submenu > http://web2py.com'
else: #else:
db.wiki_page.body.default = db(db.wiki_page.id==from_template).select(db.wiki_page.body)[0].body if int(from_template) > 0 else '## %s\n\npage content' % title_guess # db.wiki_page.body.default = db(db.wiki_page.id==from_template).select(db.wiki_page.body)[0].body if int(from_template) > 0 else '## %s\n\npage content' % title_guess
vars = current.request.post_vars vars = current.request.post_vars
if vars.body: if vars.body:
vars.body = vars.body.replace('://%s' % self.host, '://HOSTNAME') vars.body = vars.body.replace('://%s' % self.host, '://HOSTNAME')
@@ -4993,20 +4996,19 @@ class Wiki(object):
redirect(URL(args=slug)) redirect(URL(args=slug))
script = """ script = """
$(function() { $(function() {
if (!$('#wiki_page_body').length) return; if (!jQuery('#wiki_page_body').length) return;
var pagecontent = $('#wiki_page_body'); var pagecontent = jQuery('#wiki_page_body');
pagecontent.css('font-family', pagecontent.css('font-family',
'Monaco,Menlo,Consolas,"Courier New",monospace'); 'Monaco,Menlo,Consolas,"Courier New",monospace');
var prevbutton = $('<button class="btn nopreview">Preview</button>'); var prevbutton = jQuery('<button class="btn nopreview">Preview</button>');
var mediabutton = $('<button class="btn nopreview">Media</button>'); var mediabutton = jQuery('<button class="btn nopreview">Media</button>');
var preview = $('<div id="preview"></div>').hide(); var preview = jQuery('<div id="preview"></div>').hide();
var previewmedia = $('<div id="previewmedia"></div>'); var previewmedia = jQuery('<div id="previewmedia"></div>');
var table = $('form'); var form = pagecontent.closest('form');
var bodylabel = $('#wiki_page_body__label'); preview.insertBefore(form);
preview.insertBefore(pagecontent); prevbutton.insertBefore(form);
prevbutton.insertAfter(bodylabel); mediabutton.insertBefore(form);
mediabutton.insertBefore(table); previewmedia.insertBefore(form);
previewmedia.insertBefore(table);
mediabutton.toggle(function() { mediabutton.toggle(function() {
web2py_component('%(urlmedia)s', 'previewmedia'); web2py_component('%(urlmedia)s', 'previewmedia');
}, function() { }, function() {
@@ -5017,12 +5019,12 @@ class Wiki(object):
if (prevbutton.hasClass('nopreview')) { if (prevbutton.hasClass('nopreview')) {
prevbutton.addClass('preview').removeClass( prevbutton.addClass('preview').removeClass(
'nopreview').html('Edit Source'); 'nopreview').html('Edit Source');
web2py_ajax_page('post', '%(url)s', {body : $('#wiki_page_body').val()}, 'preview'); web2py_ajax_page('post', '%(url)s', {body : jQuery('#wiki_page_body').val()}, 'preview');
pagecontent.fadeOut('fast', function() {preview.fadeIn()}); form.fadeOut('fast', function() {preview.fadeIn()});
} else { } else {
prevbutton.addClass( prevbutton.addClass(
'nopreview').removeClass('preview').html('Preview'); 'nopreview').removeClass('preview').html('Preview');
preview.fadeOut('fast', function() {pagecontent.fadeIn()}); preview.fadeOut('fast', function() {form.fadeIn()});
} }
}) })
}) })
@@ -5046,7 +5048,7 @@ class Wiki(object):
csv = True csv = True
create = True create = True
if current.request.vars.embedded: if current.request.vars.embedded:
script = "var c = $('#wiki_page_body'); c.val(c.val() + $('%s').text()); return false;" script = "var c = jQuery('#wiki_page_body'); c.val(c.val() + jQuery('%s').text()); return false;"
fragment = self.auth.db.wiki_media.id.represent fragment = self.auth.db.wiki_media.id.represent
csv = False csv = False
create = False create = False
@@ -5071,13 +5073,13 @@ class Wiki(object):
slugs=db(db.wiki_page.id>0).select(db.wiki_page.id,db.wiki_page.slug) slugs=db(db.wiki_page.id>0).select(db.wiki_page.id,db.wiki_page.slug)
options=[OPTION(row.slug,_value=row.id) for row in slugs] options=[OPTION(row.slug,_value=row.id) for row in slugs]
options.insert(0, OPTION('',_value='')) options.insert(0, OPTION('',_value=''))
form = SQLFORM.factory(Field("slug", default=current.request.args(1), form = SQLFORM.factory(Field("slug", default=current.request.args(1) or self.force_prefix,
requires=(IS_SLUG(), requires=(IS_SLUG(),
IS_NOT_IN_DB(db,db.wiki_page.slug))), IS_NOT_IN_DB(db,db.wiki_page.slug))),
Field("from_template", "reference wiki_page", #Field("from_template", "reference wiki_page",
requires=IS_EMPTY_OR(IS_IN_DB(db, db.wiki_page, '%(slug)s')), # requires=IS_EMPTY_OR(IS_IN_DB(db, db.wiki_page, '%(slug)s')),
comment=current.T("Choose Template or empty for new Page")), # comment=current.T("Choose Template or empty for new Page")),
_class="well span6") _class="well span6")
form.element("[type=submit]").attributes["_value"] = current.T("Create Page from Slug") form.element("[type=submit]").attributes["_value"] = current.T("Create Page from Slug")
if form.process().accepted: if form.process().accepted:
@@ -5088,21 +5090,26 @@ class Wiki(object):
def pages(self): def pages(self):
if not self.can_manage(): if not self.can_manage():
return self.not_authorized() return self.not_authorized()
self.auth.db.wiki_page.id.represent = lambda id, row: SPAN( self.auth.db.wiki_page.slug.represent = lambda slug, row: SPAN(
'@////%s' % row.slug) '@////%s' % slug)
self.auth.db.wiki_page.title.represent = lambda title, row: \ self.auth.db.wiki_page.title.represent = lambda title, row: \
A(title, _href=URL(args=row.slug)) A(title, _href=URL(args=row.slug))
wiki_table = self.auth.db.wiki_page
content = SQLFORM.grid( content = SQLFORM.grid(
self.auth.db.wiki_page, wiki_table,
fields = [wiki_table.slug,
wiki_table.title, wiki_table.tags,
wiki_table.can_read, wiki_table.can_edit],
links=[ links=[
lambda row: lambda row:
A('edit', _href=URL(args=('_edit', row.slug))), A('edit', _href=URL(args=('_edit', row.slug)),_class='btn'),
lambda row: lambda row:
A('media', _href=URL(args=('_editmedia', row.slug)))], A('media', _href=URL(args=('_editmedia', row.slug)),_class='btn')],
details=False, editable=False, deletable=False, create=False, details=False, editable=False, deletable=False, create=False,
orderby=self.auth.db.wiki_page.title, orderby=self.auth.db.wiki_page.title,
args=['_pages'], args=['_pages'],
user_signature=False) user_signature=False)
return dict(content=content) return dict(content=content)
def media(self, id): def media(self, id):
+1 -2
View File
@@ -510,8 +510,7 @@ class web2pyDialog(object):
if not options.taskbar: if not options.taskbar:
thread.start_new_thread(start_browser, thread.start_new_thread(start_browser,
(get_url(ip, proto=proto, port=port),), (get_url(ip, proto=proto, port=port), True))
dict(startup=True))
self.password.configure(state='readonly') self.password.configure(state='readonly')
[ip.configure(state='disabled') for ip in self.ips.values()] [ip.configure(state='disabled') for ip in self.ips.values()]