Compare commits

..
28 Commits
Author SHA1 Message Date
Fred Escallier 277bf85645 Update French translation. 2019-04-30 11:47:17 +05:30
Akshay Joshi 991fe3d797 Update version for release. 2019-04-30 10:49:49 +05:30
Identifier Anonymous 4009ac3108 Update Japanese translation 2019-04-29 09:21:55 +01:00
Akshay Joshi 92e6a47713 Update message catalogs. 2019-04-26 17:39:45 +05:30
Dave Page e53c2b1a5d Ensure the treeview shows all sequences except those used to implement IDENTITY columns (which can be edited as part of the column). Show all if Show System Objects is enabled. Fixes #4154 2019-04-26 12:36:56 +01:00
Dave Page e536408ce2 Don't try to install Chromedriver using the chromedriver_installer module as it no longer works and doesn't seem to be supported. 2019-04-25 09:41:40 +01:00
Dave Page f74448a1b2 Add a script for installing the required Chromedriver version. 2019-04-25 09:38:52 +01:00
Akshay Joshi 9b5a6e2bb7 Prevent duplicate columns being included in reverse engineered SQL for tables. Fixes #4163 2019-04-24 11:35:20 +01:00
Aditya Toshniwal 3fef078ec8 Remove some unecessary React libraries. 2019-04-24 09:29:01 +01:00
Khushboo Vashi 74af8976c9 Ensure that newly added row in backgrid should be visible. 2019-04-23 17:32:00 +05:30
Khushboo Vashi ecded18333 Fix an issue where 'GRANT UPDATE' sql should be displayed for default sequence privileges. Fixes #4087 2019-04-22 12:24:47 +05:30
Khushboo Vashi 5452bff49e Fixed 'Increment value cannot be empty' error for existing tables. Fixes #4160 2019-04-22 12:04:49 +05:30
Aditya Toshniwal 05ae330030 Improvement for 'beforeunload' event, bind the event only from where it is required. 2019-04-22 11:30:33 +05:30
Dave Page 94e1820ccd Fix skip on Windows. 2019-04-18 11:18:06 +01:00
Aditya Toshniwal 49b318c39e Ensure that confirmation dialog should be popped up before reload of query tool or debugger
if it is opened in a new browser tab. Fixes #4101
2019-04-18 15:39:36 +05:30
Dave Page 4ac064de42 Urgh. Typo. 2019-04-18 09:13:55 +01:00
Dave Page 2ef71cf749 Skip the permissions test on Windows which doesn't support Unix style file permissions. 2019-04-18 09:13:00 +01:00
Aditya Toshniwal d96f453e7f Fixed 'Location cannot be empty' error when open Tablespace properties. Fixes #4134 2019-04-18 12:31:35 +05:30
Khushboo Vashi 0b47d1dbf6 1) Fix an issue where Deleting N number of rows makes first N number of rows disable. Fixes #3605
2) Ensure that on clicking Delete button should not delete rows immediately from the database server,
   it should be deleted when Save button will be clicked. Fixes #2392
2019-04-18 12:09:35 +05:30
Dave Page d728ee7436 Ensure sanity of the permissions on the storage and session directories and the config database. Fixes #4182 2019-04-17 16:57:34 +01:00
Dave Page 1a192109d6 Ensure we create a DMG file that's compatible with macOS 10.10+ 2019-04-15 12:12:46 +01:00
Aditya Toshniwal 024cfd2fab 1) Ensure that JSON strings as comments should be added properly for all the objects. Fixes #3582
2) Fixed alignment issue of columns in definition section of Index node. Fixes #4121
2019-04-12 15:07:07 +05:30
Murtuza Zabuawala a25bb2b7db Add tests for the CSV download. 2019-04-11 17:15:01 +01:00
Fred Escallier fd3337024f Update French translation. 2019-04-11 13:51:40 +01:00
Dave Page ded446fe8a Depend on psycopg2-binary in the Python wheel, rather than psycopg2. Fixes #4165 2019-04-11 10:08:44 +01:00
Khushboo Vashi a9d964b5ca 1) Added support for Default Partition. Fixes #3938
2) Ensure that record should be add/edited for root partition table with primary keys. Fixes #4104
2019-04-11 13:25:24 +05:30
Akshay Joshi 9c3925e448 Ensure that parameters of procedures for EPAS server 10 and below should be set/reset properly. Fixes #4161 2019-04-10 13:22:52 +05:30
Murtuza Zabuawala 6016a5ee98 Fix an issue where the dropdown becomes misaligned/displaced. Fixes #4138 2019-04-09 12:09:25 +05:30
91 changed files with 5800 additions and 4419 deletions

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 247 KiB

