mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-28 15:40:06 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277bf85645 | ||
|
|
991fe3d797 | ||
|
|
4009ac3108 | ||
|
|
92e6a47713 | ||
|
|
e53c2b1a5d | ||
|
|
e536408ce2 | ||
|
|
f74448a1b2 | ||
|
|
9b5a6e2bb7 | ||
|
|
3fef078ec8 | ||
|
|
74af8976c9 | ||
|
|
ecded18333 | ||
|
|
5452bff49e | ||
|
|
05ae330030 | ||
|
|
94e1820ccd | ||
|
|
49b318c39e | ||
|
|
4ac064de42 | ||
|
|
2ef71cf749 | ||
|
|
d96f453e7f | ||
|
|
0b47d1dbf6 | ||
|
|
d728ee7436 | ||
|
|
1a192109d6 | ||
|
|
024cfd2fab | ||
|
|
a25bb2b7db | ||
|
|
fd3337024f | ||
|
|
ded446fe8a | ||
|
|
a9d964b5ca | ||
|
|
9c3925e448 | ||
|
|
6016a5ee98 |
No files matched your search
Executable → Regular
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 247 KiB |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
-1
@@ -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:
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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 -%}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+11
-1
@@ -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.');
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
|
||||
+1
@@ -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 */
|
||||
|
||||
+30
-14
@@ -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;
|
||||
|
||||
+29
-22
@@ -960,17 +960,18 @@ define('pgadmin.node.table', [
|
||||
id: 'partition_key_note', label: gettext('Partition Keys'),
|
||||
type: 'note', group: 'partition', mode: ['create'],
|
||||
text: [
|
||||
'<br> ',
|
||||
'<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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+26
@@ -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':
|
||||
|
||||
+12
-2
@@ -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(',')
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+8
-2
@@ -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 %}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"defseqacl": {
|
||||
"type": "SEQUENCE",
|
||||
"acl": ["U", "r", "a"]
|
||||
"acl": ["U", "r", "w"]
|
||||
},
|
||||
"deffuncacl": {
|
||||
"type": "FUNCTION",
|
||||
|
||||
+1
-1
@@ -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 %}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Executable → Regular
BIN
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
@@ -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'
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user