+1
View File
@@ -11,6 +11,7 @@ notes for it.
.. toctree::
:maxdepth: 1
release_notes_4_6
release_notes_4_5
release_notes_4_4
release_notes_4_3
+33
View File
@@ -0,0 +1,33 @@
***********
Version 4.6
***********
Release date: 2019-05-02
This release contains a number of new features and fixes reported since the
release of pgAdmin4 4.5
Features
********
| `Feature #4165 <https://redmine.postgresql.org/issues/4165>`_ - Depend on psycopg2-binary in the Python wheel, rather than psycopg2.
Bug fixes
*********
| `Bug #2392 <https://redmine.postgresql.org/issues/2392>`_ - Ensure that on clicking Delete button should not delete rows immediately from the database server, it should be deleted when Save button will be clicked.
| `Bug #3327 <https://redmine.postgresql.org/issues/3327>`_ - Ensure that newly added row in backgrid should be visible.
| `Bug #3582 <https://redmine.postgresql.org/issues/3582>`_ - Ensure that JSON strings as comments should be added properly for all the objects.
| `Bug #3605 <https://redmine.postgresql.org/issues/3605>`_ - Fix an issue where Deleting N number of rows makes first N number of rows disable.
| `Bug #3938 <https://redmine.postgresql.org/issues/3938>`_ - Added support for Default Partition.
| `Bug #4087 <https://redmine.postgresql.org/issues/4087>`_ - Fix an issue where 'GRANT UPDATE' sql should be displayed for default sequence privileges.
| `Bug #4101 <https://redmine.postgresql.org/issues/4101>`_ - Ensure that confirmation dialog should be popped up before reload of query tool or debugger if it is opened in a new browser tab.
| `Bug #4104 <https://redmine.postgresql.org/issues/4104>`_ - Ensure that record should be add/edited for root partition table with primary keys.
| `Bug #4121 <https://redmine.postgresql.org/issues/4121>`_ - Fixed alignment issue of columns in definition section of Index node.
| `Bug #4134 <https://redmine.postgresql.org/issues/4134>`_ - Fixed 'Location cannot be empty' error when open Tablespace properties.
| `Bug #4138 <https://redmine.postgresql.org/issues/4138>`_ - Fix an issue where the dropdown becomes misaligned/displaced.
| `Bug #4154 <https://redmine.postgresql.org/issues/4154>`_ - Ensure the treeview shows all sequences except those used to implement IDENTITY columns (which can be edited as part of the column). Show all if Show System Objects is enabled.
| `Bug #4160 <https://redmine.postgresql.org/issues/4160>`_ - Fixed 'Increment value cannot be empty' error for existing tables.
| `Bug #4161 <https://redmine.postgresql.org/issues/4161>`_ - Ensure that parameters of procedures for EPAS server 10 and below should be set/reset properly.
| `Bug #4163 <https://redmine.postgresql.org/issues/4163>`_ - Prevent duplicate columns being included in reverse engineered SQL for tables.
| `Bug #4182 <https://redmine.postgresql.org/issues/4182>`_ - Ensure sanity of the permissions on the storage and session directories and the config database.
+2 -1
View File
@@ -442,6 +442,7 @@ icon (+) to add each partition:
* Move the *Operation* switch to *attach* to attach the partition, by default it
is *create*.
* Use the *Name* field to add the name of the partition.
* If partition type is Range or List then *Default* field will be enabled.
* If partition type is Range then *From* and *To* fields will be enabled.
* If partition type is List then *In* field will be enabled.
* If partition type is Hash then *Modulus* and *Remainder* fields will be
@@ -523,4 +524,4 @@ three columns and a primary key constraint on the *category_id* column.
* Click the *Info* button (i) to access online help.
* Click the *Save* button to save work.
* Click the *Cancel* button to exit without saving work.
* Click the *Reset* button to restore configuration parameters.
* Click the *Reset* button to restore configuration parameters.
+1 -1
View File
@@ -29,7 +29,7 @@ for src in "${DMG_SOURCES}"; do
done
echo "Creating image"
${HDIUTIL} create -quiet -srcfolder "$DMG_DIR" -format UDZO -volname "${DMG_VOLUME_NAME}" -ov "${DMG_IMAGE}" || exit 1
${HDIUTIL} create -quiet -srcfolder "$DMG_DIR" -fs HFS+ -format UDZO -volname "${DMG_VOLUME_NAME}" -ov "${DMG_IMAGE}" || exit 1
rm -rf "${DMG_DIR}" || exit 1
echo "Attaching License to image"
+5
View File
@@ -37,6 +37,11 @@ with open(req_file, 'r') as reqf:
for index, req in enumerate(required):
if ";" in req or req.startswith("#") or req == "":
required.remove(req)
continue
# Ensure the Wheel will use psycopg2-binary, not the source distro
if 'psycopg2' in req:
required[index] = req.replace('psycopg2', 'psycopg2-binary')
# Get the app version
if sys.version_info[:2] >= (3, 3):
+2 -2
View File
@@ -8,9 +8,9 @@
<key>CFBundleName</key>
<string>pgAdmin 4</string>
<key>CFBundleShortVersionString</key>
<string>4.5.0</string>
<string>4.6.0</string>
<key>CFBundleVersion</key>
<string>4.5.0</string>
<string>4.6.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NSHumanReadableCopyright</key>
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION = 4.5.0.0
VERSION = 4.6.0.0
QMAKE_TARGET_COMPANY = "The pgAdmin Development Team"
QMAKE_TARGET_PRODUCT = "pgAdmin 4"
QMAKE_TARGET_DESCRIPTION = "pgAdmin 4 Desktop Runtime"
+204
View File
@@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2019, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
# Get the correct version of chromedriver for the version of Chrome on the
# machine and save it to the specified location.
import argparse
import os
import platform
import subprocess
import sys
import urllib.request
import zipfile
def read_command_line():
"""Read the command line arguments.
Returns:
ArgumentParser: The parsed arguments object
"""
parser = argparse.ArgumentParser(
description='Get the correct version of chromedriver for the '
'specified Chrome installation and save it.')
if platform.system() != 'Windows':
parser.add_argument("chrome", metavar="CHROME",
help="the Chrome executable")
parser.add_argument("directory", metavar="DIRECTORY",
help="the directory in which to save chromedriver")
args = parser.parse_args()
return args
def get_chrome_version(args):
"""Get the Chrome version number.
Args:
args: The parsed arguments object
Returns:
The Chrome version number
"""
if platform.system() == 'Windows':
# On Windows we need to examine the resource section of the binary
import winreg
def _read_registry(root, key, value):
try:
hkey = winreg.OpenKey(root, key)
except Exception as e:
return None
try:
(val, typ) = winreg.QueryValueEx(hkey, value)
except Exception as e:
winreg.CloseKey(hkey)
return None
winreg.CloseKey(hkey)
return val
key = r'SOFTWARE\Google\Chrome\BLBeacon'
version_str = _read_registry(winreg.HKEY_CURRENT_USER, key, 'Version')
# On a 64b host, look for a 32b installation
if not version_str:
key = r'SOFTWARE\Wow6432Node\Google\Chrome\BLBeacon'
version_str = _read_registry(winreg.HKEY_CURRENT_USER, key,
'Version')
if not version_str:
print('The Chrome version could not be read from the registry.')
sys.exit(1)
chrome_version = '.'.join(version_str.split()[-1].split('.')[:-1])
else:
# On Linux/Mac we run the Chrome executable with the --version flag,
# then parse the output.
try:
result = subprocess.run([args.chrome, '--version'],
stdout=subprocess.PIPE)
except FileNotFoundError as e:
print('The specified Chrome executable could not be found.')
sys.exit(1)
version_str = result.stdout.decode("utf-8").strip()
# Check for 'Chrom' not 'Chrome' in case the user is using Chromium.
if "Chrom" not in version_str:
print('The specified Chrome executable output an unexpected '
'version string: {}.'.format(version_str))
sys.exit(1)
chrome_version = '.'.join(version_str.split()[-1].split('.')[:-1])
if chrome_version.count('.') != 2:
print('The specified Chrome executable output an unexpected '
'version string: {}.'.format(version_str))
sys.exit(1)
return chrome_version
def get_chromedriver_version(chrome_version):
"""Get the required chromedriver version number.
Args:
chrome_version: The chrome version number
Returns:
The chromedriver version number
"""
url = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{}' \
.format(chrome_version)
try:
fp = urllib.request.urlopen(url)
except urllib.error.URLError as e:
print('The chromedriver catalog URL could not be accessed: {}'
.format(e))
sys.exit(1)
version = fp.read().decode("utf-8").strip()
fp.close()
return version
def get_system():
"""Get the system name as known to chromedriver
Returns:
The system name
"""
if platform.system() == 'Darwin':
return 'mac64'
elif platform.system() == 'Linux':
return 'linux64'
elif platform.system() == 'Windows':
return 'win32'
else:
print("Unknown or unsupported operating system: {}"
.format(platform.system()))
sys.exit(1)
"""The core structure of the app."""
# Read the command line options
args = read_command_line()
chrome_version = get_chrome_version(args)
# Check the directory exists
if not os.path.isdir(args.directory):
print('The specified output directory could not be accessed.')
sys.exit(1)
chromedriver_version = get_chromedriver_version(chrome_version)
system = get_system()
url = 'https://chromedriver.storage.googleapis.com/{}/chromedriver_{}.zip' \
.format(chromedriver_version, system)
print('Downloading chromedriver v{} for Chrome v{} on {}...'
.format(chromedriver_version, chrome_version, system))
try:
file, headers = urllib.request.urlretrieve(url)
except urllib.error.URLError as e:
print('The chromedriver download URL could not be accessed: {}'
.format(e))
sys.exit(1)
# Unzip chromedriver
print('Extracting chromedriver...')
found = False
fp = open(file, 'rb')
z = zipfile.ZipFile(fp)
for name in z.namelist():
if (system == 'win32' and name == 'chromedriver.exe') or \
(system != 'win32' and name == 'chromedriver'):
z.extract(name, args.directory)
found = True
fp.close()
if not found:
print("chromedriver could not be found in the downloaded archive: {}"
.format(file))
sys.exit(1)
# Set the permissions
if system == 'mac64' or system == 'linux64':
os.chmod(os.path.join(args.directory, 'chromedriver'), 0o755)
print('Chromedriver downloaded to: {}'.format(args.directory))
+2 -2
View File
@@ -50,7 +50,7 @@ APP_ICON = 'pg-icon'
# Application version number components
APP_RELEASE = 4
APP_REVISION = 5
APP_REVISION = 6
# Application version suffix, e.g. 'beta1', 'dev'. Usually an empty string
# for GA releases.
@@ -59,7 +59,7 @@ APP_SUFFIX = ''
# Numeric application version for upgrade checks. Should be in the format:
# [X]XYYZZ, where X is the release version, Y is the revision, with a leading
# zero if needed, and Z represents the suffix, with a leading zero if needed
APP_VERSION_INT = 40500
APP_VERSION_INT = 40600
# DO NOT CHANGE!
# The application version string, constructed from the components
-1
View File
@@ -75,7 +75,6 @@
"moment": "^2.24.0",
"moment-timezone": "^0.5.23",
"mousetrap": "^1.6.3",
"prop-types": "^15.7.2",
"requirejs": "~2.3.6",
"select2": "^4.0.6-rc.1",
"shim-loader": "^1.0.1",
+2
View File
@@ -338,6 +338,8 @@ def create_app(app_name=None):
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
os.chmod(config.SQLITE_PATH, 0o600)
Mail(app)
import pgadmin.utils.paths as paths
@@ -805,7 +805,12 @@ class DatabaseView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comments',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
status, res = self.get_sql(gid, sid, data, did)
@@ -555,7 +555,12 @@ class ForeignDataWrapperView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
try:
@@ -578,7 +578,12 @@ class ForeignServerView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
try:
@@ -558,7 +558,12 @@ class LanguageView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
try:
@@ -724,7 +724,12 @@ It may have been removed by another user.
data = dict()
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -602,7 +602,12 @@ class CollationView(PGChildNodeView):
data = dict()
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -612,7 +612,12 @@ class FtsConfigurationView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -607,7 +607,12 @@ class FtsDictionaryView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -534,7 +534,12 @@ class FtsParserView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -507,7 +507,12 @@ class FtsTemplateView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -61,11 +61,11 @@ ALTER PROCEDURE {{ conn|qtIdent(o_data.pronamespace, name) }}{% if o_data.proarg
{% if data.variables %}
{% if 'deleted' in data.variables and data.variables.deleted|length > 0 %}
{{ VARIABLE.UNSET(conn, 'PROCEDURE', name, data.variables.deleted, o_data.pronamespace, o_data.proargtypenames) }}
{{ VARIABLE.UNSET(conn, 'PROCEDURE', name, data.variables.deleted, o_data.pronamespace) }}
{% endif -%}
{% if 'merged_variables' in data and data.merged_variables|length > 0 %}
{{ VARIABLE.SET(conn, 'PROCEDURE', name, data.merged_variables, o_data.pronamespace, o_data.proargtypenames) }}
{{ VARIABLE.SET(conn, 'PROCEDURE', name, data.merged_variables, o_data.pronamespace) }}
{% endif -%}
{% endif -%}
{% endif -%}
@@ -63,11 +63,11 @@ ALTER PROCEDURE {{ conn|qtIdent(o_data.pronamespace, name) }}{% if o_data.proarg
{% if data.variables %}
{% if 'deleted' in data.variables and data.variables.deleted|length > 0 %}
{{ VARIABLE.UNSET(conn, 'PROCEDURE', name, data.variables.deleted, o_data.pronamespace, o_data.proargtypenames) }}
{{ VARIABLE.UNSET(conn, 'PROCEDURE', name, data.variables.deleted, o_data.pronamespace) }}
{% endif -%}
{% if 'merged_variables' in data and data.merged_variables|length > 0 %}
{{ VARIABLE.SET(conn, 'PROCEDURE', name, data.merged_variables, o_data.pronamespace, o_data.proargtypenames) }}
{{ VARIABLE.SET(conn, 'PROCEDURE', name, data.merged_variables, o_data.pronamespace) }}
{% endif -%}
{% endif -%}
{% endif -%}
@@ -183,8 +183,10 @@ class SequenceView(PGChildNodeView):
if not status:
return internal_server_error(errormsg=res)
sequence_nodes = self._get_sequence_nodes(res['rows'])
return ajax_response(
response=res['rows'],
response=sequence_nodes,
status=200
)
@@ -227,16 +229,8 @@ class SequenceView(PGChildNodeView):
status=200
)
for row in rset['rows']:
if not self.blueprint.show_system_objects:
system_seq = self._get_dependency(row['oid'],
show_system_objects=True)
seq = filter(lambda dep: dep['type'] == 'column', system_seq)
if type(seq) is not list:
seq = list(seq)
if len(seq) > 0:
continue
sequence_nodes = self._get_sequence_nodes(rset['rows'])
for row in sequence_nodes:
res.append(
self.blueprint.generate_browser_node(
row['oid'],
@@ -250,6 +244,33 @@ class SequenceView(PGChildNodeView):
status=200
)
def _get_sequence_nodes(self, nodes):
"""
This function is used to iterate through all the sequences node and
hiding sequences created as part of an IDENTITY column.
:param nodes:
:return:
"""
# If show_system_objects is true then no need to hide any sequences.
if self.blueprint.show_system_objects:
return nodes
seq_nodes = []
for row in nodes:
system_seq = self._get_dependency(row['oid'],
show_system_objects=True)
seq = filter(lambda dep: dep['type'] == 'column' and
dep['field'] == 'internal', system_seq)
if type(seq) is not list:
seq = list(seq)
if len(seq) > 0:
continue
# Append the node into the newly created list
seq_nodes.append(row)
return seq_nodes
@check_precondition(action='properties')
def properties(self, gid, sid, did, scid, seid):
"""
@@ -538,7 +559,12 @@ class SequenceView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -909,7 +909,12 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -1036,7 +1041,12 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -1223,7 +1233,12 @@ class TableView(BaseTableView, DataTypeReader, VacuumSettings):
res = None
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -193,6 +193,15 @@ define('pgadmin.node.column', [
seqcache: undefined,
seqcycle: undefined,
},
initialize: function(attrs) {
if (_.size(attrs) !== 0) {
this.set({
'old_attidentity': this.get('attidentity'),
}, {silent: true});
}
pgBrowser.Node.Model.prototype.initialize.apply(this, arguments);
},
schema: [{
id: 'name', label: gettext('Name'), cell: 'string',
type: 'text', disabled: 'inSchemaWithColumnCheck',
@@ -658,7 +667,8 @@ define('pgadmin.node.column', [
maximum = this.get('seqmax'),
start = this.get('seqstart');
if (!this.isNew()) {
if (!this.isNew() && (this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') &&
(this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) {
if (_.isUndefined(this.get('seqincrement'))
|| String(this.get('seqincrement')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Increment value cannot be empty.');
@@ -495,7 +495,12 @@ class CheckConstraintView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -762,7 +767,12 @@ class CheckConstraintView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -558,7 +558,12 @@ class ExclusionConstraintView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -788,7 +793,12 @@ class ExclusionConstraintView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -563,7 +563,12 @@ class ForeignKeyConstraintView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -827,7 +832,12 @@ class ForeignKeyConstraintView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -576,7 +576,12 @@ class IndexConstraintView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -828,7 +833,12 @@ class IndexConstraintView(PGChildNodeView):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -621,7 +621,12 @@ class IndexesView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -826,7 +831,12 @@ class IndexesView(PGChildNodeView):
data = dict()
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -230,6 +230,7 @@ define('pgadmin.node.index', [
hasSQL: true,
hasDepends: true,
hasStatistics: true,
width: pgBrowser.stdW.md + 'px',
statsPrettifyFields: [gettext('Size'), gettext('Index size')],
Init: function() {
/* Avoid mulitple registration of menus */
@@ -242,6 +242,7 @@ define('pgadmin.node.table_partition_utils', [
oid: undefined,
is_attach: false,
partition_name: undefined,
is_default: undefined,
values_from: undefined,
values_to: undefined,
values_in: undefined,
@@ -252,8 +253,8 @@ define('pgadmin.node.table_partition_utils', [
schema: [{
id: 'oid', label: gettext('OID'), type: 'text',
},{
id: 'is_attach', label:gettext('Operation'), cell: 'switch',
type: 'switch', options: { 'onText': gettext('Attach'), 'offText': gettext('Create')},
id: 'is_attach', label:gettext('Operation'), cell: 'switch', type: 'switch',
options: {'onText': gettext('Attach'), 'offText': gettext('Create'), 'width': 65},
cellHeaderClasses: 'width_percent_5',
editable: function(m) {
if (m instanceof Backbone.Model && m.isNew() && !m.top.isNew())
@@ -268,39 +269,53 @@ define('pgadmin.node.table_partition_utils', [
return true;
return false;
}, cellFunction: getPartitionCell,
},{
id: 'is_default', label: gettext('Default'), type: 'switch', cell:'switch',
cellHeaderClasses: 'width_percent_5', min_version: 110000,
options: {'onText': gettext('Yes'), 'offText': gettext('No')},
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
(m.handler.top.attributes.partition_type === 'range' ||
m.handler.top.attributes.partition_type === 'list') &&
m instanceof Backbone.Model && m.isNew() &&
m.handler.top.node_info.server.version >= 110000)
return true;
return false;
},
},{
id: 'values_from', label: gettext('From'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew())
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true;
return false;
},
},{
id: 'values_to', label: gettext('To'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'range' &&
m instanceof Backbone.Model && m.isNew())
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true;
return false;
},
},{
id: 'values_in', label: gettext('In'), type:'text',
cell:Backgrid.Extension.StringDepCell,
cell:Backgrid.Extension.StringDepCell, deps: ['is_default'],
cellHeaderClasses: 'width_percent_15',
editable: function(m) {
if(m.handler && m.handler.top &&
m.handler.top.attributes &&
m.handler.top.attributes.partition_type === 'list' &&
m instanceof Backbone.Model && m.isNew())
m instanceof Backbone.Model && m.isNew() && m.get('is_default') !== true)
return true;
return false;
},
@@ -331,6 +346,7 @@ define('pgadmin.node.table_partition_utils', [
}],
validate: function() {
var partition_name = this.get('partition_name'),
is_default = this.get('is_default'),
values_from = this.get('values_from'),
values_to = this.get('values_to'),
values_in = this.get('values_in'),
@@ -350,20 +366,20 @@ define('pgadmin.node.table_partition_utils', [
}
if (this.top.get('partition_type') === 'range') {
if (_.isUndefined(values_from) || _.isNull(values_from) ||
String(values_from).replace(/^\s+|\s+$/g, '') === '') {
if (is_default !== true && (_.isUndefined(values_from) ||
_.isNull(values_from) || String(values_from).replace(/^\s+|\s+$/g, '') === '')) {
msg = gettext('For range partition From field cannot be empty.');
this.errorModel.set('values_from', msg);
return msg;
} else if (_.isUndefined(values_to) || _.isNull(values_to) ||
String(values_to).replace(/^\s+|\s+$/g, '') === '') {
} else if (is_default !== true && (_.isUndefined(values_to) || _.isNull(values_to) ||
String(values_to).replace(/^\s+|\s+$/g, '') === '')) {
msg = gettext('For range partition To field cannot be empty.');
this.errorModel.set('values_to', msg);
return msg;
}
} else if (this.top.get('partition_type') === 'list') {
if (_.isUndefined(values_in) || _.isNull(values_in) ||
String(values_in).replace(/^\s+|\s+$/g, '') === '') {
if (is_default !== true && (_.isUndefined(values_in) || _.isNull(values_in) ||
String(values_in).replace(/^\s+|\s+$/g, '') === '')) {
msg = gettext('For list partition In field cannot be empty.');
this.errorModel.set('values_in', msg);
return msg;
@@ -960,17 +960,18 @@ define('pgadmin.node.table', [
id: 'partition_key_note', label: gettext('Partition Keys'),
type: 'note', group: 'partition', mode: ['create'],
text: [
'<br>&nbsp;&nbsp;',
'<ul><li>',
gettext('Partition table supports two types of keys:'),
'<br><ul><li>',
gettext('Column: User can select any column from the list of available columns.'),
'</li><li>',
gettext('Expression: User can specify expression to create partition key.'),
'<br><p>',
gettext('Example'),
':',
'<strong>', gettext('Column: '), '</strong>',
gettext('User can select any column from the list of available columns.'),
'</li><li>',
'<strong>', gettext('Expression: '), '</strong>',
gettext('User can specify expression to create partition key.'),
'</li><li>',
'<strong>', gettext('Example: '), '</strong>',
gettext('Let\'s say, we want to create a partition table based per year for the column \'saledate\', having datatype \'date/timestamp\', then we need to specify the expression as \'extract(YEAR from saledate)\' as partition key.'),
'</p></li></ul>',
'</li></ul>',
].join(''),
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
@@ -990,7 +991,7 @@ define('pgadmin.node.table', [
canEdit: false, canDelete: true,
customDeleteTitle: gettext('Detach Partition'),
customDeleteMsg: gettext('Are you sure you wish to detach this partition?'),
columns:['is_attach', 'partition_name', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],
columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'],
control: Backform.SubNodeCollectionControl.extend({
row: Backgrid.PartitionRow,
initialize: function() {
@@ -1053,22 +1054,28 @@ define('pgadmin.node.table', [
id: 'partition_note', label: gettext('Partitions'),
type: 'note', group: 'partition',
text: [
'<ul>',
' <li>',
gettext('Create a table: User can create multiple partitions while creating new partitioned table. Operation switch is disabled in this scenario.'),
'<ul><li>',
'<strong>', gettext('Create a table: '), '</strong>',
gettext('User can create multiple partitions while creating new partitioned table. Operation switch is disabled in this scenario.'),
'</li><li>',
gettext('Edit existing table: User can create/attach/detach multiple partitions. In attach operation user can select table from the list of suitable tables to be attached.'),
'<strong>', gettext('Edit existing table: '), '</strong>',
gettext('User can create/attach/detach multiple partitions. In attach operation user can select table from the list of suitable tables to be attached.'),
'</li><li>',
'<strong>', gettext('Default: '), '</strong>',
gettext('The default partition can store rows that do not fall into any existing partition’s range or list.'),
'</li><li>',
'<strong>', gettext('From/To/In input: '), '</strong>',
gettext('From/To/In input: Values for these fields must be quoted with single quote. For more than one partition key values must be comma(,) separated.'),
'<br>',
gettext('Example'),
':<ul><li>',
gettext('From/To: Enabled for range partition. Consider partitioned table with multiple keys of type Integer, then values should be specified like \'100\',\'200\'.'),
'</li><li> ',
gettext('In: Enabled for list partition. Values must be comma(,) separated and quoted with single quote.'),
'</li></ul></li></ul>',
gettext('Modulus/Remainder: Enabled for hash partition.'),
'</li></ul></li></ul>',
'</li><li>',
'<strong>', gettext('Example: From/To: '), '</strong>',
gettext('Enabled for range partition. Consider partitioned table with multiple keys of type Integer, then values should be specified like \'100\',\'200\'.'),
'</li><li>',
'<strong>', gettext('In: '), '</strong>',
gettext('Enabled for list partition. Values must be comma(,) separated and quoted with single quote.'),
'</li><li>',
'<strong>', gettext('Modulus/Remainder: '), '</strong>',
gettext('Enabled for hash partition.'),
'</li></ul>',
].join(''),
visible: function(m) {
if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server)
@@ -33,7 +33,7 @@ FROM pg_attribute att
LEFT OUTER JOIN pg_index pi ON pi.indrelid=att.attrelid AND indisprimary
LEFT OUTER JOIN pg_collation coll ON att.attcollation=coll.oid
LEFT OUTER JOIN pg_namespace nspc ON coll.collnamespace=nspc.oid
LEFT OUTER JOIN pg_depend dep ON dep.refobjid = att.attrelid AND dep.refobjsubid = att.attnum AND dep.classid IN ( SELECT oid FROM pg_class WHERE relname IN ('pg_class')) LEFT JOIN pg_sequence seq ON dep.objid=seq.seqrelid
LEFT OUTER JOIN pg_sequence seq ON cs.oid=seq.seqrelid
WHERE att.attrelid = {{tid}}::oid
{% if clid %}
AND att.attnum = {{clid}}::int
@@ -31,6 +31,14 @@ class TableAddTestCase(BaseTestGenerator):
partition_type='range'
)
),
('Create Range partitioned table with 1 default and 2'
' value based partition',
dict(url='/browser/table/obj/',
server_min_version=110000,
partition_type='range',
is_default=True
)
),
('Create List partitioned table with 2 partitions',
dict(url='/browser/table/obj/',
server_min_version=100000,
@@ -215,6 +223,24 @@ class TableAddTestCase(BaseTestGenerator):
'is_attach': False,
'partition_name': 'emp_2011'
}]
if hasattr(self, 'is_default'):
data['partitions'] = \
[{'values_from': "'2010-01-01'",
'values_to': "'2010-12-31'",
'is_attach': False,
'partition_name': 'emp_2010_def'
},
{'values_from': "'2011-01-01'",
'values_to': "'2011-12-31'",
'is_attach': False,
'partition_name': 'emp_2011_def'
},
{'values_from': "",
'values_to': "",
'is_attach': False,
'is_default': True,
'partition_name': 'emp_2012_def'
}]
data['partition_keys'] = \
[{'key_type': 'column', 'pt_column': 'DOJ'}]
elif self.partition_type == 'list':
@@ -619,7 +619,12 @@ class TriggerView(PGChildNodeView):
for k, v in data.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except (ValueError, TypeError, KeyError):
data[k] = v
@@ -827,7 +832,12 @@ class TriggerView(PGChildNodeView):
data = dict()
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -2184,16 +2184,23 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
partition_name = row['schema_name'] + '.' + row['name']
if data['partition_type'] == 'range':
range_part = row['partition_value'].split(
'FOR VALUES FROM (')[1].split(') TO')
range_from = range_part[0]
range_to = range_part[1][2:-1]
if row['partition_value'] == 'DEFAULT':
is_default = True
range_from = None
range_to = None
else:
range_part = row['partition_value'].split(
'FOR VALUES FROM (')[1].split(') TO')
range_from = range_part[0]
range_to = range_part[1][2:-1]
is_default = False
partitions.append({
'oid': row['oid'],
'partition_name': partition_name,
'values_from': range_from,
'values_to': range_to
'values_to': range_to,
'is_default': is_default
})
elif data['partition_type'] == 'list':
range_part = \
@@ -2251,15 +2258,22 @@ class BaseTableView(PGChildNodeView, BasePartitionTable):
part_data['relispartition'] = True
part_data['name'] = row['partition_name']
if partitions['partition_type'] == 'range':
if 'is_default' in row and row['is_default'] and (
partitions['partition_type'] == 'range' or
partitions['partition_type'] == 'list'):
part_data['partition_value'] = 'DEFAULT'
elif partitions['partition_type'] == 'range':
range_from = row['values_from'].split(',')
range_to = row['values_to'].split(',')
from_str = ', '.join("{0}".format(item) for item in range_from)
to_str = ', '.join("{0}".format(item) for item in range_to)
from_str = ', '.join("{0}".format(item) for
item in range_from)
to_str = ', '.join("{0}".format(item) for
item in range_to)
part_data['partition_value'] = 'FOR VALUES FROM (' + from_str \
+ ') TO (' + to_str + ')'
part_data['partition_value'] = 'FOR VALUES FROM (' +\
from_str + ') TO (' +\
to_str + ')'
elif partitions['partition_type'] == 'list':
range_in = row['values_in'].split(',')
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -4,7 +4,10 @@
{% macro SET(conn, object_type, object_name, options, schema, func_args) -%}
{% for opt in options %}
ALTER {{object_type}} {{ conn|qtIdent(schema, object_name) }}({{func_args}})
ALTER {{object_type}} {{ conn|qtIdent(schema, object_name) }}{% if func_args is defined %}({{func_args}})
{% else %}
{% endif %}
SET {{ conn|qtIdent(opt.name) }}={{ opt.value }};
{% endfor %}
@@ -12,7 +15,10 @@ ALTER {{object_type}} {{ conn|qtIdent(schema, object_name) }}({{func_args}})
{% macro UNSET(conn, object_type, object_name, options, schema, func_args) -%}
{% for opt in options %}
ALTER {{object_type}} {{ conn|qtIdent(schema, object_name) }}({{func_args}})
ALTER {{object_type}} {{ conn|qtIdent(schema, object_name) }}{% if func_args is defined %}({{func_args}})
{% else %}
{% endif %}
RESET {{ conn|qtIdent(opt.name) }};
{% endfor %}
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -647,7 +647,12 @@ class ViewNode(PGChildNodeView, VacuumSettings):
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('comment',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError:
data[k] = v
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -17,7 +17,7 @@
},
"defseqacl": {
"type": "SEQUENCE",
"acl": ["U", "r", "a"]
"acl": ["U", "r", "w"]
},
"deffuncacl": {
"type": "FUNCTION",
@@ -457,7 +457,12 @@ class TablespaceView(PGChildNodeView):
data = dict()
for k, v in request.args.items():
try:
data[k] = json.loads(v, encoding='utf-8')
# comments should be taken as is because if user enters a
# json comment it is parsed by loads which should not happen
if k in ('description',):
data[k] = v
else:
data[k] = json.loads(v, encoding='utf-8')
except ValueError as ve:
current_app.logger.exception(ve)
data[k] = v
@@ -389,8 +389,9 @@ define('pgadmin.node.tablespace', [
|| String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Name cannot be empty.');
this.errorModel.set('name', msg);
} else if (_.isUndefined(this.get('spclocation'))
|| String(this.get('spclocation')).replace(/^\s+|\s+$/g, '') == '') {
} else if (this.isNew() &&
(_.isUndefined(this.get('spclocation'))
|| String(this.get('spclocation')).replace(/^\s+|\s+$/g, '') == '')) {
msg = gettext('Location cannot be empty.');
this.errorModel.set('spclocation', msg);
this.errorModel.unset('name');
@@ -16,13 +16,11 @@ ALTER {% if role %}ROLE {{ self.conn|qtIdent(role) }}{% if database %} IN DATABA
{################################################}
{% macro SET(conn, object_type, object_name, options) -%}
ALTER {{object_type}} {{ conn|qtIdent(object_name) }}
SET ({% for opt in options %}{% if loop.index != 1 %}
, {% endif %}{{ conn|qtIdent(opt.name) }}={{ opt.value }}{% endfor %});
{%- endmacro %}
{% macro UNSET(conn, object_type, object_name, options) -%}
ALTER {{object_type}} {{ conn|qtIdent(object_name) }}
RESET ({% for opt in options %}{% if loop.index != 1 %}
, {% endif %}{{ conn|qtIdent(opt.name) }}{% endfor %});
{%- endmacro %}
{%- endmacro %}
+22 -14
View File
@@ -539,6 +539,28 @@ define('pgadmin.browser', [
obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode, obj);
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
obj.bind_beforeunload();
},
bind_beforeunload: function() {
$(window).on('beforeunload', function(e) {
/* Can open you in new tab */
let openerBrowser = window.opener ?
window.opener.pgAdmin.Browser : window.top.pgAdmin.Browser;
let tree_save_interval = pgBrowser.get_preference('browser', 'browser_tree_state_save_interval'),
confirm_on_refresh_close = openerBrowser.get_preference('browser', 'confirm_on_refresh_close');
if (!_.isUndefined(tree_save_interval) && tree_save_interval.value !== -1)
pgAdmin.Browser.browserTreeState.save_state();
if(!_.isUndefined(confirm_on_refresh_close) && confirm_on_refresh_close.value) {
/* This message will not be displayed in Chrome, Firefox, Safari as they have disabled it*/
let msg = S(gettext('Are you sure you want to close the %s browser?')).sprintf(pgBrowser.utils.app_name).value();
e.originalEvent.returnValue = msg;
return msg;
}
});
},
add_menu_category: function(
@@ -1944,19 +1966,5 @@ define('pgadmin.browser', [
pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab';
}
$(window).on('beforeunload', function(e) {
let tree_save_interval = pgBrowser.get_preference('browser', 'browser_tree_state_save_interval'),
confirm_on_refresh_close = pgBrowser.get_preference('browser', 'confirm_on_refresh_close');
if (!_.isUndefined(tree_save_interval) && tree_save_interval.value !== -1)
pgAdmin.Browser.browserTreeState.save_state();
if(confirm_on_refresh_close.value) {
/* This message will not be displayed in Chrome, Firefox, Safari as they have disabled it*/
let msg = S(gettext('Are you sure you want to close the %s browser?')).sprintf(pgBrowser.utils.app_name).value();
e.originalEvent.returnValue = msg;
return msg;
}
});
return pgAdmin.Browser;
});
+22
View File
@@ -327,6 +327,28 @@ define([
});
};
/*
* Global function to make visible backgrid new row
*/
$.fn.pgMakeBackgridVisible = function(cls) {
return this.each(function() {
if (!this || !$(this.length))
return;
var elem = $(this),
backgridDiv = $(this).offsetParent().parent(), // Backgrid div.subnode
backgridDivTop = backgridDiv.offset().top,
backgridDivHeight = backgridDiv.height(),
backformTab = $(this).closest(cls), // Backform-tab
gridScroll = backformTab[0].offsetHeight - backgridDivTop;
if (backgridDivHeight > gridScroll) {
var top = elem.get(0).offsetTop + elem.height();
backformTab.find('.tab-content').scrollTop(top);
}
return;
});
};
/*
* NodeAjaxOptionsCell
+426 -404
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -21,12 +21,14 @@ def create_app_data_directory(config):
"""
# Create the directory containing the configuration file (if not present).
_create_directory_if_not_exists(os.path.dirname(config.SQLITE_PATH))
os.chmod(os.path.dirname(config.SQLITE_PATH), 0o700)
# Create the directory containing the log file (if not present).
_create_directory_if_not_exists(os.path.dirname(config.LOG_FILE))
# Create the session directory (if not present).
_create_directory_if_not_exists(config.SESSION_DB_PATH)
os.chmod(os.path.dirname(config.SESSION_DB_PATH), 0o700)
# Create the storage directory (if not present).
_create_directory_if_not_exists(config.STORAGE_DIR)
@@ -0,0 +1,40 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2019, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from pgadmin.utils.route import BaseTestGenerator
import os
import config
from stat import *
class PermissionsTestCase(BaseTestGenerator):
"""
This class validates filesystem permissions for data/config storage
"""
scenarios = [
('Check config database', dict(path=config.TEST_SQLITE_PATH,
permissions='600')),
('Check config directory', dict(
path=os.path.dirname(config.TEST_SQLITE_PATH),
permissions='700')),
('Check session directory', dict(
path=os.path.dirname(config.SESSION_DB_PATH),
permissions='700'))
]
def setUp(self):
if os.name == 'nt':
self.skipTest("This test is skipped on Windows which doesn't "
"support Unix style file permissions.")
def runTest(self):
self.assertTrue(
oct(os.stat(self.path)[ST_MODE])[-3:] == self.permissions
)
+11 -2
View File
@@ -1265,7 +1265,7 @@ define([
newRow = self.grid.body.rows[idx].$el;
newRow.addClass('new');
$(newRow).pgMakeVisible('backform-tab');
$(newRow).pgMakeBackgridVisible('.backform-tab');
return false;
}
@@ -1516,7 +1516,7 @@ define([
newRow.attr('class', 'new').on('click',() => {
$(this).attr('class', 'editable');
});
$(newRow).pgMakeVisible('backform-tab');
$(newRow).pgMakeBackgridVisible('.backform-tab');
return false;
}
});
@@ -1910,6 +1910,7 @@ define([
multiple: false,
emptyOptions: false,
preserveSelectionOrder: false,
isDropdownParent: false,
},
}),
@@ -1987,6 +1988,7 @@ define([
multiple: false,
emptyOptions: false,
preserveSelectionOrder: false,
isDropdownParent: false,
});
// Evaluate the disabled, visible, and required option
@@ -2015,6 +2017,13 @@ define([
if (!data.visible)
this.$el.addClass(Backform.hiddenClassName);
// Dropdown body can be render at user given location
// If isDropdownParent flag is set to true then, By default we will
// display it on the control itself.
if (data.select2.isDropdownParent) {
select2Opts.dropdownParent = data.select2.dropdownParent || this.$el;
}
this.$el.html(this.template(data)).addClass(field.name);
var select2Opts = _.extend({
@@ -96,7 +96,7 @@ define(
this.selection.setSelectedRows([]);
}
if (isEditMode()) {
if (isEditMode() && !_.isEmpty(stagedRows)) {
enableButton('#btn-delete-row');
}
} else {
+1 -1
View File
@@ -411,7 +411,7 @@ def initialize_query_tool(sgid, sid, did=None):
)
@blueprint.route('/close/<int:trans_id>', methods=["GET"], endpoint='close')
@blueprint.route('/close/<int:trans_id>', methods=["DELETE"], endpoint='close')
def close(trans_id):
"""
This method is used to close the asynchronous connection
@@ -505,7 +505,7 @@ define('pgadmin.datagrid', [
queryToolPanel.on(wcDocker.EVENT.CLOSED, function() {
$.ajax({
url: url_for('datagrid.close', {'trans_id': trans_obj.gridTransId}),
method: 'GET',
method: 'DELETE',
});
});
@@ -368,12 +368,24 @@
msgDiv = loadingDiv.find('.sql-editor-busy-text');
// Register unload event on window close.
window.onbeforeunload = function(ev) {
$.ajax({
url: "{{ url_for('datagrid.index') }}" + "close/" + {{ uniqueId }},
method: 'GET'
/* If opened in new tab, close the connection only on tab/window close and
* not on refresh attempt because the user may cancel the reload
*/
if(window.opener) {
$(window).on('unload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('datagrid.index') }}" + "close/" + {{ uniqueId }}
});
});
};
} else {
$(window).on('beforeunload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('datagrid.index') }}" + "close/" + {{ uniqueId }}
});
});
}
// Get the controller object from pgAdmin.SqlEditor
var sqlEditorController = pgAdmin.SqlEditor.create(editorPanel);
@@ -196,14 +196,14 @@ define([
let self = this;
let cacheIntervalId = setInterval(function() {
try {
self.preferences = window.top.pgAdmin.Browser;
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('debugger');
clearInterval(cacheIntervalId);
}
catch(err) {
clearInterval(cacheIntervalId);
throw err;
}
},0);
pgBrowser.onPreferencesChange('debugger', function() {
self.preferences = pgBrowser.get_preferences_for_module('debugger');
});
},
// It will check weather the function is actually debuggable or not with pre-required condition.
@@ -1572,6 +1572,8 @@ define([
});
this.panels = [];
pgBrowser.bind_beforeunload();
// Below code will be executed for indirect debugging
// indirect debugging - 0 and for direct debugging - 1
if (trans_id != undefined && !debug_type) {
@@ -10,12 +10,26 @@ try {
var $ = pgDirectDebug.jquery;
pgDirectDebug.load({{ uniqueId }}, {{ debug_type }}, '{{ function_name_with_arguments }}', '{{layout|safe}}');
window.onbeforeunload = function(ev) {
$.ajax({
url: "{{ url_for('debugger.index') }}close/{{ uniqueId }}",
method: 'DELETE'
// Register unload event on window close.
/* If opened in new tab, close the connection only on tab/window close and
* not on refresh attempt because the user may cancel the reload
*/
if(window.opener) {
$(window).on('unload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('debugger.index') }}close/{{ uniqueId }}"
});
});
};
} else {
$(window).on('beforeunload', function(ev) {
$.ajax({
method: 'DELETE',
url: "{{ url_for('debugger.index') }}close/{{ uniqueId }}"
});
});
}
},
function() {
console.log(arguments);
@@ -552,6 +552,8 @@ define('tools.querytool', [
};
});
pgBrowser.bind_beforeunload();
/* If the screen width is small and we hover over the Explain Options,
* the submenu goes behind the screen on the right side.
* Below logic will make it appear on the left.
@@ -1372,11 +1374,11 @@ define('tools.querytool', [
}
},
// Callback function for Add New Row button click.
// Callback function for delete button click.
on_delete: function() {
var self = this;
// Trigger the addrow signal to the SqlEditorController class
// Trigger the deleterow signal to the SqlEditorController class
self.handler.trigger(
'pgadmin-sqleditor:button:deleterow',
self,
@@ -2724,15 +2726,12 @@ define('tools.querytool', [
if (key in self.data_store.staged_rows) {
// Remove the row from data store so that we do not send it on server
deleted_keys.push(key);
delete self.data_store.staged_rows[key];
delete self.data_store.added[key];
delete self.data_store.added_index[key];
}
});
}
// If only newly rows to delete and no data is there to send on server
// then just re-render the grid
if (_.size(self.data_store.staged_rows) == 0) {
if (_.size(self.data_store.staged_rows) > 0 && (_.size(self.data_store.staged_rows) === _.size(deleted_keys))) {
var grid = self.slickgrid,
dataView = grid.getData();
@@ -2740,6 +2739,9 @@ define('tools.querytool', [
dataView.beginUpdate();
for (var i = 0; i < deleted_keys.length; i++) {
delete self.data_store.staged_rows[deleted_keys[i]];
delete self.data_store.added[deleted_keys[i]];
delete self.data_store.added_index[deleted_keys[i]];
dataView.deleteItem(deleted_keys[i]);
}
dataView.endUpdate();
@@ -2760,31 +2762,35 @@ define('tools.querytool', [
}
alertify.success(gettext('Row(s) deleted.'));
} else {
// There are other data to needs to be updated on server
if (is_updated) {
alertify.alert(gettext('Operation failed'),
gettext('There are unsaved changes in the grid. Please save them first to avoid data inconsistencies.')
);
return;
}
alertify.confirm(gettext('Delete Row(s)'),
gettext('Are you sure you wish to delete selected row(s)?'),
function() {
$('#btn-delete-row').prop('disabled', true);
$('#btn-copy-row').prop('disabled', true);
// Change the state
self.data_store.deleted = self.data_store.staged_rows;
self.data_store.staged_rows = {};
// Save the changes on server
self._save();
},
function() {
// Do nothing as user canceled the operation.
let strikeout = true;
_.each(_.keys(self.data_store.staged_rows), function(key) {
if(key in self.data_store.deleted) {
strikeout = false;
return;
}
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
});
if (!strikeout) {
$(self.gridView.grid.getCanvasNode()).find('div.selected').removeClass('strikeout');
_.each(_.keys(self.data_store.staged_rows), function(key) {
if(key in self.data_store.deleted) {
delete self.data_store.deleted[key];
}
});
} else {
// Strike out the rows to be deleted
self.data_store.deleted = Object.assign({}, self.data_store.deleted, self.data_store.staged_rows);
$(self.gridView.grid.getCanvasNode()).find('div.selected').addClass('strikeout');
}
if (_.size(self.data_store.added) || is_updated || _.size(self.data_store.deleted)) {
// Do not disable save button if there are
// any other changes present in grid data
$('#btn-save').prop('disabled', false);
} else {
$('#btn-save').prop('disabled', true);
}
}
},
@@ -2884,7 +2890,7 @@ define('tools.querytool', [
// Remove deleted rows from client as well
if (is_deleted) {
var rows = grid.getSelectedRows();
var rows = _.keys(self.data_store.deleted);
if (data_length == rows.length) {
// This means all the rows are selected, clear all data
data = [];
@@ -277,3 +277,18 @@ li.CodeMirror-hint-active {
.geometry-viewer-container-plain-background {
background: $color-bg;
}
div.strikeout:before {
content: " ";
position: absolute;
top: 50%;
left: 0;
border-top: 1px solid $color-danger;
width: 100%;
}
div.strikeout:after {
content: "\00B7";
font-size: 1px;
}
@@ -0,0 +1,8 @@
{# ============= Fetch the primary keys for given object id ============= #}
{% if obj_id %}
SELECT at.attname, ty.typname
FROM pg_attribute at LEFT JOIN pg_type ty ON (ty.oid = at.atttypid)
WHERE attrelid={{obj_id}}::oid AND attnum = ANY (
(SELECT con.conkey FROM pg_class rel LEFT OUTER JOIN pg_constraint con ON con.conrelid=rel.oid
AND con.contype='p' WHERE rel.relkind IN ('r','s','t', 'p') AND rel.oid = {{obj_id}}::oid)::oid[])
{% endif %}
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2019, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from regression import parent_node_dict
from regression.python_test_utils import test_utils
import json
from pgadmin.utils import server_utils, IS_PY2
import random
class TestDownloadCSV(BaseTestGenerator):
"""
This class validates download csv
"""
scenarios = [
(
'Download csv URL with valid query',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns='"A","B","C"',
output_values='1,2,3',
is_valid_tx=True,
is_valid=True
)
),
(
'Download csv URL with wrong TX id',
dict(
sql='SELECT 1 as "A",2 as "B",3 as "C"',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns=None,
output_values=None,
is_valid_tx=False,
is_valid=False
)
),
(
'Download csv URL with wrong query',
dict(
sql='SELECT * FROM this_table_does_not_exist',
init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}',
donwload_url="/sqleditor/query_tool/download/{0}",
output_columns=None,
output_values=None,
is_valid_tx=True,
is_valid=False
)
),
]
def setUp(self):
self._db_name = 'download_csv_' + str(random.randint(10000, 65535))
self._sid = self.server_information['server_id']
server_con = server_utils.connect_server(self, self._sid)
self._did = test_utils.create_database(
self.server, self._db_name
)
def runTest(self):
db_con = database_utils.connect_database(self,
test_utils.SERVER_GROUP,
self._sid,
self._did)
if not db_con["info"] == "Database connected.":
raise Exception("Could not connect to the database.")
# Initialize query tool
url = self.init_url.format(
test_utils.SERVER_GROUP, self._sid, self._did)
response = self.tester.post(url)
self.assertEquals(response.status_code, 200)
response_data = json.loads(response.data.decode('utf-8'))
self.trans_id = response_data['data']['gridTransId']
# If invalid tx test then make the Tx id invalid so that tests fails
if not self.is_valid_tx:
self.trans_id = self.trans_id + '007'
# Check character
url = self.donwload_url.format(self.trans_id)
# Disable the console logging from Flask logger
self.app.logger.disabled = True
response = self.tester.post(
url,
data={"query": self.sql, "filename": 'test.csv'}
)
# Enable the console logging from Flask logger
self.app.logger.disabled = False
if self.is_valid:
# when valid query
self.assertEquals(response.status_code, 200)
csv_data = response.data.decode()
self.assertTrue(self.output_columns in csv_data)
self.assertTrue(self.output_values in csv_data)
elif not self.is_valid and self.is_valid_tx:
# When user enters wrong query
self.assertEquals(response.status_code, 200)
response_data = json.loads(response.data.decode('utf-8'))
self.assertFalse(response_data['data']['status'])
self.assertTrue(
'relation "this_table_does_not_exist" does not exist' in
response_data['data']['result']
)
else:
# when TX id is invalid
self.assertEquals(response.status_code, 500)
database_utils.disconnect_database(self, self._sid, self._did)
def tearDown(self):
main_conn = test_utils.get_db_connection(
self.server['db'],
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port'],
self.server['sslmode']
)
test_utils.drop_database(main_conn, self._db_name)
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
-3
View File
@@ -23,6 +23,3 @@ selenium==3.14.0
###############################################################
mock===2.0.0; python_version < '3.3'
# Leave this at the end because there is a bug where the '--install-option'
# is applied to all subsequent requirements
chromedriver_installer==0.0.6 --install-option='--chromedriver-version=2.46'
+2
View File
@@ -369,6 +369,8 @@ def setup_db():
version.value = CURRENT_SCHEMA_VERSION
db.session.commit()
os.chmod(config.SQLITE_PATH, 0o600)
if __name__ == '__main__':
# Configuration settings
+1 -22
View File
@@ -5109,7 +5109,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@@ -5558,13 +5558,6 @@ loose-envify@^1.0.0:
dependencies:
js-tokens "^3.0.0"
loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@@ -7080,15 +7073,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -7246,11 +7230,6 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-is@^16.8.1:
version "16.8.4"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
read-only-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"